add basic support for websocket proxy

This commit is contained in:
砚然
2017-12-12 20:07:06 +08:00
parent e54fb23daa
commit 60bc1498ae
24 changed files with 808 additions and 149 deletions

View File

@@ -5,15 +5,12 @@
import React, { PropTypes } from 'react';
import ClassBind from 'classnames/bind';
import { Menu, Table, notification, Spin } from 'antd';
import clipboard from 'clipboard-js'
import JsonViewer from 'component/json-viewer';
import { Menu, Spin } from 'antd';
import ModalPanel from 'component/modal-panel';
import RecordRequestDetail from 'component/record-request-detail';
import RecordResponseDetail from 'component/record-response-detail';
import RecordWsMessageDetail from 'component/record-ws-message-detail';
import { hideRecordDetail } from 'action/recordAction';
import { selectText } from 'common/CommonUtil';
import { curlify } from 'common/curlUtil';
import Style from './record-detail.less';
import CommonStyle from '../style/common.less';
@@ -21,7 +18,8 @@ import CommonStyle from '../style/common.less';
const StyleBind = ClassBind.bind(Style);
const PageIndexMap = {
REQUEST_INDEX: 'REQUEST_INDEX',
RESPONSE_INDEX: 'RESPONSE_INDEX'
RESPONSE_INDEX: 'RESPONSE_INDEX',
WEBSOCKET_INDEX: 'WEBSOCKET_INDEX'
};
// the maximum length of the request body to decide whether to offer a download link for the request body
@@ -54,6 +52,10 @@ class RecordDetail extends React.Component {
});
}
hasWebSocket (recordDetail = {}) {
return recordDetail && recordDetail.method && recordDetail.method.toLowerCase() === 'websocket';
}
getRequestDiv(recordDetail) {
return <RecordRequestDetail recordDetail={recordDetail} />;
}
@@ -62,18 +64,45 @@ class RecordDetail extends React.Component {
return <RecordResponseDetail recordDetail={recordDetail} />;
}
getRecordContentDiv(recordDetail, fetchingRecord) {
getWsMessageDiv(recordDetail) {
const { globalStatus } = this.props;
return <RecordWsMessageDetail recordDetail={recordDetail} wsPort={globalStatus.wsPort} />;
}
getRecordContentDiv(recordDetail = {}, fetchingRecord) {
const getMenuBody = () => {
const menuBody = this.state.pageIndex === PageIndexMap.REQUEST_INDEX ?
this.getRequestDiv(recordDetail) : this.getResponseDiv(recordDetail);
let menuBody = null;
switch (this.state.pageIndex) {
case PageIndexMap.REQUEST_INDEX: {
menuBody = this.getRequestDiv(recordDetail);
break;
}
case PageIndexMap.RESPONSE_INDEX: {
menuBody = this.getResponseDiv(recordDetail);
break;
}
case PageIndexMap.WEBSOCKET_INDEX: {
menuBody = this.getWsMessageDiv(recordDetail);
break;
}
default: {
menuBody = this.getRequestDiv(recordDetail);
break;
}
}
return menuBody;
}
const websocketMenu = (
<Menu.Item key={PageIndexMap.WEBSOCKET_INDEX}>WebSocket</Menu.Item>
);
return (
<div className={Style.wrapper} >
<Menu onClick={this.onMenuChange} mode="horizontal" selectedKeys={[this.state.pageIndex]} >
<Menu.Item key={PageIndexMap.REQUEST_INDEX}>Request</Menu.Item>
<Menu.Item key={PageIndexMap.RESPONSE_INDEX}>Response</Menu.Item>
{this.hasWebSocket(recordDetail) ? websocketMenu : null}
</Menu>
<div className={Style.detailWrapper} >
{fetchingRecord ? this.getLoaingDiv() : getMenuBody()}
@@ -92,8 +121,9 @@ class RecordDetail extends React.Component {
}
getRecordDetailDiv() {
const recordDetail = this.props.requestRecord.recordDetail;
const fetchingRecord = this.props.globalStatus.fetchingRecord;
const { requestRecord, globalStatus } = this.props;
const recordDetail = requestRecord.recordDetail;
const fetchingRecord = globalStatus.fetchingRecord;
if (!recordDetail && !fetchingRecord) {
return null;
@@ -101,6 +131,17 @@ class RecordDetail extends React.Component {
return this.getRecordContentDiv(recordDetail, fetchingRecord);
}
componentWillReceiveProps(nextProps) {
const { requestRecord } = nextProps;
const { pageIndex } = this.state;
// if this is not websocket, reset the index to RESPONSE_INDEX
if (!this.hasWebSocket(requestRecord.recordDetail) && pageIndex === PageIndexMap.WEBSOCKET_INDEX) {
this.setState({
pageIndex: PageIndexMap.RESPONSE_INDEX
});
}
}
render() {
return (
<ModalPanel

View File

@@ -2,6 +2,7 @@
.wrapper {
padding: 5px 15px;
height: 100%;
word-wrap: break-word;
}
@@ -16,6 +17,8 @@
}
.detailWrapper {
position: relative;
min-height: 100%;
padding: 5px;
}

View File

@@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
}
static propTypes = {
requestRecord: PropTypes.object
recordDetail: PropTypes.object
}
onSelectText(e) {

View File

@@ -0,0 +1,147 @@
/**
* The panel to display the detial of the record
*
*/
import React, { PropTypes } from 'react';
import { message, Button, Icon } from 'antd';
import { formatDate } from 'common/CommonUtil';
import { initWs } from 'common/WsUtil';
import ClassBind from 'classnames/bind';
import Style from './record-ws-message-detail.less';
import CommonStyle from '../style/common.less';
const ToMessage = (props) => {
const { message: wsMessage } = props;
return (
<div className={Style.toMessage}>
<div className={`${Style.time} ${CommonStyle.right}`}>{formatDate(wsMessage.time, 'hh:mm:ss:ms')}</div>
<div className={Style.content}>{wsMessage.message}</div>
</div>
);
}
const FromMessage = (props) => {
const { message: wsMessage } = props;
return (
<div className={Style.fromMessage}>
<div className={Style.time}>{formatDate(wsMessage.time, 'hh:mm:ss:ms')}</div>
<div className={Style.content}>{wsMessage.message}</div>
</div>
);
}
class RecordWsMessageDetail extends React.Component {
constructor() {
super();
this.state = {
stateCheck: false, // a prop only to trigger state check
autoRefresh: true,
socketMessages: [] // the messages from websocket listening
};
this.updateStateRef = null; // a timeout ref to reduce the calling of update state
this.wsClient = null; // ref to the ws client
this.onMessageHandler = this.onMessageHandler.bind(this);
this.receiveNewMessage = this.receiveNewMessage.bind(this);
this.toggleRefresh = this.toggleRefresh.bind(this);
}
static propTypes = {
recordDetail: PropTypes.object,
wsPort: PropTypes.number
}
toggleRefresh () {
const { autoRefresh } = this.state;
this.state.autoRefresh = !autoRefresh;
this.setState({
stateCheck: true
});
}
receiveNewMessage (message) {
this.state.socketMessages.push(message);
this.updateStateRef && clearTimeout(this.updateStateRef);
this.updateStateRef = setTimeout(() => {
this.setState({
stateCheck: true
});
}, 100);
}
getMessageList () {
const { recordDetail } = this.props;
const { socketMessages } = this.state;
const { wsMessages = [] } = recordDetail;
const targetMessage = wsMessages.concat(socketMessages);
return targetMessage.map((messageItem, index) => {
return messageItem.isToServer ?
<ToMessage key={index} message={messageItem} /> : <FromMessage key={index} message={messageItem} />;
});
}
refreshPage () {
const { autoRefresh } = this.state;
if (autoRefresh && this.messageRef && this.messageContentRef) {
this.messageRef.scrollTop = this.messageContentRef.scrollHeight;
}
}
onMessageHandler (event) {
const { recordDetail } = this.props;
const data = JSON.parse(event.data);
const content = data.content;
if (data.type === 'updateLatestWsMsg' ) {
if (recordDetail.id === content.id) {
this.receiveNewMessage(content.message);
}
}
}
componentDidUpdate () {
this.refreshPage();
}
componentWillUnmount () {
this.wsClient && this.wsClient.removeEventListener('message', this.onMessageHandler);
}
componentDidMount () {
const { wsPort, recordDetail } = this.props;
if (!wsPort) {
return;
}
this.refreshPage();
this.wsClient = initWs(wsPort);
this.wsClient.addEventListener('message', this.onMessageHandler);
}
render() {
const { recordDetail } = this.props;
const { autoRefresh } = this.state;
if (!recordDetail) {
return null;
}
const playIcon = <Icon type="play-circle" />;
const pauseIcon = <Icon type="pause-circle" />;
return (
<div className={Style.wrapper} ref={(_ref) => this.messageRef = _ref}>
<div className={Style.contentWrapper} ref={(_ref) => this.messageContentRef = _ref}>
{this.getMessageList()}
</div>
<div className={Style.refreshBtn} onClick={this.toggleRefresh} >
{autoRefresh ? pauseIcon : playIcon}
</div>
</div>
);
}
}
export default RecordWsMessageDetail;

View File

@@ -0,0 +1,56 @@
@import '../style/constant.less';
.wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
}
.contentWrapper {
overflow: hidden;
}
.toMessage {
float: right;
clear: both;
margin: 5px auto;
.content {
background-color: @primary-color;
}
}
.fromMessage {
float: left;
clear: both;
max-width: 40%;
margin: 5px auto;
.content {
background: @success-color;
}
}
.time {
font-size: @font-size-xs;
color: @tip-color;
}
.content {
clear: both;
border-radius: @border-radius-base;
color: #fff;
padding: 7px 8px;
font-size: @font-size-sm;
word-wrap: break-word;
word-break: break-all;
}
.refreshBtn {
position: fixed;
right: 20px;
bottom: 5px;
opacity: 0.53;
font-size: @font-size-large;
cursor: pointer;
}