aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
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.jsx79
-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/Header.jsx (renamed from web/src/js/components/Header.js)0
-rw-r--r--web/src/js/components/MainView.jsx2
-rw-r--r--web/src/js/components/ProxyApp.jsx36
-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
20 files changed, 944 insertions, 988 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..d9211e11 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 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..be2cb460
--- /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 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..b8f9b50f
--- /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 '../editor'
+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..ce17c294
--- /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 '../editor'
+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/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/MainView.jsx b/web/src/js/components/MainView.jsx
index 8c6ed6d0..78a7f9bf 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -5,7 +5,7 @@ import { Query } from '../actions.js'
import { Key } from '../utils.js'
import { Splitter } from './common.js'
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/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index 81272268..967cc921 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -1,14 +1,13 @@
-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 { Splitter } from "./common.js"
-import Header from "./Header"
-import EventLog from "./EventLog"
-import Footer from "./Footer"
-import { SettingsStore } from "../store/store.js"
-import { Key } from "../utils.js"
+import Header from './Header'
+import EventLog from './EventLog'
+import Footer from './Footer'
+import { SettingsStore } from '../store/store.js'
+import { Key } from '../utils.js'
class ProxyAppMain extends Component {
@@ -67,7 +66,7 @@ class ProxyAppMain extends Component {
*/
componentDidMount() {
this.focus()
- this.settingsStore.addListener("recalculate", this.onSettingsChange)
+ this.settingsStore.addListener('recalculate', this.onSettingsChange)
}
/**
@@ -76,7 +75,7 @@ class ProxyAppMain extends Component {
* @todo stop listening to window's key events
*/
componentWillUnmount() {
- this.settingsStore.removeListener("recalculate", this.onSettingsChange)
+ this.settingsStore.removeListener('recalculate', this.onSettingsChange)
}
/**
@@ -113,13 +112,13 @@ class ProxyAppMain extends Component {
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
@@ -134,7 +133,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()
})
}
@@ -151,12 +150,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/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