aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/package.json3
-rw-r--r--web/src/css/app.less3
-rw-r--r--web/src/css/codemirror.less9
-rw-r--r--web/src/css/flowdetail.less2
-rw-r--r--web/src/css/flowview.less31
-rw-r--r--web/src/js/components/ContentView.jsx54
-rw-r--r--web/src/js/components/ContentView/ContentViews.jsx1
-rw-r--r--web/src/js/components/FlowView/FlowEditorButton.jsx39
-rw-r--r--web/src/js/components/FlowView/Messages.jsx22
-rw-r--r--web/src/js/components/common/Button.jsx8
-rw-r--r--web/src/js/components/common/CodeEditor.jsx30
-rw-r--r--web/src/js/components/common/Splitter.jsx1
-rw-r--r--web/src/js/ducks/flows.js11
-rw-r--r--web/src/js/ducks/ui.js43
14 files changed, 226 insertions, 31 deletions
diff --git a/web/package.json b/web/package.json
index 55106cb8..81b96adc 100644
--- a/web/package.json
+++ b/web/package.json
@@ -28,7 +28,8 @@
"redux": "^3.5.2",
"redux-logger": "^2.6.1",
"redux-thunk": "^2.1.0",
- "shallowequal": "^0.2.2"
+ "shallowequal": "^0.2.2",
+ "react-codemirror" : "^0.2.6"
},
"devDependencies": {
"babel-core": "^6.7.7",
diff --git a/web/src/css/app.less b/web/src/css/app.less
index 046d378a..6f27f447 100644
--- a/web/src/css/app.less
+++ b/web/src/css/app.less
@@ -16,4 +16,5 @@ html {
@import (less) "flowview.less";
@import (less) "prompt.less";
@import (less) "eventlog.less";
-@import (less) "footer.less"; \ No newline at end of file
+@import (less) "footer.less";
+@import (less) "codemirror.less";
diff --git a/web/src/css/codemirror.less b/web/src/css/codemirror.less
new file mode 100644
index 00000000..6504db50
--- /dev/null
+++ b/web/src/css/codemirror.less
@@ -0,0 +1,9 @@
+.ReactCodeMirror {
+ border: 1px solid #ccc;
+}
+
+.CodeMirror{
+ height: auto !important;
+ max-height: 1000px !important;
+}
+@import (inline) "../../node_modules/codemirror/lib/codemirror.css";
diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less
index edf97566..43078eff 100644
--- a/web/src/css/flowdetail.less
+++ b/web/src/css/flowdetail.less
@@ -109,4 +109,4 @@
text-overflow: ellipsis;
white-space: nowrap;
}
-} \ No newline at end of file
+}
diff --git a/web/src/css/flowview.less b/web/src/css/flowview.less
index aa8a2df2..419739a4 100644
--- a/web/src/css/flowview.less
+++ b/web/src/css/flowview.less
@@ -6,4 +6,33 @@
max-width: 100%;
max-height: 100%;
}
-} \ No newline at end of file
+}
+
+.edit-flow-container {
+ position: relative;
+}
+
+.edit-flow {
+ position: absolute;
+ right: 0px;
+ top: 5px;
+ height: 40px;
+ width: 40px;
+ border-radius: 20px;
+ z-index: 10000;
+
+ background-color: white;
+ border: solid 2px rgba(248, 145, 59, 0.7);
+
+ text-align: center;
+ font-size: 22px;
+ line-height: 37px;
+
+ transition: all 100ms ease-in-out;
+}
+
+.edit-flow:hover {
+ background-color: rgba(239, 108, 0, 0.7);
+ color: rgba(0,0,0,0.8);
+ border: solid 2px transparent;
+}
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx
index 6a982a5d..766de26f 100644
--- a/web/src/js/components/ContentView.jsx
+++ b/web/src/js/components/ContentView.jsx
@@ -5,7 +5,8 @@ import * as ContentViews from './ContentView/ContentViews'
import * as MetaViews from './ContentView/MetaViews'
import ContentLoader from './ContentView/ContentLoader'
import ViewSelector from './ContentView/ViewSelector'
-import { setContentView, setDisplayLarge } from '../ducks/ui'
+import { setContentView, setDisplayLarge, setModifiedFlowContent } from '../ducks/ui'
+import CodeEditor from './common/CodeEditor'
ContentView.propTypes = {
// It may seem a bit weird at the first glance:
@@ -18,7 +19,7 @@ ContentView.propTypes = {
ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2)
function ContentView(props) {
- const { flow, message, contentView, selectView, displayLarge, setDisplayLarge } = props
+ const { flow, message, contentView, selectView, displayLarge, setDisplayLarge, onContentChange, isFlowEditorOpen, setModifiedFlowContent } = props
if (message.contentLength === 0) {
return <MetaViews.ContentEmpty {...props}/>
@@ -36,20 +37,43 @@ function ContentView(props) {
return (
<div>
- {View.textView ? (
+ {isFlowEditorOpen ? (
<ContentLoader flow={flow} message={message}>
- <View content="" />
- </ContentLoader>
- ) : (
- <View flow={flow} message={message} />
+ <CodeEditor content="" onChange={content =>{setModifiedFlowContent(content)}}/>
+ </ContentLoader>
+ ): (
+ <div>
+ {View.textView ? (
+ <ContentLoader flow={flow} message={message}>
+ <View content="" />
+ </ContentLoader>
+ ) : (
+ <View flow={flow} message={message} />
+ )}
+ <div className="view-options text-center">
+ <ViewSelector onSelectView={selectView} active={View} message={message}/>
+ &nbsp;
+ <a className="btn btn-default btn-xs"
+ href={MessageUtils.getContentURL(flow, message)}
+ title="Download the content of the flow.">
+ <i className="fa fa-download"/>
+ </a>
+ &nbsp;
+ <a className="btn btn-default btn-xs"
+ onClick={() => ContentView.fileInput.click()}
+ title="Upload a file to replace the content."
+ >
+ <i className="fa fa-upload"/>
+ </a>
+ <input
+ ref={ref => ContentView.fileInput = ref}
+ className="hidden"
+ type="file"
+ onChange={e => {if(e.target.files.length > 0) onContentChange(e.target.files[0])}}
+ />
+ </div>
+ </div>
)}
- <div className="view-options text-center">
- <ViewSelector onSelectView={selectView} active={View} message={message}/>
- &nbsp;
- <a className="btn btn-default btn-xs" href={MessageUtils.getContentURL(flow, message)}>
- <i className="fa fa-download"/>
- </a>
- </div>
</div>
)
}
@@ -58,9 +82,11 @@ export default connect(
state => ({
contentView: state.ui.contentView,
displayLarge: state.ui.displayLarge,
+ isFlowEditorOpen : state.ui.isFlowEditorOpen
}),
{
selectView: setContentView,
setDisplayLarge,
+ setModifiedFlowContent
}
)(ContentView)
diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx
index b0297dcc..82ee0adc 100644
--- a/web/src/js/components/ContentView/ContentViews.jsx
+++ b/web/src/js/components/ContentView/ContentViews.jsx
@@ -2,6 +2,7 @@ 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
diff --git a/web/src/js/components/FlowView/FlowEditorButton.jsx b/web/src/js/components/FlowView/FlowEditorButton.jsx
new file mode 100644
index 00000000..3d0d1d16
--- /dev/null
+++ b/web/src/js/components/FlowView/FlowEditorButton.jsx
@@ -0,0 +1,39 @@
+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)
diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx
index 9295f97c..2f03c712 100644
--- a/web/src/js/components/FlowView/Messages.jsx
+++ b/web/src/js/components/FlowView/Messages.jsx
@@ -6,6 +6,9 @@ import { Key, formatTimeStamp } from '../../utils.js'
import ContentView from '../ContentView'
import ValueEditor from '../ValueEditor'
import Headers from './Headers'
+import * as flowActions from '../../ducks/flows'
+import FlowEditorButton from './FlowEditorButton'
+
class RequestLine extends Component {
@@ -76,20 +79,25 @@ class ResponseLine extends Component {
}
export class Request extends Component {
-
- render() {
+ render() {
const { flow, updateFlow } = this.props
+ let onContentChange = content => flowActions.updateContent(this.props.flow, content, "request")
return (
<section className="request">
+ <FlowEditorButton onContentChange={onContentChange}/>
<RequestLine ref="requestLine" flow={flow} updateFlow={updateFlow} />
<Headers
ref="headers"
message={flow.request}
onChange={headers => updateFlow({ request: { headers } })}
/>
+
<hr/>
- <ContentView flow={flow} message={flow.request}/>
+ <ContentView flow={flow}
+ onContentChange={onContentChange}
+ message={flow.request}
+ />
</section>
)
}
@@ -116,11 +124,14 @@ export class Request extends Component {
export class Response extends Component {
+
render() {
const { flow, updateFlow } = this.props
+ let onContentChange = content => flowActions.updateContent(this.props.flow, content, "response")
return (
<section className="response">
+ <FlowEditorButton onContentChange={onContentChange}/>
<ResponseLine ref="responseLine" flow={flow} updateFlow={updateFlow} />
<Headers
ref="headers"
@@ -128,7 +139,10 @@ export class Response extends Component {
onChange={headers => updateFlow({ response: { headers } })}
/>
<hr/>
- <ContentView flow={flow} message={flow.response}/>
+ <ContentView flow={flow}
+ onContentChange={onContentChange}
+ message={flow.response}
+ />
</section>
)
}
diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx
index 574288df..cd01af22 100644
--- a/web/src/js/components/common/Button.jsx
+++ b/web/src/js/components/common/Button.jsx
@@ -2,7 +2,8 @@ import React, { PropTypes } from 'react'
Button.propTypes = {
onClick: PropTypes.func.isRequired,
- text: PropTypes.string.isRequired
+ text: PropTypes.string,
+ icon: PropTypes.string
}
export default function Button({ onClick, text, icon, disabled }) {
@@ -10,9 +11,8 @@ export default function Button({ onClick, text, icon, disabled }) {
<div className={"btn btn-default"}
onClick={onClick}
disabled={disabled}>
- <i className={"fa fa-fw " + icon}/>
- &nbsp;
- {text}
+ {icon && (<i className={"fa fa-fw " + icon}/> )}
+ {text && text}
</div>
)
}
diff --git a/web/src/js/components/common/CodeEditor.jsx b/web/src/js/components/common/CodeEditor.jsx
new file mode 100644
index 00000000..5b2305a8
--- /dev/null
+++ b/web/src/js/components/common/CodeEditor.jsx
@@ -0,0 +1,30 @@
+import React, { Component, PropTypes } from 'react'
+import { render } from 'react-dom';
+import Codemirror from 'react-codemirror';
+
+
+export default class CodeEditor extends Component{
+ static propTypes = {
+ content: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ }
+
+ constructor(props){
+ super(props)
+ }
+
+ componentWillMount(){
+ this.props.onChange(this.props.content)
+ }
+
+ render() {
+ let options = {
+ lineNumbers: true
+ };
+ return (
+ <div onKeyDown={e => e.stopPropagation()}>
+ <Codemirror value={this.props.content} onChange={this.props.onChange} options={options}/>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/common/Splitter.jsx b/web/src/js/components/common/Splitter.jsx
index 9d22b6fd..bd4fb3d2 100644
--- a/web/src/js/components/common/Splitter.jsx
+++ b/web/src/js/components/common/Splitter.jsx
@@ -12,6 +12,7 @@ export default class Splitter extends Component {
this.state = { applied: false, startX: false, startY: false }
this.onMouseMove = this.onMouseMove.bind(this)
+ this.onMouseDown = this.onMouseDown.bind(this)
this.onMouseUp = this.onMouseUp.bind(this)
this.onDragEnd = this.onDragEnd.bind(this)
}
diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js
index dfcd5ba9..ffb7ac87 100644
--- a/web/src/js/ducks/flows.js
+++ b/web/src/js/ducks/flows.js
@@ -16,6 +16,7 @@ export const UNKNOWN_CMD = 'FLOWS_UNKNOWN_CMD'
export const FETCH_ERROR = 'FLOWS_FETCH_ERROR'
export const SELECT = 'FLOWS_SELECT'
+
const defaultState = {
selected: [],
...reduceList(undefined, {}),
@@ -118,6 +119,16 @@ export function update(flow, data) {
return { type: REQUEST_ACTION }
}
+export function updateContent(flow, file, type) {
+ const body = new FormData()
+ if (typeof file !== File)
+ file = new Blob([file], {type: 'plain/text'})
+ body.append('file', file)
+ fetchApi(`/flows/${flow.id}/${type}/content`, {method: 'post', body} )
+ return { type: REQUEST_ACTION }
+}
+
+
/**
* @public
*/
diff --git a/web/src/js/ducks/ui.js b/web/src/js/ducks/ui.js
index f745f0af..ccd17eb6 100644
--- a/web/src/js/ducks/ui.js
+++ b/web/src/js/ducks/ui.js
@@ -9,6 +9,9 @@ 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',
@@ -18,7 +21,9 @@ const defaultState = {
promptOpen: false,
contentView: 'ViewAuto',
query: {},
- panel: 'request'
+ panel: 'request',
+ modifiedFlow: {headers: "", content: ""},
+ isFlowEditorOpen: false
}
export default function reducer(state = defaultState, action) {
@@ -31,9 +36,10 @@ export default function reducer(state = defaultState, action) {
}
case flowsActions.SELECT:
+ let s = {...state, isFlowEditorOpen: false}
if (action.flowIds.length && !state.isFlowSelected) {
return {
- ...state,
+ ...s,
displayLarge: false,
activeMenu: 'Flow',
isFlowSelected: true,
@@ -46,14 +52,14 @@ export default function reducer(state = defaultState, action) {
activeMenu = 'Start'
}
return {
- ...state,
+ ...s,
activeMenu,
isFlowSelected: false,
}
}
return {
- ...state,
+ ...s,
displayLarge: false,
}
@@ -78,6 +84,7 @@ export default function reducer(state = defaultState, action) {
case SELECT_TAB:
return {
...state,
+ isFlowEditorOpen: false,
panel: action.panel
}
@@ -92,7 +99,21 @@ export default function reducer(state = defaultState, action) {
...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
}
@@ -126,6 +147,18 @@ 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 () => {