web: fix ValueEditor, clean up code

This commit is contained in:
Maximilian Hils 2016-07-21 01:14:55 -07:00
parent 427fffbcb8
commit 8a3a21bba1
27 changed files with 659 additions and 749 deletions

View File

@ -1,9 +1,7 @@
.ReactCodeMirror { .CodeMirror {
border: 1px solid #ccc; border: 1px solid #ccc;
height: auto !important;
max-height: 2048px !important;
} }
.CodeMirror{
height: auto !important;
max-height: 1000px !important;
}
@import (inline) "../../node_modules/codemirror/lib/codemirror.css"; @import (inline) "../../node_modules/codemirror/lib/codemirror.css";

View File

@ -26,14 +26,14 @@
word-break: break-all; word-break: break-all;
max-height: 100px; max-height: 100px;
overflow-y: auto; overflow-y: auto;
.inline-input.editable {
border-color: rgba(255,255,255,0.5);
}
} }
.request-line { .request-line {
margin-bottom: 2px; margin-bottom: 2px;
} }
/*.request .response-line,
.response .request-line {
opacity: 0.7;
}*/
hr { hr {
margin: 0 0 5px; margin: 0 0 5px;
@ -42,8 +42,14 @@
} }
.inline-input { .inline-input {
margin: 0 -5px; display: inline;
padding: 0 5px; margin: 0 -3px;
padding: 0 3px;
border: solid transparent 1px;
&.editable {
border-color: #ccc;
}
&[contenteditable] { &[contenteditable] {

View File

@ -13,15 +13,16 @@
} }
.edit-flow { .edit-flow {
cursor: pointer;
position: absolute; position: absolute;
right: 0px; right: 0;
top: 5px; top: 5px;
height: 40px; height: 40px;
width: 40px; width: 40px;
border-radius: 20px; border-radius: 20px;
z-index: 10000; z-index: 10000;
background-color: white; background-color: rgba(255, 255, 255, 0.7);
border: solid 2px rgba(248, 145, 59, 0.7); border: solid 2px rgba(248, 145, 59, 0.7);
text-align: center; text-align: center;

View File

@ -5,7 +5,7 @@ import * as ContentViews from './ContentView/ContentViews'
import * as MetaViews from './ContentView/MetaViews' import * as MetaViews from './ContentView/MetaViews'
import ContentLoader from './ContentView/ContentLoader' import ContentLoader from './ContentView/ContentLoader'
import ViewSelector from './ContentView/ViewSelector' import ViewSelector from './ContentView/ViewSelector'
import { setContentView, setDisplayLarge, setModifiedFlowContent } from '../ducks/ui' import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow'
import CodeEditor from './common/CodeEditor' import CodeEditor from './common/CodeEditor'
ContentView.propTypes = { ContentView.propTypes = {
@ -30,7 +30,7 @@ function ContentView(props) {
} }
if (!displayLarge && ContentView.isContentTooLarge(message)) { if (!displayLarge && ContentView.isContentTooLarge(message)) {
return <MetaViews.ContentTooLarge {...props} onClick={() => setDisplayLarge(true)}/> return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/>
} }
const View = ContentViews[contentView] const View = ContentViews[contentView]
@ -80,13 +80,13 @@ function ContentView(props) {
export default connect( export default connect(
state => ({ state => ({
contentView: state.ui.contentView, contentView: state.ui.flow.contentView,
displayLarge: state.ui.displayLarge, displayLarge: state.ui.flow.displayLarge,
isFlowEditorOpen : state.ui.isFlowEditorOpen isFlowEditorOpen : !!state.ui.flow.modifiedFlow // FIXME
}), }),
{ {
selectView: setContentView, selectView: setContentView,
setDisplayLarge, displayLarge,
setModifiedFlowContent updateEdit,
} }
)(ContentView) )(ContentView)

View File

@ -7,7 +7,7 @@ import { Request, Response, ErrorView as Error } from './FlowView/Messages'
import Details from './FlowView/Details' import Details from './FlowView/Details'
import Prompt from './Prompt' import Prompt from './Prompt'
import { setPrompt, selectTab } from '../ducks/ui' import { selectTab } from '../ducks/ui/flow'
export default class FlowView extends Component { export default class FlowView extends Component {
@ -90,9 +90,9 @@ export default class FlowView extends Component {
export default connect( export default connect(
state => ({ state => ({
promptOpen: state.ui.promptOpen, promptOpen: state.ui.promptOpen,
tab: state.ui.flow.tab
}), }),
{ {
setPrompt,
selectTab, selectTab,
} }
)(FlowView) )(FlowView)

View File

@ -1,39 +0,0 @@
import React, { PropTypes, Component } from 'react'
import { connect } from 'react-redux'
import {closeFlowEditor} from '../../ducks/ui.js'
import {openFlowEditor} from '../../ducks/ui.js'
FlowEditorButton.propTypes = {
isFlowEditorOpen: PropTypes.bool.isRequired,
content: PropTypes.string.isRequired,
onContentChange: PropTypes.func.isRequired
}
function FlowEditorButton ({ isFlowEditorOpen, closeFlowEditor, openFlowEditor, onContentChange, content }) {
return (
<div className="edit-flow-container">
{isFlowEditorOpen ?
<a className="edit-flow" onClick={() => {onContentChange(content); closeFlowEditor()}}>
<i className="fa fa-check"/>
</a>
:
<a className="edit-flow" onClick={() => openFlowEditor()}>
<i className="fa fa-pencil"/>
</a>
}
</div>
)
}
export default connect(
state => ({
isFlowEditorOpen: state.ui.isFlowEditorOpen,
content: state.ui.modifiedFlow.content
}),
{
closeFlowEditor,
openFlowEditor
}
)(FlowEditorButton)

View File

@ -1,12 +1,21 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ValueEditor from '../ValueEditor' import ValueEditor from '../ValueEditor/ValueEditor'
import { Key } from '../../utils.js' import { Key } from '../../utils'
class HeaderEditor extends Component { class HeaderEditor extends Component {
constructor(props) {
super(props)
this.onKeyDown = this.onKeyDown.bind(this)
}
render() { render() {
return <ValueEditor ref="input" {...this.props} onKeyDown={this.onKeyDown} inline/> let { onTab, ...props } = this.props
return <ValueEditor
{...props}
onKeyDown={this.onKeyDown}
/>
} }
focus() { focus() {
@ -21,6 +30,7 @@ class HeaderEditor extends Component {
this.props.onRemove(e) this.props.onRemove(e)
} }
break break
case Key.ENTER:
case Key.TAB: case Key.TAB:
if (!e.shiftKey) { if (!e.shiftKey) {
this.props.onTab(e) this.props.onTab(e)
@ -66,7 +76,12 @@ export default class Headers extends Component {
onTab(row, col, e) { onTab(row, col, e) {
const headers = this.props.message.headers const headers = this.props.message.headers
if (row !== headers.length - 1 || col !== 1) { if (col === 0) {
this._nextSel = `${row}-value`
return
}
if (row !== headers.length - 1) {
this._nextSel = `${row + 1}-key`
return return
} }
@ -96,7 +111,7 @@ export default class Headers extends Component {
} }
render() { render() {
const { message } = this.props const { message, readonly } = this.props
return ( return (
<table className="header-table"> <table className="header-table">
@ -107,6 +122,7 @@ export default class Headers extends Component {
<HeaderEditor <HeaderEditor
ref={`${i}-key`} ref={`${i}-key`}
content={header[0]} content={header[0]}
readonly={readonly}
onDone={val => this.onChange(i, 0, val)} onDone={val => this.onChange(i, 0, val)}
onRemove={event => this.onRemove(i, 0, event)} onRemove={event => this.onRemove(i, 0, event)}
onTab={event => this.onTab(i, 0, event)} onTab={event => this.onTab(i, 0, event)}
@ -116,6 +132,7 @@ export default class Headers extends Component {
<HeaderEditor <HeaderEditor
ref={`${i}-value`} ref={`${i}-value`}
content={header[1]} content={header[1]}
readonly={readonly}
onDone={val => this.onChange(i, 1, val)} onDone={val => this.onChange(i, 1, val)}
onRemove={event => this.onRemove(i, 1, event)} onRemove={event => this.onRemove(i, 1, event)}
onTab={event => this.onTab(i, 1, event)} onTab={event => this.onTab(i, 1, event)}

View File

@ -1,108 +1,108 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import _ from 'lodash' import { connect } from 'react-redux'
import { RequestUtils, isValidHttpVersion, parseUrl, parseHttpVersion } from '../../flow/utils.js'
import { Key, formatTimeStamp } from '../../utils.js' import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js'
import { formatTimeStamp } from '../../utils.js'
import ContentView from '../ContentView' import ContentView from '../ContentView'
import ValueEditor from '../ValueEditor' import ValidateEditor from '../ValueEditor/ValidateEditor'
import ValueEditor from '../ValueEditor/ValueEditor'
import Headers from './Headers' import Headers from './Headers'
import * as flowActions from '../../ducks/flows' import { startEdit, updateEdit } from '../../ducks/ui/flow'
import FlowEditorButton from './FlowEditorButton' import ToggleEdit from './ToggleEdit'
class RequestLine extends Component {
render() {
const { flow, updateFlow } = this.props
function RequestLine({ flow, readonly, updateFlow }) {
return ( return (
<div className="first-line request-line"> <div className="first-line request-line">
<div>
<ValueEditor <ValueEditor
ref="method"
content={flow.request.method} content={flow.request.method}
readonly={readonly}
onDone={method => updateFlow({ request: { method } })} onDone={method => updateFlow({ request: { method } })}
inline
/> />
&nbsp; &nbsp;
<ValueEditor <ValidateEditor
ref="url"
content={RequestUtils.pretty_url(flow.request)} content={RequestUtils.pretty_url(flow.request)}
onDone={url => updateFlow({ request: Object.assign({ path: '' }, parseUrl(url)) })} readonly={readonly}
onDone={url => updateFlow({ request: {path: '', ...parseUrl(url)}})}
isValid={url => !!parseUrl(url).host} isValid={url => !!parseUrl(url).host}
inline
/> />
&nbsp; &nbsp;
<ValueEditor <ValidateEditor
ref="httpVersion"
content={flow.request.http_version} content={flow.request.http_version}
onDone={ver => updateFlow({ request: { http_version: parseHttpVersion(ver) } })} readonly={readonly}
onDone={http_version => updateFlow({ request: { http_version } })}
isValid={isValidHttpVersion} isValid={isValidHttpVersion}
inline
/> />
</div> </div>
</div>
) )
}
} }
class ResponseLine extends Component { function ResponseLine({ flow, readonly, updateFlow }) {
render() {
const { flow, updateFlow } = this.props
return ( return (
<div className="first-line response-line"> <div className="first-line response-line">
<ValueEditor <ValidateEditor
ref="httpVersion"
content={flow.response.http_version} content={flow.response.http_version}
onDone={nextVer => updateFlow({ response: { http_version: parseHttpVersion(nextVer) } })} readonly={readonly}
onDone={nextVer => updateFlow({ response: { http_version: nextVer } })}
isValid={isValidHttpVersion} isValid={isValidHttpVersion}
inline
/> />
&nbsp; &nbsp;
<ValueEditor <ValidateEditor
ref="code"
content={flow.response.status_code + ''} content={flow.response.status_code + ''}
readonly={readonly}
onDone={code => updateFlow({ response: { code: parseInt(code) } })} onDone={code => updateFlow({ response: { code: parseInt(code) } })}
isValid={code => /^\d+$/.test(code)} isValid={code => /^\d+$/.test(code)}
inline
/> />
&nbsp; &nbsp;
<ValueEditor <ValueEditor
ref="msg"
content={flow.response.reason} content={flow.response.reason}
readonly={readonly}
onDone={msg => updateFlow({ response: { msg } })} onDone={msg => updateFlow({ response: { msg } })}
inline
/> />
</div> </div>
) )
}
} }
const Message = connect(
state => ({
flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]],
isEdit: !!state.ui.flow.modifiedFlow,
}),
{
updateFlow: updateEdit,
}
)
export class Request extends Component { export class Request extends Component {
render() { render() {
const { flow, updateFlow } = this.props const { flow, isEdit, updateFlow } = this.props
let onContentChange = content => flowActions.updateContent(this.props.flow, content, "request")
return ( return (
<section className="request"> <section className="request">
<FlowEditorButton onContentChange={onContentChange}/> <ToggleEdit/>
<RequestLine ref="requestLine" flow={flow} updateFlow={updateFlow} /> <RequestLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers <Headers
ref="headers"
message={flow.request} message={flow.request}
readonly={!isEdit}
onChange={headers => updateFlow({ request: { headers } })} onChange={headers => updateFlow({ request: { headers } })}
/> />
<hr/> <hr/>
<ContentView flow={flow} <ContentView flow={flow} message={flow.request}/>
onContentChange={onContentChange}
message={flow.request}
/>
</section> </section>
) )
} }
edit(k) { edit(k) {
throw "unimplemented"
/*
switch (k) { switch (k) {
case 'm': case 'm':
this.refs.requestLine.refs.method.focus() this.refs.requestLine.refs.method.focus()
@ -119,35 +119,39 @@ export class Request extends Component {
default: default:
throw new Error(`Unimplemented: ${k}`) throw new Error(`Unimplemented: ${k}`)
} }
*/
} }
} }
Request = Message(Request)
export class Response extends Component { export class Response extends Component {
render() { render() {
const { flow, updateFlow } = this.props const { flow, isEdit, updateFlow } = this.props
let onContentChange = content => flowActions.updateContent(this.props.flow, content, "response")
return ( return (
<section className="response"> <section className="response">
<FlowEditorButton onContentChange={onContentChange}/> <ToggleEdit/>
<ResponseLine ref="responseLine" flow={flow} updateFlow={updateFlow} /> <ResponseLine
flow={flow}
readonly={!isEdit}
updateFlow={updateFlow}/>
<Headers <Headers
ref="headers"
message={flow.response} message={flow.response}
readonly={!isEdit}
onChange={headers => updateFlow({ response: { headers } })} onChange={headers => updateFlow({ response: { headers } })}
/> />
<hr/> <hr/>
<ContentView flow={flow} <ContentView flow={flow} message={flow.response}/>
onContentChange={onContentChange}
message={flow.response}
/>
</section> </section>
) )
} }
edit(k) { edit(k) {
throw "unimplemented"
/*
switch (k) { switch (k) {
case 'c': case 'c':
this.refs.responseLine.refs.status_code.focus() this.refs.responseLine.refs.status_code.focus()
@ -164,9 +168,13 @@ export class Response extends Component {
default: default:
throw new Error(`'Unimplemented: ${k}`) throw new Error(`'Unimplemented: ${k}`)
} }
*/
} }
} }
Response = Message(Response)
ErrorView.propTypes = { ErrorView.propTypes = {
flow: PropTypes.object.isRequired, flow: PropTypes.object.isRequired,
} }

View File

@ -0,0 +1,38 @@
import React, { PropTypes, Component } from 'react'
import { connect } from 'react-redux'
import { startEdit, stopEdit } from '../../ducks/ui/flow'
ToggleEdit.propTypes = {
isEdit: PropTypes.bool.isRequired,
flow: PropTypes.object.isRequired,
startEdit: PropTypes.func.isRequired,
stopEdit: PropTypes.func.isRequired,
}
function ToggleEdit({ isEdit, startEdit, stopEdit, flow }) {
return (
<div className="edit-flow-container">
{isEdit ?
<a className="edit-flow" onClick={() => stopEdit(flow)}>
<i className="fa fa-check"/>
</a>
:
<a className="edit-flow" onClick={() => startEdit(flow)}>
<i className="fa fa-pencil"/>
</a>
}
</div>
)
}
export default connect(
state => ({
isEdit: !!state.ui.flow.modifiedFlow,
flow: state.ui.flow.modifiedFlow || state.flows.byId[state.flows.selected[0]]
}),
{
startEdit,
stopEdit,
}
)(ToggleEdit)

View File

@ -48,6 +48,6 @@ function Footer({ settings }) {
export default connect( export default connect(
state => ({ state => ({
settings: state.settings.settings, settings: state.settings,
}) })
)(Footer) )(Footer)

View File

@ -6,7 +6,7 @@ import ViewMenu from './Header/ViewMenu'
import OptionMenu from './Header/OptionMenu' import OptionMenu from './Header/OptionMenu'
import FileMenu from './Header/FileMenu' import FileMenu from './Header/FileMenu'
import FlowMenu from './Header/FlowMenu' import FlowMenu from './Header/FlowMenu'
import {setActiveMenu} from '../ducks/ui.js' import {setActiveMenu} from '../ducks/ui/header'
class Header extends Component { class Header extends Component {
static entries = [MainMenu, ViewMenu, OptionMenu] static entries = [MainMenu, ViewMenu, OptionMenu]
@ -17,7 +17,7 @@ class Header extends Component {
} }
render() { render() {
const { query, selectedFlowId, activeMenu} = this.props const { selectedFlowId, activeMenu} = this.props
let entries = [...Header.entries] let entries = [...Header.entries]
if(selectedFlowId) if(selectedFlowId)
@ -39,10 +39,7 @@ class Header extends Component {
))} ))}
</nav> </nav>
<div className="menu"> <div className="menu">
<Active <Active/>
ref="active"
query={query}
/>
</div> </div>
</header> </header>
) )
@ -52,13 +49,9 @@ class Header extends Component {
export default connect( export default connect(
state => ({ state => ({
selectedFlowId: state.flows.selected[0], selectedFlowId: state.flows.selected[0],
activeMenu: state.ui.activeMenu, activeMenu: state.ui.header.activeMenu,
}), }),
{ {
setActiveMenu, setActiveMenu,
},
null,
{
withRef: true,
} }
)(Header) )(Header)

View File

@ -1,92 +1,50 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import FilterInput from './FilterInput' import FilterInput from './FilterInput'
import { Query } from '../../actions.js'
import { update as updateSettings } from '../../ducks/settings' import { update as updateSettings } from '../../ducks/settings'
import { updateQuery, setSelectedInput } from '../../ducks/ui' import { updateFilter, updateHighlight } from '../../ducks/flowView'
class MainMenu extends Component { MainMenu.title = "Start"
static title = 'Start'
static route = 'flows'
static propTypes = {
query: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
updateSettings: PropTypes.func.isRequired,
updateQuery: PropTypes.func.isRequired,
}
constructor(props, context) {
super(props, context)
this.onSearchChange = this.onSearchChange.bind(this)
this.onHighlightChange = this.onHighlightChange.bind(this)
}
componentWillReceiveProps(nextProps) {
if(this.refs[nextProps.selectedInput]) {
this.refs[nextProps.selectedInput].select()
}
this.props.setSelectedInput(undefined)
}
onSearchChange(val) {
this.props.updateQuery({ [Query.SEARCH]: val })
}
onHighlightChange(val) {
this.props.updateQuery({ [Query.HIGHLIGHT]: val })
}
render() {
const { query, settings, updateSettings } = this.props
export default function MainMenu() {
return ( return (
<div> <div>
<div className="menu-row"> <div className="menu-row">
<FilterInput <FlowFilterInput/>
ref="search" <HighlightInput/>
placeholder="Search" <InterceptInput/>
type="search"
color="black"
value={query[Query.SEARCH] || ''}
onChange={this.onSearchChange}
/>
<FilterInput
ref="highlight"
placeholder="Highlight"
type="tag"
color="hsl(48, 100%, 50%)"
value={query[Query.HIGHLIGHT] || ''}
onChange={this.onHighlightChange}
/>
<FilterInput
ref="intercept"
placeholder="Intercept"
type="pause"
color="hsl(208, 56%, 53%)"
value={settings.intercept || ''}
onChange={intercept => updateSettings({ intercept })}
/>
</div> </div>
<div className="clearfix"></div> <div className="clearfix"></div>
</div> </div>
) )
}
} }
export default connect( const InterceptInput = connect(
state => ({ state => ({
settings: state.settings.settings, value: state.settings.intercept || '',
selectedInput: state.ui.selectedInput placeholder: 'Intercept',
type: 'pause',
color: 'hsl(208, 56%, 53%)'
}), }),
{ { onChange: intercept => updateSettings({ intercept }) }
updateSettings, )(FilterInput);
updateQuery,
setSelectedInput const FlowFilterInput = connect(
}, state => ({
null, value: state.flowView.filter || '',
{ placeholder: 'Search',
withRef: true, type: 'search',
} color: 'black'
)(MainMenu); }),
{ onChange: updateFilter }
)(FilterInput);
const HighlightInput = connect(
state => ({
value: state.flowView.highlight || '',
placeholder: 'Highlight',
type: 'tag',
color: 'hsl(48, 100%, 50%)'
}),
{ onChange: updateHighlight }
)(FilterInput);

View File

@ -8,53 +8,52 @@ OptionMenu.title = 'Options'
OptionMenu.propTypes = { OptionMenu.propTypes = {
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
onSettingsChange: PropTypes.func.isRequired, updateSettings: PropTypes.func.isRequired,
} }
function OptionMenu({ settings, onSettingsChange }) { function OptionMenu({ settings, updateSettings }) {
// @todo use settings.map
return ( return (
<div> <div>
<div className="menu-row"> <div className="menu-row">
<ToggleButton text="showhost" <ToggleButton text="showhost"
checked={settings.showhost} checked={settings.showhost}
onToggle={() => onSettingsChange({ showhost: !settings.showhost })} onToggle={() => updateSettings({ showhost: !settings.showhost })}
/> />
<ToggleButton text="no_upstream_cert" <ToggleButton text="no_upstream_cert"
checked={settings.no_upstream_cert} checked={settings.no_upstream_cert}
onToggle={() => onSettingsChange({ no_upstream_cert: !settings.no_upstream_cert })} onToggle={() => updateSettings({ no_upstream_cert: !settings.no_upstream_cert })}
/> />
<ToggleButton text="rawtcp" <ToggleButton text="rawtcp"
checked={settings.rawtcp} checked={settings.rawtcp}
onToggle={() => onSettingsChange({ rawtcp: !settings.rawtcp })} onToggle={() => updateSettings({ rawtcp: !settings.rawtcp })}
/> />
<ToggleButton text="http2" <ToggleButton text="http2"
checked={settings.http2} checked={settings.http2}
onToggle={() => onSettingsChange({ http2: !settings.http2 })} onToggle={() => updateSettings({ http2: !settings.http2 })}
/> />
<ToggleButton text="anticache" <ToggleButton text="anticache"
checked={settings.anticache} checked={settings.anticache}
onToggle={() => onSettingsChange({ anticache: !settings.anticache })} onToggle={() => updateSettings({ anticache: !settings.anticache })}
/> />
<ToggleButton text="anticomp" <ToggleButton text="anticomp"
checked={settings.anticomp} checked={settings.anticomp}
onToggle={() => onSettingsChange({ anticomp: !settings.anticomp })} onToggle={() => updateSettings({ anticomp: !settings.anticomp })}
/> />
<ToggleInputButton name="stickyauth" placeholder="Sticky auth filter" <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
checked={!!settings.stickyauth} checked={!!settings.stickyauth}
txt={settings.stickyauth || ''} txt={settings.stickyauth || ''}
onToggleChanged={txt => onSettingsChange({ stickyauth: !settings.stickyauth ? txt : null })} onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })}
/> />
<ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter" <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
checked={!!settings.stickycookie} checked={!!settings.stickycookie}
txt={settings.stickycookie || ''} txt={settings.stickycookie || ''}
onToggleChanged={txt => onSettingsChange({ stickycookie: !settings.stickycookie ? txt : null })} onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })}
/> />
<ToggleInputButton name="stream" placeholder="stream..." <ToggleInputButton name="stream" placeholder="stream..."
checked={!!settings.stream} checked={!!settings.stream}
txt={settings.stream || ''} txt={settings.stream || ''}
inputType="number" inputType="number"
onToggleChanged={txt => onSettingsChange({ stream: !settings.stream ? txt : null })} onToggleChanged={txt => updateSettings({ stream: !settings.stream ? txt : null })}
/> />
</div> </div>
<div className="clearfix"/> <div className="clearfix"/>
@ -64,9 +63,9 @@ function OptionMenu({ settings, onSettingsChange }) {
export default connect( export default connect(
state => ({ state => ({
settings: state.settings.settings, settings: state.settings,
}), }),
{ {
onSettingsChange: updateSettings, updateSettings,
} }
)(OptionMenu) )(OptionMenu)

View File

@ -1,6 +1,5 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Query } from '../actions.js'
import Splitter from './common/Splitter' import Splitter from './common/Splitter'
import FlowTable from './FlowTable' import FlowTable from './FlowTable'
import FlowView from './FlowView' import FlowView from './FlowView'
@ -14,19 +13,6 @@ class MainView extends Component {
sort: PropTypes.object, sort: PropTypes.object,
} }
/**
* @todo move to actions
* @todo replace with mapStateToProps
*/
componentWillReceiveProps(nextProps) {
if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) {
this.props.updateFilter(nextProps.location.query[Query.SEARCH], false)
}
if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) {
this.props.updateHighlight(nextProps.location.query[Query.HIGHLIGHT], false)
}
}
render() { render() {
const { flows, selectedFlow, highlight } = this.props const { flows, selectedFlow, highlight } = this.props
return ( return (
@ -66,9 +52,5 @@ export default connect(
updateFilter, updateFilter,
updateHighlight, updateHighlight,
updateFlow: flowsActions.update, updateFlow: flowsActions.update,
},
undefined,
{
withRef: true
} }
)(MainView) )(MainView)

View File

@ -1,14 +1,11 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { init as appInit, destruct as appDestruct } from '../ducks/app' import { init as appInit, destruct as appDestruct } from '../ducks/app'
import { onKeyDown } from '../ducks/ui' import { onKeyDown } from '../ducks/ui/keyboard'
import Header from './Header' import Header from './Header'
import EventLog from './EventLog' import EventLog from './EventLog'
import Footer from './Footer' import Footer from './Footer'
import { Key } from '../utils.js'
class ProxyAppMain extends Component { class ProxyAppMain extends Component {
@ -27,6 +24,15 @@ class ProxyAppMain extends Component {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
/*
FIXME: improve react-router -> redux integration.
if (nextProps.location.query[Query.SEARCH] !== nextProps.filter) {
this.props.updateFilter(nextProps.location.query[Query.SEARCH], false)
}
if (nextProps.location.query[Query.HIGHLIGHT] !== nextProps.highlight) {
this.props.updateHighlight(nextProps.location.query[Query.HIGHLIGHT], false)
}
*/
if (nextProps.query === this.props.query && nextProps.selectedFlowId === this.props.selectedFlowId && nextProps.panel === this.props.panel) { if (nextProps.query === this.props.query && nextProps.selectedFlowId === this.props.selectedFlowId && nextProps.panel === this.props.panel) {
return return
} }
@ -35,13 +41,14 @@ class ProxyAppMain extends Component {
} else { } else {
this.context.router.replace({ pathname: '/flows', query: nextProps.query }) this.context.router.replace({ pathname: '/flows', query: nextProps.query })
} }
} }
render() { render() {
const { showEventLog, location, children, query } = this.props const { showEventLog, location, children, query } = this.props
return ( return (
<div id="container" tabIndex="0"> <div id="container" tabIndex="0">
<Header ref="header" query={query} /> <Header/>
{React.cloneElement( {React.cloneElement(
children, children,
{ ref: 'view', location, query } { ref: 'view', location, query }
@ -58,8 +65,8 @@ class ProxyAppMain extends Component {
export default connect( export default connect(
state => ({ state => ({
showEventLog: state.eventLog.visible, showEventLog: state.eventLog.visible,
query: state.ui.query, query: state.flowView.filter,
panel: state.ui.panel, panel: state.ui.flow.tab,
selectedFlowId: state.flows.selected[0] selectedFlowId: state.flows.selected[0]
}), }),
{ {

View File

@ -1,26 +0,0 @@
import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ValidateEditor from './ValueEditor/ValidateEditor'
export default class ValueEditor extends Component {
static propTypes = {
content: PropTypes.string.isRequired,
onDone: PropTypes.func.isRequired,
inline: PropTypes.bool,
}
render() {
var tag = this.props.inline ? 'span' : 'div'
return (
<ValidateEditor
{...this.props}
tag={tag}
/>
)
}
focus() {
ReactDOM.findDOMNode(this).focus();
}
}

View File

@ -1,57 +1,57 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom' import ValueEditor from './ValueEditor'
import EditorBase from './EditorBase' import classnames from 'classnames'
export default class ValidateEditor extends Component { export default class ValidateEditor extends Component {
static propTypes = { static propTypes = {
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
readonly: PropTypes.bool,
onDone: PropTypes.func.isRequired, onDone: PropTypes.func.isRequired,
onInput: PropTypes.func,
isValid: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
isValid: PropTypes.func.isRequired,
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { currentContent: props.content } this.state = { valid: props.isValid(props.content) }
this.onInput = this.onInput.bind(this) this.onInput = this.onInput.bind(this)
this.onDone = this.onDone.bind(this) this.onDone = this.onDone.bind(this)
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
this.setState({ currentContent: nextProps.content }) this.setState({ valid: nextProps.isValid(nextProps.content) })
} }
onInput(currentContent) { onInput(content) {
this.setState({ currentContent }) this.setState({ valid: this.props.isValid(content) })
this.props.onInput && this.props.onInput(currentContent)
} }
onDone(content) { onDone(content) {
if (this.props.isValid && !this.props.isValid(content)) { if (!this.props.isValid(content)) {
this.refs.editor.reset() this.editor.reset()
content = this.props.content content = this.props.content
} }
this.props.onDone(content) this.props.onDone(content)
} }
render() { render() {
let className = this.props.className || '' let className = classnames(
if (this.props.isValid) { this.props.className,
if (this.props.isValid(this.state.currentContent)) { {
className += ' has-success' 'has-success': this.state.valid,
} else { 'has-warning': !this.state.valid
className += ' has-warning'
}
} }
)
return ( return (
<EditorBase <ValueEditor
{...this.props} content={this.props.content}
ref="editor" readonly={this.props.readonly}
className={className}
onDone={this.onDone} onDone={this.onDone}
onInput={this.onInput} onInput={this.onInput}
className={className}
ref={(e) => this.editor = e}
/> />
) )
} }

View File

@ -1,61 +1,66 @@
import React, { Component, PropTypes } from 'react' import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom' import _ from "lodash"
import {Key} from '../../utils.js' import classnames from 'classnames'
export default class EditorBase extends Component { import { Key } from '../../utils'
export default class ValueEditor extends Component {
static propTypes = { static propTypes = {
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
readonly: PropTypes.bool,
onDone: PropTypes.func.isRequired, onDone: PropTypes.func.isRequired,
contentToHtml: PropTypes.func,
nodeToContent: PropTypes.func,
onStop: PropTypes.func,
submitOnEnter: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
tag: PropTypes.string, onInput: PropTypes.func,
onKeyDown: PropTypes.func,
} }
static defaultProps = { static defaultProps = {
contentToHtml: content => _.escape(content), onInput: () => {},
nodeToContent: node => node.textContent, onKeyDown: () => {},
submitOnEnter: true,
className: '',
tag: 'div',
onStop: _.noop,
onMouseDown: _.noop,
onBlur: _.noop,
onInput: _.noop,
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.state = {editable: false} this.state = { editable: false }
this.onPaste = this.onPaste.bind(this) this.onPaste = this.onPaste.bind(this)
this.onMouseDown = this.onMouseDown.bind(this) this.onMouseDown = this.onMouseDown.bind(this)
this.onMouseUp = this.onMouseUp.bind(this) this.onMouseUp = this.onMouseUp.bind(this)
this.onFocus = this.onFocus.bind(this) this.onFocus = this.onFocus.bind(this)
this.onClick = this.onClick.bind(this) this.onClick = this.onClick.bind(this)
this.stop = this.stop.bind(this) this.blur = this.blur.bind(this)
this.onBlur = this.onBlur.bind(this) this.onBlur = this.onBlur.bind(this)
this.reset = this.reset.bind(this) this.reset = this.reset.bind(this)
this.onKeyDown = this.onKeyDown.bind(this) this.onKeyDown = this.onKeyDown.bind(this)
this.onInput = this.onInput.bind(this) this.onInput = this.onInput.bind(this)
} }
stop() { blur() {
// a stop would cause a blur as a side-effect. // a stop would cause a blur as a side-effect.
// but a blur event must trigger a stop as well. // 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. // to fix this, make stop = blur and do the actual stop in the onBlur handler.
ReactDOM.findDOMNode(this).blur() this.input.blur()
this.props.onStop() }
reset() {
this.input.innerHTML = _.escape(this.props.content)
} }
render() { render() {
let className = classnames(
'inline-input',
{
'readonly': this.props.readonly,
'editable': !this.props.readonly
},
this.props.className
)
return ( return (
<this.props.tag <div
tabIndex="0" ref={input => this.input = input}
className={`inline-input ${this.props.className}`} tabIndex={!this.props.readonly && "0"}
className={className}
contentEditable={this.state.editable || undefined} contentEditable={this.state.editable || undefined}
onFocus={this.onFocus} onFocus={this.onFocus}
onMouseDown={this.onMouseDown} onMouseDown={this.onMouseDown}
@ -64,8 +69,8 @@ export default class EditorBase extends Component {
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
onInput={this.onInput} onInput={this.onInput}
onPaste={this.onPaste} onPaste={this.onPaste}
dangerouslySetInnerHTML={{ __html: this.props.contentToHtml(this.props.content) }} dangerouslySetInnerHTML={{ __html: _.escape(this.props.content) }}
/> ></div>
) )
} }
@ -78,7 +83,6 @@ export default class EditorBase extends Component {
onMouseDown(e) { onMouseDown(e) {
this._mouseDown = true this._mouseDown = true
window.addEventListener('mouseup', this.onMouseUp) window.addEventListener('mouseup', this.onMouseUp)
this.props.onMouseDown(e)
} }
onMouseUp() { onMouseUp() {
@ -94,7 +98,7 @@ export default class EditorBase extends Component {
} }
onFocus(e) { onFocus(e) {
if (this._mouseDown || this._ignore_events || this.state.editable) { if (this._mouseDown || this._ignore_events || this.state.editable || this.props.readonly) {
return return
} }
@ -114,31 +118,29 @@ export default class EditorBase extends Component {
range = document.caretRangeFromPoint(e.clientX, e.clientY) range = document.caretRangeFromPoint(e.clientX, e.clientY)
} else { } else {
range = document.createRange() range = document.createRange()
range.selectNodeContents(ReactDOM.findDOMNode(this)) range.selectNodeContents(this.input)
} }
this._ignore_events = true this._ignore_events = true
this.setState({ editable: true }, () => { this.setState({ editable: true }, () => {
const node = ReactDOM.findDOMNode(this) this.input.blur()
node.blur() this.input.focus()
node.focus()
this._ignore_events = false this._ignore_events = false
range.selectNodeContents(this.input)
sel.removeAllRanges();
sel.addRange(range);
}) })
} }
onBlur(e) { onBlur(e) {
if (this._ignore_events) { if (this._ignore_events || this.props.readonly) {
return return
} }
window.getSelection().removeAllRanges() //make sure that selection is cleared on blur window.getSelection().removeAllRanges() //make sure that selection is cleared on blur
this.setState({ editable: false }) this.setState({ editable: false })
this.props.onDone(this.props.nodeToContent(ReactDOM.findDOMNode(this))) this.props.onDone(this.input.textContent)
this.props.onBlur(e)
} }
reset() {
ReactDOM.findDOMNode(this).innerHTML = this.props.contentToHtml(this.props.content)
}
onKeyDown(e) { onKeyDown(e) {
e.stopPropagation() e.stopPropagation()
@ -146,20 +148,21 @@ export default class EditorBase extends Component {
case Key.ESC: case Key.ESC:
e.preventDefault() e.preventDefault()
this.reset() this.reset()
this.stop() this.blur()
break break
case Key.ENTER: case Key.ENTER:
if (this.props.submitOnEnter && !e.shiftKey) { if (!e.shiftKey) {
e.preventDefault() e.preventDefault()
this.stop() this.blur()
} }
break break
default: default:
break break
} }
this.props.onKeyDown(e)
} }
onInput() { onInput() {
this.props.onInput(this.props.nodeToContent(ReactDOM.findDOMNode(this))) this.props.onInput(this.input.textContent)
} }
} }

View File

@ -67,56 +67,49 @@ export default function reduce(state = defaultState, action) {
* @public * @public
*/ */
export function accept(flow) { export function accept(flow) {
fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' }) return dispatch => fetchApi(`/flows/${flow.id}/accept`, { method: 'POST' })
return { type: REQUEST_ACTION }
} }
/** /**
* @public * @public
*/ */
export function acceptAll() { export function acceptAll() {
fetchApi('/flows/accept', { method: 'POST' }) return dispatch => fetchApi('/flows/accept', { method: 'POST' })
return { type: REQUEST_ACTION }
} }
/** /**
* @public * @public
*/ */
export function remove(flow) { export function remove(flow) {
fetchApi(`/flows/${flow.id}`, { method: 'DELETE' }) return dispatch => fetchApi(`/flows/${flow.id}`, { method: 'DELETE' })
return { type: REQUEST_ACTION }
} }
/** /**
* @public * @public
*/ */
export function duplicate(flow) { export function duplicate(flow) {
fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' }) return dispatch => fetchApi(`/flows/${flow.id}/duplicate`, { method: 'POST' })
return { type: REQUEST_ACTION }
} }
/** /**
* @public * @public
*/ */
export function replay(flow) { export function replay(flow) {
fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' }) return dispatch => fetchApi(`/flows/${flow.id}/replay`, { method: 'POST' })
return { type: REQUEST_ACTION }
} }
/** /**
* @public * @public
*/ */
export function revert(flow) { export function revert(flow) {
fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' }) return dispatch => fetchApi(`/flows/${flow.id}/revert`, { method: 'POST' })
return { type: REQUEST_ACTION }
} }
/** /**
* @public * @public
*/ */
export function update(flow, data) { export function update(flow, data) {
fetchApi.put(`/flows/${flow.id}`, data) return dispatch => fetchApi.put(`/flows/${flow.id}`, data)
return { type: REQUEST_ACTION }
} }
export function updateContent(flow, file, type) { export function updateContent(flow, file, type) {
@ -124,8 +117,7 @@ export function updateContent(flow, file, type) {
if (typeof file !== File) if (typeof file !== File)
file = new Blob([file], {type: 'plain/text'}) file = new Blob([file], {type: 'plain/text'})
body.append('file', file) body.append('file', file)
fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} ) return dispatch => fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} )
return { type: REQUEST_ACTION }
} }
@ -133,8 +125,7 @@ export function updateContent(flow, file, type) {
* @public * @public
*/ */
export function clear() { export function clear() {
fetchApi('/clear', { method: 'POST' }) return dispatch => fetchApi('/clear', { method: 'POST' })
return { type: REQUEST_ACTION }
} }
/** /**
@ -151,8 +142,7 @@ export function download() {
export function upload(file) { export function upload(file) {
const body = new FormData() const body = new FormData()
body.append('file', file) body.append('file', file)
fetchApi('/flows/dump', { method: 'post', body }) return dispatch => fetchApi('/flows/dump', { method: 'post', body })
return { type: REQUEST_ACTION }
} }

View File

@ -4,7 +4,7 @@ import websocket from './websocket'
import flows from './flows' import flows from './flows'
import flowView from './flowView' import flowView from './flowView'
import settings from './settings' import settings from './settings'
import ui from './ui' import ui from './ui/index'
import msgQueue from './msgQueue' import msgQueue from './msgQueue'
export default combineReducers({ export default combineReducers({

View File

@ -11,22 +11,19 @@ export const REQUEST_UPDATE = 'REQUEST_UPDATE'
export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD' export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD'
const defaultState = { const defaultState = {
settings: {},
} }
export default function reducer(state = defaultState, action) { export default function reducer(state = defaultState, action) {
switch (action.type) { switch (action.type) {
case RECEIVE: case RECEIVE:
return { return action.settings
...state,
settings: action.settings,
}
case UPDATE: case UPDATE:
return { return {
...state, ...state,
settings: { ...state.settings, ...action.settings }, ...action.settings,
} }
default: default:

View File

@ -1,292 +0,0 @@
import { selectRelative as selectFlowRelative } from './flowView'
import { Key } from '../utils.js'
import * as flowsActions from './flows'
export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU'
export const SET_CONTENT_VIEW = 'UI_SET_CONTENT_VIEW'
export const SET_SELECTED_INPUT = 'UI_SET_SELECTED_INPUT'
export const UPDATE_QUERY = 'UI_UPDATE_QUERY'
export const SELECT_TAB = 'UI_SELECT_TAB'
export const SET_PROMPT = 'UI_SET_PROMPT'
export const SET_DISPLAY_LARGE = 'UI_SET_DISPLAY_LARGE'
export const OPEN_FLOW_EDITOR= 'UI_OPEN_FLOW_EDITOR'
export const CLOSE_FLOW_EDITOR = 'UI_CLOSE_FLOW_EDITOR'
export const SET_MODIFIED_FLOW_CONTENT = 'UI_SET_MODIFIED_FLOW'
const defaultState = {
activeMenu: 'Start',
isFlowSelected: false,
selectedInput: null,
displayLarge: false,
promptOpen: false,
contentView: 'ViewAuto',
query: {},
panel: 'request',
modifiedFlow: {headers: "", content: ""},
isFlowEditorOpen: false
}
export default function reducer(state = defaultState, action) {
switch (action.type) {
case SET_ACTIVE_MENU:
return {
...state,
activeMenu: action.activeMenu,
}
case flowsActions.SELECT:
let s = {...state, isFlowEditorOpen: false}
if (action.flowIds.length && !state.isFlowSelected) {
return {
...s,
displayLarge: false,
activeMenu: 'Flow',
isFlowSelected: true,
}
}
if (!action.flowIds.length && state.isFlowSelected) {
let activeMenu = state.activeMenu
if (activeMenu == 'Flow') {
activeMenu = 'Start'
}
return {
...s,
activeMenu,
isFlowSelected: false,
}
}
return {
...s,
displayLarge: false,
}
case SET_CONTENT_VIEW:
return {
...state,
contentView: action.contentView,
}
case SET_SELECTED_INPUT:
return {
...state,
selectedInput: action.input
}
case UPDATE_QUERY:
return {
...state,
query: { ...state.query, ...action.query }
}
case SELECT_TAB:
return {
...state,
isFlowEditorOpen: false,
panel: action.panel
}
case SET_PROMPT:
return {
...state,
promptOpen: action.open,
}
case SET_DISPLAY_LARGE:
return {
...state,
displayLarge: action.displayLarge,
}
case OPEN_FLOW_EDITOR:
return {
...state,
isFlowEditorOpen: true
}
case CLOSE_FLOW_EDITOR:
return {
...state,
isFlowEditorOpen: false
}
case SET_MODIFIED_FLOW_CONTENT:
return{
...state,
modifiedFlow: {...state.modifiedFlow, content: action.content}
}
default:
return state
}
}
export function setActiveMenu(activeMenu) {
return { type: SET_ACTIVE_MENU, activeMenu }
}
export function setContentView(contentView) {
return { type: SET_CONTENT_VIEW, contentView }
}
export function setSelectedInput(input) {
return { type: SET_SELECTED_INPUT, input }
}
export function updateQuery(query) {
return { type: UPDATE_QUERY, query }
}
export function selectTab(panel) {
return { type: SELECT_TAB, panel }
}
export function setPrompt(open) {
return { type: SET_PROMPT, open }
}
export function setDisplayLarge(displayLarge) {
return { type: SET_DISPLAY_LARGE, displayLarge }
}
export function openFlowEditor(){
return { type: OPEN_FLOW_EDITOR }
}
export function closeFlowEditor(){
return { type: CLOSE_FLOW_EDITOR }
}
export function setModifiedFlowContent(content) {
return { type: SET_MODIFIED_FLOW_CONTENT, content }
}
export function onKeyDown(e) {
if (e.ctrlKey) {
return () => {
}
}
var key = e.keyCode
var shiftKey = e.shiftKey
e.preventDefault()
return (dispatch, getState) => {
const flow = getState().flows.byId[getState().flows.selected[0]]
switch (key) {
case Key.I:
dispatch(setSelectedInput('intercept'))
break
case Key.L:
dispatch(setSelectedInput('search'))
break
case Key.H:
dispatch(setSelectedInput('highlight'))
break
case Key.K:
case Key.UP:
dispatch(selectFlowRelative(-1))
break
case Key.J:
case Key.DOWN:
dispatch(selectFlowRelative(+1))
break
case Key.SPACE:
case Key.PAGE_DOWN:
dispatch(selectFlowRelative(+10))
break
case Key.PAGE_UP:
dispatch(selectFlowRelative(-10))
break
case Key.END:
dispatch(selectFlowRelative(+1e10))
break
case Key.HOME:
dispatch(selectFlowRelative(-1e10))
break
case Key.ESC:
dispatch(flowsActions.select(null))
break
case Key.LEFT:
{
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.panel,
nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length]
dispatch(selectTab(nextTab))
break
}
case Key.TAB:
case Key.RIGHT:
{
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.panel,
nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length]
dispatch(selectTab(nextTab))
break
}
case Key.C:
if (shiftKey) {
dispatch(flowsActions.clear())
}
break
case Key.D:
{
if (!flow) {
return
}
if (shiftKey) {
dispatch(flowsActions.duplicate(flow))
} else {
dispatch(flowsActions.remove(flow))
}
break
}
case Key.A:
{
if (shiftKey) {
dispatch(flowsActions.acceptAll())
} else if (flow && flow.intercepted) {
dispatch(flowsActions.accept(flow))
}
break
}
case Key.R:
{
if (!shiftKey && flow) {
dispatch(flowsActions.replay(flow))
}
break
}
case Key.V:
{
if (!shiftKey && flow && flow.modified) {
dispatch(flowsActions.revert(flow))
}
break
}
case Key.E:
dispatch(setPrompt(true))
break
default:
return () => {
}
}
}
}

View File

@ -0,0 +1,97 @@
import * as flowsActions from '../flows'
import _ from 'lodash'
export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW',
DISPLAY_LARGE = 'UI_FLOWVIEW_DISPLAY_LARGE',
SET_TAB = "UI_FLOWVIEW_SET_TAB",
START_EDIT = 'UI_FLOWVIEW_START_EDIT',
UPDATE_EDIT = 'UI_FLOWVIEW_UPDATE_EDIT',
STOP_EDIT = 'UI_FLOWVIEW_STOP_EDIT'
const defaultState = {
displayLarge: false,
modifiedFlow: false,
contentView: 'ViewAuto',
tab: 'request',
}
export default function reducer(state = defaultState, action) {
switch (action.type) {
case START_EDIT:
return {
...state,
modifiedFlow: action.flow
}
case UPDATE_EDIT:
return {
...state,
modifiedFlow: _.merge({}, state.modifiedFlow, action.update)
}
case STOP_EDIT:
return {
...state,
modifiedFlow: false
}
case flowsActions.SELECT:
return {
...state,
modifiedFlow: false,
displayLarge: false,
}
case SET_TAB:
return {
...state,
tab: action.tab,
displayLarge: false,
}
case SET_CONTENT_VIEW:
return {
...state,
contentView: action.contentView,
}
case DISPLAY_LARGE:
return {
...state,
displayLarge: true,
}
default:
return state
}
}
export function setContentView(contentView) {
return { type: SET_CONTENT_VIEW, contentView }
}
export function displayLarge() {
return { type: DISPLAY_LARGE }
}
export function selectTab(tab) {
return { type: SET_TAB, tab }
}
export function startEdit(flow) {
return { type: START_EDIT, flow }
}
export function updateEdit(update) {
return { type: UPDATE_EDIT, update }
}
export function stopEdit(flow) {
return (dispatch) => {
dispatch(flowsActions.update(flow, flow)).then(() => {
dispatch(flowsActions.updateFlow(flow))
dispatch({ type: STOP_EDIT })
})
}
}

View File

@ -0,0 +1,50 @@
import * as flowsActions from '../flows'
export const SET_ACTIVE_MENU = 'UI_SET_ACTIVE_MENU'
const defaultState = {
activeMenu: 'Start',
isFlowSelected: false,
}
export default function reducer(state = defaultState, action) {
switch (action.type) {
case SET_ACTIVE_MENU:
return {
...state,
activeMenu: action.activeMenu,
}
case flowsActions.SELECT:
// First Select
if (action.flowIds.length && !state.isFlowSelected) {
return {
...state,
activeMenu: 'Flow',
isFlowSelected: true,
}
}
// Deselect
if (!action.flowIds.length && state.isFlowSelected) {
let activeMenu = state.activeMenu
if (activeMenu == 'Flow') {
activeMenu = 'Start'
}
return {
...state,
activeMenu,
isFlowSelected: false,
}
}
return state
default:
return state
}
}
export function setActiveMenu(activeMenu) {
return { type: SET_ACTIVE_MENU, activeMenu }
}

View File

@ -0,0 +1,8 @@
import { combineReducers } from 'redux'
import flow from './flow'
import header from './header'
export default combineReducers({
flow,
header,
})

View File

@ -0,0 +1,122 @@
import { Key } from '../../utils'
import { selectRelative as selectFlowRelative } from '../flowView'
import { selectTab } from './flow'
import * as flowsActions from '../flows'
export function onKeyDown(e) {
console.debug("onKeyDown", e)
if (e.ctrlKey) {
return () => {
}
}
var key = e.keyCode
var shiftKey = e.shiftKey
e.preventDefault()
return (dispatch, getState) => {
const flow = getState().flows.byId[getState().flows.selected[0]]
switch (key) {
case Key.K:
case Key.UP:
dispatch(selectFlowRelative(-1))
break
case Key.J:
case Key.DOWN:
dispatch(selectFlowRelative(+1))
break
case Key.SPACE:
case Key.PAGE_DOWN:
dispatch(selectFlowRelative(+10))
break
case Key.PAGE_UP:
dispatch(selectFlowRelative(-10))
break
case Key.END:
dispatch(selectFlowRelative(+1e10))
break
case Key.HOME:
dispatch(selectFlowRelative(-1e10))
break
case Key.ESC:
dispatch(flowsActions.select(null))
break
case Key.LEFT:
{
if(!flow) break
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.flow.tab,
nextTab = tabs[(tabs.indexOf(currentTab) - 1 + tabs.length) % tabs.length]
dispatch(selectTab(nextTab))
break
}
case Key.TAB:
case Key.RIGHT:
{
if(!flow) break
let tabs = ['request', 'response', 'error'].filter(k => flow[k]).concat(['details']),
currentTab = getState().ui.flow.tab,
nextTab = tabs[(tabs.indexOf(currentTab) + 1) % tabs.length]
dispatch(selectTab(nextTab))
break
}
case Key.C:
if (shiftKey) {
dispatch(flowsActions.clear())
}
break
case Key.D:
{
if (!flow) {
return
}
if (shiftKey) {
dispatch(flowsActions.duplicate(flow))
} else {
dispatch(flowsActions.remove(flow))
}
break
}
case Key.A:
{
if (shiftKey) {
dispatch(flowsActions.acceptAll())
} else if (flow && flow.intercepted) {
dispatch(flowsActions.accept(flow))
}
break
}
case Key.R:
{
if (!shiftKey && flow) {
dispatch(flowsActions.replay(flow))
}
break
}
case Key.V:
{
if (!shiftKey && flow && flow.modified) {
dispatch(flowsActions.revert(flow))
}
break
}
default:
return
}
}
}

View File

@ -107,10 +107,3 @@ var isValidHttpVersion_regex = /^HTTP\/\d+(\.\d+)*$/i;
export var isValidHttpVersion = function (httpVersion) { export var isValidHttpVersion = function (httpVersion) {
return isValidHttpVersion_regex.test(httpVersion); return isValidHttpVersion_regex.test(httpVersion);
}; };
export var parseHttpVersion = function (httpVersion) {
httpVersion = httpVersion.replace("HTTP/", "").split(".");
return _.map(httpVersion, function (x) {
return parseInt(x);
});
};