diff options
Diffstat (limited to 'web/src/js')
-rw-r--r-- | web/src/js/components/ContentView.jsx | 24 | ||||
-rw-r--r-- | web/src/js/components/ContentView/CodeEditor.jsx | 2 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentLoader.jsx | 5 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentViewOptions.jsx | 31 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentViews.jsx | 89 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ShowFullContentButton.jsx | 36 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ViewSelector.jsx | 95 | ||||
-rw-r--r-- | web/src/js/components/FlowView.jsx | 2 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Details.jsx | 2 | ||||
-rw-r--r-- | web/src/js/components/FlowView/Messages.jsx | 98 | ||||
-rw-r--r-- | web/src/js/components/common/Button.jsx | 5 | ||||
-rw-r--r-- | web/src/js/ducks/ui/flow.js | 67 | ||||
-rw-r--r-- | web/src/js/flow/utils.js | 6 |
13 files changed, 329 insertions, 133 deletions
diff --git a/web/src/js/components/ContentView.jsx b/web/src/js/components/ContentView.jsx index 75662509..398438ab 100644 --- a/web/src/js/components/ContentView.jsx +++ b/web/src/js/components/ContentView.jsx @@ -2,11 +2,10 @@ import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' import * as ContentViews from './ContentView/ContentViews' import * as MetaViews from './ContentView/MetaViews' -import ViewSelector from './ContentView/ViewSelector' -import UploadContentButton from './ContentView/UploadContentButton' -import DownloadContentButton from './ContentView/DownloadContentButton' +import ShowFullContentButton from './ContentView/ShowFullContentButton' -import { setContentView, displayLarge, updateEdit } from '../ducks/ui/flow' + +import { displayLarge, updateEdit } from '../ducks/ui/flow' ContentView.propTypes = { // It may seem a bit weird at the first glance: @@ -19,7 +18,7 @@ ContentView.propTypes = { ContentView.isContentTooLarge = msg => msg.contentLength > 1024 * 1024 * (ContentViews.ViewImage.matches(msg) ? 10 : 0.2) function ContentView(props) { - const { flow, message, contentView, isDisplayLarge, displayLarge, uploadContent, onContentChange, readonly } = props + const { flow, message, contentView, isDisplayLarge, displayLarge, onContentChange, readonly } = props if (message.contentLength === 0 && readonly) { return <MetaViews.ContentEmpty {...props}/> @@ -33,18 +32,11 @@ function ContentView(props) { return <MetaViews.ContentTooLarge {...props} onClick={displayLarge}/> } - const View = ContentViews[contentView] + const View = ContentViews[contentView] || ContentViews['ViewServer'] return ( - <div> - <View flow={flow} message={message} readonly={readonly} onChange={onContentChange}/> - - <div className="view-options text-center"> - <ViewSelector message={message}/> - - <DownloadContentButton flow={flow} message={message}/> - - <UploadContentButton uploadContent={uploadContent}/> - </div> + <div className="contentview"> + <View flow={flow} message={message} contentView={contentView} readonly={readonly} onChange={onContentChange}/> + <ShowFullContentButton/> </div> ) } diff --git a/web/src/js/components/ContentView/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx index 95f1b98b..d0430e6f 100644 --- a/web/src/js/components/ContentView/CodeEditor.jsx +++ b/web/src/js/components/ContentView/CodeEditor.jsx @@ -14,7 +14,7 @@ export default function CodeEditor ( { content, onChange} ){ lineNumbers: true }; return ( - <div onKeyDown={e => e.stopPropagation()}> + <div className="codeeditor" onKeyDown={e => e.stopPropagation()}> <Codemirror value={content} onChange={onChange} options={options}/> </div> ) diff --git a/web/src/js/components/ContentView/ContentLoader.jsx b/web/src/js/components/ContentView/ContentLoader.jsx index ba6702ca..e7a6f379 100644 --- a/web/src/js/components/ContentView/ContentLoader.jsx +++ b/web/src/js/components/ContentView/ContentLoader.jsx @@ -28,7 +28,8 @@ export default View => class extends React.Component { componentWillReceiveProps(nextProps) { if ( nextProps.message.content !== this.props.message.content || - nextProps.message.contentHash !== this.props.message.contentHash + nextProps.message.contentHash !== this.props.message.contentHash || + nextProps.contentView !== this.props.contentView ) { this.updateContent(nextProps) } @@ -52,7 +53,7 @@ export default View => class extends React.Component { return this.setState({request: undefined, content: ""}) } - let requestUrl = MessageUtils.getContentURL(props.flow, props.message) + let requestUrl = MessageUtils.getContentURL(props.flow, props.message, (View.name == 'ViewServer' ? props.contentView : undefined)) // We use XMLHttpRequest instead of fetch() because fetch() is not (yet) abortable. let request = new XMLHttpRequest(); diff --git a/web/src/js/components/ContentView/ContentViewOptions.jsx b/web/src/js/components/ContentView/ContentViewOptions.jsx new file mode 100644 index 00000000..fed3a088 --- /dev/null +++ b/web/src/js/components/ContentView/ContentViewOptions.jsx @@ -0,0 +1,31 @@ +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import ViewSelector from './ViewSelector' +import UploadContentButton from './UploadContentButton' +import DownloadContentButton from './DownloadContentButton' + +ContentViewOptions.propTypes = { + flow: React.PropTypes.object.isRequired, + message: React.PropTypes.object.isRequired, +} + +function ContentViewOptions(props) { + const { flow, message, uploadContent, readonly, contentViewDescription } = props + return ( + <div className="view-options"> + <ViewSelector message={message}/> + + <DownloadContentButton flow={flow} message={message}/> + + <UploadContentButton uploadContent={uploadContent}/> + + <span>{contentViewDescription}</span> + </div> + ) +} + +export default connect( + state => ({ + contentViewDescription: state.ui.flow.viewDescription + }) +)(ContentViewOptions) diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index a1adebea..cd593023 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React, { PropTypes, Component } from 'react' +import { connect } from 'react-redux' +import { setContentViewDescription, setContent } from '../../ducks/ui/flow' import ContentLoader from './ContentLoader' import { MessageUtils } from '../../flow/utils' import CodeEditor from './CodeEditor' @@ -18,43 +20,68 @@ function ViewImage({ flow, message }) { ) } - -ViewRaw.matches = () => true -ViewRaw.propTypes = { +Edit.propTypes = { content: React.PropTypes.string.isRequired, } -function ViewRaw({ content, readonly, onChange }) { - return readonly ? <pre>{content}</pre> : <CodeEditor content={content} onChange={onChange}/> + +function Edit({ content, onChange }) { + return <CodeEditor content={content} onChange={onChange}/> } -ViewRaw = ContentLoader(ViewRaw) +Edit = ContentLoader(Edit) +class ViewServer extends Component { -const isJSON = /^application\/json$/i -ViewJSON.matches = msg => isJSON.test(MessageUtils.getContentType(msg)) -ViewJSON.propTypes = { - content: React.PropTypes.string.isRequired, -} -function ViewJSON({ content }) { - let json = content - try { - json = JSON.stringify(JSON.parse(content), null, 2); - } catch (e) { - // @noop + componentWillMount(){ + this.setContentView(this.props) } - return <pre>{json}</pre> -} -ViewJSON = ContentLoader(ViewJSON) + componentWillReceiveProps(nextProps){ + if (nextProps.content != this.props.content) { + this.setContentView(nextProps) + } + } + setContentView(props){ + try { + this.data = JSON.parse(props.content) + }catch(err) { + this.data = {lines: [], description: err.message} + } + + 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> + <pre> + {lines.map((line, i) => + <div key={`line${i}`}> + {line.map((tuple, j) => + <span key={`tuple${j}`} className={tuple[0]}> + {tuple[1]} + </span> + )} + </div> + )} + </pre> + {ViewImage.matches(message) && + <ViewImage {...this.props} /> + } + </div> + } -ViewAuto.matches = () => false -ViewAuto.findView = msg => [ViewImage, ViewJSON, ViewRaw].find(v => v.matches(msg)) || ViewRaw -ViewAuto.propTypes = { - message: React.PropTypes.object.isRequired, - flow: React.PropTypes.object.isRequired, -} -function ViewAuto({ message, flow, readonly, onChange }) { - const View = ViewAuto.findView(message) - return <View message={message} flow={flow} readonly={readonly} onChange={onChange}/> } -export { ViewImage, ViewRaw, ViewAuto, ViewJSON } +ViewServer = connect( + state => ({ + showFullContent: state.ui.flow.showFullContent, + maxLines: state.ui.flow.maxContentLines + }), + { + setContentViewDescription, + setContent + } +)(ContentLoader(ViewServer)) + +export { Edit, ViewServer, ViewImage } diff --git a/web/src/js/components/ContentView/ShowFullContentButton.jsx b/web/src/js/components/ContentView/ShowFullContentButton.jsx new file mode 100644 index 00000000..676068e9 --- /dev/null +++ b/web/src/js/components/ContentView/ShowFullContentButton.jsx @@ -0,0 +1,36 @@ +import React, { Component, PropTypes } from 'react' +import { connect } from 'react-redux' +import { render } from 'react-dom'; +import Button from '../common/Button'; +import { setShowFullContent } from '../../ducks/ui/flow' + + + +ShowFullContentButton.propTypes = { + setShowFullContent: PropTypes.func.isRequired, + showFullContent: PropTypes.bool.isRequired +} + +function ShowFullContentButton ( {setShowFullContent, showFullContent, visibleLines, contentLines} ){ + + return ( + !showFullContent && + <div> + <Button className="view-all-content-btn btn-xs" onClick={() => setShowFullContent(true)} text="Show full content"/> + <span className="pull-right"> {visibleLines}/{contentLines} are visible </span> + </div> + ) +} + +export default connect( + state => ({ + showFullContent: state.ui.flow.showFullContent, + visibleLines: state.ui.flow.maxContentLines, + contentLines: state.ui.flow.content.length + + }), + { + setShowFullContent + } +)(ShowFullContentButton) + diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 89b36231..59ec4276 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -1,47 +1,80 @@ -import React, { PropTypes } from 'react' +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 ViewButton({ name, setContentView, children, activeView }) { +function ViewItem({ name, setContentView, children }) { return ( - <button - onClick={() => setContentView(name)} - className={classnames('btn btn-default', { active: name === activeView })}> - {children} - </button> + <li> + <a href="#" onClick={() => setContentView(name)}> + {children} + </a> + </li> ) } -ViewButton = connect(state => ({ - activeView: state.ui.flow.contentView -}), { - setContentView -})(ViewButton) -ViewSelector.propTypes = { - message: PropTypes.object.isRequired, -} -export default function ViewSelector({ message }) { +/*ViewSelector.propTypes = { + contentViews: PropTypes.array.isRequired, + activeView: PropTypes.string.isRequired, + isEdit: PropTypes.bool.isRequired, + isContentViewSelectorOpen: PropTypes.bool.isRequired, + setContentViewSelectorOpen: PropTypes.func.isRequired +}*/ - let autoView = ContentViews.ViewAuto.findView(message) - let autoViewName = (autoView.displayName || autoView.name) - .toLowerCase() - .replace('view', '') - .replace(/ContentLoader\((.+)\)/,"$1") - return ( - <div className="view-selector btn-group btn-group-xs"> +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) + } - <ViewButton name="ViewAuto">auto: {autoViewName}</ViewButton> + onDropdown(e){ + e.preventDefault() + this.setState({open: !this.state.open}) + document.addEventListener('click', this.close) + } - {Object.keys(ContentViews).map(name => - name !== "ViewAuto" && - <ViewButton key={name} name={name}>{name.toLowerCase().replace('view', '')}</ViewButton> - )} + render() { + const {contentViews, activeView, isEdit, setContentView} = this.props + let edit = ContentViews.Edit.displayName - </div> - ) + 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> + </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> + ) + } } + +export default connect ( + state => ({ + contentViews: state.settings.contentViews, + activeView: state.ui.flow.contentView, + isEdit: !!state.ui.flow.modifiedFlow, + }), { + setContentView, + } +)(ViewSelector) diff --git a/web/src/js/components/FlowView.jsx b/web/src/js/components/FlowView.jsx index a80dc040..72cffdfe 100644 --- a/web/src/js/components/FlowView.jsx +++ b/web/src/js/components/FlowView.jsx @@ -71,7 +71,7 @@ export default class FlowView extends Component { const Tab = FlowView.allTabs[_.capitalize(active)] return ( - <div className="flow-detail" onScroll={this.adjustHead}> + <div className="flow-detail"> <Nav flow={flow} tabs={tabs} diff --git a/web/src/js/components/FlowView/Details.jsx b/web/src/js/components/FlowView/Details.jsx index 6a16a98e..10ec6553 100644 --- a/web/src/js/components/FlowView/Details.jsx +++ b/web/src/js/components/FlowView/Details.jsx @@ -114,7 +114,7 @@ export function Timing({ flow }) { export default function Details({ flow }) { return ( - <section> + <section className="detail"> <h4>Client Connection</h4> <ConnectionInfo conn={flow.client_conn}/> diff --git a/web/src/js/components/FlowView/Messages.jsx b/web/src/js/components/FlowView/Messages.jsx index 9de25b5b..93c52660 100644 --- a/web/src/js/components/FlowView/Messages.jsx +++ b/web/src/js/components/FlowView/Messages.jsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux' import { RequestUtils, isValidHttpVersion, parseUrl } from '../../flow/utils.js' import { formatTimeStamp } from '../../utils.js' import ContentView from '../ContentView' +import ContentViewOptions from '../ContentView/ContentViewOptions' import ValidateEditor from '../ValueEditor/ValidateEditor' import ValueEditor from '../ValueEditor/ValueEditor' @@ -81,27 +82,37 @@ const Message = connect( export class Request extends Component { render() { const { flow, isEdit, updateFlow, uploadContent } = this.props - + let noContent = !isEdit && (flow.request.contentLength == 0 || flow.request.contentLength == null) return ( <section className="request"> - <ToggleEdit/> - <RequestLine - flow={flow} - readonly={!isEdit} - updateFlow={updateFlow}/> - <Headers - message={flow.request} - readonly={!isEdit} - onChange={headers => updateFlow({ request: { headers } })} - /> - - <hr/> - <ContentView - readonly={!isEdit} - flow={flow} - onContentChange={content => updateFlow({ request: {content}})} - uploadContent={content => uploadContent(flow, content, "request")} - message={flow.request}/> + <article> + <ToggleEdit/> + <RequestLine + flow={flow} + readonly={!isEdit} + updateFlow={updateFlow}/> + <Headers + message={flow.request} + readonly={!isEdit} + onChange={headers => updateFlow({ request: { headers } })} + /> + + <hr/> + <ContentView + readonly={!isEdit} + flow={flow} + onContentChange={content => updateFlow({ request: {content}})} + message={flow.request}/> + </article> + {!noContent && + <footer> + <ContentViewOptions + flow={flow} + readonly={!isEdit} + message={flow.request} + uploadContent={content => uploadContent(flow, content, "request")}/> + </footer> + } </section> ) } @@ -137,27 +148,38 @@ Request = Message(Request) export class Response extends Component { render() { const { flow, isEdit, updateFlow, uploadContent } = this.props + let noContent = !isEdit && (flow.response.contentLength == 0 || flow.response.contentLength == null) return ( <section className="response"> - <ToggleEdit/> - <ResponseLine - flow={flow} - readonly={!isEdit} - updateFlow={updateFlow}/> - <Headers - message={flow.response} - readonly={!isEdit} - onChange={headers => updateFlow({ response: { headers } })} - /> - <hr/> - <ContentView - readonly={!isEdit} - flow={flow} - onContentChange={content => updateFlow({ response: {content}})} - uploadContent={content => uploadContent(flow, content, "response")} - message={flow.response} - /> + <article> + <ToggleEdit/> + <ResponseLine + flow={flow} + readonly={!isEdit} + updateFlow={updateFlow}/> + <Headers + message={flow.response} + readonly={!isEdit} + onChange={headers => updateFlow({ response: { headers } })} + /> + <hr/> + <ContentView + readonly={!isEdit} + flow={flow} + onContentChange={content => updateFlow({ response: {content}})} + message={flow.response} + /> + </article> + {!noContent && + <footer > + <ContentViewOptions + flow={flow} + message={flow.response} + uploadContent={content => uploadContent(flow, content, "response")} + readonly={!isEdit}/> + </footer> + } </section> ) } @@ -194,7 +216,7 @@ ErrorView.propTypes = { export function ErrorView({ flow }) { return ( - <section> + <section className="error"> <div className="alert alert-warning"> {flow.error.msg} <div> diff --git a/web/src/js/components/common/Button.jsx b/web/src/js/components/common/Button.jsx index cd01af22..bfbb455d 100644 --- a/web/src/js/components/common/Button.jsx +++ b/web/src/js/components/common/Button.jsx @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react' +import classnames from 'classnames' Button.propTypes = { onClick: PropTypes.func.isRequired, @@ -6,9 +7,9 @@ Button.propTypes = { icon: PropTypes.string } -export default function Button({ onClick, text, icon, disabled }) { +export default function Button({ onClick, text, icon, disabled, className }) { return ( - <div className={"btn btn-default"} + <div className={classnames(className, 'btn btn-default')} onClick={onClick} disabled={disabled}> {icon && (<i className={"fa fa-fw " + icon}/> )} diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index c9435676..fb2a846d 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -3,28 +3,38 @@ import { getDiff } from "../../utils" 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', - UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT' +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', + UPLOAD_CONTENT = 'UI_FLOWVIEW_UPLOAD_CONTENT', + SET_SHOW_FULL_CONTENT = 'UI_SET_SHOW_FULL_CONTENT', + SET_CONTENT_VIEW_DESCRIPTION = "UI_SET_CONTENT_VIEW_DESCRIPTION", + SET_CONTENT = "UI_SET_CONTENT" const defaultState = { displayLarge: false, + contentViewDescription: '', + showFullContent: false, modifiedFlow: false, - contentView: 'ViewAuto', + contentView: 'Auto', tab: 'request', + content: [], + maxContentLines: 80, } export default function reducer(state = defaultState, action) { + let wasInEditMode = !!(state.modifiedFlow) switch (action.type) { case START_EDIT: return { ...state, modifiedFlow: action.flow, + contentView: 'Edit', + showFullContent: true } case UPDATE_EDIT: @@ -38,6 +48,9 @@ export default function reducer(state = defaultState, action) { ...state, modifiedFlow: false, displayLarge: false, + contentView: (wasInEditMode ? 'Auto' : state.contentView), + viewDescription: '', + showFullContent: false, } case flowsActions.UPDATE: @@ -49,23 +62,47 @@ export default function reducer(state = defaultState, action) { ...state, modifiedFlow: false, displayLarge: false, + contentView: (wasInEditMode ? 'Auto' : state.contentView), + viewDescription: '', + showFullContent: false } } else { return state } + case SET_CONTENT_VIEW_DESCRIPTION: + return { + ...state, + viewDescription: action.description + } + + case SET_SHOW_FULL_CONTENT: + return { + ...state, + showFullContent: action.show + } case SET_TAB: return { ...state, tab: action.tab, displayLarge: false, + showFullContent: false } case SET_CONTENT_VIEW: return { ...state, contentView: action.contentView, + showFullContent: action.contentView == 'Edit' + } + + case SET_CONTENT: + let isFullContentShown = action.content.length < state.maxContentLines + return { + ...state, + content: action.content, + showFullContent: isFullContentShown } case DISPLAY_LARGE: @@ -98,6 +135,22 @@ export function updateEdit(update) { return { type: UPDATE_EDIT, update } } +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 setContent(content){ + return { type: SET_CONTENT, content} +} + export function stopEdit(flow, modifiedFlow) { let diff = getDiff(flow, modifiedFlow) return flowsActions.update(flow, diff) diff --git a/web/src/js/flow/utils.js b/web/src/js/flow/utils.js index d24f984c..cd174069 100644 --- a/web/src/js/flow/utils.js +++ b/web/src/js/flow/utils.js @@ -43,14 +43,14 @@ export var MessageUtils = { } return false; }, - getContentURL: function (flow, message) { + getContentURL: function (flow, message, view) { if (message === flow.request) { message = "request"; } else if (message === flow.response) { message = "response"; } - return "/flows/" + flow.id + "/" + message + "/content"; - }, + return `/flows/${flow.id}/${message}/content` + (view ? `/${view}` : ''); + } }; export var RequestUtils = _.extend(MessageUtils, { |