mirror of
https://github.com/alibaba/anyproxy.git
synced 2025-08-04 21:39:04 +00:00
add basic support for websocket proxy
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class RecordResponseDetail extends React.Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
requestRecord: PropTypes.object
|
||||
recordDetail: PropTypes.object
|
||||
}
|
||||
|
||||
onSelectText(e) {
|
||||
|
||||
147
web/src/component/record-ws-message-detail.jsx
Normal file
147
web/src/component/record-ws-message-detail.jsx
Normal 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;
|
||||
56
web/src/component/record-ws-message-detail.less
Normal file
56
web/src/component/record-ws-message-detail.less
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user