aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/__tests__/ducks/ui/flowSpec.js76
-rw-r--r--web/src/js/__tests__/ducks/utils/listSpec.js1
-rw-r--r--web/src/js/app.jsx10
-rw-r--r--web/src/js/components/ContentView/CodeEditor.jsx3
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx33
-rw-r--r--web/src/js/components/ContentView/ShowFullContentButton.jsx2
-rw-r--r--web/src/js/components/ContentView/UploadContentButton.jsx24
-rw-r--r--web/src/js/components/ContentView/ViewSelector.jsx80
-rw-r--r--web/src/js/components/Footer.jsx33
-rw-r--r--web/src/js/components/Header/FileMenu.jsx133
-rw-r--r--web/src/js/components/Header/FlowMenu.jsx6
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx6
-rw-r--r--web/src/js/components/MainView.jsx6
-rw-r--r--web/src/js/components/ProxyApp.jsx84
-rw-r--r--web/src/js/components/common/Dropdown.jsx53
-rw-r--r--web/src/js/components/common/FileChooser.jsx27
-rw-r--r--web/src/js/components/common/ToggleInputButton.jsx30
-rw-r--r--web/src/js/ducks/flows.js12
-rw-r--r--web/src/js/ducks/ui/flow.js26
-rw-r--r--web/src/js/utils.js7
20 files changed, 373 insertions, 279 deletions
diff --git a/web/src/js/__tests__/ducks/ui/flowSpec.js b/web/src/js/__tests__/ducks/ui/flowSpec.js
new file mode 100644
index 00000000..f838fbaa
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/flowSpec.js
@@ -0,0 +1,76 @@
+jest.unmock('../../../ducks/ui/flow')
+jest.unmock('../../../ducks/flows')
+jest.unmock('lodash')
+
+import _ from 'lodash'
+import reducer, {
+ startEdit,
+ setContentViewDescription,
+ setShowFullContent,
+ setContent,
+ updateEdit
+ } from '../../../ducks/ui/flow'
+
+import { select, updateFlow } from '../../../ducks/flows'
+
+describe('flow reducer', () => {
+ it('should change to edit mode', () => {
+ let testFlow = {flow : 'foo'}
+ const newState = reducer(undefined, startEdit({ flow: 'foo' }))
+ expect(newState.contentView).toEqual('Edit')
+ expect(newState.modifiedFlow).toEqual(testFlow)
+ expect(newState.showFullContent).toEqual(true)
+ })
+ it('should set the view description', () => {
+ expect(reducer(undefined, setContentViewDescription('description')).viewDescription)
+ .toEqual('description')
+ })
+
+ it('should set show full content', () => {
+ expect(reducer({showFullContent: false}, setShowFullContent()).showFullContent)
+ .toBeTruthy()
+ })
+
+ it('should set showFullContent to true', () => {
+ let maxLines = 10
+ let content = _.range(maxLines)
+ const newState = reducer({maxContentLines: maxLines}, setContent(content) )
+ expect(newState.showFullContent).toBeTruthy()
+ expect(newState.content).toEqual(content)
+ })
+
+ it('should set showFullContent to false', () => {
+ let maxLines = 5
+ let content = _.range(maxLines+1);
+ const newState = reducer({maxContentLines: maxLines}, setContent(_.range(maxLines+1)))
+ expect(newState.showFullContent).toBeFalsy()
+ expect(newState.content).toEqual(content)
+ })
+
+ it('should not change the contentview mode', () => {
+ expect(reducer({contentView: 'foo'}, select(1)).contentView).toEqual('foo')
+ })
+
+ it('should change the contentview mode to auto after editing when a new flow will be selected', () => {
+ expect(reducer({contentView: 'foo', modifiedFlow : 'test_flow'}, select(1)).contentView).toEqual('Auto')
+ })
+
+ it('should set update and merge the modifiedflow with the update values', () => {
+ let modifiedFlow = {headers: []}
+ let updateValues = {content: 'bar'}
+ let result = {headers: [], content: 'bar'}
+ expect(reducer({modifiedFlow}, updateEdit(updateValues)).modifiedFlow).toEqual(result)
+ })
+
+ it('should not change the state when a flow is updated which is not selected', () => {
+ let modifiedFlow = {id: 1}
+ let updatedFlow = {id: 0}
+ expect(reducer({modifiedFlow}, updateFlow(updatedFlow)).modifiedFlow).toEqual(modifiedFlow)
+ })
+
+ it('should stop editing when the selected flow is updated', () => {
+ let modifiedFlow = {id: 1}
+ let updatedFlow = {id: 1}
+ expect(reducer({modifiedFlow}, updateFlow(updatedFlow)).modifiedFlow).toBeFalsy()
+ })
+})
diff --git a/web/src/js/__tests__/ducks/utils/listSpec.js b/web/src/js/__tests__/ducks/utils/listSpec.js
index 72d162f2..0f5d0f34 100644
--- a/web/src/js/__tests__/ducks/utils/listSpec.js
+++ b/web/src/js/__tests__/ducks/utils/listSpec.js
@@ -2,6 +2,7 @@ jest.unmock('lodash')
jest.unmock('../../../ducks/utils/list')
import reduce, * as list from '../../../ducks/utils/list'
+import _ from 'lodash'
describe('list reduce', () => {
diff --git a/web/src/js/app.jsx b/web/src/js/app.jsx
index 726b2ae1..f04baea0 100644
--- a/web/src/js/app.jsx
+++ b/web/src/js/app.jsx
@@ -3,10 +3,8 @@ import { render } from 'react-dom'
import { applyMiddleware, createStore } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
-import { Route, Router as ReactRouter, hashHistory, Redirect } from 'react-router'
import ProxyApp from './components/ProxyApp'
-import MainView from './components/MainView'
import rootReducer from './ducks/index'
import { add as addLog } from './ducks/eventLog'
@@ -32,13 +30,7 @@ window.addEventListener('error', msg => {
document.addEventListener('DOMContentLoaded', () => {
render(
<Provider store={store}>
- <ReactRouter history={hashHistory}>
- <Redirect from="/" to="/flows" />
- <Route path="/" component={ProxyApp}>
- <Route path="flows" component={MainView}/>
- <Route path="flows/:flowId/:detailTab" component={MainView}/>
- </Route>
- </ReactRouter>
+ <ProxyApp />
</Provider>,
document.getElementById("mitmproxy")
)
diff --git a/web/src/js/components/ContentView/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx
index d0430e6f..8afc128f 100644
--- a/web/src/js/components/ContentView/CodeEditor.jsx
+++ b/web/src/js/components/ContentView/CodeEditor.jsx
@@ -1,5 +1,4 @@
-import React, { Component, PropTypes } from 'react'
-import { render } from 'react-dom';
+import React, {PropTypes} from 'react'
import Codemirror from 'react-codemirror';
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
index cd593023..32a07564 100644
--- a/web/src/js/components/ContentView/ContentViews.jsx
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -30,6 +30,12 @@ function Edit({ content, onChange }) {
Edit = ContentLoader(Edit)
class ViewServer extends Component {
+ static propTypes = {
+ showFullContent: PropTypes.bool.isRequired,
+ maxLines: PropTypes.number.isRequired,
+ setContentViewDescription : PropTypes.func.isRequired,
+ setContent: PropTypes.func.isRequired
+ }
componentWillMount(){
this.setContentView(this.props)
@@ -40,6 +46,7 @@ class ViewServer extends Component {
this.setContentView(nextProps)
}
}
+
setContentView(props){
try {
this.data = JSON.parse(props.content)
@@ -50,25 +57,31 @@ class ViewServer extends Component {
props.setContentViewDescription(props.contentView != this.data.description ? this.data.description : '')
props.setContent(this.data.lines)
}
+
render() {
const {content, contentView, message, maxLines} = this.props
let lines = this.props.showFullContent ? this.data.lines : this.data.lines.slice(0, maxLines)
- return <div>
+ return (
+ <div>
<pre>
{lines.map((line, i) =>
<div key={`line${i}`}>
- {line.map((tuple, j) =>
- <span key={`tuple${j}`} className={tuple[0]}>
- {tuple[1]}
- </span>
- )}
+ {line.map((element, j) => {
+ let [style, text] = element
+ return (
+ <span key={`tuple${j}`} className={style}>
+ {text}
+ </span>
+ )
+ })}
</div>
)}
</pre>
- {ViewImage.matches(message) &&
- <ViewImage {...this.props} />
- }
- </div>
+ {ViewImage.matches(message) &&
+ <ViewImage {...this.props} />
+ }
+ </div>
+ )
}
}
diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx
index 676068e9..cfd96dd8 100644
--- a/web/src/js/components/ContentView/ShowFullContentButton.jsx
+++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx
@@ -16,7 +16,7 @@ function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLi
return (
!showFullContent &&
<div>
- <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent(true)} text="Show full content"/>
+ <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent()} text="Show full content"/>
<span className="pull-right"> {visibleLines}/{contentLines} are visible &nbsp; </span>
</div>
)
diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx
index 0652b584..de349af4 100644
--- a/web/src/js/components/ContentView/UploadContentButton.jsx
+++ b/web/src/js/components/ContentView/UploadContentButton.jsx
@@ -1,28 +1,18 @@
import { PropTypes } from 'react'
+import FileChooser from '../common/FileChooser'
UploadContentButton.propTypes = {
uploadContent: PropTypes.func.isRequired,
}
export default function UploadContentButton({ uploadContent }) {
-
- let fileInput;
-
+
return (
- <a className="btn btn-default btn-xs"
- onClick={() => fileInput.click()}
- title="Upload a file to replace the content.">
- <i className="fa fa-upload"/>
- <input
- ref={ref => fileInput = ref}
- className="hidden"
- type="file"
- onChange={e => {
- if (e.target.files.length > 0) uploadContent(e.target.files[0])
- }}
- />
- </a>
-
+ <FileChooser
+ icon="fa-upload"
+ title="Upload a file to replace the content."
+ onOpenFile={uploadContent}
+ className="btn btn-default btn-xs"/>
)
}
diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx
index 59ec4276..ab433ea3 100644
--- a/web/src/js/components/ContentView/ViewSelector.jsx
+++ b/web/src/js/components/ContentView/ViewSelector.jsx
@@ -1,72 +1,36 @@
import React, { PropTypes, Component } from 'react'
-import classnames from 'classnames'
import { connect } from 'react-redux'
import * as ContentViews from './ContentViews'
-import { setContentView } from "../../ducks/ui/flow";
-
-function ViewItem({ name, setContentView, children }) {
- return (
- <li>
- <a href="#" onClick={() => setContentView(name)}>
- {children}
- </a>
- </li>
- )
-}
+import { setContentView } from '../../ducks/ui/flow';
+import Dropdown from '../common/Dropdown'
-/*ViewSelector.propTypes = {
+ViewSelector.propTypes = {
contentViews: PropTypes.array.isRequired,
activeView: PropTypes.string.isRequired,
isEdit: PropTypes.bool.isRequired,
- isContentViewSelectorOpen: PropTypes.bool.isRequired,
- setContentViewSelectorOpen: PropTypes.func.isRequired
-}*/
-
-
-class ViewSelector extends Component {
- constructor(props, context) {
- super(props, context)
- this.close = this.close.bind(this)
- this.state = {open: false}
- }
- close() {
- this.setState({open: false})
- document.removeEventListener('click', this.close)
- }
-
- onDropdown(e){
- e.preventDefault()
- this.setState({open: !this.state.open})
- document.addEventListener('click', this.close)
- }
+ setContentView: PropTypes.func.isRequired
+}
- render() {
- const {contentViews, activeView, isEdit, setContentView} = this.props
- let edit = ContentViews.Edit.displayName
+function ViewSelector ({contentViews, activeView, isEdit, setContentView}){
+ let edit = ContentViews.Edit.displayName
+ let inner = <span> <b>View:</b> {activeView}<span className="caret"></span> </span>
- return (
- <div className={classnames('dropup pull-left', { open: this.state.open })}>
- <a className="btn btn-default btn-xs"
- onClick={ e => this.onDropdown(e) }
- href="#">
- <b>View:</b> {activeView}<span className="caret"></span>
+ return (
+ <Dropdown dropup className="pull-left" btnClass="btn btn-default btn-xs" text={inner}>
+ {contentViews.map(name =>
+ <a href="#" key={name} onClick={e => {e.preventDefault(); setContentView(name)}}>
+ {name.toLowerCase().replace('_', ' ')}
</a>
- <ul className="dropdown-menu" role="menu">
- {contentViews.map(name =>
- <ViewItem key={name} setContentView={setContentView} name={name}>
- {name.toLowerCase().replace('_', ' ')}
- </ViewItem>
- )}
- {isEdit &&
- <ViewItem key={edit} setContentView={setContentView} name={edit}>
- {edit.toLowerCase()}
- </ViewItem>
- }
- </ul>
- </div>
- )
- }
+ )
+ }
+ {isEdit &&
+ <a href="#" onClick={e => {e.preventDefault(); setContentView(edit)}}>
+ {edit.toLowerCase()}
+ </a>
+ }
+ </Dropdown>
+ )
}
export default connect (
diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx
index 2bda70e1..96e7b7db 100644
--- a/web/src/js/components/Footer.jsx
+++ b/web/src/js/components/Footer.jsx
@@ -7,40 +7,41 @@ Footer.propTypes = {
}
function Footer({ settings }) {
+ let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickyauth, stickycookie, stream} = settings;
return (
<footer>
- {settings.mode && settings.mode != "regular" && (
- <span className="label label-success">{settings.mode} mode</span>
+ {mode && mode != "regular" && (
+ <span className="label label-success">{mode} mode</span>
)}
- {settings.intercept && (
- <span className="label label-success">Intercept: {settings.intercept}</span>
+ {intercept && (
+ <span className="label label-success">Intercept: {intercept}</span>
)}
- {settings.showhost && (
+ {showhost && (
<span className="label label-success">showhost</span>
)}
- {settings.no_upstream_cert && (
+ {no_upstream_cert && (
<span className="label label-success">no-upstream-cert</span>
)}
- {settings.rawtcp && (
+ {rawtcp && (
<span className="label label-success">raw-tcp</span>
)}
- {!settings.http2 && (
+ {!http2 && (
<span className="label label-success">no-http2</span>
)}
- {settings.anticache && (
+ {anticache && (
<span className="label label-success">anticache</span>
)}
- {settings.anticomp && (
+ {anticomp && (
<span className="label label-success">anticomp</span>
)}
- {settings.stickyauth && (
- <span className="label label-success">stickyauth: {settings.stickyauth}</span>
+ {stickyauth && (
+ <span className="label label-success">stickyauth: {stickyauth}</span>
)}
- {settings.stickycookie && (
- <span className="label label-success">stickycookie: {settings.stickycookie}</span>
+ {stickycookie && (
+ <span className="label label-success">stickycookie: {stickycookie}</span>
)}
- {settings.stream && (
- <span className="label label-success">stream: {formatSize(settings.stream)}</span>
+ {stream && (
+ <span className="label label-success">stream: {formatSize(stream)}</span>
)}
</footer>
)
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
index d3786475..53c63ea1 100644
--- a/web/src/js/components/Header/FileMenu.jsx
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -1,103 +1,46 @@
-import React, { Component } from 'react'
+import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
-import classnames from 'classnames'
+import FileChooser from '../common/FileChooser'
+import Dropdown, {Divider} from '../common/Dropdown'
import * as flowsActions from '../../ducks/flows'
-class FileMenu extends Component {
-
- constructor(props, context) {
- super(props, context)
- this.state = { show: false }
-
- this.close = this.close.bind(this)
- this.onFileClick = this.onFileClick.bind(this)
- this.onNewClick = this.onNewClick.bind(this)
- this.onOpenClick = this.onOpenClick.bind(this)
- this.onOpenFile = this.onOpenFile.bind(this)
- this.onSaveClick = this.onSaveClick.bind(this)
- }
-
- close() {
- this.setState({ show: false })
- document.removeEventListener('click', this.close)
- }
-
- onFileClick(e) {
- e.preventDefault()
-
- if (this.state.show) {
- return
- }
-
- document.addEventListener('click', this.close)
- this.setState({ show: true })
- }
-
- onNewClick(e) {
- e.preventDefault()
- if (confirm('Delete all flows?')) {
- this.props.clearFlows()
- }
- }
-
- onOpenClick(e) {
- e.preventDefault()
- this.fileInput.click()
- }
-
- onOpenFile(e) {
- e.preventDefault()
- if (e.target.files.length > 0) {
- this.props.loadFlows(e.target.files[0])
- this.fileInput.value = ''
- }
- }
+FileMenu.propTypes = {
+ clearFlows: PropTypes.func.isRequired,
+ loadFlows: PropTypes.func.isRequired,
+ saveFlows: PropTypes.func.isRequired
+}
- onSaveClick(e) {
- e.preventDefault()
- this.props.saveFlows()
- }
+FileMenu.onNewClick = (e, clearFlows) => {
+ e.preventDefault();
+ if (confirm('Delete all flows?'))
+ clearFlows()
+}
- render() {
- return (
- <div className={classnames('dropdown pull-left', { open: this.state.show })}>
- <a href="#" className="special" onClick={this.onFileClick}>mitmproxy</a>
- <ul className="dropdown-menu" role="menu">
- <li>
- <a href="#" onClick={this.onNewClick}>
- <i className="fa fa-fw fa-file"></i>
- New
- </a>
- </li>
- <li>
- <a href="#" onClick={this.onOpenClick}>
- <i className="fa fa-fw fa-folder-open"></i>
- Open...
- </a>
- <input
- ref={ref => this.fileInput = ref}
- className="hidden"
- type="file"
- onChange={this.onOpenFile}
- />
- </li>
- <li>
- <a href="#" onClick={this.onSaveClick}>
- <i className="fa fa-fw fa-floppy-o"></i>
- Save...
- </a>
- </li>
- <li role="presentation" className="divider"></li>
- <li>
- <a href="http://mitm.it/" target="_blank">
- <i className="fa fa-fw fa-external-link"></i>
- Install Certificates...
- </a>
- </li>
- </ul>
- </div>
- )
- }
+function FileMenu ({clearFlows, loadFlows, saveFlows}) {
+ return (
+ <Dropdown className="pull-left" btnClass="special" text="mitmproxy">
+ <a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}>
+ <i className="fa fa-fw fa-file"></i>
+ New
+ </a>
+ <FileChooser
+ icon="fa-folder-open"
+ text="Open..."
+ onOpenFile={file => loadFlows(file)}
+ />
+ <a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}>
+ <i className="fa fa-fw fa-floppy-o"></i>
+ Save...
+ </a>
+
+ <Divider/>
+
+ <a href="http://mitm.it/" target="_blank">
+ <i className="fa fa-fw fa-external-link"></i>
+ Install Certificates...
+ </a>
+ </Dropdown>
+ )
}
export default connect(
diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx
index bdd30d5e..e78a49aa 100644
--- a/web/src/js/components/Header/FlowMenu.jsx
+++ b/web/src/js/components/Header/FlowMenu.jsx
@@ -8,10 +8,14 @@ FlowMenu.title = 'Flow'
FlowMenu.propTypes = {
flow: PropTypes.object.isRequired,
+ acceptFlow: PropTypes.func.isRequired,
+ replayFlow: PropTypes.func.isRequired,
+ duplicateFlow: PropTypes.func.isRequired,
+ removeFlow: PropTypes.func.isRequired,
+ revertFlow: PropTypes.func.isRequired
}
function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) {
-
return (
<div>
<div className="menu-row">
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
index a338fed0..a11062f2 100644
--- a/web/src/js/components/Header/OptionMenu.jsx
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -41,17 +41,17 @@ function OptionMenu({ settings, updateSettings }) {
/>
<ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
checked={!!settings.stickyauth}
- txt={settings.stickyauth || ''}
+ txt={settings.stickyauth}
onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })}
/>
<ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
checked={!!settings.stickycookie}
- txt={settings.stickycookie || ''}
+ txt={settings.stickycookie}
onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })}
/>
<ToggleInputButton name="stream" placeholder="stream..."
checked={!!settings.stream}
- txt={settings.stream || ''}
+ txt={settings.stream}
inputType="number"
onToggleChanged={txt => updateSettings({ stream: !settings.stream ? txt : null })}
/>
diff --git a/web/src/js/components/MainView.jsx b/web/src/js/components/MainView.jsx
index f45f9eef..8be6f21c 100644
--- a/web/src/js/components/MainView.jsx
+++ b/web/src/js/components/MainView.jsx
@@ -29,8 +29,7 @@ class MainView extends Component {
<FlowView
key="flowDetails"
ref="flowDetails"
- tab={this.props.routeParams.detailTab}
- query={this.props.query}
+ tab={this.props.tab}
updateFlow={data => this.props.updateFlow(selectedFlow, data)}
flow={selectedFlow}
/>
@@ -45,7 +44,8 @@ export default connect(
flows: state.flowView.data,
filter: state.flowView.filter,
highlight: state.flowView.highlight,
- selectedFlow: state.flows.byId[state.flows.selected[0]]
+ selectedFlow: state.flows.byId[state.flows.selected[0]],
+ tab: state.ui.flow.tab,
}),
{
selectFlow: flowsActions.select,
diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx
index f8a6e262..d76816e5 100644
--- a/web/src/js/components/ProxyApp.jsx
+++ b/web/src/js/components/ProxyApp.jsx
@@ -1,58 +1,77 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
+import { createHashHistory, useQueries } from 'history'
import { init as appInit, destruct as appDestruct } from '../ducks/app'
import { onKeyDown } from '../ducks/ui/keyboard'
+import { updateFilter, updateHighlight } from '../ducks/flowView'
+import { selectTab } from '../ducks/ui/flow'
+import { select as selectFlow } from '../ducks/flows'
+import { Query } from '../actions'
+import MainView from './MainView'
import Header from './Header'
import EventLog from './EventLog'
import Footer from './Footer'
class ProxyAppMain extends Component {
- static contextTypes = {
- router: PropTypes.object.isRequired,
+ flushToStore(location) {
+ const components = location.pathname.split('/').filter(v => v)
+ const query = location.query || {}
+
+ if (components.length > 2) {
+ this.props.selectFlow(components[1])
+ this.props.selectTab(components[2])
+ } else {
+ this.props.selectFlow(null)
+ this.props.selectTab(null)
+ }
+
+ this.props.updateFilter(query[Query.SEARCH])
+ this.props.updateHighlight(query[Query.HIGHLIGHT])
+ }
+
+ flushToHistory(props) {
+ const query = { ...query }
+
+ if (props.filter) {
+ query[Query.SEARCH] = props.filter
+ }
+
+ if (props.highlight) {
+ query[Query.HIGHLIGHT] = props.highlight
+ }
+
+ if (props.selectedFlowId) {
+ this.history.push({ pathname: `/flows/${props.selectedFlowId}/${props.tab}`, query })
+ } else {
+ this.history.push({ pathname: '/flows', query })
+ }
}
componentWillMount() {
- this.props.appInit(this.context.router)
+ this.props.appInit()
+ this.history = useQueries(createHashHistory)()
+ this.unlisten = this.history.listen(location => this.flushToStore(location))
window.addEventListener('keydown', this.props.onKeyDown);
}
componentWillUnmount() {
- this.props.appDestruct(this.context.router)
+ this.props.appDestruct()
+ this.unlisten()
window.removeEventListener('keydown', this.props.onKeyDown);
}
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) {
- return
- }
- if (nextProps.selectedFlowId) {
- this.context.router.replace({ pathname: `/flows/${nextProps.selectedFlowId}/${nextProps.panel}`, query: nextProps.query })
- } else {
- this.context.router.replace({ pathname: '/flows', query: nextProps.query })
- }
-
+ this.flushToHistory(nextProps)
}
render() {
- const { showEventLog, location, children, query } = this.props
+ const { showEventLog, location, filter, highlight } = this.props
return (
<div id="container" tabIndex="0">
<Header/>
- {React.cloneElement(
- children,
- { ref: 'view', location, query }
- )}
+ <MainView />
{showEventLog && (
<EventLog key="eventlog"/>
)}
@@ -65,13 +84,18 @@ class ProxyAppMain extends Component {
export default connect(
state => ({
showEventLog: state.eventLog.visible,
- query: state.flowView.filter,
- panel: state.ui.flow.tab,
+ filter: state.flowView.filter,
+ highlight: state.flowView.highlight,
+ tab: state.ui.flow.tab,
selectedFlowId: state.flows.selected[0]
}),
{
appInit,
appDestruct,
- onKeyDown
+ onKeyDown,
+ updateFilter,
+ updateHighlight,
+ selectTab,
+ selectFlow
}
)(ProxyAppMain)
diff --git a/web/src/js/components/common/Dropdown.jsx b/web/src/js/components/common/Dropdown.jsx
new file mode 100644
index 00000000..cc95a6dc
--- /dev/null
+++ b/web/src/js/components/common/Dropdown.jsx
@@ -0,0 +1,53 @@
+import React, { Component, PropTypes } from 'react'
+import classnames from 'classnames'
+
+export const Divider = () => <hr className="divider"/>
+
+export default class Dropdown extends Component {
+
+ static propTypes = {
+ dropup: PropTypes.bool,
+ className: PropTypes.string,
+ btnClass: PropTypes.string.isRequired
+ }
+
+ static defaultProps = {
+ dropup: false
+ }
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { open: false }
+ this.close = this.close.bind(this)
+ this.open = this.open.bind(this)
+ }
+
+ close() {
+ this.setState({ open: false })
+ document.removeEventListener('click', this.close)
+ }
+
+ open(e){
+ e.preventDefault()
+ if (this.state.open) {
+ return
+ }
+ this.setState({open: !this.state.open})
+ document.addEventListener('click', this.close)
+ }
+
+ render() {
+ const {dropup, className, btnClass, text, children} = this.props
+ return (
+ <div className={classnames( (dropup ? 'dropup' : 'dropdown'), className, { open: this.state.open })}>
+ <a href='#' className={btnClass}
+ onClick={this.open}>
+ {text}
+ </a>
+ <ul className="dropdown-menu" role="menu">
+ {children.map ( (item, i) => <li key={i}> {item} </li> )}
+ </ul>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/common/FileChooser.jsx b/web/src/js/components/common/FileChooser.jsx
new file mode 100644
index 00000000..d59d2d6d
--- /dev/null
+++ b/web/src/js/components/common/FileChooser.jsx
@@ -0,0 +1,27 @@
+import React, { PropTypes } from 'react'
+
+FileChooser.propTypes = {
+ icon: PropTypes.string,
+ text: PropTypes.string,
+ className: PropTypes.string,
+ title: PropTypes.string,
+ onOpenFile: PropTypes.func.isRequired
+}
+
+export default function FileChooser({ icon, text, className, title, onOpenFile }) {
+ let fileInput;
+ return (
+ <a href='#' onClick={() => fileInput.click()}
+ className={className}
+ title={title}>
+ <i className={'fa fa-fw ' + icon}></i>
+ {text}
+ <input
+ ref={ref => fileInput = ref}
+ className="hidden"
+ type="file"
+ onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput = "";}}
+ />
+ </a>
+ )
+}
diff --git a/web/src/js/components/common/ToggleInputButton.jsx b/web/src/js/components/common/ToggleInputButton.jsx
index 25d620ae..5fa24c10 100644
--- a/web/src/js/components/common/ToggleInputButton.jsx
+++ b/web/src/js/components/common/ToggleInputButton.jsx
@@ -6,17 +6,16 @@ export default class ToggleInputButton extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
- txt: PropTypes.string.isRequired,
- onToggleChanged: PropTypes.func.isRequired
+ txt: PropTypes.string,
+ onToggleChanged: PropTypes.func.isRequired,
+ checked: PropTypes.bool.isRequired,
+ placeholder: PropTypes.string.isRequired,
+ inputType: PropTypes.string
}
constructor(props) {
super(props)
- this.state = { txt: props.txt }
- }
-
- onChange(e) {
- this.setState({ txt: e.target.value })
+ this.state = { txt: props.txt || '' }
}
onKeyDown(e) {
@@ -27,23 +26,24 @@ export default class ToggleInputButton extends Component {
}
render() {
+ const {checked, onToggleChanged, name, inputType, placeholder} = this.props
return (
<div className="input-group toggle-input-btn">
<span className="input-group-btn"
- onClick={() => this.props.onToggleChanged(this.state.txt)}>
- <div className={classnames('btn', this.props.checked ? 'btn-primary' : 'btn-default')}>
- <span className={classnames('fa', this.props.checked ? 'fa-check-square-o' : 'fa-square-o')}/>
+ onClick={() => onToggleChanged(this.state.txt)}>
+ <div className={classnames('btn', checked ? 'btn-primary' : 'btn-default')}>
+ <span className={classnames('fa', checked ? 'fa-check-square-o' : 'fa-square-o')}/>
&nbsp;
- {this.props.name}
+ {name}
</div>
</span>
<input
className="form-control"
- placeholder={this.props.placeholder}
- disabled={this.props.checked}
+ placeholder={placeholder}
+ disabled={checked}
value={this.state.txt}
- type={this.props.inputType}
- onChange={e => this.onChange(e)}
+ type={inputType || 'text'}
+ onChange={e => this.setState({ txt: e.target.value })}
onKeyDown={e => this.onKeyDown(e)}
/>
</div>
diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js
index f96653a9..404db0d1 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -1,5 +1,6 @@
import { fetchApi } from '../utils'
import reduceList, * as listActions from './utils/list'
+import { selectRelative } from './flowView'
import * as msgQueueActions from './msgQueue'
import * as websocketActions from './websocket'
@@ -210,5 +211,14 @@ export function updateFlow(item) {
* @private
*/
export function removeFlow(id) {
- return { type: REMOVE, id }
+ return (dispatch, getState) => {
+ let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]]
+ let maxIndex = getState().flowView.data.length - 1
+ let deleteLastEntry = maxIndex == 0
+ if (deleteLastEntry)
+ dispatch(select())
+ else
+ dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) )
+ dispatch({ type: REMOVE, id })
+ }
}
diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js
index fb2a846d..4a6d64cd 100644
--- a/web/src/js/ducks/ui/flow.js
+++ b/web/src/js/ducks/ui/flow.js
@@ -16,7 +16,7 @@ export const SET_CONTENT_VIEW = 'UI_FLOWVIEW_SET_CONTENT_VIEW',
const defaultState = {
displayLarge: false,
- contentViewDescription: '',
+ viewDescription: '',
showFullContent: false,
modifiedFlow: false,
contentView: 'Auto',
@@ -27,6 +27,10 @@ const defaultState = {
export default function reducer(state = defaultState, action) {
let wasInEditMode = !!(state.modifiedFlow)
+
+ let content = action.content || state.content
+ let isFullContentShown = content && content.length <= state.maxContentLines
+
switch (action.type) {
case START_EDIT:
@@ -49,8 +53,7 @@ export default function reducer(state = defaultState, action) {
modifiedFlow: false,
displayLarge: false,
contentView: (wasInEditMode ? 'Auto' : state.contentView),
- viewDescription: '',
- showFullContent: false,
+ showFullContent: isFullContentShown,
}
case flowsActions.UPDATE:
@@ -63,7 +66,6 @@ export default function reducer(state = defaultState, action) {
modifiedFlow: false,
displayLarge: false,
contentView: (wasInEditMode ? 'Auto' : state.contentView),
- viewDescription: '',
showFullContent: false
}
} else {
@@ -79,13 +81,13 @@ export default function reducer(state = defaultState, action) {
case SET_SHOW_FULL_CONTENT:
return {
...state,
- showFullContent: action.show
+ showFullContent: true
}
case SET_TAB:
return {
...state,
- tab: action.tab,
+ tab: action.tab ? action.tab : 'request',
displayLarge: false,
showFullContent: false
}
@@ -98,7 +100,6 @@ export default function reducer(state = defaultState, action) {
}
case SET_CONTENT:
- let isFullContentShown = action.content.length < state.maxContentLines
return {
...state,
content: action.content,
@@ -139,12 +140,8 @@ export function setContentViewDescription(description) {
return { type: SET_CONTENT_VIEW_DESCRIPTION, description }
}
-export function setShowFullContent(show) {
- return { type: SET_SHOW_FULL_CONTENT, show }
-}
-
-export function updateEdit(update) {
- return { type: UPDATE_EDIT, update }
+export function setShowFullContent() {
+ return { type: SET_SHOW_FULL_CONTENT }
}
export function setContent(content){
@@ -152,6 +149,5 @@ export function setContent(content){
}
export function stopEdit(flow, modifiedFlow) {
- let diff = getDiff(flow, modifiedFlow)
- return flowsActions.update(flow, diff)
+ return flowsActions.update(flow, getDiff(flow, modifiedFlow))
}
diff --git a/web/src/js/utils.js b/web/src/js/utils.js
index e44182d0..e8470cec 100644
--- a/web/src/js/utils.js
+++ b/web/src/js/utils.js
@@ -107,14 +107,15 @@ fetchApi.put = (url, json, options) => fetchApi(
...options
}
)
-
+// deep comparison of two json objects (dicts). arrays are handeled as a single value.
+// return: json object including only the changed keys value pairs.
export function getDiff(obj1, obj2) {
let result = {...obj2};
for(let key in obj1) {
if(_.isEqual(obj2[key], obj1[key]))
result[key] = undefined
- else if(!(Array.isArray(obj2[key]) && Array.isArray(obj1[key])) &&
- typeof obj2[key] == 'object' && typeof obj1[key] == 'object')
+ else if(Object.prototype.toString.call(obj2[key]) === '[object Object]' &&
+ Object.prototype.toString.call(obj1[key]) === '[object Object]' )
result[key] = getDiff(obj1[key], obj2[key])
}
return result