diff options
Diffstat (limited to 'web')
-rw-r--r-- | web/package.json | 3 | ||||
-rw-r--r-- | web/src/css/app.less | 3 | ||||
-rw-r--r-- | web/src/css/codemirror.less | 9 | ||||
-rw-r--r-- | web/src/css/flowdetail.less | 2 | ||||
-rw-r--r-- | web/src/css/flowview.less | 31 | ||||
-rw-r--r-- | web/src/js/components/ContentView.jsx | 54 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentViews.jsx | 1 | ||||
-rw-r--r-- | web/src/js/components/FlowView/FlowEditorButton.jsx | 39 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Messages.jsx | 22 | ||||
-rw-r--r-- | web/src/js/components/common/Button.jsx | 8 | ||||
-rw-r--r-- | web/src/js/components/common/CodeEditor.jsx | 30 | ||||
-rw-r--r-- | web/src/js/components/common/Splitter.jsx | 1 | ||||
-rw-r--r-- | web/src/js/ducks/flows.js | 11 | ||||
-rw-r--r-- | web/src/js/ducks/ui.js | 43 |
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}/> + + <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> + + <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}/> - - <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}/> - - {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 () => { |