aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
authorJason <jason.daurus@gmail.com>2016-06-17 21:58:24 +0800
committerJason <jason.daurus@gmail.com>2016-06-17 21:58:24 +0800
commitc82d27b2a215c075dba71254cafdc86e98e0f2d9 (patch)
tree84324135951cf67617e4887923bee0627c1d1085 /web/src/js
parent6ad2f13341208b8460eae0dd0105c3109e773bae (diff)
parent9c6199db9be34fad18eaedb86463333671ae190a (diff)
downloadmitmproxy-c82d27b2a215c075dba71254cafdc86e98e0f2d9.tar.gz
mitmproxy-c82d27b2a215c075dba71254cafdc86e98e0f2d9.tar.bz2
mitmproxy-c82d27b2a215c075dba71254cafdc86e98e0f2d9.zip
Merge branch 'master' into settings
Conflicts: mitmproxy/web/static/app.css mitmproxy/web/static/app.js web/src/js/components/ProxyApp.jsx
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/components/ContentView.jsx78
-rw-r--r--web/src/js/components/ContentView/ContentErrors.jsx28
-rw-r--r--web/src/js/components/ContentView/ContentLoader.jsx67
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx70
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx28
-rw-r--r--web/src/js/components/EventLog.jsx81
-rw-r--r--web/src/js/components/FlowTable/FlowTableHead.jsx6
-rw-r--r--web/src/js/components/FlowView.jsx107
-rw-r--r--web/src/js/components/FlowView/Details.jsx133
-rw-r--r--web/src/js/components/FlowView/Headers.jsx130
-rw-r--r--web/src/js/components/FlowView/Messages.jsx168
-rw-r--r--web/src/js/components/FlowView/Nav.jsx57
-rw-r--r--web/src/js/components/Footer.jsx1
-rw-r--r--web/src/js/components/Header.jsx (renamed from web/src/js/components/Header.js)0
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx8
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx5
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx2
-rw-r--r--web/src/js/components/MainView.jsx4
-rwxr-xr-xweb/src/js/components/Prompt.jsx71
-rw-r--r--web/src/js/components/ProxyApp.jsx37
-rwxr-xr-xweb/src/js/components/ValueEditor.jsx36
-rwxr-xr-xweb/src/js/components/ValueEditor/EditorBase.jsx166
-rwxr-xr-xweb/src/js/components/ValueEditor/ValidateEditor.jsx58
-rw-r--r--web/src/js/components/common.js173
-rw-r--r--web/src/js/components/common/Button.jsx16
-rw-r--r--web/src/js/components/common/Splitter.jsx99
-rw-r--r--web/src/js/components/common/ToggleButton.jsx17
-rw-r--r--web/src/js/components/common/ToggleInputButton.jsx52
-rw-r--r--web/src/js/components/editor.js238
-rw-r--r--web/src/js/components/flowview/contentview.js267
-rw-r--r--web/src/js/components/flowview/details.js181
-rw-r--r--web/src/js/components/flowview/index.js114
-rw-r--r--web/src/js/components/flowview/messages.js320
-rw-r--r--web/src/js/components/flowview/nav.js61
-rw-r--r--web/src/js/components/prompt.js102
35 files changed, 1468 insertions, 1513 deletions
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
new file mode 100644
index 00000000..af3bffc1
--- /dev/null
+++ b/web/src/js/components/ContentView.jsx
@@ -0,0 +1,78 @@
+import React, { Component, PropTypes } from 'react'
+import { MessageUtils } from '../flow/utils.js'
+import { ViewAuto, ViewImage } from './ContentView/ContentViews'
+import * as ContentErrors from './ContentView/ContentErrors'
+import ContentLoader from './ContentView/ContentLoader'
+import ViewSelector from './ContentView/ViewSelector'
+
+export default class ContentView extends Component {
+
+ static propTypes = {
+ // It may seem a bit weird at the first glance:
+ // Every view takes the flow and the message as props, e.g.
+ // <Auto flow={flow} message={flow.request}/>
+ flow: React.PropTypes.object.isRequired,
+ message: React.PropTypes.object.isRequired,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { displayLarge: false, View: ViewAuto }
+ this.selectView = this.selectView.bind(this)
+ }
+
+ selectView(View) {
+ this.setState({ View })
+ }
+
+ displayLarge() {
+ this.setState({ displayLarge: true })
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.message !== this.props.message) {
+ this.setState({ displayLarge: false, View: ViewAuto })
+ }
+ }
+
+ isContentTooLarge(msg) {
+ return msg.contentLength > 1024 * 1024 * (ViewImage.matches(msg) ? 10 : 0.2)
+ }
+
+ render() {
+ const { flow, message } = this.props
+ const { displayLarge, View } = this.state
+
+ if (message.contentLength === 0) {
+ return <ContentErrors.ContentEmpty {...this.props}/>
+ }
+
+ if (message.contentLength === null) {
+ return <ContentErrors.ContentMissing {...this.props}/>
+ }
+
+ if (!displayLarge && this.isContentTooLarge(message)) {
+ return <ContentErrors.ContentTooLarge {...this.props} onClick={this.displayLarge}/>
+ }
+
+ return (
+ <div>
+ {View.textView ? (
+ <ContentLoader flow={flow} message={message}>
+ <this.state.View content="" />
+ </ContentLoader>
+ ) : (
+ <View flow={flow} message={message} />
+ )}
+ <div className="view-options text-center">
+ <ViewSelector onSelectView={this.selectView} active={View} message={message}/>
+ &nbsp;
+ <a className="btn btn-default btn-xs" href={MessageUtils.getContentURL(flow, message)}>
+ <i className="fa fa-download"/>
+ </a>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/ContentView/ContentErrors.jsx b/web/src/js/components/ContentView/ContentErrors.jsx
new file mode 100644
index 00000000..11594c7f
--- /dev/null
+++ b/web/src/js/components/ContentView/ContentErrors.jsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import { ViewImage } from './ContentViews'
+import {formatSize} from '../../utils.js'
+
+export function ContentEmpty({ flow, message }) {
+ return (
+ <div className="alert alert-info">
+ No {flow.request === message ? 'request' : 'response'} content.
+ </div>
+ )
+}
+
+export function ContentMissing({ flow, message }) {
+ return (
+ <div className="alert alert-info">
+ {flow.request === message ? 'Request' : 'Response'} content missing.
+ </div>
+ )
+}
+
+export function ContentTooLarge({ message, onClick }) {
+ 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>
+ )
+}
diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx
new file mode 100644
index 00000000..f346dc01
--- /dev/null
+++ b/web/src/js/components/ContentView/ContentLoader.jsx
@@ -0,0 +1,67 @@
+import React, { Component, PropTypes } from 'react'
+import { MessageUtils } from '../../flow/utils.js'
+
+export default class ContentLoader extends Component {
+
+ static propTypes = {
+ 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()
+ }
+
+ const request = MessageUtils.getContent(nextProps.flow, nextProps.message)
+
+ 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)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.message !== this.props.message) {
+ this.requestContent(nextProps)
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.state.request) {
+ this.state.request.abort()
+ }
+ }
+
+ render() {
+ return this.state.content ? (
+ React.cloneElement(this.props.children, {
+ content: this.state.content
+ })
+ ) : (
+ <div className="text-center">
+ <i className="fa fa-spinner fa-spin"></i>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
new file mode 100644
index 00000000..b0297dcc
--- /dev/null
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -0,0 +1,70 @@
+import React, { PropTypes } from 'react'
+import ContentLoader from './ContentLoader'
+import { MessageUtils } from '../../flow/utils.js'
+
+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))
+
+ViewImage.propTypes = {
+ flow: PropTypes.object.isRequired,
+ message: PropTypes.object.isRequired,
+}
+
+export function ViewImage({ flow, message }) {
+ return (
+ <div className="flowview-image">
+ <img src={MessageUtils.getContentURL(flow, message)} alt="preview" className="img-thumbnail"/>
+ </div>
+ )
+}
+
+ViewRaw.textView = true
+ViewRaw.matches = () => true
+
+ViewRaw.propTypes = {
+ content: React.PropTypes.string.isRequired,
+}
+
+export function ViewRaw({ content }) {
+ return <pre>{content}</pre>
+}
+
+ViewJSON.textView = true
+ViewJSON.regex = /^application\/json$/i
+ViewJSON.matches = msg => ViewJSON.regex.test(MessageUtils.getContentType(msg))
+
+ViewJSON.propTypes = {
+ content: React.PropTypes.string.isRequired,
+}
+
+export function ViewJSON({ content }) {
+ let json = content
+ try {
+ json = JSON.stringify(JSON.parse(content), null, 2);
+ } catch (e) {
+ // @noop
+ }
+ return <pre>{json}</pre>
+}
+
+
+ViewAuto.matches = () => false
+ViewAuto.findView = msg => views.find(v => v.matches(msg)) || views[views.length - 1]
+
+ViewAuto.propTypes = {
+ message: React.PropTypes.object.isRequired,
+ flow: React.PropTypes.object.isRequired,
+}
+
+export function ViewAuto({ message, flow }) {
+ const View = ViewAuto.findView(message)
+ if (View.textView) {
+ return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
+ } else {
+ return <View message={message} flow={flow} />
+ }
+}
+
+export default views
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
new file mode 100644
index 00000000..df3a5b83
--- /dev/null
+++ b/web/src/js/components/ContentView/ViewSelector.jsx
@@ -0,0 +1,28 @@
+import React, { PropTypes } from 'react'
+import classnames from 'classnames'
+import views, { ViewAuto } from './ContentViews'
+
+ViewSelector.propTypes = {
+ active: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ onSelectView: PropTypes.func.isRequired,
+}
+
+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)}
+ className={classnames('btn btn-default', { active: View === active })}>
+ {View === ViewAuto ? (
+ `auto: ${ViewAuto.findView(message).name.toLowerCase().replace('view', '')}`
+ ) : (
+ View.name.toLowerCase().replace('view', '')
+ )}
+ </button>
+ ))}
+ </div>
+ )
+}
diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx
index 58cecd1a..169162ee 100644
--- a/web/src/js/components/EventLog.jsx
+++ b/web/src/js/components/EventLog.jsx
@@ -1,31 +1,70 @@
-import React, { PropTypes } from 'react'
+import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { toggleEventLogFilter, toggleEventLogVisibility } from '../ducks/eventLog'
-import { ToggleButton } from './common'
+import ToggleButton from './common/ToggleButton'
import EventList from './EventLog/EventList'
-EventLog.propTypes = {
- filters: PropTypes.object.isRequired,
- events: PropTypes.array.isRequired,
- onToggleFilter: PropTypes.func.isRequired,
- onClose: PropTypes.func.isRequired
-}
+class EventLog extends Component {
+
+ static propTypes = {
+ filters: PropTypes.object.isRequired,
+ events: PropTypes.array.isRequired,
+ onToggleFilter: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ defaultHeight: PropTypes.number,
+ }
+
+ static defaultProps = {
+ defaultHeight: 200,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { height: this.props.defaultHeight }
+
+ this.onDragStart = this.onDragStart.bind(this)
+ this.onDragMove = this.onDragMove.bind(this)
+ this.onDragStop = this.onDragStop.bind(this)
+ }
-function EventLog({ filters, events, onToggleFilter, onClose }) {
- return (
- <div className="eventlog">
- <div>
- Eventlog
- <div className="pull-right">
- {['debug', 'info', 'web'].map(type => (
- <ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
- ))}
- <i onClick={onClose} className="fa fa-close"></i>
+ onDragStart(event) {
+ event.preventDefault()
+ this.dragStart = this.state.height + event.pageY
+ window.addEventListener('mousemove', this.onDragMove)
+ window.addEventListener('mouseup', this.onDragStop)
+ window.addEventListener('dragend', this.onDragStop)
+ }
+
+ onDragMove(event) {
+ event.preventDefault()
+ this.setState({ height: this.dragStart - event.pageY })
+ }
+
+ onDragStop(event) {
+ event.preventDefault()
+ window.removeEventListener('mousemove', this.onDragMove)
+ }
+
+ render() {
+ const { height } = this.state
+ const { filters, events, onToggleFilter, onClose } = this.props
+
+ return (
+ <div className="eventlog" style={{ height }}>
+ <div onMouseDown={this.onDragStart}>
+ Eventlog
+ <div className="pull-right">
+ {['debug', 'info', 'web'].map(type => (
+ <ToggleButton key={type} text={type} checked={filters[type]} onToggle={() => onToggleFilter(type)}/>
+ ))}
+ <i onClick={onClose} className="fa fa-close"></i>
+ </div>
</div>
+ <EventList events={events} />
</div>
- <EventList events={events} />
- </div>
- )
+ )
+ }
}
export default connect(
diff --git a/web/src/js/components/FlowTable/FlowTableHead.jsx b/web/src/js/components/FlowTable/FlowTableHead.jsx
index 1df38aba..840f6a34 100644
--- a/web/src/js/components/FlowTable/FlowTableHead.jsx
+++ b/web/src/js/components/FlowTable/FlowTableHead.jsx
@@ -19,16 +19,12 @@ function FlowTableHead({ sortColumn, sortDesc, onSort }) {
{columns.map(Column => (
<th className={classnames(Column.headerClass, sortColumn === Column.name && sortType)}
key={Column.name}
- onClick={() => onClick(Column)}>
+ onClick={() => onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })}>
{Column.headerName}
</th>
))}
</tr>
)
-
- function onClick(Column) {
- onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })
- }
}
export default connect(
diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx
new file mode 100644
index 00000000..23f8b3ea
--- /dev/null
+++ b/web/src/js/components/FlowView.jsx
@@ -0,0 +1,107 @@
+import React, { Component } from 'react'
+import _ from 'lodash'
+
+import Nav from './FlowView/Nav'
+import { Request, Response, Error } from './FlowView/Messages'
+import Details from './FlowView/Details'
+import Prompt from './Prompt'
+
+export default class FlowView extends Component {
+
+ static allTabs = { Request, Response, Error, Details }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { prompt: false }
+
+ this.closePrompt = this.closePrompt.bind(this)
+ this.selectTab = this.selectTab.bind(this)
+ }
+
+ getTabs() {
+ return ['request', 'response', 'error'].filter(k => this.props.flow[k]).concat(['details'])
+ }
+
+ nextTab(increment) {
+ const tabs = this.getTabs()
+ // JS modulo operator doesn't correct negative numbers, make sure that we are positive.
+ this.selectTab(tabs[(tabs.indexOf(this.props.tab) + increment + tabs.length) % tabs.length])
+ }
+
+ selectTab(panel) {
+ this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`)
+ }
+
+ closePrompt(edit) {
+ this.setState({ prompt: false })
+ if (edit) {
+ this.refs.tab.edit(edit)
+ }
+ }
+
+ promptEdit() {
+ let options
+
+ switch (this.props.tab) {
+
+ case 'request':
+ options = [
+ 'method',
+ 'url',
+ { text: 'http version', key: 'v' },
+ 'header'
+ ]
+ break
+
+ case 'response':
+ options = [
+ { text: 'http version', key: 'v' },
+ 'code',
+ 'message',
+ 'header'
+ ]
+ break
+
+ case 'details':
+ return
+
+ default:
+ throw 'Unknown tab for edit: ' + this.props.tab
+ }
+
+ this.setState({ prompt: { options, done: this.closePrompt } })
+ }
+
+ render() {
+ const tabs = this.getTabs()
+ let { flow, tab: active } = this.props
+
+ if (tabs.indexOf(active) < 0) {
+ if (active === 'response' && flow.error) {
+ active = 'error'
+ } else if (active === 'error' && flow.response) {
+ active = 'response'
+ } else {
+ active = tabs[0]
+ }
+ }
+
+ const Tab = FlowView.allTabs[_.capitalize(active)]
+
+ return (
+ <div className="flow-detail" onScroll={this.adjustHead}>
+ <Nav
+ flow={flow}
+ tabs={tabs}
+ active={active}
+ onSelectTab={this.selectTab}
+ />
+ <Tab ref="tab" flow={flow}/>
+ {this.state.prompt && (
+ <Prompt {...this.state.prompt}/>
+ )}
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/FlowView/Details.jsx b/web/src/js/components/FlowView/Details.jsx
new file mode 100644
index 00000000..78e68ecf
--- /dev/null
+++ b/web/src/js/components/FlowView/Details.jsx
@@ -0,0 +1,133 @@
+import React from 'react'
+import _ from 'lodash'
+import { formatTimeStamp, formatTimeDelta } from '../../utils.js'
+
+export function TimeStamp({ t, deltaTo, title }) {
+ return t ? (
+ <tr>
+ <td>{title}:</td>
+ <td>
+ {formatTimeStamp(t)}
+ {deltaTo && (
+ <span className="text-muted">
+ ({formatTimeDelta(1000 * (t - deltaTo))})
+ </span>
+ )}
+ </td>
+ </tr>
+ ) : (
+ <tr></tr>
+ )
+}
+
+export function ConnectionInfo({ conn }) {
+ return (
+ <table className="connection-table">
+ <tbody>
+ <tr key="address">
+ <td>Address:</td>
+ <td>{conn.address.address.join(':')}</td>
+ </tr>
+ {conn.sni ? (
+ <tr key="sni"></tr>
+ ) : (
+ <tr key="sni">
+ <td>
+ <abbr title="TLS Server Name Indication">TLS SNI:</abbr>
+ </td>
+ <td>{conn.sni}</td>
+ </tr>
+ )}
+ </tbody>
+ </table>
+ )
+}
+
+export function CertificateInfo({ flow }) {
+ // @todo We should fetch human-readable certificate representation from the server
+ return (
+ <div>
+ {flow.client_conn.cert && [
+ <h4 key="name">Client Certificate</h4>,
+ <pre key="value" style={{ maxHeight: 100 }}>{flow.client_conn.cert}</pre>
+ ]}
+
+ {flow.server_conn.cert && [
+ <h4 key="name">Server Certificate</h4>,
+ <pre key="value" style={{ maxHeight: 100 }}>{flow.server_conn.cert}</pre>
+ ]}
+ </div>
+ )
+}
+
+export function Timing({ flow }) {
+ const { server_conn: sc, client_conn: cc, request: req, response: res } = flow
+
+ const timestamps = [
+ {
+ title: "Server conn. initiated",
+ t: sc.timestamp_start,
+ deltaTo: req.timestamp_start
+ }, {
+ title: "Server conn. TCP handshake",
+ t: sc.timestamp_tcp_setup,
+ deltaTo: req.timestamp_start
+ }, {
+ title: "Server conn. SSL handshake",
+ t: sc.timestamp_ssl_setup,
+ deltaTo: req.timestamp_start
+ }, {
+ title: "Client conn. established",
+ t: cc.timestamp_start,
+ deltaTo: req.timestamp_start
+ }, {
+ title: "Client conn. SSL handshake",
+ t: cc.timestamp_ssl_setup,
+ deltaTo: req.timestamp_start
+ }, {
+ title: "First request byte",
+ t: req.timestamp_start,
+ }, {
+ title: "Request complete",
+ t: req.timestamp_end,
+ deltaTo: req.timestamp_start
+ }, res && {
+ title: "First response byte",
+ t: res.timestamp_start,
+ deltaTo: req.timestamp_start
+ }, res && {
+ title: "Response complete",
+ t: res.timestamp_end,
+ deltaTo: req.timestamp_start
+ }
+ ]
+
+ return (
+ <div>
+ <h4>Timing</h4>
+ <table className="timing-table">
+ <tbody>
+ {timestamps.filter(v => v).sort((a, b) => a.t - b.t).map(item => (
+ <TimeStamp key={item.title} {...item}/>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )
+}
+
+export default function Details({ flow }) {
+ return (
+ <section>
+ <h4>Client Connection</h4>
+ <ConnectionInfo conn={flow.client_conn}/>
+
+ <h4>Server Connection</h4>
+ <ConnectionInfo conn={flow.server_conn}/>
+
+ <CertificateInfo flow={flow}/>
+
+ <Timing flow={flow}/>
+ </section>
+ )
+}
diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx
new file mode 100644
index 00000000..880eeda1
--- /dev/null
+++ b/web/src/js/components/FlowView/Headers.jsx
@@ -0,0 +1,130 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import ValueEditor from '../ValueEditor'
+import { Key } from '../../utils.js'
+
+class HeaderEditor extends Component {
+
+ render() {
+ return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>
+ }
+
+ focus() {
+ ReactDOM.findDOMNode(this).focus()
+ }
+
+ onKeyDown(e) {
+ switch (e.keyCode) {
+ case Key.BACKSPACE:
+ var s = window.getSelection().getRangeAt(0)
+ if (s.startOffset === 0 && s.endOffset === 0) {
+ this.props.onRemove(e)
+ }
+ break
+ case Key.TAB:
+ if (!e.shiftKey) {
+ this.props.onTab(e)
+ }
+ break
+ }
+ }
+}
+
+export default class Headers extends Component {
+
+ static propTypes = {
+ onChange: PropTypes.func.isRequired,
+ message: PropTypes.object.isRequired,
+ }
+
+ onChange(row, col, val) {
+ const nextHeaders = _.cloneDeep(this.props.message.headers)
+
+ nextHeaders[row][col] = val
+
+ if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
+ // do not delete last row
+ if (nextHeaders.length === 1) {
+ nextHeaders[0][0] = 'Name'
+ nextHeaders[0][1] = 'Value'
+ } else {
+ nextHeaders.splice(row, 1)
+ // manually move selection target if this has been the last row.
+ if (row === nextHeaders.length) {
+ this._nextSel = `${row - 1}-value`
+ }
+ }
+ }
+
+ this.props.onChange(nextHeaders)
+ }
+
+ edit() {
+ this.refs['0-key'].focus()
+ }
+
+ onTab(row, col, e) {
+ const headers = this.props.message.headers
+
+ if (row !== headers.length - 1 || col !== 1) {
+ return
+ }
+
+ e.preventDefault()
+
+ const nextHeaders = _.cloneDeep(this.props.message.headers)
+ nextHeaders.push(['Name', 'Value'])
+ this.props.onChange(nextHeaders)
+ this._nextSel = `${row + 1}-key`
+ }
+
+ componentDidUpdate() {
+ if (this._nextSel && this.refs[this._nextSel]) {
+ this.refs[this._nextSel].focus()
+ this._nextSel = undefined
+ }
+ }
+
+ onRemove(row, col, e) {
+ if (col === 1) {
+ e.preventDefault()
+ this.refs[`${row}-key`].focus()
+ } else if (row > 0) {
+ e.preventDefault()
+ this.refs[`${row - 1}-value`].focus()
+ }
+ }
+
+ render() {
+ const { message } = this.props
+
+ return (
+ <table className="header-table">
+ <tbody>
+ {message.headers.map((header, i) => (
+ <tr key={i}>
+ <td className="header-name">
+ <HeaderEditor
+ ref={`${i}-key`}
+ content={header[0]}
+ onDone={val => this.onChange(i, 0, val)}
+ onRemove={event => this.onRemove(i, 0, event)}
+ onTab={event => this.onTab(i, 0, event)}
+ />:
+ </td>
+ <td className="header-value">
+ <HeaderEditor
+ ref={`${i}-value`}
+ content={header[1]}
+ onDone={val => this.onChange(i, 1, val)}
+ onRemove={event => this.onRemove(i, 1, event)}
+ onTab={event => this.onTab(i, 1, event)}
+ />
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ )
+ }
+}
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
new file mode 100644
index 00000000..ba6a5f2b
--- /dev/null
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -0,0 +1,168 @@
+import React, { Component } from 'react'
+import _ from 'lodash'
+
+import { FlowActions } from '../../actions.js'
+import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js'
+import { Key, formatTimeStamp } from '../../utils.js'
+import ContentView from '../ContentView'
+import ValueEditor from '../ValueEditor'
+import Headers from './Headers'
+
+class RequestLine extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <div className="first-line request-line">
+ <ValueEditor
+ ref="method"
+ content={flow.request.method}
+ onDone={method => FlowActions.update(flow, { request: { method } })}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="url"
+ content={RequestUtils.pretty_url(flow.request)}
+ onDone={url => FlowActions.update(flow, { request: Object.assign({ path: '' }, parseUrl(url)) })}
+ isValid={url => !!parseUrl(url).host}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="httpVersion"
+ content={flow.request.http_version}
+ onDone={ver => FlowActions.update(flow, { request: { http_version: parseHttpVersion(ver) } })}
+ isValid={isValidHttpVersion}
+ inline
+ />
+ </div>
+ )
+ }
+}
+
+class ResponseLine extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <div className="first-line response-line">
+ <ValueEditor
+ ref="httpVersion"
+ content={flow.response.http_version}
+ onDone={nextVer => FlowActions.update(flow, { response: { http_version: parseHttpVersion(nextVer) } })}
+ isValid={isValidHttpVersion}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="code"
+ content={flow.response.status_code + ''}
+ onDone={code => FlowActions.update(flow, { response: { code: parseInt(code) } })}
+ isValid={code => /^\d+$/.test(code)}
+ inline
+ />
+ &nbsp;
+ <ValueEditor
+ ref="msg"
+ content={flow.response.reason}
+ onDone={msg => FlowActions.update(flow, { response: { msg } })}
+ inline
+ />
+ </div>
+ )
+ }
+}
+
+export class Request extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <section className="request">
+ <RequestLine ref="requestLine" flow={flow}/>
+ <Headers
+ ref="headers"
+ message={flow.request}
+ onChange={headers => FlowActions.update(flow, { request: { headers } })}
+ />
+ <hr/>
+ <ContentView flow={flow} message={flow.request}/>
+ </section>
+ )
+ }
+
+ edit(k) {
+ switch (k) {
+ case 'm':
+ this.refs.requestLine.refs.method.focus()
+ break
+ case 'u':
+ this.refs.requestLine.refs.url.focus()
+ break
+ case 'v':
+ this.refs.requestLine.refs.httpVersion.focus()
+ break
+ case 'h':
+ this.refs.headers.edit()
+ break
+ default:
+ throw new Error(`Unimplemented: ${k}`)
+ }
+ }
+}
+
+export class Response extends Component {
+
+ render() {
+ const { flow } = this.props
+
+ return (
+ <section className="response">
+ <ResponseLine ref="responseLine" flow={flow}/>
+ <Headers
+ ref="headers"
+ message={flow.response}
+ onChange={headers => FlowActions.update(flow, { response: { headers } })}
+ />
+ <hr/>
+ <ContentView flow={flow} message={flow.response}/>
+ </section>
+ )
+ }
+
+ edit(k) {
+ switch (k) {
+ case 'c':
+ this.refs.responseLine.refs.status_code.focus()
+ break
+ case 'm':
+ this.refs.responseLine.refs.msg.focus()
+ break
+ case 'v':
+ this.refs.responseLine.refs.httpVersion.focus()
+ break
+ case 'h':
+ this.refs.headers.edit()
+ break
+ default:
+ throw new Error(`'Unimplemented: ${k}`)
+ }
+ }
+}
+
+export function Error({ flow }) {
+ return (
+ <section>
+ <div className="alert alert-warning">
+ {flow.error.msg}
+ <div>
+ <small>{formatTimeStamp(flow.error.timestamp)}</small>
+ </div>
+ </div>
+ </section>
+ )
+}
diff --git a/web/src/js/components/FlowView/Nav.jsx b/web/src/js/components/FlowView/Nav.jsx
new file mode 100644
index 00000000..386c3a6c
--- /dev/null
+++ b/web/src/js/components/FlowView/Nav.jsx
@@ -0,0 +1,57 @@
+import React, { PropTypes } from 'react'
+import classnames from 'classnames'
+import { FlowActions } from '../../actions.js'
+
+NavAction.propTypes = {
+ icon: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ onClick: PropTypes.func.isRequired,
+}
+
+function NavAction({ icon, title, onClick }) {
+ return (
+ <a title={title}
+ href="#"
+ className="nav-action"
+ onClick={event => {
+ event.preventDefault()
+ onClick(event)
+ }}>
+ <i className={`fa fa-fw ${icon}`}></i>
+ </a>
+ )
+}
+
+Nav.propTypes = {
+ flow: PropTypes.object.isRequired,
+ active: PropTypes.string.isRequired,
+ tabs: PropTypes.array.isRequired,
+ onSelectTab: PropTypes.func.isRequired,
+}
+
+export default function Nav({ flow, active, tabs, onSelectTab }) {
+ return (
+ <nav className="nav-tabs nav-tabs-sm">
+ {tabs.map(tab => (
+ <a key={tab}
+ href="#"
+ className={classnames({ active: active === tab })}
+ onClick={event => {
+ event.preventDefault()
+ onSelectTab(tab)
+ }}>
+ {_.capitalize(tab)}
+ </a>
+ ))}
+ <NavAction title="[d]elete flow" icon="fa-trash" onClick={() => FlowActions.delete(flow)} />
+ <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={() => FlowActions.duplicate(flow)} />
+ <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={() => FlowActions.replay(flow)} />
+ {flow.intercepted && (
+ <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={() => FlowActions.accept(flow)} />
+ )}
+ {flow.modified && (
+ <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={() => FlowActions.revert(flow)} />
+ )}
+ </nav>
+ )
+}
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 903522f4..1f6de2d7 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -1,6 +1,5 @@
import React from 'react'
import { formatSize } from '../utils.js'
-import { SettingsState } from './common.js'
Footer.propTypes = {
settings: React.PropTypes.object.isRequired,
diff --git a/web/src/js/components/Header.js b/web/src/js/components/Header.jsx
index 93ca5154..93ca5154 100644
--- a/web/src/js/components/Header.js
+++ b/web/src/js/components/Header.jsx
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index 4a43f40f..96f42652 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -1,10 +1,10 @@
import React, { PropTypes } from 'react'
-import { Button } from '../common.js'
-import {FlowActions} from "../../actions.js";
-import {MessageUtils} from "../../flow/utils.js";
+import Button from '../common/Button'
+import { FlowActions } from '../../actions.js'
+import { MessageUtils } from '../../flow/utils.js'
import { connect } from 'react-redux'
-FlowMenu.title = "Flow"
+FlowMenu.title = 'Flow'
FlowMenu.propTypes = {
flow: PropTypes.object.isRequired,
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index 6bbf15d5..44f309fd 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -1,8 +1,9 @@
import React, { PropTypes } from 'react'
-import { ToggleInputButton, ToggleButton } from '../common.js'
+import ToggleButton from '../common/ToggleButton'
+import ToggleInputButton from '../common/ToggleInputButton'
import { SettingsActions } from '../../actions.js'
-OptionMenu.title = "Options"
+OptionMenu.title = 'Options'
OptionMenu.propTypes = {
settings: PropTypes.object.isRequired,
diff --git a/web/src/js/components/Header/ViewMenu.jsx b/web/src/js/components/Header/ViewMenu.jsx
index a58b0a16..8d662c28 100644
--- a/web/src/js/components/Header/ViewMenu.jsx
+++ b/web/src/js/components/Header/ViewMenu.jsx
@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
-import { ToggleButton } from '../common.js'
+import ToggleButton from '../common/ToggleButton'
import { toggleEventLogVisibility } from '../../ducks/eventLog'
ViewMenu.title = 'View'
diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx
index 8c6ed6d0..7064d3bf 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -3,9 +3,9 @@ import { connect } from 'react-redux'
import { FlowActions } from '../actions.js'
import { Query } from '../actions.js'
import { Key } from '../utils.js'
-import { Splitter } from './common.js'
+import Splitter from './common/Splitter'
import FlowTable from './FlowTable'
-import FlowView from './flowview/index.js'
+import FlowView from './FlowView'
import { selectFlow, setFilter, setHighlight } from '../ducks/flows'
class MainView extends Component {
diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx
new file mode 100755
index 00000000..6b19b3b3
--- /dev/null
+++ b/web/src/js/components/Prompt.jsx
@@ -0,0 +1,71 @@
+import React, { PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import _ from 'lodash'
+
+import {Key} from '../utils.js'
+
+Prompt.contextTypes = {
+ returnFocus: PropTypes.func
+}
+
+Prompt.propTypes = {
+ options: PropTypes.array.isRequired,
+ done: PropTypes.func.isRequired,
+ prompt: PropTypes.string,
+}
+
+export default function Prompt({ prompt, done, options }, context) {
+ const opts = []
+
+ function keyTaken(k) {
+ return _.map(opts, 'key').includes(k)
+ }
+
+ for (let i = 0; i < options.length; i++) {
+ let opt = options[i]
+ if (_.isString(opt)) {
+ let str = opt
+ while (str.length > 0 && keyTaken(str[0])) {
+ str = str.substr(1)
+ }
+ opt = { text: opt, key: str[0] }
+ }
+ if (!opt.text || !opt.key || keyTaken(opt.key)) {
+ throw 'invalid options'
+ }
+ opts.push(opt)
+ }
+
+ return (
+ <div tabIndex="0" onKeyDown={onKeyDown} onClick={onClick} className="prompt-dialog">
+ <div className="prompt-content">
+ {prompt || <strong>Select: </strong> }
+ {opts.map(opt => {
+ const idx = opt.text.indexOf(opt.key)
+ function onClick(event) {
+ done(opt.key)
+ event.stopPropagation()
+ }
+ return (
+ <span key={opt.key} className="option" onClick={onClick}>
+ {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('}
+ {prefix}<strong className="text-primary">{opt.key}</strong>
+ {idx !== -1 ? opt.text.substring(idx + 1) : ')'}
+ </span>
+ )
+ })}
+ </div>
+ </div>
+ )
+
+ function onKeyDown(event) {
+ event.stopPropagation()
+ event.preventDefault()
+ const key = opts.find(opt => Key[opt.key.toUpperCase()] === event.keyCode)
+ if (!key && event.keyCode !== Key.ESC && event.keyCode !== Key.ENTER) {
+ return
+ }
+ done(k || false)
+ context.returnFocus()
+ }
+}
diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index 1d27614f..8129b0f0 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -1,20 +1,18 @@
-import React, { Component, PropTypes } from "react"
-import ReactDOM from "react-dom"
-import _ from "lodash"
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import _ from 'lodash'
import { connect } from 'react-redux'
import { fetch as fetchSettings } from '../ducks/settings'
-import { Splitter } from "./common.js"
-import Header from "./Header"
-import EventLog from "./EventLog"
-import Footer from "./Footer"
-import { Key } from "../utils.js"
+import Header from './Header'
+import EventLog from './EventLog'
+import Footer from './Footer'
+import { Key } from '../utils.js'
class ProxyAppMain extends Component {
static childContextTypes = {
returnFocus: PropTypes.func.isRequired,
- location: PropTypes.object.isRequired,
}
static contextTypes = {
@@ -68,10 +66,7 @@ class ProxyAppMain extends Component {
* @todo use props
*/
getChildContext() {
- return {
- returnFocus: this.focus,
- location: this.props.location
- }
+ return { returnFocus: this.focus }
}
/**
@@ -85,19 +80,20 @@ class ProxyAppMain extends Component {
/**
* @todo move to actions
+ * @todo bind on window
*/
onKeyDown(e) {
let name = null
switch (e.keyCode) {
case Key.I:
- name = "intercept"
+ name = 'intercept'
break
case Key.L:
- name = "search"
+ name = 'search'
break
case Key.H:
- name = "highlight"
+ name = 'highlight'
break
default:
let main = this.refs.view
@@ -112,7 +108,7 @@ class ProxyAppMain extends Component {
if (name) {
const headerComponent = this.refs.header
- headerComponent.setState({ active: Header.entries.MainMenu }, () => {
+ headerComponent.setState({ active: Header.entries[0] }, () => {
headerComponent.refs.active.refs[name].select()
})
}
@@ -128,12 +124,11 @@ class ProxyAppMain extends Component {
<Header ref="header" settings={settings} updateLocation={this.updateLocation} query={query} />
{React.cloneElement(
children,
- { ref: "view", location, query, updateLocation: this.updateLocation }
+ { ref: 'view', location, query, updateLocation: this.updateLocation }
)}
- {showEventLog && [
- <Splitter key="splitter" axis="y"/>,
+ {showEventLog && (
<EventLog key="eventlog"/>
- ]}
+ )}
<Footer settings={settings}/>
</div>
)
diff --git a/web/src/js/components/ValueEditor.jsx b/web/src/js/components/ValueEditor.jsx
new file mode 100755
index 00000000..0316924f
--- /dev/null
+++ b/web/src/js/components/ValueEditor.jsx
@@ -0,0 +1,36 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import ValidateEditor from './ValueEditor/ValidateEditor'
+
+export default class ValueEditor extends Component {
+
+ static contextTypes = {
+ returnFocus: PropTypes.func,
+ }
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ inline: PropTypes.bool,
+ }
+
+ constructor(props) {
+ super(props)
+ this.focus = this.focus.bind(this)
+ }
+
+ render() {
+ var tag = this.props.inline ? "span" : 'div'
+ return (
+ <ValidateEditor
+ {...this.props}
+ onStop={() => this.context.returnFocus()}
+ tag={tag}
+ />
+ )
+ }
+
+ focus() {
+ ReactDOM.findDOMNode(this).focus();
+ }
+}
diff --git a/web/src/js/components/ValueEditor/EditorBase.jsx b/web/src/js/components/ValueEditor/EditorBase.jsx
new file mode 100755
index 00000000..e737d2af
--- /dev/null
+++ b/web/src/js/components/ValueEditor/EditorBase.jsx
@@ -0,0 +1,166 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import {Key} from '../../utils.js'
+
+export default class EditorBase extends Component {
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ contentToHtml: PropTypes.func,
+ nodeToContent: PropTypes.func,
+ onStop: PropTypes.func,
+ submitOnEnter: PropTypes.bool,
+ className: PropTypes.string,
+ tag: PropTypes.string,
+ }
+
+ static defaultProps = {
+ contentToHtml: content => _.escape(content),
+ nodeToContent: node => node.textContent,
+ submitOnEnter: true,
+ className: '',
+ tag: 'div',
+ onStop: _.noop,
+ onMouseDown: _.noop,
+ onBlur: _.noop,
+ onInput: _.noop,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {editable: false}
+
+ this.onPaste = this.onPaste.bind(this)
+ this.onMouseDown = this.onMouseDown.bind(this)
+ this.onMouseUp = this.onMouseUp.bind(this)
+ this.onFocus = this.onFocus.bind(this)
+ this.onClick = this.onClick.bind(this)
+ this.stop = this.stop.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ this.reset = this.reset.bind(this)
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onInput = this.onInput.bind(this)
+ }
+
+ stop() {
+ // a stop would cause a blur as a side-effect.
+ // but a blur event must trigger a stop as well.
+ // to fix this, make stop = blur and do the actual stop in the onBlur handler.
+ ReactDOM.findDOMNode(this).blur()
+ this.props.onStop()
+ }
+
+ render() {
+ return (
+ <this.props.tag
+ {...this.props}
+ tabIndex="0"
+ className={`inline-input ${this.props.className}`}
+ contentEditable={this.state.editable || undefined}
+ onFocus={this.onFocus}
+ onMouseDown={this.onMouseDown}
+ onClick={this.onClick}
+ onBlur={this.onBlur}
+ onKeyDown={this.onKeyDown}
+ onInput={this.onInput}
+ onPaste={this.onPaste}
+ dangerouslySetInnerHTML={{ __html: this.props.contentToHtml(this.props.content) }}
+ />
+ )
+ }
+
+ onPaste(e) {
+ e.preventDefault()
+ var content = e.clipboardData.getData('text/plain')
+ document.execCommand('insertHTML', false, content)
+ }
+
+ onMouseDown(e) {
+ this._mouseDown = true
+ window.addEventListener('mouseup', this.onMouseUp)
+ this.props.onMouseDown(e)
+ }
+
+ onMouseUp() {
+ if (this._mouseDown) {
+ this._mouseDown = false
+ window.removeEventListener('mouseup', this.onMouseUp)
+ }
+ }
+
+ onClick(e) {
+ this.onMouseUp()
+ this.onFocus(e)
+ }
+
+ onFocus(e) {
+ if (this._mouseDown || this._ignore_events || this.state.editable) {
+ return
+ }
+
+ // contenteditable in FireFox is more or less broken.
+ // - we need to blur() and then focus(), otherwise the caret is not shown.
+ // - blur() + focus() == we need to save the caret position before
+ // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint
+ const sel = window.getSelection()
+ let range
+ if (sel.rangeCount > 0) {
+ range = sel.getRangeAt(0)
+ } else if (document.caretPositionFromPoint && e.clientX && e.clientY) {
+ const pos = document.caretPositionFromPoint(e.clientX, e.clientY)
+ range = document.createRange()
+ range.setStart(pos.offsetNode, pos.offset)
+ } else if (document.caretRangeFromPoint && e.clientX && e.clientY) {
+ range = document.caretRangeFromPoint(e.clientX, e.clientY)
+ } else {
+ range = document.createRange()
+ range.selectNodeContents(ReactDOM.findDOMNode(this))
+ }
+
+ this._ignore_events = true
+ this.setState({ editable: true }, () => {
+ const node = ReactDOM.findDOMNode(this)
+ node.blur()
+ node.focus()
+ this._ignore_events = false
+ })
+ }
+
+ onBlur(e) {
+ if (this._ignore_events) {
+ return
+ }
+ window.getSelection().removeAllRanges() //make sure that selection is cleared on blur
+ this.setState({ editable: false })
+ this.props.onDone(this.props.nodeToContent(ReactDOM.findDOMNode(this)))
+ this.props.onBlur(e)
+ }
+
+ reset() {
+ ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content)
+ }
+
+ onKeyDown(e) {
+ e.stopPropagation()
+ switch (e.keyCode) {
+ case Key.ESC:
+ e.preventDefault()
+ this.reset()
+ this.stop()
+ break
+ case Key.ENTER:
+ if (this.props.submitOnEnter && !e.shiftKey) {
+ e.preventDefault()
+ this.stop()
+ }
+ break
+ default:
+ break
+ }
+ }
+
+ onInput() {
+ this.props.onInput(this.props.nodeToContent(ReactDOM.findDOMNode(this)))
+ }
+}
diff --git a/web/src/js/components/ValueEditor/ValidateEditor.jsx b/web/src/js/components/ValueEditor/ValidateEditor.jsx
new file mode 100755
index 00000000..2f362986
--- /dev/null
+++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx
@@ -0,0 +1,58 @@
+import React, { Component, PropTypes } from 'react'
+import ReactDOM from 'react-dom'
+import EditorBase from './EditorBase'
+
+export default class ValidateEditor extends Component {
+
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onDone: PropTypes.func.isRequired,
+ onInput: PropTypes.func,
+ isValid: PropTypes.func,
+ className: PropTypes.string,
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = { currentContent: props.content }
+ this.onInput = this.onInput.bind(this)
+ this.onDone = this.onDone.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({ currentContent: nextProps.content })
+ }
+
+ onInput(currentContent) {
+ this.setState({ currentContent })
+ this.props.onInput && this.props.onInput(currentContent)
+ }
+
+ onDone(content) {
+ if (this.props.isValid && !this.props.isValid(content)) {
+ this.refs.editor.reset()
+ content = this.props.content
+ }
+ this.props.onDone(content)
+ }
+
+ render() {
+ let className = this.props.className || ''
+ if (this.props.isValid) {
+ if (this.props.isValid(this.state.currentContent)) {
+ className += ' has-success'
+ } else {
+ className += ' has-warning'
+ }
+ }
+ return (
+ <EditorBase
+ {...this.props}
+ ref="editor"
+ className={className}
+ onDone={this.onDone}
+ onInput={this.onInput}
+ />
+ )
+ }
+}
diff --git a/web/src/js/components/common.js b/web/src/js/components/common.js
deleted file mode 100644
index 1497e15d..00000000
--- a/web/src/js/components/common.js
+++ /dev/null
@@ -1,173 +0,0 @@
-import React from "react"
-import ReactDOM from "react-dom"
-import {Key} from "../utils.js";
-import _ from "lodash"
-
-export var Splitter = React.createClass({
- getDefaultProps: function () {
- return {
- axis: "x"
- };
- },
- getInitialState: function () {
- return {
- applied: false,
- startX: false,
- startY: false
- };
- },
- onMouseDown: function (e) {
- this.setState({
- startX: e.pageX,
- startY: e.pageY
- });
- window.addEventListener("mousemove", this.onMouseMove);
- window.addEventListener("mouseup", this.onMouseUp);
- // Occasionally, only a dragEnd event is triggered, but no mouseUp.
- window.addEventListener("dragend", this.onDragEnd);
- },
- onDragEnd: function () {
- ReactDOM.findDOMNode(this).style.transform = "";
- window.removeEventListener("dragend", this.onDragEnd);
- window.removeEventListener("mouseup", this.onMouseUp);
- window.removeEventListener("mousemove", this.onMouseMove);
- },
- onMouseUp: function (e) {
- this.onDragEnd();
-
- var node = ReactDOM.findDOMNode(this);
- var prev = node.previousElementSibling;
- var next = node.nextElementSibling;
-
- var dX = e.pageX - this.state.startX;
- var dY = e.pageY - this.state.startY;
- var flexBasis;
- if (this.props.axis === "x") {
- flexBasis = prev.offsetWidth + dX;
- } else {
- flexBasis = prev.offsetHeight + dY;
- }
-
- prev.style.flex = "0 0 " + Math.max(0, flexBasis) + "px";
- next.style.flex = "1 1 auto";
-
- this.setState({
- applied: true
- });
- this.onResize();
- },
- onMouseMove: function (e) {
- var dX = 0, dY = 0;
- if (this.props.axis === "x") {
- dX = e.pageX - this.state.startX;
- } else {
- dY = e.pageY - this.state.startY;
- }
- ReactDOM.findDOMNode(this).style.transform = "translate(" + dX + "px," + dY + "px)";
- },
- onResize: function () {
- // Trigger a global resize event. This notifies components that employ virtual scrolling
- // that their viewport may have changed.
- window.setTimeout(function () {
- window.dispatchEvent(new CustomEvent("resize"));
- }, 1);
- },
- reset: function (willUnmount) {
- if (!this.state.applied) {
- return;
- }
- var node = ReactDOM.findDOMNode(this);
- var prev = node.previousElementSibling;
- var next = node.nextElementSibling;
-
- prev.style.flex = "";
- next.style.flex = "";
-
- if (!willUnmount) {
- this.setState({
- applied: false
- });
- }
- this.onResize();
- },
- componentWillUnmount: function () {
- this.reset(true);
- },
- render: function () {
- var className = "splitter";
- if (this.props.axis === "x") {
- className += " splitter-x";
- } else {
- className += " splitter-y";
- }
- return (
- <div className={className}>
- <div onMouseDown={this.onMouseDown} draggable="true"></div>
- </div>
- );
- }
-});
-
-export const ToggleButton = ({checked, onToggle, text}) =>
- <div className={"btn btn-toggle " + (checked ? "btn-primary" : "btn-default")} onClick={onToggle}>
- <i className={"fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o")}/>
- &nbsp;
- {text}
- </div>;
-
-ToggleButton.propTypes = {
- checked: React.PropTypes.bool.isRequired,
- onToggle: React.PropTypes.func.isRequired,
- text: React.PropTypes.string.isRequired
-};
-
-export const Button = ({onClick, text, icon}) =>
- <div className={"btn btn-default"} onClick={onClick}>
- <i className={"fa fa-fw " + icon}/>
- &nbsp;
- {text}
- </div>;
-
-Button.propTypes = {
- onClick: React.PropTypes.func.isRequired,
- text: React.PropTypes.string.isRequired
-};
-
-export class ToggleInputButton extends React.Component {
- constructor(props) {
- super(props);
- this.state = {txt: props.txt};
- }
-
- render() {
- return (
- <div className="input-group toggle-input-btn">
- <span
- className="input-group-btn"
- onClick={() => this.props.onToggleChanged(this.state.txt)}>
- <div className={"btn " + (this.props.checked ? "btn-primary" : "btn-default")}>
- <span className={"fa " + (this.props.checked ? "fa-check-square-o" : "fa-square-o")}/>
- &nbsp;{this.props.name}
- </div>
- </span>
- <input
- className="form-control"
- placeholder={this.props.placeholder}
- disabled={this.props.checked}
- value={this.state.txt}
- type={this.props.inputType}
- onChange={e => this.setState({txt: e.target.value})}
- onKeyDown={e => {if (e.keyCode === Key.ENTER) this.props.onToggleChanged(this.state.txt); e.stopPropagation()}}/>
- </div>
- );
- }
-}
-
-ToggleInputButton.propTypes = {
- name: React.PropTypes.string.isRequired,
- txt: React.PropTypes.string.isRequired,
- onToggleChanged: React.PropTypes.func.isRequired
-};
-
-
-
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
new file mode 100644
index 00000000..cc2fe9dd
--- /dev/null
+++ b/web/src/js/components/common/Button.jsx
@@ -0,0 +1,16 @@
+import React, { PropTypes } from 'react'
+
+Button.propTypes = {
+ onClick: PropTypes.func.isRequired,
+ text: PropTypes.string.isRequired
+}
+
+export default function Button({ onClick, text, icon }) {
+ return (
+ <div className={"btn btn-default"} onClick={onClick}>
+ <i className={"fa fa-fw " + icon}/>
+ &nbsp;
+ {text}
+ </div>
+ )
+}
diff --git a/web/src/js/components/common/Splitter.jsx b/web/src/js/components/common/Splitter.jsx
new file mode 100644
index 00000000..9d22b6fd
--- /dev/null
+++ b/web/src/js/components/common/Splitter.jsx
@@ -0,0 +1,99 @@
+import React, { Component } from 'react'
+import ReactDOM from 'react-dom'
+import classnames from 'classnames'
+
+export default class Splitter extends Component {
+
+ static defaultProps = { axis: 'x' }
+
+ constructor(props, context) {
+ super(props, context)
+
+ this.state = { applied: false, startX: false, startY: false }
+
+ this.onMouseMove = this.onMouseMove.bind(this)
+ this.onMouseUp = this.onMouseUp.bind(this)
+ this.onDragEnd = this.onDragEnd.bind(this)
+ }
+
+ onMouseDown(e) {
+ this.setState({ startX: e.pageX, startY: e.pageY })
+
+ window.addEventListener('mousemove', this.onMouseMove)
+ window.addEventListener('mouseup', this.onMouseUp)
+ // Occasionally, only a dragEnd event is triggered, but no mouseUp.
+ window.addEventListener('dragend', this.onDragEnd)
+ }
+
+ onDragEnd() {
+ ReactDOM.findDOMNode(this).style.transform = ''
+
+ window.removeEventListener('dragend', this.onDragEnd)
+ window.removeEventListener('mouseup', this.onMouseUp)
+ window.removeEventListener('mousemove', this.onMouseMove)
+ }
+
+ onMouseUp(e) {
+ this.onDragEnd()
+
+ const node = ReactDOM.findDOMNode(this)
+ const prev = node.previousElementSibling
+
+ let flexBasis = prev.offsetHeight + e.pageY - this.state.startY
+
+ if (this.props.axis === 'x') {
+ flexBasis = prev.offsetWidth + e.pageX - this.state.startX
+ }
+
+ prev.style.flex = `0 0 ${Math.max(0, flexBasis)}px`
+ node.nextElementSibling.style.flex = '1 1 auto'
+
+ this.setState({ applied: true })
+ this.onResize()
+ }
+
+ onMouseMove(e) {
+ let dX = 0
+ let dY = 0
+ if (this.props.axis === 'x') {
+ dX = e.pageX - this.state.startX
+ } else {
+ dY = e.pageY - this.state.startY
+ }
+ ReactDOM.findDOMNode(this).style.transform = `translate(${dX}px, ${dY}px)`
+ }
+
+ onResize() {
+ // Trigger a global resize event. This notifies components that employ virtual scrolling
+ // that their viewport may have changed.
+ window.setTimeout(() => window.dispatchEvent(new CustomEvent('resize')), 1)
+ }
+
+ reset(willUnmount) {
+ if (!this.state.applied) {
+ return
+ }
+
+ const node = ReactDOM.findDOMNode(this)
+
+ node.previousElementSibling.style.flex = ''
+ node.nextElementSibling.style.flex = ''
+
+ if (!willUnmount) {
+ this.setState({ applied: false })
+ }
+ this.onResize()
+ }
+
+ componentWillUnmount() {
+ this.reset(true)
+ }
+
+ render() {
+ return (
+ <div className={classnames('splitter', this.props.axis === 'x' ? 'splitter-x' : 'splitter-y')}>
+ <div onMouseDown={this.onMouseDown} draggable="true"></div>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/common/ToggleButton.jsx b/web/src/js/components/common/ToggleButton.jsx
new file mode 100644
index 00000000..6027728b
--- /dev/null
+++ b/web/src/js/components/common/ToggleButton.jsx
@@ -0,0 +1,17 @@
+import React, { PropTypes } from 'react'
+
+ToggleButton.propTypes = {
+ checked: PropTypes.bool.isRequired,
+ onToggle: PropTypes.func.isRequired,
+ text: PropTypes.string.isRequired
+}
+
+export default function ToggleButton({ checked, onToggle, text }) {
+ return (
+ <div className={"btn btn-toggle " + (checked ? "btn-primary" : "btn-default")} onClick={onToggle}>
+ <i className={"fa fa-fw " + (checked ? "fa-check-square-o" : "fa-square-o")}/>
+ &nbsp;
+ {text}
+ </div>
+ )
+}
diff --git a/web/src/js/components/common/ToggleInputButton.jsx b/web/src/js/components/common/ToggleInputButton.jsx
new file mode 100644
index 00000000..25d620ae
--- /dev/null
+++ b/web/src/js/components/common/ToggleInputButton.jsx
@@ -0,0 +1,52 @@
+import React, { Component, PropTypes } from 'react'
+import classnames from 'classnames'
+import { Key } from '../../utils'
+
+export default class ToggleInputButton extends Component {
+
+ static propTypes = {
+ name: PropTypes.string.isRequired,
+ txt: PropTypes.string.isRequired,
+ onToggleChanged: PropTypes.func.isRequired
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = { txt: props.txt }
+ }
+
+ onChange(e) {
+ this.setState({ txt: e.target.value })
+ }
+
+ onKeyDown(e) {
+ e.stopPropagation()
+ if (e.keyCode === Key.ENTER) {
+ this.props.onToggleChanged(this.state.txt)
+ }
+ }
+
+ render() {
+ return (
+ <div className="input-group toggle-input-btn">
+ <span className="input-group-btn"
+ onClick={() => this.props.onToggleChanged(this.state.txt)}>
+ <div className={classnames('btn', this.props.checked ? 'btn-primary' : 'btn-default')}>
+ <span className={classnames('fa', this.props.checked ? 'fa-check-square-o' : 'fa-square-o')}/>
+ &nbsp;
+ {this.props.name}
+ </div>
+ </span>
+ <input
+ className="form-control"
+ placeholder={this.props.placeholder}
+ disabled={this.props.checked}
+ value={this.state.txt}
+ type={this.props.inputType}
+ onChange={e => this.onChange(e)}
+ onKeyDown={e => this.onKeyDown(e)}
+ />
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/editor.js b/web/src/js/components/editor.js
deleted file mode 100644
index eed2f7c6..00000000
--- a/web/src/js/components/editor.js
+++ /dev/null
@@ -1,238 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import {Key} from "../utils.js";
-
-var contentToHtml = function (content) {
- return _.escape(content);
-};
-var nodeToContent = function (node) {
- return node.textContent;
-};
-
-/*
- Basic Editor Functionality
- */
-var EditorBase = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- onDone: React.PropTypes.func.isRequired,
- contentToHtml: React.PropTypes.func,
- nodeToContent: React.PropTypes.func, // content === nodeToContent( Node<innerHTML=contentToHtml(content)> )
- onStop: React.PropTypes.func,
- submitOnEnter: React.PropTypes.bool,
- className: React.PropTypes.string,
- tag: React.PropTypes.string
- },
- getDefaultProps: function () {
- return {
- contentToHtml: contentToHtml,
- nodeToContent: nodeToContent,
- submitOnEnter: true,
- className: "",
- tag: "div"
- };
- },
- getInitialState: function () {
- return {
- editable: false
- };
- },
- render: function () {
- var className = "inline-input " + this.props.className;
- var html = {__html: this.props.contentToHtml(this.props.content)};
- var Tag = this.props.tag;
- return <Tag
- {...this.props}
- tabIndex="0"
- className={className}
- contentEditable={this.state.editable || undefined } // workaround: use undef instead of false to remove attr
- onFocus={this.onFocus}
- onMouseDown={this.onMouseDown}
- onClick={this.onClick}
- onBlur={this._stop}
- onKeyDown={this.onKeyDown}
- onInput={this.onInput}
- onPaste={this.onPaste}
- dangerouslySetInnerHTML={html}
- />;
- },
- onPaste: function (e) {
- e.preventDefault();
- var content = e.clipboardData.getData("text/plain");
- document.execCommand("insertHTML", false, content);
- },
- onMouseDown: function (e) {
- this._mouseDown = true;
- window.addEventListener("mouseup", this.onMouseUp);
- this.props.onMouseDown && this.props.onMouseDown(e);
- },
- onMouseUp: function () {
- if (this._mouseDown) {
- this._mouseDown = false;
- window.removeEventListener("mouseup", this.onMouseUp)
- }
- },
- onClick: function (e) {
- this.onMouseUp();
- this.onFocus(e);
- },
- onFocus: function (e) {
- console.log("onFocus", this._mouseDown, this._ignore_events, this.state.editable);
- if (this._mouseDown || this._ignore_events || this.state.editable) {
- return;
- }
-
- //contenteditable in FireFox is more or less broken.
- // - we need to blur() and then focus(), otherwise the caret is not shown.
- // - blur() + focus() == we need to save the caret position before
- // Firefox sometimes just doesn't set a caret position => use caretPositionFromPoint
- var sel = window.getSelection();
- var range;
- if (sel.rangeCount > 0) {
- range = sel.getRangeAt(0);
- } else if (document.caretPositionFromPoint && e.clientX && e.clientY) {
- var pos = document.caretPositionFromPoint(e.clientX, e.clientY);
- range = document.createRange();
- range.setStart(pos.offsetNode, pos.offset);
- } else if (document.caretRangeFromPoint && e.clientX && e.clientY) {
- range = document.caretRangeFromPoint(e.clientX, e.clientY);
- } else {
- range = document.createRange();
- range.selectNodeContents(ReactDOM.findDOMNode(this));
- }
-
- this._ignore_events = true;
- this.setState({editable: true}, function () {
- var node = ReactDOM.findDOMNode(this);
- node.blur();
- node.focus();
- this._ignore_events = false;
- //sel.removeAllRanges();
- //sel.addRange(range);
-
-
- });
- },
- stop: function () {
- // a stop would cause a blur as a side-effect.
- // but a blur event must trigger a stop as well.
- // to fix this, make stop = blur and do the actual stop in the onBlur handler.
- ReactDOM.findDOMNode(this).blur();
- this.props.onStop && this.props.onStop();
- },
- _stop: function (e) {
- if (this._ignore_events) {
- return;
- }
- console.log("_stop", _.extend({}, e));
- window.getSelection().removeAllRanges(); //make sure that selection is cleared on blur
- var node = ReactDOM.findDOMNode(this);
- var content = this.props.nodeToContent(node);
- this.setState({editable: false});
- this.props.onDone(content);
- this.props.onBlur && this.props.onBlur(e);
- },
- reset: function () {
- ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content);
- },
- onKeyDown: function (e) {
- e.stopPropagation();
- switch (e.keyCode) {
- case Key.ESC:
- e.preventDefault();
- this.reset();
- this.stop();
- break;
- case Key.ENTER:
- if (this.props.submitOnEnter && !e.shiftKey) {
- e.preventDefault();
- this.stop();
- }
- break;
- default:
- break;
- }
- },
- onInput: function () {
- var node = ReactDOM.findDOMNode(this);
- var content = this.props.nodeToContent(node);
- this.props.onInput && this.props.onInput(content);
- }
-});
-
-/*
- Add Validation to EditorBase
- */
-var ValidateEditor = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- onDone: React.PropTypes.func.isRequired,
- onInput: React.PropTypes.func,
- isValid: React.PropTypes.func,
- className: React.PropTypes.string,
- },
- getInitialState: function () {
- return {
- currentContent: this.props.content
- };
- },
- componentWillReceiveProps: function () {
- this.setState({currentContent: this.props.content});
- },
- onInput: function (content) {
- this.setState({currentContent: content});
- this.props.onInput && this.props.onInput(content);
- },
- render: function () {
- var className = this.props.className || "";
- if (this.props.isValid) {
- if (this.props.isValid(this.state.currentContent)) {
- className += " has-success";
- } else {
- className += " has-warning"
- }
- }
- return <EditorBase
- {...this.props}
- ref="editor"
- className={className}
- onDone={this.onDone}
- onInput={this.onInput}
- />;
- },
- onDone: function (content) {
- if (this.props.isValid && !this.props.isValid(content)) {
- this.refs.editor.reset();
- content = this.props.content;
- }
- this.props.onDone(content);
- }
-});
-
-/*
- Text Editor with mitmweb-specific convenience features
- */
-export var ValueEditor = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- propTypes: {
- content: React.PropTypes.string.isRequired,
- onDone: React.PropTypes.func.isRequired,
- inline: React.PropTypes.bool,
- },
- render: function () {
- var tag = this.props.inline ? "span" : "div";
- return <ValidateEditor
- {...this.props}
- onStop={this.onStop}
- tag={tag}
- />;
- },
- focus: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onStop: function () {
- this.context.returnFocus();
- }
-}); \ No newline at end of file
diff --git a/web/src/js/components/flowview/contentview.js b/web/src/js/components/flowview/contentview.js
deleted file mode 100644
index cbac9a75..00000000
--- a/web/src/js/components/flowview/contentview.js
+++ /dev/null
@@ -1,267 +0,0 @@
-import React from "react";
-import _ from "lodash";
-
-import {MessageUtils} from "../../flow/utils.js";
-import {formatSize} from "../../utils.js";
-
-var ViewImage = React.createClass({
- propTypes: {
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
- },
- statics: {
- regex: /^image\/(png|jpe?g|gif|vnc.microsoft.icon|x-icon)$/i,
- matches: function (message) {
- return ViewImage.regex.test(MessageUtils.getContentType(message));
- }
- },
- render: function () {
- var url = MessageUtils.getContentURL(this.props.flow, this.props.message);
- return <div className="flowview-image">
- <img src={url} alt="preview" className="img-thumbnail"/>
- </div>;
- }
-});
-
-var ContentLoader = React.createClass({
- propTypes: {
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
- },
- getInitialState: function () {
- return {
- content: undefined,
- request: undefined
- }
- },
- requestContent: function (nextProps) {
- if (this.state.request) {
- this.state.request.abort();
- }
- var request = MessageUtils.getContent(nextProps.flow, nextProps.message);
- this.setState({
- content: undefined,
- request: request
- });
- request.done(function (data) {
- this.setState({content: data});
- }.bind(this)).fail(function (jqXHR, textStatus, errorThrown) {
- if (textStatus === "abort") {
- return;
- }
- this.setState({content: "AJAX Error: " + textStatus + "\r\n" + errorThrown});
- }.bind(this)).always(function () {
- this.setState({request: undefined});
- }.bind(this));
-
- },
- componentWillMount: function () {
- this.requestContent(this.props);
- },
- componentWillReceiveProps: function (nextProps) {
- if (nextProps.message !== this.props.message) {
- this.requestContent(nextProps);
- }
- },
- componentWillUnmount: function () {
- if (this.state.request) {
- this.state.request.abort();
- }
- },
- render: function () {
- if (!this.state.content) {
- return <div className="text-center">
- <i className="fa fa-spinner fa-spin"></i>
- </div>;
- }
- return React.cloneElement(this.props.children, {
- content: this.state.content
- })
- }
-});
-
-var ViewRaw = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- },
- statics: {
- textView: true,
- matches: function (message) {
- return true;
- }
- },
- render: function () {
- return <pre>{this.props.content}</pre>;
- }
-});
-
-var ViewJSON = React.createClass({
- propTypes: {
- content: React.PropTypes.string.isRequired,
- },
- statics: {
- textView: true,
- regex: /^application\/json$/i,
- matches: function (message) {
- return ViewJSON.regex.test(MessageUtils.getContentType(message));
- }
- },
- render: function () {
- var json = this.props.content;
- try {
- json = JSON.stringify(JSON.parse(json), null, 2);
- } catch (e) {
- // @noop
- }
- return <pre>{json}</pre>;
- }
-});
-
-var ViewAuto = React.createClass({
- propTypes: {
- message: React.PropTypes.object.isRequired,
- flow: React.PropTypes.object.isRequired,
- },
- statics: {
- matches: function () {
- return false; // don't match itself
- },
- findView: function (message) {
- for (var i = 0; i < all.length; i++) {
- if (all[i].matches(message)) {
- return all[i];
- }
- }
- return all[all.length - 1];
- }
- },
- render: function () {
- var { message, flow } = this.props
- var View = ViewAuto.findView(this.props.message);
- if (View.textView) {
- return <ContentLoader message={message} flow={flow}><View content="" /></ContentLoader>
- } else {
- return <View message={message} flow={flow} />
- }
- }
-});
-
-var all = [ViewAuto, ViewImage, ViewJSON, ViewRaw];
-
-var ContentEmpty = React.createClass({
- render: function () {
- var message_name = this.props.flow.request === this.props.message ? "request" : "response";
- return <div className="alert alert-info">No {message_name} content.</div>;
- }
-});
-
-var ContentMissing = React.createClass({
- render: function () {
- var message_name = this.props.flow.request === this.props.message ? "Request" : "Response";
- return <div className="alert alert-info">{message_name} content missing.</div>;
- }
-});
-
-var TooLarge = React.createClass({
- statics: {
- isTooLarge: function (message) {
- var max_mb = ViewImage.matches(message) ? 10 : 0.2;
- return message.contentLength > 1024 * 1024 * max_mb;
- }
- },
- render: function () {
- var size = formatSize(this.props.message.contentLength);
- return <div className="alert alert-warning">
- <button onClick={this.props.onClick} className="btn btn-xs btn-warning pull-right">Display anyway</button>
- {size} content size.
- </div>;
- }
-});
-
-var ViewSelector = React.createClass({
- render: function () {
- var views = [];
- for (var i = 0; i < all.length; i++) {
- var view = all[i];
- var className = "btn btn-default";
- if (view === this.props.active) {
- className += " active";
- }
- var text;
- if (view === ViewAuto) {
- text = "auto: " + ViewAuto.findView(this.props.message).displayName.toLowerCase().replace("view", "");
- } else {
- text = view.displayName.toLowerCase().replace("view", "");
- }
- views.push(
- <button
- key={view.displayName}
- onClick={this.props.selectView.bind(null, view)}
- className={className}>
- {text}
- </button>
- );
- }
-
- return <div className="view-selector btn-group btn-group-xs">{views}</div>;
- }
-});
-
-var ContentView = React.createClass({
- getInitialState: function () {
- return {
- displayLarge: false,
- View: ViewAuto
- };
- },
- propTypes: {
- // It may seem a bit weird at the first glance:
- // Every view takes the flow and the message as props, e.g.
- // <Auto flow={flow} message={flow.request}/>
- flow: React.PropTypes.object.isRequired,
- message: React.PropTypes.object.isRequired,
- },
- selectView: function (view) {
- this.setState({
- View: view
- });
- },
- displayLarge: function () {
- this.setState({displayLarge: true});
- },
- componentWillReceiveProps: function (nextProps) {
- if (nextProps.message !== this.props.message) {
- this.setState(this.getInitialState());
- }
- },
- render: function () {
- var { flow, message } = this.props
- var message = this.props.message;
- if (message.contentLength === 0) {
- return <ContentEmpty {...this.props}/>;
- } else if (message.contentLength === null) {
- return <ContentMissing {...this.props}/>;
- } else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) {
- return <TooLarge {...this.props} onClick={this.displayLarge}/>;
- }
-
- var downloadUrl = MessageUtils.getContentURL(this.props.flow, message);
-
- return <div>
- {this.state.View.textView ? (
- <ContentLoader flow={flow} message={message}><this.state.View content="" /></ContentLoader>
- ) : (
- <this.state.View flow={flow} message={message} />
- )}
- <div className="view-options text-center">
- <ViewSelector selectView={this.selectView} active={this.state.View} message={message}/>
- &nbsp;
- <a className="btn btn-default btn-xs" href={downloadUrl}>
- <i className="fa fa-download"/>
- </a>
- </div>
- </div>;
- }
-});
-
-export default ContentView;
diff --git a/web/src/js/components/flowview/details.js b/web/src/js/components/flowview/details.js
deleted file mode 100644
index 45fe1292..00000000
--- a/web/src/js/components/flowview/details.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import React from "react";
-import _ from "lodash";
-
-import {formatTimeStamp, formatTimeDelta} from "../../utils.js";
-
-var TimeStamp = React.createClass({
- render: function () {
-
- if (!this.props.t) {
- //should be return null, but that triggers a React bug.
- return <tr></tr>;
- }
-
- var ts = formatTimeStamp(this.props.t);
-
- var delta;
- if (this.props.deltaTo) {
- delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo));
- delta = <span className="text-muted">{"(" + delta + ")"}</span>;
- } else {
- delta = null;
- }
-
- return <tr>
- <td>{this.props.title + ":"}</td>
- <td>{ts} {delta}</td>
- </tr>;
- }
-});
-
-var ConnectionInfo = React.createClass({
-
- render: function () {
- var conn = this.props.conn;
- var address = conn.address.address.join(":");
-
- var sni = <tr key="sni"></tr>; //should be null, but that triggers a React bug.
- if (conn.sni) {
- sni = <tr key="sni">
- <td>
- <abbr title="TLS Server Name Indication">TLS SNI:</abbr>
- </td>
- <td>{conn.sni}</td>
- </tr>;
- }
- return (
- <table className="connection-table">
- <tbody>
- <tr key="address">
- <td>Address:</td>
- <td>{address}</td>
- </tr>
- {sni}
- </tbody>
- </table>
- );
- }
-});
-
-var CertificateInfo = React.createClass({
- render: function () {
- //TODO: We should fetch human-readable certificate representation
- // from the server
- var flow = this.props.flow;
- var client_conn = flow.client_conn;
- var server_conn = flow.server_conn;
-
- var preStyle = {maxHeight: 100};
- return (
- <div>
- {client_conn.cert ? <h4>Client Certificate</h4> : null}
- {client_conn.cert ? <pre style={preStyle}>{client_conn.cert}</pre> : null}
-
- {server_conn.cert ? <h4>Server Certificate</h4> : null}
- {server_conn.cert ? <pre style={preStyle}>{server_conn.cert}</pre> : null}
- </div>
- );
- }
-});
-
-var Timing = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var sc = flow.server_conn;
- var cc = flow.client_conn;
- var req = flow.request;
- var resp = flow.response;
-
- var timestamps = [
- {
- title: "Server conn. initiated",
- t: sc.timestamp_start,
- deltaTo: req.timestamp_start
- }, {
- title: "Server conn. TCP handshake",
- t: sc.timestamp_tcp_setup,
- deltaTo: req.timestamp_start
- }, {
- title: "Server conn. SSL handshake",
- t: sc.timestamp_ssl_setup,
- deltaTo: req.timestamp_start
- }, {
- title: "Client conn. established",
- t: cc.timestamp_start,
- deltaTo: req.timestamp_start
- }, {
- title: "Client conn. SSL handshake",
- t: cc.timestamp_ssl_setup,
- deltaTo: req.timestamp_start
- }, {
- title: "First request byte",
- t: req.timestamp_start,
- }, {
- title: "Request complete",
- t: req.timestamp_end,
- deltaTo: req.timestamp_start
- }
- ];
-
- if (flow.response) {
- timestamps.push(
- {
- title: "First response byte",
- t: resp.timestamp_start,
- deltaTo: req.timestamp_start
- }, {
- title: "Response complete",
- t: resp.timestamp_end,
- deltaTo: req.timestamp_start
- }
- );
- }
-
- //Add unique key for each row.
- timestamps.forEach(function (e) {
- e.key = e.title;
- });
-
- timestamps = _.sortBy(timestamps, 't');
-
- var rows = timestamps.map(function (e) {
- return <TimeStamp {...e}/>;
- });
-
- return (
- <div>
- <h4>Timing</h4>
- <table className="timing-table">
- <tbody>
- {rows}
- </tbody>
- </table>
- </div>
- );
- }
-});
-
-var Details = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var client_conn = flow.client_conn;
- var server_conn = flow.server_conn;
- return (
- <section>
-
- <h4>Client Connection</h4>
- <ConnectionInfo conn={client_conn}/>
-
- <h4>Server Connection</h4>
- <ConnectionInfo conn={server_conn}/>
-
- <CertificateInfo flow={flow}/>
-
- <Timing flow={flow}/>
-
- </section>
- );
- }
-});
-
-export default Details; \ No newline at end of file
diff --git a/web/src/js/components/flowview/index.js b/web/src/js/components/flowview/index.js
deleted file mode 100644
index 7f5e9768..00000000
--- a/web/src/js/components/flowview/index.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from "react";
-
-import Nav from "./nav.js";
-import {Request, Response, Error} from "./messages.js";
-import Details from "./details.js";
-import Prompt from "../prompt.js";
-
-
-var allTabs = {
- request: Request,
- response: Response,
- error: Error,
- details: Details
-};
-
-var FlowView = React.createClass({
- getInitialState: function () {
- return {
- prompt: false
- };
- },
- getTabs: function (flow) {
- var tabs = [];
- ["request", "response", "error"].forEach(function (e) {
- if (flow[e]) {
- tabs.push(e);
- }
- });
- tabs.push("details");
- return tabs;
- },
- nextTab: function (i) {
- var tabs = this.getTabs(this.props.flow);
- var currentIndex = tabs.indexOf(this.props.tab);
- // JS modulo operator doesn't correct negative numbers, make sure that we are positive.
- var nextIndex = (currentIndex + i + tabs.length) % tabs.length;
- this.selectTab(tabs[nextIndex]);
- },
- selectTab: function (panel) {
- this.props.updateLocation(`/flows/${this.props.flow.id}/${panel}`);
- },
- promptEdit: function () {
- var options;
- switch (this.props.tab) {
- case "request":
- options = [
- "method",
- "url",
- {text: "http version", key: "v"},
- "header"
- /*, "content"*/];
- break;
- case "response":
- options = [
- {text: "http version", key: "v"},
- "code",
- "message",
- "header"
- /*, "content"*/];
- break;
- case "details":
- return;
- default:
- throw "Unknown tab for edit: " + this.props.tab;
- }
-
- this.setState({
- prompt: {
- done: function (k) {
- this.setState({prompt: false});
- if (k) {
- this.refs.tab.edit(k);
- }
- }.bind(this),
- options: options
- }
- });
- },
- render: function () {
- var flow = this.props.flow;
- var tabs = this.getTabs(flow);
- var active = this.props.tab;
-
- if (tabs.indexOf(active) < 0) {
- if (active === "response" && flow.error) {
- active = "error";
- } else if (active === "error" && flow.response) {
- active = "response";
- } else {
- active = tabs[0];
- }
- }
-
- var prompt = null;
- if (this.state.prompt) {
- prompt = <Prompt {...this.state.prompt}/>;
- }
-
- var Tab = allTabs[active];
- return (
- <div className="flow-detail" onScroll={this.adjustHead}>
- <Nav ref="head"
- flow={flow}
- tabs={tabs}
- active={active}
- selectTab={this.selectTab}/>
- <Tab ref="tab" flow={flow}/>
- {prompt}
- </div>
- );
- }
-});
-
-export default FlowView;
diff --git a/web/src/js/components/flowview/messages.js b/web/src/js/components/flowview/messages.js
deleted file mode 100644
index 2885b3b1..00000000
--- a/web/src/js/components/flowview/messages.js
+++ /dev/null
@@ -1,320 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import _ from "lodash";
-
-import {FlowActions} from "../../actions.js";
-import {RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion} from "../../flow/utils.js";
-import {Key, formatTimeStamp} from "../../utils.js";
-import ContentView from "./contentview.js";
-import {ValueEditor} from "../editor.js";
-
-var Headers = React.createClass({
- propTypes: {
- onChange: React.PropTypes.func.isRequired,
- message: React.PropTypes.object.isRequired
- },
- onChange: function (row, col, val) {
- var nextHeaders = _.cloneDeep(this.props.message.headers);
- nextHeaders[row][col] = val;
- if (!nextHeaders[row][0] && !nextHeaders[row][1]) {
- // do not delete last row
- if (nextHeaders.length === 1) {
- nextHeaders[0][0] = "Name";
- nextHeaders[0][1] = "Value";
- } else {
- nextHeaders.splice(row, 1);
- // manually move selection target if this has been the last row.
- if (row === nextHeaders.length) {
- this._nextSel = (row - 1) + "-value";
- }
- }
- }
- this.props.onChange(nextHeaders);
- },
- edit: function () {
- this.refs["0-key"].focus();
- },
- onTab: function (row, col, e) {
- var headers = this.props.message.headers;
- if (row === headers.length - 1 && col === 1) {
- e.preventDefault();
-
- var nextHeaders = _.cloneDeep(this.props.message.headers);
- nextHeaders.push(["Name", "Value"]);
- this.props.onChange(nextHeaders);
- this._nextSel = (row + 1) + "-key";
- }
- },
- componentDidUpdate: function () {
- if (this._nextSel && this.refs[this._nextSel]) {
- this.refs[this._nextSel].focus();
- this._nextSel = undefined;
- }
- },
- onRemove: function (row, col, e) {
- if (col === 1) {
- e.preventDefault();
- this.refs[row + "-key"].focus();
- } else if (row > 0) {
- e.preventDefault();
- this.refs[(row - 1) + "-value"].focus();
- }
- },
- render: function () {
-
- var rows = this.props.message.headers.map(function (header, i) {
-
- var kEdit = <HeaderEditor
- ref={i + "-key"}
- content={header[0]}
- onDone={this.onChange.bind(null, i, 0)}
- onRemove={this.onRemove.bind(null, i, 0)}
- onTab={this.onTab.bind(null, i, 0)}/>;
- var vEdit = <HeaderEditor
- ref={i + "-value"}
- content={header[1]}
- onDone={this.onChange.bind(null, i, 1)}
- onRemove={this.onRemove.bind(null, i, 1)}
- onTab={this.onTab.bind(null, i, 1)}/>;
- return (
- <tr key={i}>
- <td className="header-name">{kEdit}:</td>
- <td className="header-value">{vEdit}</td>
- </tr>
- );
- }.bind(this));
- return (
- <table className="header-table">
- <tbody>
- {rows}
- </tbody>
- </table>
- );
- }
-});
-
-var HeaderEditor = React.createClass({
- render: function () {
- return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/>;
- },
- focus: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onKeyDown: function (e) {
- switch (e.keyCode) {
- case Key.BACKSPACE:
- var s = window.getSelection().getRangeAt(0);
- if (s.startOffset === 0 && s.endOffset === 0) {
- this.props.onRemove(e);
- }
- break;
- case Key.TAB:
- if (!e.shiftKey) {
- this.props.onTab(e);
- }
- break;
- }
- }
-});
-
-var RequestLine = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var url = RequestUtils.pretty_url(flow.request);
- var httpver = flow.request.http_version;
-
- return <div className="first-line request-line">
- <ValueEditor
- ref="method"
- content={flow.request.method}
- onDone={this.onMethodChange}
- inline/>
- &nbsp;
- <ValueEditor
- ref="url"
- content={url}
- onDone={this.onUrlChange}
- isValid={this.isValidUrl}
- inline/>
- &nbsp;
- <ValueEditor
- ref="httpVersion"
- content={httpver}
- onDone={this.onHttpVersionChange}
- isValid={isValidHttpVersion}
- inline/>
- </div>
- },
- isValidUrl: function (url) {
- var u = parseUrl(url);
- return !!u.host;
- },
- onMethodChange: function (nextMethod) {
- FlowActions.update(
- this.props.flow,
- {request: {method: nextMethod}}
- );
- },
- onUrlChange: function (nextUrl) {
- var props = parseUrl(nextUrl);
- props.path = props.path || "";
- FlowActions.update(
- this.props.flow,
- {request: props}
- );
- },
- onHttpVersionChange: function (nextVer) {
- var ver = parseHttpVersion(nextVer);
- FlowActions.update(
- this.props.flow,
- {request: {http_version: ver}}
- );
- }
-});
-
-var ResponseLine = React.createClass({
- render: function () {
- var flow = this.props.flow;
- var httpver = flow.response.http_version;
- return <div className="first-line response-line">
- <ValueEditor
- ref="httpVersion"
- content={httpver}
- onDone={this.onHttpVersionChange}
- isValid={isValidHttpVersion}
- inline/>
- &nbsp;
- <ValueEditor
- ref="code"
- content={flow.response.status_code + ""}
- onDone={this.onCodeChange}
- isValid={this.isValidCode}
- inline/>
- &nbsp;
- <ValueEditor
- ref="msg"
- content={flow.response.reason}
- onDone={this.onMsgChange}
- inline/>
- </div>;
- },
- isValidCode: function (code) {
- return /^\d+$/.test(code);
- },
- onHttpVersionChange: function (nextVer) {
- var ver = parseHttpVersion(nextVer);
- FlowActions.update(
- this.props.flow,
- {response: {http_version: ver}}
- );
- },
- onMsgChange: function (nextMsg) {
- FlowActions.update(
- this.props.flow,
- {response: {msg: nextMsg}}
- );
- },
- onCodeChange: function (nextCode) {
- nextCode = parseInt(nextCode);
- FlowActions.update(
- this.props.flow,
- {response: {code: nextCode}}
- );
- }
-});
-
-export var Request = React.createClass({
- render: function () {
- var flow = this.props.flow;
- return (
- <section className="request">
- <RequestLine ref="requestLine" flow={flow}/>
- {/*<ResponseLine flow={flow}/>*/}
- <Headers ref="headers" message={flow.request} onChange={this.onHeaderChange}/>
- <hr/>
- <ContentView flow={flow} message={flow.request}/>
- </section>
- );
- },
- edit: function (k) {
- switch (k) {
- case "m":
- this.refs.requestLine.refs.method.focus();
- break;
- case "u":
- this.refs.requestLine.refs.url.focus();
- break;
- case "v":
- this.refs.requestLine.refs.httpVersion.focus();
- break;
- case "h":
- this.refs.headers.edit();
- break;
- default:
- throw "Unimplemented: " + k;
- }
- },
- onHeaderChange: function (nextHeaders) {
- FlowActions.update(this.props.flow, {
- request: {
- headers: nextHeaders
- }
- });
- }
-});
-
-export var Response = React.createClass({
- render: function () {
- var flow = this.props.flow;
- return (
- <section className="response">
- {/*<RequestLine flow={flow}/>*/}
- <ResponseLine ref="responseLine" flow={flow}/>
- <Headers ref="headers" message={flow.response} onChange={this.onHeaderChange}/>
- <hr/>
- <ContentView flow={flow} message={flow.response}/>
- </section>
- );
- },
- edit: function (k) {
- switch (k) {
- case "c":
- this.refs.responseLine.refs.status_code.focus();
- break;
- case "m":
- this.refs.responseLine.refs.msg.focus();
- break;
- case "v":
- this.refs.responseLine.refs.httpVersion.focus();
- break;
- case "h":
- this.refs.headers.edit();
- break;
- default:
- throw "Unimplemented: " + k;
- }
- },
- onHeaderChange: function (nextHeaders) {
- FlowActions.update(this.props.flow, {
- response: {
- headers: nextHeaders
- }
- });
- }
-});
-
-export var Error = React.createClass({
- render: function () {
- var flow = this.props.flow;
- return (
- <section>
- <div className="alert alert-warning">
- {flow.error.msg}
- <div>
- <small>{ formatTimeStamp(flow.error.timestamp) }</small>
- </div>
- </div>
- </section>
- );
- }
-});
diff --git a/web/src/js/components/flowview/nav.js b/web/src/js/components/flowview/nav.js
deleted file mode 100644
index a12fd1fd..00000000
--- a/web/src/js/components/flowview/nav.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from "react";
-
-import {FlowActions} from "../../actions.js";
-
-var NavAction = React.createClass({
- onClick: function (e) {
- e.preventDefault();
- this.props.onClick();
- },
- render: function () {
- return (
- <a title={this.props.title}
- href="#"
- className="nav-action"
- onClick={this.onClick}>
- <i className={"fa fa-fw " + this.props.icon}></i>
- </a>
- );
- }
-});
-
-var Nav = React.createClass({
- render: function () {
- var flow = this.props.flow;
-
- var tabs = this.props.tabs.map(function (e) {
- var str = e.charAt(0).toUpperCase() + e.slice(1);
- var className = this.props.active === e ? "active" : "";
- var onClick = function (event) {
- this.props.selectTab(e);
- event.preventDefault();
- }.bind(this);
- return <a key={e}
- href="#"
- className={className}
- onClick={onClick}>{str}</a>;
- }.bind(this));
-
- var acceptButton = null;
- if(flow.intercepted){
- acceptButton = <NavAction title="[a]ccept intercepted flow" icon="fa-play" onClick={FlowActions.accept.bind(null, flow)} />;
- }
- var revertButton = null;
- if(flow.modified){
- revertButton = <NavAction title="revert changes to flow [V]" icon="fa-history" onClick={FlowActions.revert.bind(null, flow)} />;
- }
-
- return (
- <nav ref="head" className="nav-tabs nav-tabs-sm">
- {tabs}
- <NavAction title="[d]elete flow" icon="fa-trash" onClick={FlowActions.delete.bind(null, flow)} />
- <NavAction title="[D]uplicate flow" icon="fa-copy" onClick={FlowActions.duplicate.bind(null, flow)} />
- <NavAction disabled title="[r]eplay flow" icon="fa-repeat" onClick={FlowActions.replay.bind(null, flow)} />
- {acceptButton}
- {revertButton}
- </nav>
- );
- }
-});
-
-export default Nav; \ No newline at end of file
diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js
deleted file mode 100644
index 5ab26b82..00000000
--- a/web/src/js/components/prompt.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import React from "react";
-import ReactDOM from 'react-dom';
-import _ from "lodash";
-
-import {Key} from "../utils.js";
-
-var Prompt = React.createClass({
- contextTypes: {
- returnFocus: React.PropTypes.func
- },
- propTypes: {
- options: React.PropTypes.array.isRequired,
- done: React.PropTypes.func.isRequired,
- prompt: React.PropTypes.string
- },
- componentDidMount: function () {
- ReactDOM.findDOMNode(this).focus();
- },
- onKeyDown: function (e) {
- e.stopPropagation();
- e.preventDefault();
- var opts = this.getOptions();
- for (var i = 0; i < opts.length; i++) {
- var k = opts[i].key;
- if (Key[k.toUpperCase()] === e.keyCode) {
- this.done(k);
- return;
- }
- }
- if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
- this.done(false);
- }
- },
- onClick: function (e) {
- this.done(false);
- },
- done: function (ret) {
- this.props.done(ret);
- this.context.returnFocus();
- },
- getOptions: function () {
- var opts = [];
-
- var keyTaken = function (k) {
- return _.includes(_.map(opts, "key"), k);
- };
-
- for (var i = 0; i < this.props.options.length; i++) {
- var opt = this.props.options[i];
- if (_.isString(opt)) {
- var str = opt;
- while (str.length > 0 && keyTaken(str[0])) {
- str = str.substr(1);
- }
- opt = {
- text: opt,
- key: str[0]
- };
- }
- if (!opt.text || !opt.key || keyTaken(opt.key)) {
- throw "invalid options";
- } else {
- opts.push(opt);
- }
- }
- return opts;
- },
- render: function () {
- var opts = this.getOptions();
- opts = _.map(opts, function (o) {
- var prefix, suffix;
- var idx = o.text.indexOf(o.key);
- if (idx !== -1) {
- prefix = o.text.substring(0, idx);
- suffix = o.text.substring(idx + 1);
-
- } else {
- prefix = o.text + " (";
- suffix = ")";
- }
- var onClick = function (e) {
- this.done(o.key);
- e.stopPropagation();
- }.bind(this);
- return <span
- key={o.key}
- className="option"
- onClick={onClick}>
- {prefix}
- <strong className="text-primary">{o.key}</strong>{suffix}
- </span>;
- }.bind(this));
- return <div tabIndex="0" onKeyDown={this.onKeyDown} onClick={this.onClick} className="prompt-dialog">
- <div className="prompt-content">
- {this.props.prompt || <strong>Select: </strong> }
- {opts}
- </div>
- </div>;
- }
-});
-
-export default Prompt;