From e5bf1e930a5b6ba0b3300b02daf792d65d795202 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 14 Jun 2016 23:52:00 +0800 Subject: [web] FlowView and ContentView --- web/src/js/components/ContentView.jsx | 78 +++++ .../js/components/ContentView/ContentErrors.jsx | 28 ++ .../js/components/ContentView/ContentLoader.jsx | 67 +++++ web/src/js/components/ContentView/ContentViews.jsx | 70 +++++ web/src/js/components/ContentView/ViewSelector.jsx | 28 ++ web/src/js/components/EventLog.jsx | 79 +++-- web/src/js/components/FlowTable/FlowTableHead.jsx | 6 +- web/src/js/components/FlowView.jsx | 107 +++++++ web/src/js/components/FlowView/Details.jsx | 133 +++++++++ web/src/js/components/FlowView/Headers.jsx | 130 +++++++++ web/src/js/components/FlowView/Messages.jsx | 168 +++++++++++ web/src/js/components/FlowView/Nav.jsx | 57 ++++ web/src/js/components/Header.js | 67 ----- web/src/js/components/Header.jsx | 67 +++++ web/src/js/components/MainView.jsx | 2 +- web/src/js/components/ProxyApp.jsx | 36 ++- web/src/js/components/flowview/contentview.js | 267 ----------------- web/src/js/components/flowview/details.js | 181 ------------ web/src/js/components/flowview/index.js | 114 -------- web/src/js/components/flowview/messages.js | 320 --------------------- web/src/js/components/flowview/nav.js | 61 ---- 21 files changed, 1011 insertions(+), 1055 deletions(-) create mode 100644 web/src/js/components/ContentView.jsx create mode 100644 web/src/js/components/ContentView/ContentErrors.jsx create mode 100644 web/src/js/components/ContentView/ContentLoader.jsx create mode 100644 web/src/js/components/ContentView/ContentViews.jsx create mode 100644 web/src/js/components/ContentView/ViewSelector.jsx create mode 100644 web/src/js/components/FlowView.jsx create mode 100644 web/src/js/components/FlowView/Details.jsx create mode 100644 web/src/js/components/FlowView/Headers.jsx create mode 100644 web/src/js/components/FlowView/Messages.jsx create mode 100644 web/src/js/components/FlowView/Nav.jsx delete mode 100644 web/src/js/components/Header.js create mode 100644 web/src/js/components/Header.jsx delete mode 100644 web/src/js/components/flowview/contentview.js delete mode 100644 web/src/js/components/flowview/details.js delete mode 100644 web/src/js/components/flowview/index.js delete mode 100644 web/src/js/components/flowview/messages.js delete mode 100644 web/src/js/components/flowview/nav.js (limited to 'web/src/js') 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. + // + 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 + } + + if (message.contentLength === null) { + return + } + + if (!displayLarge && this.isContentTooLarge(message)) { + return + } + + return ( +
+ {View.textView ? ( + + + + ) : ( + + )} +
+ +   + + + +
+
+ ) + } +} 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 ( +
+ No {flow.request === message ? 'request' : 'response'} content. +
+ ) +} + +export function ContentMissing({ flow, message }) { + return ( +
+ {flow.request === message ? 'Request' : 'Response'} content missing. +
+ ) +} + +export function ContentTooLarge({ message, onClick }) { + return ( +
+ + {formatSize(message.contentLength)} content size. +
+ ) +} 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 + }) + ) : ( +
+ +
+ ) + } +} 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 ( +
+ preview +
+ ) +} + +ViewRaw.textView = true +ViewRaw.matches = () => true + +ViewRaw.propTypes = { + content: React.PropTypes.string.isRequired, +} + +export function ViewRaw({ content }) { + return
{content}
+} + +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
{json}
+} + + +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 + } else { + return + } +} + +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 ( +
+ {views.map(View => ( + + ))} +
+ ) +} 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 ( -
-
- Eventlog -
- {['debug', 'info', 'web'].map(type => ( - onToggleFilter(type)}/> - ))} - + 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 ( +
+
+ Eventlog +
+ {['debug', 'info', 'web'].map(type => ( + onToggleFilter(type)}/> + ))} + +
+
- -
- ) + ) + } } 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 => ( onClick(Column)}> + onClick={() => onSort({ sortColumn: Column.name, sortDesc: Column.name !== sortColumn ? false : !sortDesc })}> {Column.headerName} ))} ) - - 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 ( +
+
+ ) + } +} 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 ? ( + + {title}: + + {formatTimeStamp(t)} + {deltaTo && ( + + ({formatTimeDelta(1000 * (t - deltaTo))}) + + )} + + + ) : ( + + ) +} + +export function ConnectionInfo({ conn }) { + return ( + + + + + + + {conn.sni ? ( + + ) : ( + + + + + )} + +
Address:{conn.address.address.join(':')}
+ TLS SNI: + {conn.sni}
+ ) +} + +export function CertificateInfo({ flow }) { + // @todo We should fetch human-readable certificate representation from the server + return ( +
+ {flow.client_conn.cert && [ +

Client Certificate

, +
{flow.client_conn.cert}
+ ]} + + {flow.server_conn.cert && [ +

Server Certificate

, +
{flow.server_conn.cert}
+ ]} +
+ ) +} + +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 ( +
+

Timing

+ + + {timestamps.filter(v => v).sort((a, b) => a.t - b.t).map(item => ( + + ))} + +
+
+ ) +} + +export default function Details({ flow }) { + return ( +
+

Client Connection

+ + +

Server Connection

+ + + + + +
+ ) +} 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 + } + + 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 ( + + + {message.headers.map((header, i) => ( + + + + + ))} + +
+ this.onChange(i, 0, val)} + onRemove={event => this.onRemove(i, 0, event)} + onTab={event => this.onTab(i, 0, event)} + />: + + this.onChange(i, 1, val)} + onRemove={event => this.onRemove(i, 1, event)} + onTab={event => this.onTab(i, 1, event)} + /> +
+ ) + } +} 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 ( +
+ FlowActions.update(flow, { request: { method } })} + inline + /> +   + FlowActions.update(flow, { request: Object.assign({ path: '' }, parseUrl(url)) })} + isValid={url => !!parseUrl(url).host} + inline + /> +   + FlowActions.update(flow, { request: { http_version: parseHttpVersion(ver) } })} + isValid={isValidHttpVersion} + inline + /> +
+ ) + } +} + +class ResponseLine extends Component { + + render() { + const { flow } = this.props + + return ( +
+ FlowActions.update(flow, { response: { http_version: parseHttpVersion(nextVer) } })} + isValid={isValidHttpVersion} + inline + /> +   + FlowActions.update(flow, { response: { code: parseInt(code) } })} + isValid={code => /^\d+$/.test(code)} + inline + /> +   + FlowActions.update(flow, { response: { msg } })} + inline + /> +
+ ) + } +} + +export class Request extends Component { + + render() { + const { flow } = this.props + + return ( +
+ + FlowActions.update(flow, { request: { headers } })} + /> +
+ +
+ ) + } + + 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 ( +
+ + FlowActions.update(flow, { response: { headers } })} + /> +
+ +
+ ) + } + + 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 ( +
+
+ {flow.error.msg} +
+ {formatTimeStamp(flow.error.timestamp)} +
+
+
+ ) +} 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 ( + { + event.preventDefault() + onClick(event) + }}> + + + ) +} + +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 ( + + ) +} diff --git a/web/src/js/components/Header.js b/web/src/js/components/Header.js deleted file mode 100644 index 93ca5154..00000000 --- a/web/src/js/components/Header.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component, PropTypes } from 'react' -import { connect } from 'react-redux' -import classnames from 'classnames' -import { toggleEventLogVisibility } from '../ducks/eventLog' -import MainMenu from './Header/MainMenu' -import ViewMenu from './Header/ViewMenu' -import OptionMenu from './Header/OptionMenu' -import FileMenu from './Header/FileMenu' -import FlowMenu from './Header/FlowMenu' -import {setActiveMenu} from '../ducks/ui.js' - -class Header extends Component { - static entries = [MainMenu, ViewMenu, OptionMenu] - - static propTypes = { - settings: PropTypes.object.isRequired, - } - - handleClick(active, e) { - e.preventDefault() - this.props.setActiveMenu(active.title) - // this.props.updateLocation(active.route) - // this.setState({ active }) - } - - render() { - const { settings, updateLocation, query, selectedFlow, activeMenu} = this.props - - let entries = [...Header.entries] - if(selectedFlow) - entries.push(FlowMenu) - - const Active = _.find(entries, (e) => e.title == activeMenu) - - return ( -
- -
- -
-
- ) - } -} -export default connect( - (state) => ({ - selectedFlow: state.flows.selected[0], - activeMenu: state.ui.activeMenu - }), - { - setActiveMenu, - } -)(Header) diff --git a/web/src/js/components/Header.jsx b/web/src/js/components/Header.jsx new file mode 100644 index 00000000..93ca5154 --- /dev/null +++ b/web/src/js/components/Header.jsx @@ -0,0 +1,67 @@ +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import classnames from 'classnames' +import { toggleEventLogVisibility } from '../ducks/eventLog' +import MainMenu from './Header/MainMenu' +import ViewMenu from './Header/ViewMenu' +import OptionMenu from './Header/OptionMenu' +import FileMenu from './Header/FileMenu' +import FlowMenu from './Header/FlowMenu' +import {setActiveMenu} from '../ducks/ui.js' + +class Header extends Component { + static entries = [MainMenu, ViewMenu, OptionMenu] + + static propTypes = { + settings: PropTypes.object.isRequired, + } + + handleClick(active, e) { + e.preventDefault() + this.props.setActiveMenu(active.title) + // this.props.updateLocation(active.route) + // this.setState({ active }) + } + + render() { + const { settings, updateLocation, query, selectedFlow, activeMenu} = this.props + + let entries = [...Header.entries] + if(selectedFlow) + entries.push(FlowMenu) + + const Active = _.find(entries, (e) => e.title == activeMenu) + + return ( +
+ +
+ +
+
+ ) + } +} +export default connect( + (state) => ({ + selectedFlow: state.flows.selected[0], + activeMenu: state.ui.activeMenu + }), + { + setActiveMenu, + } +)(Header) 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 {
{React.cloneElement( children, - { ref: "view", location, query, updateLocation: this.updateLocation } + { ref: 'view', location, query, updateLocation: this.updateLocation } )} - {showEventLog && [ - , + {showEventLog && ( - ]} + )}
) 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
- preview -
; - } -}); - -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
- -
; - } - 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
{this.props.content}
; - } -}); - -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
{json}
; - } -}); - -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 - } else { - return - } - } -}); - -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
No {message_name} content.
; - } -}); - -var ContentMissing = React.createClass({ - render: function () { - var message_name = this.props.flow.request === this.props.message ? "Request" : "Response"; - return
{message_name} content missing.
; - } -}); - -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
- - {size} content size. -
; - } -}); - -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( - - ); - } - - return
{views}
; - } -}); - -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. - // - 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 ; - } else if (message.contentLength === null) { - return ; - } else if (!this.state.displayLarge && TooLarge.isTooLarge(message)) { - return ; - } - - var downloadUrl = MessageUtils.getContentURL(this.props.flow, message); - - return
- {this.state.View.textView ? ( - - ) : ( - - )} -
- -   - - - -
-
; - } -}); - -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 ; - } - - var ts = formatTimeStamp(this.props.t); - - var delta; - if (this.props.deltaTo) { - delta = formatTimeDelta(1000 * (this.props.t - this.props.deltaTo)); - delta = {"(" + delta + ")"}; - } else { - delta = null; - } - - return - {this.props.title + ":"} - {ts} {delta} - ; - } -}); - -var ConnectionInfo = React.createClass({ - - render: function () { - var conn = this.props.conn; - var address = conn.address.address.join(":"); - - var sni = ; //should be null, but that triggers a React bug. - if (conn.sni) { - sni = - - TLS SNI: - - {conn.sni} - ; - } - return ( - - - - - - - {sni} - -
Address:{address}
- ); - } -}); - -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 ( -
- {client_conn.cert ?

Client Certificate

: null} - {client_conn.cert ?
{client_conn.cert}
: null} - - {server_conn.cert ?

Server Certificate

: null} - {server_conn.cert ?
{server_conn.cert}
: null} -
- ); - } -}); - -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 ; - }); - - return ( -
-

Timing

- - - {rows} - -
-
- ); - } -}); - -var Details = React.createClass({ - render: function () { - var flow = this.props.flow; - var client_conn = flow.client_conn; - var server_conn = flow.server_conn; - return ( -
- -

Client Connection

- - -

Server Connection

- - - - - - -
- ); - } -}); - -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 = ; - } - - var Tab = allTabs[active]; - return ( -
-
- ); - } -}); - -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 = ; - var vEdit = ; - return ( - - {kEdit}: - {vEdit} - - ); - }.bind(this)); - return ( - - - {rows} - -
- ); - } -}); - -var HeaderEditor = React.createClass({ - render: function () { - return ; - }, - 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
- -   - -   - -
- }, - 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
- -   - -   - -
; - }, - 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 ( -
- - {/**/} - -
- -
- ); - }, - 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 ( -
- {/**/} - - -
- -
- ); - }, - 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 ( -
-
- {flow.error.msg} -
- { formatTimeStamp(flow.error.timestamp) } -
-
-
- ); - } -}); 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 ( - - - - ); - } -}); - -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 {str}; - }.bind(this)); - - var acceptButton = null; - if(flow.intercepted){ - acceptButton = ; - } - var revertButton = null; - if(flow.modified){ - revertButton = ; - } - - return ( - - ); - } -}); - -export default Nav; \ No newline at end of file -- cgit v1.2.3 From f5c597a9e351b8dfb84f0fe3f09046e772482fc6 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 15 Jun 2016 00:46:59 +0800 Subject: [web] Editor & Prompt --- web/src/js/components/FlowView.jsx | 4 +- web/src/js/components/FlowView/Headers.jsx | 2 +- web/src/js/components/FlowView/Messages.jsx | 2 +- web/src/js/components/Prompt.jsx | 75 +++++++ web/src/js/components/ValueEditor.jsx | 36 ++++ web/src/js/components/ValueEditor/EditorBase.jsx | 166 ++++++++++++++ .../js/components/ValueEditor/ValidateEditor.jsx | 58 +++++ web/src/js/components/editor.js | 238 --------------------- web/src/js/components/prompt.js | 102 --------- 9 files changed, 339 insertions(+), 344 deletions(-) create mode 100755 web/src/js/components/Prompt.jsx create mode 100755 web/src/js/components/ValueEditor.jsx create mode 100755 web/src/js/components/ValueEditor/EditorBase.jsx create mode 100755 web/src/js/components/ValueEditor/ValidateEditor.jsx delete mode 100644 web/src/js/components/editor.js delete mode 100644 web/src/js/components/prompt.js (limited to 'web/src/js') diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx index be2cb460..23f8b3ea 100644 --- a/web/src/js/components/FlowView.jsx +++ b/web/src/js/components/FlowView.jsx @@ -4,7 +4,7 @@ import _ from 'lodash' import Nav from './FlowView/Nav' import { Request, Response, Error } from './FlowView/Messages' import Details from './FlowView/Details' -import Prompt from './prompt' +import Prompt from './Prompt' export default class FlowView extends Component { @@ -97,7 +97,7 @@ export default class FlowView extends Component { active={active} onSelectTab={this.selectTab} /> - + {this.state.prompt && ( )} diff --git a/web/src/js/components/FlowView/Headers.jsx b/web/src/js/components/FlowView/Headers.jsx index b8f9b50f..880eeda1 100644 --- a/web/src/js/components/FlowView/Headers.jsx +++ b/web/src/js/components/FlowView/Headers.jsx @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from 'react' import ReactDOM from 'react-dom' -import { ValueEditor } from '../editor' +import ValueEditor from '../ValueEditor' import { Key } from '../../utils.js' class HeaderEditor extends Component { diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index ce17c294..ba6a5f2b 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -5,7 +5,7 @@ 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 ValueEditor from '../ValueEditor' import Headers from './Headers' class RequestLine extends Component { diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx new file mode 100755 index 00000000..878aae29 --- /dev/null +++ b/web/src/js/components/Prompt.jsx @@ -0,0 +1,75 @@ +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, +} + +Prompt.componentDidMount = function() { + ReactDOM.findDOMNode(this).focus() +} + +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 ( +
+
+ {prompt || Select: } + {opts.map(opt => { + const idx = opt.text.indexOf(opt.key) + function onClick(event) { + done(opt.key) + event.stopPropagation() + } + return ( + + {idx !== -1 ? opt.text.substring(0, idx) : opt.text + '('} + {prefix}{opt.key} + {idx !== -1 ? opt.text.substring(idx + 1) : ')'} + + ) + })} +
+
+ ) + + 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/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 ( + 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..b659cf6d --- /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 ( + + ) + } + + 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..ea4e8803 --- /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 ( + + ) + } +} 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 ) - 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 ; - }, - 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 ; - }, - 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 ; - }, - focus: function () { - ReactDOM.findDOMNode(this).focus(); - }, - onStop: function () { - this.context.returnFocus(); - } -}); \ 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 - {prefix} - {o.key}{suffix} - ; - }.bind(this)); - return
-
- {this.props.prompt || Select: } - {opts} -
-
; - } -}); - -export default Prompt; -- cgit v1.2.3 From 4ce7a9ba7728e7988f4455723ba5fed4fcd8d149 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 17 Jun 2016 06:26:46 +0800 Subject: [web] editor config for line endings --- web/src/js/components/ValueEditor/EditorBase.jsx | 332 ++++++++++----------- .../js/components/ValueEditor/ValidateEditor.jsx | 116 +++---- 2 files changed, 224 insertions(+), 224 deletions(-) (limited to 'web/src/js') diff --git a/web/src/js/components/ValueEditor/EditorBase.jsx b/web/src/js/components/ValueEditor/EditorBase.jsx index b659cf6d..e737d2af 100755 --- a/web/src/js/components/ValueEditor/EditorBase.jsx +++ b/web/src/js/components/ValueEditor/EditorBase.jsx @@ -1,166 +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 ( - - ) - } - - 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))) - } -} +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 ( + + ) + } + + 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 index ea4e8803..2f362986 100755 --- a/web/src/js/components/ValueEditor/ValidateEditor.jsx +++ b/web/src/js/components/ValueEditor/ValidateEditor.jsx @@ -1,58 +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 ( - - ) - } -} +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 ( + + ) + } +} -- cgit v1.2.3 From 7a78970d10ab2dfc9b86dd30a76fb1513f43dd35 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 17 Jun 2016 07:07:38 +0800 Subject: [web] common --- web/src/js/components/EventLog.jsx | 2 +- web/src/js/components/Footer.jsx | 1 - web/src/js/components/Header/FlowMenu.jsx | 8 +- web/src/js/components/Header/OptionMenu.jsx | 5 +- web/src/js/components/Header/ViewMenu.jsx | 2 +- web/src/js/components/MainView.jsx | 2 +- web/src/js/components/common.js | 173 --------------------- web/src/js/components/common/Button.jsx | 16 ++ web/src/js/components/common/Splitter.jsx | 99 ++++++++++++ web/src/js/components/common/ToggleButton.jsx | 17 ++ web/src/js/components/common/ToggleInputButton.jsx | 52 +++++++ 11 files changed, 194 insertions(+), 183 deletions(-) delete mode 100644 web/src/js/components/common.js create mode 100644 web/src/js/components/common/Button.jsx create mode 100644 web/src/js/components/common/Splitter.jsx create mode 100644 web/src/js/components/common/ToggleButton.jsx create mode 100644 web/src/js/components/common/ToggleInputButton.jsx (limited to 'web/src/js') diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx index d9211e11..169162ee 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -1,7 +1,7 @@ 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' class EventLog extends Component { 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/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 78a7f9bf..7064d3bf 100644 --- a/web/src/js/components/MainView.jsx +++ b/web/src/js/components/MainView.jsx @@ -3,7 +3,7 @@ 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' import { selectFlow, setFilter, setHighlight } from '../ducks/flows' 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 ( -
-
-
- ); - } -}); - -export const ToggleButton = ({checked, onToggle, text}) => -
- -   - {text} -
; - -ToggleButton.propTypes = { - checked: React.PropTypes.bool.isRequired, - onToggle: React.PropTypes.func.isRequired, - text: React.PropTypes.string.isRequired -}; - -export const Button = ({onClick, text, icon}) => -
- -   - {text} -
; - -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 ( -
- this.props.onToggleChanged(this.state.txt)}> -
- -  {this.props.name} -
-
- this.setState({txt: e.target.value})} - onKeyDown={e => {if (e.keyCode === Key.ENTER) this.props.onToggleChanged(this.state.txt); e.stopPropagation()}}/> -
- ); - } -} - -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 ( +
+ +   + {text} +
+ ) +} 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 ( +
+
+
+ ) + } +} 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 ( +
+ +   + {text} +
+ ) +} 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 ( +
+ this.props.onToggleChanged(this.state.txt)}> +
+ +   + {this.props.name} +
+
+ this.onChange(e)} + onKeyDown={e => this.onKeyDown(e)} + /> +
+ ) + } +} -- cgit v1.2.3 From 034287edcf00eb734cb67e62de58c3bfebf6bb44 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 17 Jun 2016 07:57:06 +0800 Subject: [web] clean up --- web/src/js/components/Prompt.jsx | 4 ---- web/src/js/components/ProxyApp.jsx | 7 ++----- 2 files changed, 2 insertions(+), 9 deletions(-) (limited to 'web/src/js') diff --git a/web/src/js/components/Prompt.jsx b/web/src/js/components/Prompt.jsx index 878aae29..6b19b3b3 100755 --- a/web/src/js/components/Prompt.jsx +++ b/web/src/js/components/Prompt.jsx @@ -14,10 +14,6 @@ Prompt.propTypes = { prompt: PropTypes.string, } -Prompt.componentDidMount = function() { - ReactDOM.findDOMNode(this).focus() -} - export default function Prompt({ prompt, done, options }, context) { const opts = [] diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index 967cc921..c458639d 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -13,7 +13,6 @@ class ProxyAppMain extends Component { static childContextTypes = { returnFocus: PropTypes.func.isRequired, - location: PropTypes.object.isRequired, } static contextTypes = { @@ -89,10 +88,7 @@ class ProxyAppMain extends Component { * @todo use props */ getChildContext() { - return { - returnFocus: this.focus, - location: this.props.location - } + return { returnFocus: this.focus } } /** @@ -106,6 +102,7 @@ class ProxyAppMain extends Component { /** * @todo move to actions + * @todo bind on window */ onKeyDown(e) { let name = null -- cgit v1.2.3