mirror of
https://github.com/alibaba/anyproxy.git
synced 2025-08-04 21:39:04 +00:00
update to 4.0
This commit is contained in:
161
web/src/component/download-root-ca.jsx
Normal file
161
web/src/component/download-root-ca.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* The panel to edit the filter
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import { connect } from 'react-redux';
|
||||
import { message, Button, Spin } from 'antd';
|
||||
import ResizablePanel from 'component/resizable-panel';
|
||||
import { hideRootCA, updateIsRootCAExists } from 'action/globalStatusAction';
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
import { getJSON, ajaxGet, postJSON } from 'common/ApiUtil';
|
||||
|
||||
import Style from './download-root-ca.less';
|
||||
import CommonStyle from '../style/common.less';
|
||||
|
||||
class DownloadRootCA extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
loadingCAQr: false,
|
||||
generatingCA: false
|
||||
};
|
||||
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.getQrCodeContent = this.getQrCodeContent.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object
|
||||
}
|
||||
|
||||
fetchData () {
|
||||
this.setState({
|
||||
loadingCAQr: true
|
||||
});
|
||||
|
||||
getJSON('/api/getQrCode')
|
||||
.then((response) => {
|
||||
this.setState({
|
||||
loadingCAQr: false,
|
||||
CAQrCodeImageDom: response.qrImgDom,
|
||||
isRootCAFileExists: response.isRootCAFileExists,
|
||||
url: response.url
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
message.error(error.errorMsg || 'Failed to get the QR code of RootCA path.');
|
||||
});
|
||||
}
|
||||
|
||||
onClose () {
|
||||
this.props.dispatch(hideRootCA());
|
||||
}
|
||||
|
||||
getQrCodeContent () {
|
||||
const imgDomContent = { __html: this.state.CAQrCodeImageDom };
|
||||
const content = (
|
||||
<div className={Style.qrCodeWrapper} >
|
||||
<div dangerouslySetInnerHTML={imgDomContent} />
|
||||
<span>Scan to download rootCA.crt to your Phone</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const spin = <Spin />;
|
||||
return this.state.loadingCAQr ? spin : content;
|
||||
}
|
||||
|
||||
getGenerateRootCADiv () {
|
||||
|
||||
const doToggleRemoteIntercept = () => {
|
||||
postJSON('/api/generateRootCA')
|
||||
.then((result) => {
|
||||
this.setState({
|
||||
generateRootCA: false,
|
||||
isRootCAFileExists: true
|
||||
});
|
||||
this.props.dispatch(updateIsRootCAExists(true));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
generatingCA: false
|
||||
});
|
||||
message.error('生成根证书失败,请重试');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Style.wrapper}>
|
||||
<div className={Style.title} >
|
||||
RootCA
|
||||
</div>
|
||||
|
||||
<div className={Style.generateRootCaTip} >
|
||||
<span >Your RootCA has not been generated yet, please click the button to generate before you download it.</span>
|
||||
<span className={Style.strongColor} >Please install and trust the generated RootCA.</span>
|
||||
</div>
|
||||
|
||||
<div className={Style.generateCAButton} >
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={doToggleRemoteIntercept}
|
||||
loading={this.state.generateRootCA}
|
||||
>
|
||||
OK, GENERATE
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getDownloadDiv () {
|
||||
return (
|
||||
<div className={Style.wrapper} >
|
||||
<div className={Style.fullHeightWrapper} >
|
||||
<div className={Style.title} >
|
||||
RootCA
|
||||
</div>
|
||||
<div className={Style.arCodeDivWrapper} >
|
||||
{this.getQrCodeContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={Style.buttons} >
|
||||
<a href="/fetchCrtFile" target="_blank">
|
||||
<Button type="primary" size="large" > Download </Button>
|
||||
</a>
|
||||
<span className={Style.tipSpan} >Or click the button to download.</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
render() {
|
||||
const panelVisible = this.props.globalStatus.activeMenuKey === MenuKeyMap.ROOT_CA;
|
||||
|
||||
return (
|
||||
<ResizablePanel onClose={this.onClose} visible={panelVisible} >
|
||||
{this.props.globalStatus.isRootCAFileExists ? this.getDownloadDiv() : this.getGenerateRootCADiv()}
|
||||
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function select (state) {
|
||||
return {
|
||||
globalStatus: state.globalStatus
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(select)(DownloadRootCA);
|
||||
58
web/src/component/download-root-ca.less
Normal file
58
web/src/component/download-root-ca.less
Normal file
@@ -0,0 +1,58 @@
|
||||
@import '../style/constant.less';
|
||||
.wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 10px 13px 15px;
|
||||
color: @tip-color;
|
||||
text-align: center;
|
||||
:global {
|
||||
.ant-btn {
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: @middlepanel-font-size;
|
||||
text-align: left;
|
||||
font-weight: 200;
|
||||
color: @hint-color;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.fullHeightWrapper {
|
||||
height: 100%;
|
||||
padding-bottom: 100px;
|
||||
margin-bottom: -100px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.arCodeDivWrapper {
|
||||
margin-top: 63px;
|
||||
}
|
||||
|
||||
.generateRootCaTip {
|
||||
padding-top: 100px;
|
||||
.strongColor {
|
||||
color: @default-color;
|
||||
padding-top: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.generateCAButton {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
width: 100%;
|
||||
.tipSpan {
|
||||
margin-top: 18px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
292
web/src/component/header-menu.jsx
Normal file
292
web/src/component/header-menu.jsx
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 页面顶部菜单的组件
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import { connect } from 'react-redux';
|
||||
import InlineSVG from 'svg-inline-react';
|
||||
import { message, Modal, Popover, Button } from 'antd';
|
||||
import { getQueryParameter } from 'common/CommonUtil';
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
|
||||
import {
|
||||
resumeRecording,
|
||||
stopRecording,
|
||||
updateLocalInterceptHttpsFlag,
|
||||
updateLocalGlobalProxyFlag,
|
||||
toggleRemoteInterceptHttpsFlag,
|
||||
toggleRemoteGlobalProxyFlag,
|
||||
updateShouldClearRecord,
|
||||
updateIsRootCAExists,
|
||||
updateGlobalWsPort,
|
||||
showFilter,
|
||||
updateLocalAppVersion
|
||||
} from 'action/globalStatusAction';
|
||||
|
||||
const {
|
||||
RECORD_FILTER: RECORD_FILTER_MENU_KEY
|
||||
} = MenuKeyMap;
|
||||
|
||||
import { getJSON } from 'common/ApiUtil';
|
||||
|
||||
import Style from './header-menu.less';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
|
||||
class HeaderMenu extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
ruleSummary: '',
|
||||
inAppMode: getQueryParameter('in_app_mode'),
|
||||
runningDetailVisible: false
|
||||
};
|
||||
|
||||
this.stopRecording = this.stopRecording.bind(this);
|
||||
this.resumeRecording = this.resumeRecording.bind(this);
|
||||
this.clearAllRecord = this.clearAllRecord.bind(this);
|
||||
this.initEvent = this.initEvent.bind(this);
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
this.togglerHttpsIntercept = this.togglerHttpsIntercept.bind(this);
|
||||
this.showRunningInfo = this.showRunningInfo.bind(this);
|
||||
this.handleRuningInfoVisibleChange = this.handleRuningInfoVisibleChange.bind(this);
|
||||
this.toggleGlobalProxyFlag = this.toggleGlobalProxyFlag.bind(this);
|
||||
this.showFilter = this.showFilter.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object,
|
||||
resumeRefreshFunc: PropTypes.func
|
||||
}
|
||||
|
||||
stopRecording() {
|
||||
this.props.dispatch(stopRecording());
|
||||
}
|
||||
|
||||
resumeRecording() {
|
||||
console.info('Resuming...');
|
||||
this.props.dispatch(resumeRecording());
|
||||
}
|
||||
|
||||
clearAllRecord() {
|
||||
this.props.dispatch(updateShouldClearRecord(true));
|
||||
this.props.resumeRefreshFunc && this.props.resumeRefreshFunc();
|
||||
}
|
||||
|
||||
handleRuningInfoVisibleChange(visible) {
|
||||
this.setState({
|
||||
runningDetailVisible: visible
|
||||
});
|
||||
}
|
||||
|
||||
showRunningInfo() {
|
||||
this.setState({
|
||||
runningDetailVisible: true
|
||||
});
|
||||
}
|
||||
|
||||
showFilter() {
|
||||
this.props.dispatch(showFilter());
|
||||
}
|
||||
|
||||
togglerHttpsIntercept() {
|
||||
const self = this;
|
||||
// if no rootCA exists, inform the user about trust the root
|
||||
if (!this.props.globalStatus.isRootCAFileExists) {
|
||||
Modal.info({
|
||||
title: 'AnyProxy is about to generate the root CA for you',
|
||||
content: (
|
||||
<div>
|
||||
<span>Trust the root CA before AnyProxy can do HTTPS proxy for you.</span>
|
||||
<span>They will be located in
|
||||
<a href="javascript:void(0)" >{' ' + this.state.rootCADirPath}</a>
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
width: 500,
|
||||
onOk() {
|
||||
doToggleRemoteIntercept();
|
||||
this.props.dispatch(updateIsRootCAExists(true));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
doToggleRemoteIntercept();
|
||||
}
|
||||
|
||||
function doToggleRemoteIntercept() {
|
||||
const currentHttpsFlag = self.props.globalStatus.interceptHttpsFlag;
|
||||
self.props.dispatch(toggleRemoteInterceptHttpsFlag(!currentHttpsFlag));
|
||||
}
|
||||
}
|
||||
|
||||
toggleGlobalProxyFlag() {
|
||||
const currentGlobalProxyFlag = this.props.globalStatus.globalProxyFlag;
|
||||
this.props.dispatch(toggleRemoteGlobalProxyFlag(!currentGlobalProxyFlag));
|
||||
}
|
||||
|
||||
initEvent() {
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.keyCode === 88 && e.ctrlKey) {
|
||||
this.clearAllRecord();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
getJSON('/api/getInitData')
|
||||
.then((response) => {
|
||||
this.setState({
|
||||
ruleSummary: response.ruleSummary,
|
||||
rootCADirPath: response.rootCADirPath,
|
||||
ipAddress: response.ipAddress,
|
||||
port: response.port,
|
||||
wsPort: response.wsPort
|
||||
});
|
||||
this.props.dispatch(updateLocalInterceptHttpsFlag(response.currentInterceptFlag));
|
||||
this.props.dispatch(updateLocalGlobalProxyFlag(response.currentGlobalProxyFlag));
|
||||
this.props.dispatch(updateLocalAppVersion(response.appVersion));
|
||||
this.props.dispatch(updateIsRootCAExists(response.rootCAExists));
|
||||
this.props.dispatch(updateGlobalWsPort(response.wsPort));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
message.error('Failed to get rule summary');
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchData();
|
||||
this.initEvent();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { globalStatus } = this.props;
|
||||
const { activeMenuKey } = globalStatus;
|
||||
const { ipAddress, inAppMode } = this.state;
|
||||
|
||||
const stopMenuStyle = StyleBind('menuItem', { disabled: globalStatus.recording !== true });
|
||||
const resumeMenuStyle = StyleBind('menuItem', { disabled: globalStatus.recording === true });
|
||||
|
||||
const runningTipStyle = StyleBind('menuItem', 'rightMenuItem', { active: this.state.runningDetailVisible });
|
||||
|
||||
const showFilterMenuStyle = StyleBind('menuItem', { active: activeMenuKey === RECORD_FILTER_MENU_KEY });
|
||||
|
||||
const addressDivs = ipAddress ? (
|
||||
this.state.ipAddress.map((singleIpAddress) => {
|
||||
return <div key={singleIpAddress} className={Style.ipAddress}>{singleIpAddress}</div>;
|
||||
})) : null;
|
||||
|
||||
const runningInfoDiv = (
|
||||
<div >
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Active Rule:</strong>
|
||||
<span>{this.state.ruleSummary}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Host Address:</strong>
|
||||
<span>{addressDivs}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Listening on:</strong>
|
||||
<span>{this.state.port}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Proxy Protocol:</strong>
|
||||
<span>HTTP</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={Style.okButton}>
|
||||
<Button type="primary" onClick={this.handleRuningInfoVisibleChange.bind(this, false)} > OK </Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const stopRecordingMenu = (
|
||||
<a
|
||||
className={stopMenuStyle}
|
||||
href="javascript:void(0)"
|
||||
onClick={this.stopRecording}
|
||||
>
|
||||
<div className={Style.filterIcon}>
|
||||
<InlineSVG src={require('svg-inline!assets/stop.svg')} />
|
||||
</div>
|
||||
<span>Stop</span>
|
||||
</a>
|
||||
);
|
||||
|
||||
const resumeRecordingMenu = (
|
||||
<a
|
||||
className={resumeMenuStyle}
|
||||
href="javascript:void(0)"
|
||||
onClick={this.resumeRecording}
|
||||
>
|
||||
<div className={Style.stopIcon}>
|
||||
<InlineSVG src={require('svg-inline!assets/play.svg')} />
|
||||
</div>
|
||||
<span>Resume</span>
|
||||
</a>
|
||||
);
|
||||
|
||||
const filterMenu = (
|
||||
<a
|
||||
className={showFilterMenuStyle}
|
||||
href="javascript:void(0)"
|
||||
onClick={this.showFilter}
|
||||
>
|
||||
<div className={Style.stopIcon}>
|
||||
<InlineSVG src={require('svg-inline!assets/filter.svg')} />
|
||||
</div>
|
||||
<span>Filter</span>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={Style.wrapper} >
|
||||
<div className={Style.menuList} >
|
||||
{globalStatus.recording ? stopRecordingMenu : resumeRecordingMenu}
|
||||
<a
|
||||
className={Style.menuItem}
|
||||
href="javascript:void(0)"
|
||||
onClick={this.clearAllRecord}
|
||||
title="Ctrl + X"
|
||||
>
|
||||
<InlineSVG src={require('svg-inline!assets/clear.svg')} />
|
||||
<span>Clear</span>
|
||||
</a>
|
||||
{inAppMode ? filterMenu : null}
|
||||
|
||||
<Popover
|
||||
content={runningInfoDiv}
|
||||
trigger="click"
|
||||
title="AnyProxy Running Info"
|
||||
visible={this.state.runningDetailVisible}
|
||||
onVisibleChange={this.handleRuningInfoVisibleChange}
|
||||
placement="bottomRight"
|
||||
overlayClassName={Style.runningInfoDivWrapper}
|
||||
>
|
||||
<a
|
||||
className={runningTipStyle}
|
||||
href="javascript:void(0)"
|
||||
>
|
||||
<div className={Style.tipIcon} >
|
||||
<InlineSVG src={require('svg-inline!assets/tip.svg')} />
|
||||
</div>
|
||||
<span>Proxy Info</span>
|
||||
</a>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
globalStatus: state.globalStatus
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(select)(HeaderMenu)
|
||||
209
web/src/component/header-menu.less
Normal file
209
web/src/component/header-menu.less
Normal file
@@ -0,0 +1,209 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
@svg-default-color: #3A3A3A;
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menuList {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
color: @default-color;
|
||||
font-size: @font-size-xs;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
opacity: 0.87;
|
||||
min-width: 95px;
|
||||
padding: 0 25px;
|
||||
text-align: center;
|
||||
-webkit-app-region: no-drag;
|
||||
-webkit-user-select: text;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
i {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.playIcon, .stopIcon {
|
||||
svg {
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.eyeIcon {
|
||||
svg {
|
||||
width: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
.tipIcon {
|
||||
svg {
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 23px;
|
||||
height: 21px;
|
||||
cursor: pointer;
|
||||
polyline {
|
||||
fill: @svg-default-color;
|
||||
stroke: @svg-default-color;
|
||||
}
|
||||
g {
|
||||
fill: @svg-default-color;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
g > use {
|
||||
fill: @svg-default-color;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
line-height: 30px;
|
||||
color: @top-menu-span-color;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
span {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
polyline {
|
||||
fill: @primary-color;
|
||||
stroke: @primary-color;
|
||||
}
|
||||
g {
|
||||
fill: @primary-color;
|
||||
}
|
||||
|
||||
use {
|
||||
fill: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: @tip-color;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: @primary-color;
|
||||
span {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
svg {
|
||||
polyline {
|
||||
fill: @primary-color;
|
||||
stroke: @primary-color;
|
||||
}
|
||||
|
||||
// fill: @primary-color;
|
||||
// stroke: @primary-color;
|
||||
g {
|
||||
fill: @primary-color;
|
||||
}
|
||||
|
||||
use {
|
||||
fill: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.rightMenuItem {
|
||||
float: right;
|
||||
padding-right: 0;
|
||||
margin-right: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItemSpliter {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 1px;
|
||||
background-color: @top-menu-spliter-color;
|
||||
height: 30px;
|
||||
margin: 5px 20px 0;
|
||||
}
|
||||
|
||||
.ruleTip {
|
||||
color: @tip-color;
|
||||
line-height: 26px;
|
||||
i {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.modalInfo {
|
||||
li {
|
||||
font-size: @font-size-reg;
|
||||
overflow: hidden;
|
||||
strong {
|
||||
font-weight: normal;
|
||||
float: left;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
color: @tip-color;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-modal-title {
|
||||
color: @default-color;
|
||||
font-weight: 400;
|
||||
}
|
||||
.ant-btn {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.runningInfoDivWrapper {
|
||||
line-height: 1.5;
|
||||
li {
|
||||
overflow: hidden;
|
||||
strong {
|
||||
float: left;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
padding-left: 5px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.ant-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-popover {
|
||||
font-size: @font-size-reg;
|
||||
}
|
||||
|
||||
.ant-popover-title {
|
||||
color: @default-color;
|
||||
font-weight: bold;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding-bottom: 25px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
web/src/component/json-viewer.jsx
Normal file
95
web/src/component/json-viewer.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* A copoment to display content in the a modal
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Menu } from 'antd';
|
||||
import ReactDOM from 'react-dom';
|
||||
import JSONTree from 'react-json-tree';
|
||||
import Style from './json-viewer.less';
|
||||
|
||||
const PageIndexMap = {
|
||||
'JSON_STRING': 'JSON_STRING',
|
||||
'JSON_TREE': 'JSON_TREE'
|
||||
};
|
||||
|
||||
const theme = {
|
||||
scheme: 'google',
|
||||
author: 'seth wright (http://sethawright.com)',
|
||||
base00: '#1d1f21',
|
||||
base01: '#282a2e',
|
||||
base02: '#373b41',
|
||||
base03: '#969896',
|
||||
base04: '#b4b7b4',
|
||||
base05: '#c5c8c6',
|
||||
base06: '#e0e0e0',
|
||||
base07: '#ffffff',
|
||||
base08: '#CC342B',
|
||||
base09: '#F96A38',
|
||||
base0A: '#FBA922',
|
||||
base0B: '#198844',
|
||||
base0C: '#3971ED',
|
||||
base0D: '#3971ED',
|
||||
base0E: '#A36AC7',
|
||||
base0F: '#3971ED'
|
||||
};
|
||||
|
||||
class JsonViewer extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
pageIndex: PageIndexMap.JSON_STRING
|
||||
};
|
||||
|
||||
this.getMenuDiv = this.getMenuDiv.bind(this);
|
||||
this.handleMenuClick = this.handleMenuClick.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
data: PropTypes.string
|
||||
}
|
||||
|
||||
handleMenuClick(e) {
|
||||
this.setState({
|
||||
pageIndex: e.key,
|
||||
});
|
||||
}
|
||||
|
||||
getMenuDiv () {
|
||||
return (
|
||||
<Menu onClick={this.handleMenuClick} mode="horizontal" selectedKeys={[this.state.pageIndex]} >
|
||||
<Menu.Item key={PageIndexMap.JSON_STRING}>Source</Menu.Item>
|
||||
<Menu.Item key={PageIndexMap.JSON_TREE}>Preview</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let jsonTreeDiv = <div>{this.props.data}</div>;
|
||||
|
||||
try {
|
||||
// In an invalid JSON string returned, handle the exception
|
||||
const jsonObj = JSON.parse(this.props.data);
|
||||
jsonTreeDiv = <JSONTree data={jsonObj} theme={theme} />;
|
||||
} catch (e) {
|
||||
console.warn('Failed to get JSON Tree:', e);
|
||||
}
|
||||
|
||||
const jsonStringDiv = <div>{this.props.data}</div>;
|
||||
return (
|
||||
<div className={Style.wrapper} >
|
||||
{this.getMenuDiv()}
|
||||
<div className={Style.contentDiv} >
|
||||
{this.state.pageIndex === PageIndexMap.JSON_STRING ? jsonStringDiv : jsonTreeDiv}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default JsonViewer;
|
||||
8
web/src/component/json-viewer.less
Normal file
8
web/src/component/json-viewer.less
Normal file
@@ -0,0 +1,8 @@
|
||||
@import '../style/constant.less';
|
||||
.wrapper {
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
.contentDiv {
|
||||
padding: 20px 25px;
|
||||
background: #fff;
|
||||
}
|
||||
135
web/src/component/left-menu.jsx
Normal file
135
web/src/component/left-menu.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* A copoment to for left main menu
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import InlineSVG from 'svg-inline-react';
|
||||
import { getQueryParameter } from 'common/CommonUtil';
|
||||
|
||||
import Style from './left-menu.less';
|
||||
import ClassBind from 'classnames/bind';
|
||||
|
||||
import {
|
||||
showFilter,
|
||||
showRootCA
|
||||
} from 'action/globalStatusAction';
|
||||
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
const {
|
||||
RECORD_FILTER: RECORD_FILTER_MENU_KEY,
|
||||
ROOT_CA: ROOT_CA_MENU_KEY
|
||||
} = MenuKeyMap;
|
||||
|
||||
class LeftMenu extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
inAppMode: getQueryParameter('in_app_mode')
|
||||
};
|
||||
|
||||
// this.showMapLocal = this.showMapLocal.bind(this);
|
||||
this.showFilter = this.showFilter.bind(this);
|
||||
this.showRootCA = this.showRootCA.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object
|
||||
}
|
||||
|
||||
// showMapLocal() {
|
||||
// this.props.dispatch(showMapLocal());
|
||||
// }
|
||||
|
||||
showFilter() {
|
||||
this.props.dispatch(showFilter());
|
||||
}
|
||||
|
||||
showRootCA() {
|
||||
this.props.dispatch(showRootCA());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { filterStr, activeMenuKey, recording } = this.props.globalStatus;
|
||||
|
||||
const filterMenuStyle = StyleBind('menuItem', {
|
||||
working: filterStr.length > 0,
|
||||
active: activeMenuKey === RECORD_FILTER_MENU_KEY
|
||||
});
|
||||
|
||||
const rootCAMenuStyle = StyleBind('menuItem', {
|
||||
active: activeMenuKey === ROOT_CA_MENU_KEY
|
||||
});
|
||||
|
||||
const wrapperStyle = StyleBind('wrapper', { inApp: this.state.inAppMode });
|
||||
const circleStyle = StyleBind('circles', { active: recording, stop: !recording });
|
||||
|
||||
return (
|
||||
<div className={wrapperStyle} >
|
||||
<div className={Style.logo} >
|
||||
<div className={Style.brand} >
|
||||
<span className={Style.any}>Any</span>
|
||||
<span className={Style.proxy}>Proxy</span>
|
||||
</div>
|
||||
<div className={circleStyle} >
|
||||
<span className={Style.circle1} />
|
||||
<span className={Style.circle2} />
|
||||
<span className={Style.circle3} />
|
||||
<span className={Style.circle4} />
|
||||
<span className={Style.circle5} />
|
||||
<span className={Style.circle6} />
|
||||
<span className={Style.circle7} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={Style.menuList} >
|
||||
<a
|
||||
className={filterMenuStyle}
|
||||
href="javascript:void(0)"
|
||||
onClick={this.showFilter}
|
||||
title="Only show the filtered result"
|
||||
>
|
||||
<span className={Style.filterIcon}>
|
||||
<InlineSVG src={require('svg-inline!assets/filter.svg')} />
|
||||
</span>
|
||||
<span>Filter</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
className={rootCAMenuStyle}
|
||||
href="javascript:void(0)"
|
||||
onClick={this.showRootCA}
|
||||
title="Download the root CA to the computer and your phone"
|
||||
>
|
||||
<span className={Style.downloadIcon}>
|
||||
<InlineSVG src={require('svg-inline!assets/download.svg')} />
|
||||
</span>
|
||||
<span>RootCA</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className={Style.bottom} >
|
||||
<a className={Style.bottomItem} href="http://anyproxy.io/" target="_blank">AnyProxy.io</a>
|
||||
<div className={Style.bottomBorder} >
|
||||
<span className={Style.bottomBorder1} />
|
||||
<span className={Style.bottomBorder2} />
|
||||
<span className={Style.bottomBorder3} />
|
||||
</div>
|
||||
<span className={Style.bottomItem}>
|
||||
Version {this.props.globalStatus.appVersion}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
globalStatus: state.globalStatus
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(select)(LeftMenu);
|
||||
239
web/src/component/left-menu.less
Normal file
239
web/src/component/left-menu.less
Normal file
@@ -0,0 +1,239 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
@keyframes lightBulb{
|
||||
from { opacity: 1 }
|
||||
to { opacity: 0.1 }
|
||||
}
|
||||
|
||||
@total-duration: 5s;
|
||||
@single-duration: 0.7s;
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
background-color: @left-menu-background-color;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 14px;
|
||||
&.inApp {
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
.systemTitleButton {
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-top: 7px;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
.brand {
|
||||
-webkit-app-region: no-drag;
|
||||
font-size: @logo-font-size;
|
||||
font-weight: 100;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.any {
|
||||
font-family: "PingFangSC-Semibold", "Helvetica Neue", Helvetica, "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.proxy {
|
||||
font-family: "PingFangSC-Ultralight", "Helvetica Neue", Helvetica, "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.circles {
|
||||
display: inline-block;
|
||||
span {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background-color: #10A1FF;
|
||||
margin: 0 4.7px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.stop {
|
||||
.circle1, .circle7 {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.circle2, .circle6 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.circle3, .circle5 {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.circle4 {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
span {
|
||||
animation: lightBulb @total-duration infinite;
|
||||
}
|
||||
.circle7 {
|
||||
animation-delay: (1*@single-duration);
|
||||
}
|
||||
.circle6 {
|
||||
animation-delay: (2*@single-duration);
|
||||
}
|
||||
.circle5 {
|
||||
animation-delay: (3*@single-duration);
|
||||
}
|
||||
.circle4 {
|
||||
animation-delay: (4*@single-duration);
|
||||
}
|
||||
.circle3 {
|
||||
animation-delay: (5*@single-duration);
|
||||
}
|
||||
.circle2 {
|
||||
animation-delay: (6*@single-duration);
|
||||
}
|
||||
.circle1 {
|
||||
animation-delay: (7*@single-duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.menuList {
|
||||
overflow: hidden;
|
||||
margin-top: 50px;
|
||||
display: block;
|
||||
font-size: @left-menu-font-size;
|
||||
font-weight: 200;
|
||||
-webkit-app-region: no-drag;
|
||||
a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
color: @left-menu-color;
|
||||
float: left;
|
||||
width: 100%;
|
||||
line-height: 47px;
|
||||
border-left: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.filterIcon, .downloadIcon {
|
||||
width: 51px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
svg {
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.retweetIcon {
|
||||
width: 51px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
svg {
|
||||
width: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 23px;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #252630;
|
||||
color: #fff;
|
||||
svg {
|
||||
polyline {
|
||||
fill: #fff;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
g {
|
||||
fill: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
use {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: @primary-color;
|
||||
color: #fff;
|
||||
background-color: #252630;
|
||||
svg {
|
||||
polyline {
|
||||
fill: #fff;
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
g {
|
||||
fill: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
use {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.working {
|
||||
// color: #fff;
|
||||
}
|
||||
i {
|
||||
width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
font-size: 12px;
|
||||
padding-left: 16px;
|
||||
color: @left-menu-color;
|
||||
.bottomItem {
|
||||
display: inline-block;
|
||||
margin-top: 7px;
|
||||
line-height: 1.5;
|
||||
color: @left-menu-color;
|
||||
}
|
||||
.bottomBorder {
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
position: relative;
|
||||
margin-top: -3.52px;
|
||||
}
|
||||
.bottomBorder1 {
|
||||
float: left;
|
||||
width: 16px;
|
||||
margin-right: 2px;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
.bottomBorder2 {
|
||||
float: left;
|
||||
width: 29px;
|
||||
margin-right: 2px;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
.bottomBorder3 {
|
||||
float: left;
|
||||
width: 16px;
|
||||
border-top: 1px solid;
|
||||
}
|
||||
}
|
||||
254
web/src/component/map-local.jsx
Normal file
254
web/src/component/map-local.jsx
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* The panel map request to local
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import { connect } from 'react-redux';
|
||||
import { Tree, Form, Input, Button } from 'antd';
|
||||
import ResizablePanel from 'component/resizable-panel';
|
||||
import PromiseUtil from 'common/PromiseUtil';
|
||||
import { fetchDirectory, hideMapLocal, fetchMappedConfig, updateRemoteMappedConfig } from 'action/globalStatusAction';
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
|
||||
import Style from './map-local.less';
|
||||
import CommonStyle from '../style/common.less';
|
||||
|
||||
const TreeNode = Tree.TreeNode;
|
||||
const createForm = Form.create;
|
||||
const FormItem = Form.Item;
|
||||
|
||||
class MapLocal extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
selectedLocalPath: ''
|
||||
};
|
||||
|
||||
this.loadTreeNode = this.loadTreeNode.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.getFormDiv = this.getFormDiv.bind(this);
|
||||
this.onNodeSelect = this.onNodeSelect.bind(this);
|
||||
this.getMappedConfigDiv = this.getMappedConfigDiv.bind(this);
|
||||
this.loadMappedConfig = this.loadMappedConfig.bind(this);
|
||||
this.addMappedConfig = this.addMappedConfig.bind(this);
|
||||
this.removeMappedConfig = this.removeMappedConfig.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object,
|
||||
form: PropTypes.object
|
||||
}
|
||||
|
||||
loadTreeNode(node) {
|
||||
const d = PromiseUtil.defer();
|
||||
const key = node ? node.props.eventKey : '';
|
||||
this.props.dispatch(fetchDirectory(key));
|
||||
|
||||
setTimeout(function() {
|
||||
d.resolve();
|
||||
}, 500);
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
loadMappedConfig() {
|
||||
this.props.dispatch(fetchMappedConfig());
|
||||
}
|
||||
|
||||
addMappedConfig () {
|
||||
const config = this.props.globalStatus.mappedConfig.slice();
|
||||
this.props.form.validateFieldsAndScroll((error, value) => {
|
||||
config.push({
|
||||
keyword: value.keyword,
|
||||
local: value.local
|
||||
});
|
||||
this.props.dispatch(updateRemoteMappedConfig(config));
|
||||
});
|
||||
}
|
||||
|
||||
removeMappedConfig (index) {
|
||||
const config = this.props.globalStatus.mappedConfig.slice();
|
||||
config.splice(index, 1);
|
||||
this.props.dispatch(updateRemoteMappedConfig(config));
|
||||
}
|
||||
|
||||
loopTreeNode(nodes) {
|
||||
const treeNodes = nodes.map((item) => {
|
||||
if (item.children) {
|
||||
return (
|
||||
<TreeNode title={item.name} key={item.fullPath}>
|
||||
{this.loopTreeNode(item.children)}
|
||||
</TreeNode>
|
||||
);
|
||||
} else {
|
||||
return <TreeNode title={item.name} key={item.fullPath} isLeaf={item.isLeaf} />;
|
||||
}
|
||||
});
|
||||
|
||||
return treeNodes;
|
||||
}
|
||||
|
||||
onClose () {
|
||||
this.props.dispatch(hideMapLocal());
|
||||
}
|
||||
|
||||
onNodeSelect (selectedKeys, { selected, selectedNodes }) {
|
||||
const node = selectedNodes[0];
|
||||
|
||||
// Only a file will be mapped
|
||||
if (node && node.props.isLeaf) {
|
||||
this.setState({
|
||||
selectedLocalPath: selectedKeys[0]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getFormDiv () {
|
||||
const { getFieldDecorator, getFieldError } = this.props.form;
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { span: 6 },
|
||||
wrapperCol: { span: 18 },
|
||||
};
|
||||
|
||||
const keywordProps = getFieldDecorator('keyword', {
|
||||
initialValue: '',
|
||||
validate: [
|
||||
{
|
||||
trigger: 'onBlur',
|
||||
rules: [
|
||||
{
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
required: true,
|
||||
message: '请录入需要映射的url匹配'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const localProps = getFieldDecorator('local', {
|
||||
initialValue: this.state.selectedLocalPath,
|
||||
validate: [
|
||||
{
|
||||
trigger: 'onBlur',
|
||||
rules: [
|
||||
{
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
required: true,
|
||||
message: '请输入本地文件路径'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={Style.form} >
|
||||
<Form vertical >
|
||||
<FormItem
|
||||
label="Keyword"
|
||||
>
|
||||
{keywordProps(<Input placeholder="The pattern to map" />)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="Local file"
|
||||
>
|
||||
{localProps(<Input placeholder="Local file for the mapped url" />)}
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getMappedConfigDiv () {
|
||||
const { mappedConfig } = this.props.globalStatus;
|
||||
const mappedLiDiv = mappedConfig.map((item, index) => {
|
||||
return (
|
||||
<li key={index} >
|
||||
<div>
|
||||
<div className={Style.mappedKeyDiv} >
|
||||
<strong>{item.keyword}</strong>
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
onClick={this.removeMappedConfig.bind(this, index)}
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
</div>
|
||||
<div className={Style.mappedLocal} >
|
||||
{item.local}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={Style.mappedConfigWrapper} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Current Configuration</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
<ul className={Style.mappedList} >
|
||||
{mappedLiDiv}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.loadTreeNode();
|
||||
this.loadMappedConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const treeNodes = this.loopTreeNode(this.props.globalStatus.directory);
|
||||
const panelVisible = this.props.globalStatus.activeMenuKey === MenuKeyMap.MAP_LOCAL;
|
||||
|
||||
return (
|
||||
<ResizablePanel onClose={this.onClose} visible={panelVisible} >
|
||||
<div className={Style.mapLocalWrapper} >
|
||||
<div className={Style.title} >
|
||||
Map Local
|
||||
</div>
|
||||
{this.getMappedConfigDiv()}
|
||||
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Add Local Map</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
{this.getFormDiv()}
|
||||
<div className={Style.treeWrapper} >
|
||||
<Tree
|
||||
loadData={this.loadTreeNode}
|
||||
onSelect={this.onNodeSelect}
|
||||
>
|
||||
{treeNodes}
|
||||
</Tree>
|
||||
</div>
|
||||
<div className={Style.operations} >
|
||||
<Button type="primary" onClick={this.addMappedConfig} >Add</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function select (state) {
|
||||
return {
|
||||
globalStatus: state.globalStatus
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(select)(createForm()(MapLocal));
|
||||
58
web/src/component/map-local.less
Normal file
58
web/src/component/map-local.less
Normal file
@@ -0,0 +1,58 @@
|
||||
@import '../style/constant.less';
|
||||
@panel-width: 100%;
|
||||
.mapLocalWrapper {
|
||||
padding: 10px 15px 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: @middlepanel-font-size;
|
||||
text-align: left;
|
||||
font-weight: 200;
|
||||
margin-bottom: 12px;
|
||||
color: @hint-color;
|
||||
}
|
||||
|
||||
.treeWrapper {
|
||||
height: 310px;
|
||||
width: @panel-width;
|
||||
overflow: auto;
|
||||
border: 1px solid @light-border-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: @panel-width;
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.mappedKeyDiv {
|
||||
position: relative;
|
||||
strong {
|
||||
display: block;
|
||||
width: 230px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mappedList {
|
||||
padding-left: 25px;
|
||||
padding-bottom: 15px;
|
||||
li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
.operations {
|
||||
margin-top: 10px;
|
||||
button {
|
||||
width: @panel-width;
|
||||
}
|
||||
}
|
||||
129
web/src/component/modal-panel.jsx
Normal file
129
web/src/component/modal-panel.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* A copoment to display content in the a modal
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Icon } from 'antd';
|
||||
|
||||
import Style from './modal-panel.less';
|
||||
import ClassBind from 'classnames/bind';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
|
||||
class ModalPanel extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
dragBarLeft: '',
|
||||
contentLeft: ''
|
||||
};
|
||||
this.onDragbarMoveUp = this.onDragbarMoveUp.bind(this);
|
||||
this.onDragbarMove = this.onDragbarMove.bind(this);
|
||||
this.onDragbarMoveDown = this.onDragbarMoveDown.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.doClose = this.doClose.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.addKeyEvent = this.addKeyEvent.bind(this);
|
||||
this.removeKeyEvent = this.removeKeyEvent.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
onClose: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
hideBackModal: PropTypes.bool,
|
||||
left: PropTypes.string
|
||||
}
|
||||
|
||||
onDragbarMove (event) {
|
||||
this.setState({
|
||||
dragBarLeft: event.pageX
|
||||
});
|
||||
}
|
||||
|
||||
onKeyUp (e) {
|
||||
if (e.keyCode == 27) {
|
||||
this.doClose();
|
||||
}
|
||||
}
|
||||
|
||||
addKeyEvent () {
|
||||
document.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
removeKeyEvent () {
|
||||
document.removeEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
onDragbarMoveUp (event) {
|
||||
this.setState({
|
||||
contentLeft: event.pageX
|
||||
});
|
||||
|
||||
document.removeEventListener('mousemove', this.onDragbarMove);
|
||||
document.removeEventListener('mouseup', this.onDragbarMoveUp);
|
||||
}
|
||||
|
||||
onDragbarMoveDown (event) {
|
||||
document.addEventListener('mousemove', this.onDragbarMove);
|
||||
|
||||
document.addEventListener('mouseup', this.onDragbarMoveUp);
|
||||
}
|
||||
|
||||
onClose (event) {
|
||||
if (event.target === event.currentTarget) {
|
||||
this.props.onClose && this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
doClose () {
|
||||
this.props.onClose && this.props.onClose();
|
||||
}
|
||||
|
||||
render () {
|
||||
// will not remove the dom but hidden it, so the dom will not be relayouted
|
||||
let renderLeft = '100%';
|
||||
if (!this.props.visible) {
|
||||
this.removeKeyEvent();
|
||||
// return null;
|
||||
} else {
|
||||
const { dragBarLeft, contentLeft } = this.state;
|
||||
const propsLeft = this.props.left;
|
||||
renderLeft = dragBarLeft || propsLeft;
|
||||
this.addKeyEvent();
|
||||
}
|
||||
|
||||
|
||||
// const dragBarStyle = dragBarLeft || propsLeft ? { 'left': dragBarLeft || propsLeft } : null;
|
||||
// const contentStyle = contentLeft || propsLeft ? { 'left': contentLeft || propsLeft } : null;
|
||||
|
||||
const dragBarStyle = { 'left': renderLeft };
|
||||
const modalStyle = { 'left': renderLeft };
|
||||
|
||||
// const modalStyle = this.props.hideBackModal ? contentStyle : { 'left': 0 };
|
||||
return (
|
||||
<div className={Style.wrapper} onClick={this.onClose} style={modalStyle} >
|
||||
<div className={Style.relativeWrapper}>
|
||||
<div className={Style.closeIcon} title="Close, Esc" onClick={this.doClose} >
|
||||
<Icon type="close" />
|
||||
</div>
|
||||
<div
|
||||
className={Style.dragBar}
|
||||
style={dragBarStyle}
|
||||
onMouseDown={this.onDragbarMoveDown}
|
||||
/>
|
||||
<div className={Style.contentWrapper} >
|
||||
<div className={Style.content} >
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalPanel;
|
||||
74
web/src/component/modal-panel.less
Normal file
74
web/src/component/modal-panel.less
Normal file
@@ -0,0 +1,74 @@
|
||||
@import '../style/constant.less';
|
||||
.wrapper {
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 60%;
|
||||
right: 0;
|
||||
background-color: @opacity-background-color;
|
||||
overflow: hidden;
|
||||
-webkit-box-shadow: -3px 0px 6px 0px rgba(128,128,128,0.56);
|
||||
-moz-box-shadow: -3px 0px 6px 0px rgba(128,128,128,0.56);
|
||||
box-shadow: -3px 0px 6px 0px rgba(128,128,128,0.56);
|
||||
will-change: left;
|
||||
}
|
||||
|
||||
.relativeWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
i {
|
||||
font-size: @font-size-big;
|
||||
}
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.relativeWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
color: @default-color;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dragBar {
|
||||
position: fixed;
|
||||
left: 60%;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
width: 5px;
|
||||
background-color: @hint-color;
|
||||
opacity: 0;
|
||||
z-index: 2000;
|
||||
cursor: col-resize;
|
||||
&:hover {
|
||||
opacity: 0.87;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #fff;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
118
web/src/component/record-detail.jsx
Normal file
118
web/src/component/record-detail.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* The panel to display the detial of the record
|
||||
*
|
||||
*/
|
||||
|
||||
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 ModalPanel from 'component/modal-panel';
|
||||
import RecordRequestDetail from 'component/record-request-detail';
|
||||
import RecordResponseDetail from 'component/record-response-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';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
const PageIndexMap = {
|
||||
REQUEST_INDEX: 'REQUEST_INDEX',
|
||||
RESPONSE_INDEX: 'RESPONSE_INDEX'
|
||||
};
|
||||
|
||||
// the maximum length of the request body to decide whether to offer a download link for the request body
|
||||
const MAXIMUM_REQ_BODY_LENGTH = 10000;
|
||||
|
||||
class RecordDetail extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.state = {
|
||||
pageIndex: PageIndexMap.REQUEST_INDEX
|
||||
};
|
||||
|
||||
this.onMenuChange = this.onMenuChange.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object,
|
||||
requestRecord: PropTypes.object
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.props.dispatch(hideRecordDetail());
|
||||
}
|
||||
|
||||
onMenuChange(e) {
|
||||
this.setState({
|
||||
pageIndex: e.key,
|
||||
});
|
||||
}
|
||||
|
||||
getRequestDiv(recordDetail) {
|
||||
return <RecordRequestDetail recordDetail={recordDetail} />;
|
||||
}
|
||||
|
||||
getResponseDiv(recordDetail) {
|
||||
return <RecordResponseDetail recordDetail={recordDetail} />;
|
||||
}
|
||||
|
||||
getRecordContentDiv(recordDetail, fetchingRecord) {
|
||||
const getMenuBody = () => {
|
||||
const menuBody = this.state.pageIndex === PageIndexMap.REQUEST_INDEX ?
|
||||
this.getRequestDiv(recordDetail) : this.getResponseDiv(recordDetail);
|
||||
return menuBody;
|
||||
}
|
||||
|
||||
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>
|
||||
</Menu>
|
||||
<div className={Style.detailWrapper} >
|
||||
{fetchingRecord ? this.getLoaingDiv() : getMenuBody()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getLoaingDiv() {
|
||||
return (
|
||||
<div className={Style.loading}>
|
||||
<Spin />
|
||||
<div className={Style.loadingText}>LOADING...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getRecordDetailDiv() {
|
||||
const recordDetail = this.props.requestRecord.recordDetail;
|
||||
const fetchingRecord = this.props.globalStatus.fetchingRecord;
|
||||
|
||||
if (!recordDetail && !fetchingRecord) {
|
||||
return null;
|
||||
}
|
||||
return this.getRecordContentDiv(recordDetail, fetchingRecord);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalPanel
|
||||
onClose={this.onClose}
|
||||
hideBackModal
|
||||
visible={this.props.requestRecord.recordDetail !== null}
|
||||
left="50%"
|
||||
>
|
||||
{this.getRecordDetailDiv()}
|
||||
</ModalPanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RecordDetail;
|
||||
91
web/src/component/record-detail.less
Normal file
91
web/src/component/record-detail.less
Normal file
@@ -0,0 +1,91 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
.wrapper {
|
||||
padding: 5px 15px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding-top: 100px;
|
||||
.loadingText {
|
||||
margin-top: 15px;
|
||||
color: @primary-color;
|
||||
font-size: @font-size-big;
|
||||
}
|
||||
}
|
||||
|
||||
.detailWrapper {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 10px 0;
|
||||
font-size: @font-size-xs;
|
||||
border-bottom: 1px solid @border-color-base;
|
||||
&.noBorder {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.okStatus {
|
||||
color: @success-color;
|
||||
}
|
||||
|
||||
.reqBody, .resBody {
|
||||
min-width: 200px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.imageBody {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.ulItem {
|
||||
padding-left: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.liItem {
|
||||
overflow: hidden;
|
||||
strong {
|
||||
float: left;
|
||||
// min-width: 125px;
|
||||
// text-align: right;
|
||||
opacity: 0.8;
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
opacity: 0.87;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.cookieWrapper {
|
||||
padding-top: 15px;
|
||||
:global {
|
||||
.ant-table-middle .ant-table-thead > tr > th,
|
||||
.ant-table-middle .ant-table-tbody > tr > td {
|
||||
padding: 5px 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
.noCookes {
|
||||
text-align: center;
|
||||
color: @tip-color;
|
||||
padding: 7px 0 8px;
|
||||
border-bottom: 1px solid @light-border-color;
|
||||
}
|
||||
.odd {
|
||||
background-color: @light-background-color;
|
||||
}
|
||||
|
||||
.codeWrapper {
|
||||
overflow: auto;
|
||||
padding: 15px;
|
||||
background-color: @info-bkg-color;
|
||||
border: 1px solid @light-border-color;
|
||||
}
|
||||
89
web/src/component/record-filter.jsx
Normal file
89
web/src/component/record-filter.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* The panel to edit the filter
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import { connect } from 'react-redux';
|
||||
import { Input, Alert } from 'antd';
|
||||
import ResizablePanel from 'component/resizable-panel';
|
||||
import { hideFilter, updateFilter } from 'action/globalStatusAction';
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
|
||||
import Style from './record-filter.less';
|
||||
import CommonStyle from '../style/common.less';
|
||||
|
||||
|
||||
class RecordFilter extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.filterTimeoutId = null;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object
|
||||
}
|
||||
|
||||
onChange (event) {
|
||||
this.props.dispatch(updateFilter(event.target.value));
|
||||
}
|
||||
|
||||
onClose () {
|
||||
this.props.dispatch(hideFilter());
|
||||
}
|
||||
|
||||
render() {
|
||||
const description = (
|
||||
<ul className={Style.tipList} >
|
||||
<li>Multiple filters supported, write them in a single line.</li>
|
||||
<li>Each line will be treaded as a Reg expression.</li>
|
||||
<li>The result will be an 'OR' of the filters.</li>
|
||||
<li>All the filters will be tested against the URL.</li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
const panelVisible = this.props.globalStatus.activeMenuKey === MenuKeyMap.RECORD_FILTER;
|
||||
|
||||
return (
|
||||
<ResizablePanel onClose={this.onClose} visible={panelVisible} >
|
||||
<div className={Style.filterWrapper} >
|
||||
<div className={Style.title} >
|
||||
Filter
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace30} />
|
||||
<div className={Style.filterInput} >
|
||||
<Input
|
||||
type="textarea"
|
||||
placeholder="Type the filter here"
|
||||
rows={ 6 }
|
||||
onChange={this.onChange}
|
||||
value={this.props.globalStatus.filterStr}
|
||||
/>
|
||||
</div>
|
||||
<div className={Style.filterTip} >
|
||||
<Alert
|
||||
type="info"
|
||||
message="TIPS"
|
||||
description={description}
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ResizablePanel>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function select (state) {
|
||||
return {
|
||||
globalStatus: state.globalStatus
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(select)(RecordFilter);
|
||||
29
web/src/component/record-filter.less
Normal file
29
web/src/component/record-filter.less
Normal file
@@ -0,0 +1,29 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
.filterWrapper {
|
||||
padding: 10px 15px 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: @middlepanel-font-size;
|
||||
text-align: left;
|
||||
font-weight: 200;
|
||||
color: @hint-color;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
|
||||
}
|
||||
|
||||
.filterTip {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.tipList {
|
||||
color: @tip-color;
|
||||
li {
|
||||
list-style-type: decimal;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
84
web/src/component/record-list-diff-worker.jsx
Normal file
84
web/src/component/record-list-diff-worker.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* A webworker to identify whether the component need to be re-rendered
|
||||
*/
|
||||
const getFilterReg = function (filterStr) {
|
||||
let filterReg = null;
|
||||
if (filterStr) {
|
||||
let regFilterStr = filterStr
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\n\n/g, '\n');
|
||||
|
||||
// remove the last /\n$/ in case an accidential br
|
||||
regFilterStr = regFilterStr.replace(/\n$/, '');
|
||||
|
||||
if (regFilterStr[0] === '/' && regFilterStr[regFilterStr.length - 1] === '/') {
|
||||
regFilterStr = regFilterStr.substring(1, regFilterStr.length - 2);
|
||||
}
|
||||
|
||||
regFilterStr = regFilterStr.replace(/((.+)\n|(.+)$)/g, (matchStr, $1, $2) => {
|
||||
// if there is '\n' in the string
|
||||
if ($2) {
|
||||
return `(${$2})|`;
|
||||
} else {
|
||||
return `(${$1})`;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
filterReg = new RegExp(regFilterStr);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return filterReg;
|
||||
};
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const { limit, currentData, nextData, filterStr } = data;
|
||||
const filterReg = getFilterReg(filterStr);
|
||||
const filterdRecords = [];
|
||||
const length = nextData.length;
|
||||
|
||||
// mark if the component need to be refreshed
|
||||
let shouldUpdate = false;
|
||||
|
||||
// filtered out the records
|
||||
for (let i = 0; i < length; i++) {
|
||||
const item = nextData[i];
|
||||
if (filterReg && filterReg.test(item.url)) {
|
||||
filterdRecords.push(item);
|
||||
}
|
||||
|
||||
if (!filterReg) {
|
||||
filterdRecords.push(item);
|
||||
}
|
||||
|
||||
if (filterdRecords.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newDataLength = filterdRecords.length;
|
||||
const currentDataLength = currentData.length;
|
||||
|
||||
if (newDataLength !== currentDataLength) {
|
||||
shouldUpdate = true;
|
||||
} else {
|
||||
// only the two with same index and the `_render` === true then we'll need to render
|
||||
for (let i = 0; i < currentData.length; i++) {
|
||||
const item = currentData[i];
|
||||
const targetItem = filterdRecords[i];
|
||||
if (item.id !== targetItem.id || targetItem._render === true) {
|
||||
shouldUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.postMessage(JSON.stringify({
|
||||
shouldUpdate,
|
||||
data: filterdRecords
|
||||
}));
|
||||
});
|
||||
195
web/src/component/record-panel.jsx
Normal file
195
web/src/component/record-panel.jsx
Normal file
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* A copoment for the request log table
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Icon } from 'antd';
|
||||
import RecordRow from 'component/record-row';
|
||||
import Style from './record-panel.less';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import { fetchRecordDetail } from 'action/recordAction';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
|
||||
class RecordPanel extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
};
|
||||
|
||||
this.wsClient = null;
|
||||
|
||||
this.getRecordDetail = this.getRecordDetail.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.addKeyEvent = this.addKeyEvent.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
data: PropTypes.array,
|
||||
lastActiveRecordId: PropTypes.number,
|
||||
currentActiveRecordId: PropTypes.number,
|
||||
loadingNext: PropTypes.bool,
|
||||
loadingPrev: PropTypes.bool,
|
||||
stopRefresh: PropTypes.func
|
||||
}
|
||||
|
||||
getRecordDetail(id) {
|
||||
this.props.dispatch(fetchRecordDetail(id));
|
||||
this.props.stopRefresh();
|
||||
}
|
||||
|
||||
// get next detail with cursor, to go previous and next
|
||||
getNextDetail(cursor) {
|
||||
const currentId = this.props.currentActiveRecordId;
|
||||
this.props.dispatch(fetchRecordDetail(currentId + cursor));
|
||||
}
|
||||
|
||||
onKeyUp(e) {
|
||||
if (typeof this.props.currentActiveRecordId === 'number') {
|
||||
// up arrow
|
||||
if (e.keyCode === 38) {
|
||||
this.getNextDetail(-1);
|
||||
}
|
||||
|
||||
// down arrow
|
||||
if (e.keyCode === 40) {
|
||||
this.getNextDetail(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addKeyEvent() {
|
||||
document.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
getTrs() {
|
||||
const trs = [];
|
||||
|
||||
const { lastActiveRecordId, currentActiveRecordId } = this.props;
|
||||
const { data: recordList } = this.props;
|
||||
|
||||
const length = recordList.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
// only display records less than max limit
|
||||
if (i >= this.state.maxAllowedRecords) {
|
||||
break;
|
||||
}
|
||||
|
||||
const item = recordList[i];
|
||||
|
||||
const tableRowStyle = StyleBind('row', {
|
||||
lightBackgroundColor: item.id % 2 === 1,
|
||||
lightColor: item.statusCode === '',
|
||||
activeRow: currentActiveRecordId === item.id
|
||||
});
|
||||
|
||||
if (currentActiveRecordId === item.id || lastActiveRecordId === item.id) {
|
||||
item._render = true;
|
||||
}
|
||||
|
||||
trs.push(
|
||||
<RecordRow
|
||||
data={item}
|
||||
className={tableRowStyle}
|
||||
detailHandler={this.getRecordDetail}
|
||||
key={item.id}
|
||||
/>);
|
||||
}
|
||||
|
||||
return trs;
|
||||
}
|
||||
|
||||
getLoadingPreviousDiv() {
|
||||
if (!this.props.loadingPrev) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={Style.loading}>
|
||||
<td colSpan="7">
|
||||
<span > <Icon type="loading" />正在加载...</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
// <div className={Style.loading}> <Icon type="loading" />正在加载...</div>;
|
||||
}
|
||||
|
||||
getLoadingNextDiv() {
|
||||
if (!this.props.loadingNext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={Style.loading}>
|
||||
<td colSpan="7">
|
||||
<span > <Icon type="loading" />正在加载...</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { currentActiveRecordId, loadingNext, loadingPrev } = this.props;
|
||||
const shouldUpdate = nextProps.data !== this.props.data
|
||||
|| nextProps.loadingNext !== loadingNext
|
||||
|| nextProps.loadingPrev !== loadingPrev
|
||||
|| nextProps.currentActiveRecordId !== currentActiveRecordId;
|
||||
|
||||
// console.info(nextProps.data.length, this.props.data.length, shouldUpdate, Date.now());
|
||||
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.addKeyEvent();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Style.wrapper} >
|
||||
<div className="ant-table ant-table-small ant-table-scroll-position-left">
|
||||
<div className="ant-table-content">
|
||||
<table className="ant-table-body">
|
||||
<colgroup>
|
||||
<col style={{ width: '70px', minWidth: '70px' }} />
|
||||
<col style={{ width: '100px', minWidth: '100px' }} />
|
||||
<col style={{ width: '70px', minWidth: '70px' }} />
|
||||
<col style={{ width: '200px', minWidth: '200px' }} />
|
||||
<col style={{ width: 'auto', minWidth: '600px' }} />
|
||||
<col style={{ width: '160px', minWidth: '160px' }} />
|
||||
<col style={{ width: '100px', minWidth: '100px' }} />
|
||||
</colgroup>
|
||||
<thead className="ant-table-thead">
|
||||
<tr>
|
||||
<th className={Style.firstRow} >#</th>
|
||||
<th className={Style.leftRow} >Method</th>
|
||||
<th className={Style.centerRow} >Code</th>
|
||||
<th>Host</th>
|
||||
<th className={Style.pathRow} >Path</th>
|
||||
<th>Mime</th>
|
||||
<th>Start</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="ant-table-tbody" >
|
||||
{this.getLoadingPreviousDiv()}
|
||||
{this.getTrs()}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RecordPanel;
|
||||
81
web/src/component/record-panel.less
Normal file
81
web/src/component/record-panel.less
Normal file
@@ -0,0 +1,81 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
:global {
|
||||
.ant-table {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 0;
|
||||
padding-bottom: 50px;
|
||||
.ant-table-body {
|
||||
height: calc(100% - 114px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
}
|
||||
th {
|
||||
padding-top: 15px !important;
|
||||
padding-bottom: 12px !important;
|
||||
border: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.firstRow {
|
||||
padding-left: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.leftRow {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.centerRow {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pathRow {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
.row {
|
||||
cursor: pointer;
|
||||
font-size: @font-size-xs;
|
||||
td {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 5px !important;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lightBackgroundColor {
|
||||
background: @light-background-color;
|
||||
}
|
||||
|
||||
.lightColor {
|
||||
color: @tip-color;
|
||||
}
|
||||
|
||||
.activeRow {
|
||||
background: @active-color !important;
|
||||
color: #fff;
|
||||
:global {
|
||||
td {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.okStatus {
|
||||
color: @ok-color;
|
||||
}
|
||||
|
||||
tr.loading {
|
||||
text-align: center;
|
||||
color: @tip-color;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
213
web/src/component/record-request-detail.jsx
Normal file
213
web/src/component/record-request-detail.jsx
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* The panel to display the detial of the record
|
||||
*
|
||||
*/
|
||||
|
||||
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 ModalPanel from 'component/modal-panel';
|
||||
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';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
const PageIndexMap = {
|
||||
REQUEST_INDEX: 'REQUEST_INDEX',
|
||||
RESPONSE_INDEX: 'RESPONSE_INDEX'
|
||||
};
|
||||
|
||||
// the maximum length of the request body to decide whether to offer a download link for the request body
|
||||
const MAXIMUM_REQ_BODY_LENGTH = 10000;
|
||||
|
||||
class RecordRequestDetail extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
};
|
||||
|
||||
this.copyCurlCmd = this.copyCurlCmd.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
requestRecord: PropTypes.object
|
||||
}
|
||||
|
||||
onSelectText(e) {
|
||||
selectText(e.target);
|
||||
}
|
||||
|
||||
getLiDivs(targetObj) {
|
||||
const liDom = Object.keys(targetObj).map((key) => {
|
||||
return (
|
||||
<li key={key} className={Style.liItem} >
|
||||
<strong>{key} : </strong>
|
||||
<span>{targetObj[key]}</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return liDom;
|
||||
}
|
||||
|
||||
getCookieDiv(cookies) {
|
||||
let cookieArray = [];
|
||||
if (cookies) {
|
||||
const cookieStringArray = cookies.split(';');
|
||||
cookieArray = cookieStringArray.map((cookieString) => {
|
||||
const cookie = cookieString.split('=');
|
||||
return {
|
||||
name: cookie[0],
|
||||
value: cookie.slice(1).join('=') // cookie的值本身可能含有"=", 此处进行修正
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return <div className={Style.noCookes}>No Cookies</div>;
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'value'
|
||||
}
|
||||
];
|
||||
|
||||
const rowClassFunc = function (record, index) {
|
||||
// return index % 2 === 0 ? null : Style.odd;
|
||||
return null;
|
||||
};
|
||||
|
||||
const locale = {
|
||||
emptyText: 'No Cookies'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Style.cookieWrapper} >
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={cookieArray}
|
||||
pagination={false}
|
||||
size="middle"
|
||||
rowClassName={rowClassFunc}
|
||||
bordered
|
||||
locale={locale}
|
||||
rowKey="name"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getReqBodyDiv() {
|
||||
const { recordDetail } = this.props;
|
||||
const requestBody = recordDetail.reqBody;
|
||||
|
||||
const reqDownload = <a href={`/fetchReqBody?id=${recordDetail.id}&_t=${Date.now()}`} target="_blank">download</a>;
|
||||
const getReqBodyContent = () => {
|
||||
const bodyLength = requestBody.length;
|
||||
if (bodyLength > MAXIMUM_REQ_BODY_LENGTH) {
|
||||
return reqDownload;
|
||||
} else {
|
||||
return <div>{requestBody}</div>
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Style.reqBody} >
|
||||
{getReqBodyContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
notify(message, type = 'info', duration = 1.6, opts = {}) {
|
||||
notification[type]({ message, duration, ...opts })
|
||||
}
|
||||
|
||||
copyCurlCmd() {
|
||||
const recordDetail = this.props.recordDetail
|
||||
clipboard
|
||||
.copy(curlify(recordDetail))
|
||||
.then(() => this.notify('COPY SUCCESS', 'success'))
|
||||
.catch(() => this.notify('COPY FAILED', 'error'))
|
||||
}
|
||||
|
||||
getRequestDiv() {
|
||||
const recordDetail = this.props.recordDetail;
|
||||
const reqHeader = Object.assign({}, recordDetail.reqHeader);
|
||||
const cookieString = reqHeader.cookie || reqHeader.Cookie;
|
||||
delete reqHeader.cookie; // cookie will be displayed seperately
|
||||
|
||||
const { protocol, host, path } = recordDetail;
|
||||
return (
|
||||
<div>
|
||||
<div className={Style.section} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>General</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
<ul className={Style.ulItem} >
|
||||
<li className={Style.liItem} >
|
||||
<strong>Method:</strong>
|
||||
<span>{recordDetail.method} </span>
|
||||
</li>
|
||||
<li className={Style.liItem} >
|
||||
<strong>URL:</strong>
|
||||
<span onClick={this.onSelectText} >{`${protocol}://${host}${path}`} </span>
|
||||
</li>
|
||||
<li className={Style.liItem} >
|
||||
<strong>Protocol:</strong>
|
||||
<span >HTTP/1.1</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
<ul className={Style.ulItem} >
|
||||
<li className={Style.liItem} >
|
||||
<strong>CURL:</strong>
|
||||
<span>
|
||||
<a href="javascript:void(0)" onClick={this.copyCurlCmd} >copy as CURL</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={Style.section} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Header</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
<ul className={Style.ulItem} >
|
||||
{this.getLiDivs(reqHeader)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={Style.section + ' ' + Style.noBorder} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Cookies</span>
|
||||
</div>
|
||||
{this.getCookieDiv(cookieString)}
|
||||
</div>
|
||||
|
||||
<div className={Style.section} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Body</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
{this.getReqBodyDiv()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.getRequestDiv();
|
||||
}
|
||||
}
|
||||
|
||||
export default RecordRequestDetail;
|
||||
137
web/src/component/record-response-detail.jsx
Normal file
137
web/src/component/record-response-detail.jsx
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* The panel to display the response detial of the record
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import { Menu, Table, notification, Spin } from 'antd';
|
||||
import JsonViewer from 'component/json-viewer';
|
||||
import ModalPanel from 'component/modal-panel';
|
||||
|
||||
import Style from './record-detail.less';
|
||||
import CommonStyle from '../style/common.less';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
const PageIndexMap = {
|
||||
REQUEST_INDEX: 'REQUEST_INDEX',
|
||||
RESPONSE_INDEX: 'RESPONSE_INDEX'
|
||||
};
|
||||
|
||||
// the maximum length of the request body to decide whether to offer a download link for the request body
|
||||
const MAXIMUM_REQ_BODY_LENGTH = 10000;
|
||||
|
||||
class RecordResponseDetail extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
requestRecord: PropTypes.object
|
||||
}
|
||||
|
||||
onSelectText(e) {
|
||||
selectText(e.target);
|
||||
}
|
||||
|
||||
getLiDivs(targetObj) {
|
||||
const liDom = Object.keys(targetObj).map((key) => {
|
||||
return (
|
||||
<li key={key} className={Style.liItem} >
|
||||
<strong>{key} : </strong>
|
||||
<span>{targetObj[key]}</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return liDom;
|
||||
}
|
||||
|
||||
getImageBody(recordDetail) {
|
||||
return <img src={recordDetail.ref} className={Style.imageBody} />;
|
||||
}
|
||||
|
||||
getJsonBody(recordDetail) {
|
||||
return <JsonViewer data={recordDetail.resBody} />;
|
||||
}
|
||||
|
||||
getResBodyDiv() {
|
||||
const { recordDetail } = this.props;
|
||||
|
||||
const self = this;
|
||||
|
||||
let reqBodyDiv = <div className={Style.codeWrapper}> <pre>{recordDetail.resBody} </pre></div>;
|
||||
|
||||
switch (recordDetail.type) {
|
||||
case 'image': {
|
||||
reqBodyDiv = <div > {self.getImageBody(recordDetail)} </div>;
|
||||
break;
|
||||
}
|
||||
case 'json': {
|
||||
reqBodyDiv = self.getJsonBody(recordDetail);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (!recordDetail.resBody && recordDetail.ref) {
|
||||
reqBodyDiv = <a href={recordDetail.ref} target="_blank">{recordDetail.fileName}</a>;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Style.resBody} >
|
||||
{reqBodyDiv}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getResponseDiv(recordDetail) {
|
||||
const statusStyle = StyleBind({ okStatus: recordDetail.statusCode === 200 });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={Style.section} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>General</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
<ul className={Style.ulItem} >
|
||||
<li className={Style.liItem} >
|
||||
<strong>Status Code:</strong>
|
||||
<span className={statusStyle} > {recordDetail.statusCode} </span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={Style.section} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Header</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
<ul className={Style.ulItem} >
|
||||
{this.getLiDivs(recordDetail.resHeader)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className={Style.section} >
|
||||
<div >
|
||||
<span className={CommonStyle.sectionTitle}>Body</span>
|
||||
</div>
|
||||
<div className={CommonStyle.whiteSpace10} />
|
||||
{this.getResBodyDiv()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.getResponseDiv(this.props.recordDetail);
|
||||
}
|
||||
}
|
||||
|
||||
export default RecordResponseDetail;
|
||||
69
web/src/component/record-row.jsx
Normal file
69
web/src/component/record-row.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* A copoment for the request log table
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import { formatDate } from 'common/CommonUtil';
|
||||
|
||||
import Style from './record-row.less';
|
||||
import CommonStyle from '../style/common.less';
|
||||
import ClassBind from 'classnames/bind';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
|
||||
class RecordRow extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
data: PropTypes.object,
|
||||
detailHanlder: PropTypes.func,
|
||||
className: PropTypes.string
|
||||
}
|
||||
|
||||
getMethodDiv(item) {
|
||||
const httpsIcon = <div className={Style.https} title="https" />;
|
||||
return <div className={CommonStyle.topAlign} ><div>{item.method}</div> {item.protocol === 'https' ? httpsIcon : null}</div>;
|
||||
}
|
||||
|
||||
getCodeDiv(item) {
|
||||
const statusCode = parseInt(item.statusCode, 10);
|
||||
const className = StyleBind({ okStatus: statusCode === 200, errorStatus: statusCode >= 400 });
|
||||
return <span className={className} >{item.statusCode}</span>;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.data._render) {
|
||||
nextProps.data._render = false;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.data;
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={this.props.className} onClick={this.props.detailHandler.bind(this, data.id)} >
|
||||
<td className={Style.id} >{data.id}</td>
|
||||
<td className={Style.method} >{this.getMethodDiv(data)}</td>
|
||||
<td className={Style.code} >{this.getCodeDiv(data)}</td>
|
||||
<td className={Style.host} >{data.host}</td>
|
||||
<td className={Style.path} title={data.path} >{data.path}</td>
|
||||
<td className={Style.mime} title={data.mime} >{data.mime}</td>
|
||||
<td className={Style.time} >{formatDate(data.startTime, 'hh:mm:ss')}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RecordRow;
|
||||
69
web/src/component/record-row.less
Normal file
69
web/src/component/record-row.less
Normal file
@@ -0,0 +1,69 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
.tableRow {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-size: @font-size-sm;
|
||||
td {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lightBackgroundColor {
|
||||
background: @light-background-color;
|
||||
}
|
||||
|
||||
.id {
|
||||
padding-left: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.code {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// .host {
|
||||
// width: 200px;
|
||||
// }
|
||||
|
||||
.path {
|
||||
max-width: 0;
|
||||
min-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mime {
|
||||
min-width: 160px;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// .time {
|
||||
// width: 100px;
|
||||
// }
|
||||
|
||||
.okStatus {
|
||||
color: @ok-color;
|
||||
}
|
||||
|
||||
.errorStatus {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
.https {
|
||||
background: url('../assets/https.png');
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin-left: 1px;
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
}
|
||||
305
web/src/component/record-worker.jsx
Normal file
305
web/src/component/record-worker.jsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
* A webworker to identify whether the component need to be re-rendered
|
||||
*
|
||||
* the core idea is that, we do the caclulation here, compare the new filtered record with current one.
|
||||
* if they two are same, we'll send no update event out.
|
||||
* otherwise, will send out a full filtered record list, to replace the current one.
|
||||
*
|
||||
* The App itself will just need to display all the filtered records, the filter and max-limit logic are handled here.
|
||||
*/
|
||||
let recordList = [];
|
||||
// store all the filtered record, so there will be no need to re-calculate the filtere record fully through all records.
|
||||
self.FILTERED_RECORD_LIST = [];
|
||||
const defaultLimit = 500;
|
||||
self.currentStateData = []; // the data now used by state
|
||||
self.filterStr = '';
|
||||
|
||||
self.canLoadMore = false;
|
||||
self.updateQueryTimer = null;
|
||||
self.refreshing = true;
|
||||
self.beginIndex = 0;
|
||||
self.endIndex = self.beginIndex + defaultLimit - 1;
|
||||
self.IN_DIFF = false; // mark if currently in diff working
|
||||
|
||||
const getFilterReg = function (filterStr) {
|
||||
let filterReg = null;
|
||||
if (filterStr) {
|
||||
let regFilterStr = filterStr
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\n\n/g, '\n');
|
||||
|
||||
// remove the last /\n$/ in case an accidential br
|
||||
regFilterStr = regFilterStr.replace(/\n*$/, '');
|
||||
|
||||
if (regFilterStr[0] === '/' && regFilterStr[regFilterStr.length - 1] === '/') {
|
||||
regFilterStr = regFilterStr.substring(1, regFilterStr.length - 2);
|
||||
}
|
||||
|
||||
regFilterStr = regFilterStr.replace(/((.+)\n|(.+)$)/g, (matchStr, $1, $2) => {
|
||||
// if there is '\n' in the string
|
||||
if ($2) {
|
||||
return `(${$2})|`;
|
||||
} else {
|
||||
return `(${$1})`;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
filterReg = new RegExp(regFilterStr);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return filterReg;
|
||||
};
|
||||
|
||||
self.resetDisplayRecordIndex = function () {
|
||||
self.beginIndex = 0;
|
||||
self.endIndex = self.beginIndex + defaultLimit - 1;
|
||||
};
|
||||
|
||||
self.getFilteredRecords = function () {
|
||||
// const filterReg = getFilterReg(self.filterStr);
|
||||
// const filterdRecords = [];
|
||||
// const length = recordList.length;
|
||||
|
||||
// // filtered out the records
|
||||
// for (let i = 0 ; i < length; i++) {
|
||||
// const item = recordList[i];
|
||||
// if (filterReg && filterReg.test(item.url)) {
|
||||
// filterdRecords.push(item);
|
||||
// }
|
||||
|
||||
// if (!filterReg) {
|
||||
// filterdRecords.push(item);
|
||||
// }
|
||||
// }
|
||||
|
||||
// return filterdRecords;
|
||||
|
||||
return self.FILTERED_RECORD_LIST;
|
||||
};
|
||||
|
||||
/*
|
||||
* calculate the filtered records, at each time the origin list is updated
|
||||
* @param isFullyCalculate bool,
|
||||
whether to calculate the filtered record fully, if ture, will do a fully calculation;
|
||||
Otherwise, will only calculate the "listForThisTime",
|
||||
this usually means there are some updates for the 'filtered' list, or some new records arrived
|
||||
* @param listForThisTime object,
|
||||
the list which to be calculated for this time, usually the from the new event,
|
||||
contains some update to exist list, or some new records
|
||||
*/
|
||||
self.calculateFilteredRecords = function (isFullyCalculate, listForThisTime = []) {
|
||||
const filterReg = getFilterReg(self.filterStr);
|
||||
if (isFullyCalculate) {
|
||||
self.FILTERED_RECORD_LIST = [];
|
||||
const length = recordList.length;
|
||||
// filtered out the records
|
||||
for (let i = 0; i < length; i++) {
|
||||
const item = recordList[i];
|
||||
if (!filterReg || (filterReg && filterReg.test(item.url))) {
|
||||
self.FILTERED_RECORD_LIST.push(item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listForThisTime.forEach((item) => {
|
||||
const index = self.FILTERED_RECORD_LIST.findIndex((record) => {
|
||||
return item.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
self.FILTERED_RECORD_LIST[index] = item;
|
||||
} else if (!filterReg || (filterReg && filterReg.test(item.url))) {
|
||||
self.FILTERED_RECORD_LIST.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// diff the record, so when the refreshing is stoped, the page will not be updated
|
||||
// cause the filtered records will be unchanged
|
||||
self.diffRecords = function () {
|
||||
if (self.IN_DIFF) {
|
||||
return;
|
||||
}
|
||||
self.IN_DIFF = true;
|
||||
// mark if the component need to be refreshed
|
||||
let shouldUpdateRecord = false;
|
||||
|
||||
const filterdRecords = self.getFilteredRecords();
|
||||
|
||||
if (self.refreshing) {
|
||||
self.beginIndex = filterdRecords.length - 1 - defaultLimit;
|
||||
self.endIndex = filterdRecords.length - 1;
|
||||
} else {
|
||||
if (self.endIndex > filterdRecords.length) {
|
||||
self.endIndex = filterdRecords.length;
|
||||
}
|
||||
}
|
||||
|
||||
const newStateRecords = filterdRecords.slice(self.beginIndex, self.endIndex + 1);
|
||||
const currentDataLength = self.currentStateData.length;
|
||||
const newDataLength = newStateRecords.length;
|
||||
|
||||
if (newDataLength !== currentDataLength) {
|
||||
shouldUpdateRecord = true;
|
||||
} else {
|
||||
// only the two with same index and the `_render` === true then we'll need to render
|
||||
for (let i = 0; i < currentDataLength; i++) {
|
||||
const item = self.currentStateData[i];
|
||||
const targetItem = newStateRecords[i];
|
||||
if (item.id !== targetItem.id || targetItem._render === true) {
|
||||
shouldUpdateRecord = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.currentStateData = newStateRecords;
|
||||
|
||||
self.postMessage(JSON.stringify({
|
||||
type: 'updateData',
|
||||
shouldUpdateRecord,
|
||||
recordList: newStateRecords
|
||||
}));
|
||||
self.IN_DIFF = false;
|
||||
};
|
||||
|
||||
// check if there are many new records arrivied
|
||||
self.checkNewRecordsTip = function () {
|
||||
if (self.IN_DIFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRecordLength = self.getFilteredRecords().length;
|
||||
self.postMessage(JSON.stringify({
|
||||
type: 'updateTip',
|
||||
data: (newRecordLength - self.endIndex) > 0
|
||||
}));
|
||||
};
|
||||
|
||||
self.updateSingle = function (record) {
|
||||
recordList.forEach((item) => {
|
||||
item._render = false;
|
||||
});
|
||||
|
||||
const index = recordList.findIndex((item) => {
|
||||
return item.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
// set the mark to ensure the item get re-rendered
|
||||
record._render = true;
|
||||
recordList[index] = record;
|
||||
} else {
|
||||
recordList.push(record);
|
||||
}
|
||||
self.calculateFilteredRecords(false, [record]);
|
||||
};
|
||||
|
||||
self.updateMultiple = function (records) {
|
||||
recordList.forEach((item) => {
|
||||
item._render = false;
|
||||
});
|
||||
|
||||
records.forEach((record) => {
|
||||
const index = recordList.findIndex((item) => {
|
||||
return item.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
// set the mark to ensure the item get re-rendered
|
||||
record._render = true;
|
||||
recordList[index] = record;
|
||||
} else {
|
||||
recordList.push(record);
|
||||
}
|
||||
});
|
||||
|
||||
self.calculateFilteredRecords(false, records);
|
||||
};
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
switch (data.type) {
|
||||
case 'diff' : {
|
||||
self.diffRecords();
|
||||
break;
|
||||
}
|
||||
case 'updateQuery': {
|
||||
// if filterStr or limit changed
|
||||
if (data.filterStr !== self.filterStr) {
|
||||
self.updateQueryTimer && clearTimeout(self.updateQueryTimer);
|
||||
self.updateQueryTimer = setTimeout(() => {
|
||||
self.resetDisplayRecordIndex();
|
||||
self.filterStr = data.filterStr;
|
||||
self.calculateFilteredRecords(true);
|
||||
self.diffRecords();
|
||||
}, 150);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'updateSingle': {
|
||||
self.updateSingle(data.data);
|
||||
if (self.refreshing) {
|
||||
self.diffRecords();
|
||||
} else {
|
||||
self.checkNewRecordsTip();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateMultiple': {
|
||||
self.updateMultiple(data.data);
|
||||
if (self.refreshing) {
|
||||
self.diffRecords();
|
||||
} else {
|
||||
self.checkNewRecordsTip();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'initRecord': {
|
||||
recordList = data.data;
|
||||
self.calculateFilteredRecords(true);
|
||||
self.diffRecords();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'clear': {
|
||||
recordList = [];
|
||||
self.calculateFilteredRecords(true);
|
||||
self.diffRecords();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loadMore': {
|
||||
if (self.IN_DIFF) {
|
||||
return;
|
||||
}
|
||||
self.refreshing = false;
|
||||
if (data.data > 0) {
|
||||
self.endIndex += data.data;
|
||||
} else {
|
||||
self.beginIndex = Math.max(self.beginIndex + data.data, 0);
|
||||
}
|
||||
self.diffRecords();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateRefreshing': {
|
||||
if (typeof data.refreshing === 'boolean') {
|
||||
self.refreshing = data.refreshing;
|
||||
if (self.refreshing) {
|
||||
self.diffRecords();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
109
web/src/component/resizable-panel.jsx
Normal file
109
web/src/component/resizable-panel.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* A copoment to display content in the a resizable panel
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Icon } from 'antd';
|
||||
|
||||
import Style from './resizable-panel.less';
|
||||
import ClassBind from 'classnames/bind';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
|
||||
class ResizablePanel extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
dragBarLeft: '',
|
||||
contentLeft: ''
|
||||
};
|
||||
this.onDragbarMoveUp = this.onDragbarMoveUp.bind(this);
|
||||
this.onDragbarMove = this.onDragbarMove.bind(this);
|
||||
this.onDragbarMoveDown = this.onDragbarMoveDown.bind(this);
|
||||
this.doClose = this.doClose.bind(this);
|
||||
this.onKeyUp = this.onKeyUp.bind(this);
|
||||
this.addKeyEvent = this.addKeyEvent.bind(this);
|
||||
this.removeKeyEvent = this.removeKeyEvent.bind(this);
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.element,
|
||||
onClose: PropTypes.func,
|
||||
visible: PropTypes.bool
|
||||
}
|
||||
|
||||
onDragbarMove (event) {
|
||||
this.setState({
|
||||
dragBarLeft: event.pageX
|
||||
});
|
||||
}
|
||||
|
||||
onKeyUp (e) {
|
||||
if (e.keyCode == 27) {
|
||||
this.doClose();
|
||||
}
|
||||
}
|
||||
|
||||
addKeyEvent () {
|
||||
document.addEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
removeKeyEvent () {
|
||||
document.removeEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
onDragbarMoveUp (event) {
|
||||
this.setState({
|
||||
contentLeft: event.pageX
|
||||
});
|
||||
|
||||
document.removeEventListener('mousemove', this.onDragbarMove);
|
||||
document.removeEventListener('mouseup', this.onDragbarMoveUp);
|
||||
}
|
||||
|
||||
onDragbarMoveDown (event) {
|
||||
document.addEventListener('mousemove', this.onDragbarMove);
|
||||
|
||||
document.addEventListener('mouseup', this.onDragbarMoveUp);
|
||||
}
|
||||
|
||||
doClose () {
|
||||
this.props.onClose && this.props.onClose();
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.visible) {
|
||||
this.removeKeyEvent();
|
||||
return null;
|
||||
}
|
||||
this.addKeyEvent();
|
||||
|
||||
const { dragBarLeft, contentLeft } = this.state;
|
||||
const propsLeft = this.props.left;
|
||||
const dragBarStyle = dragBarLeft || propsLeft ? { 'left': dragBarLeft || propsLeft } : null;
|
||||
const contentStyle = contentLeft || propsLeft ? { 'left': contentLeft || propsLeft } : null;
|
||||
|
||||
const modalStyle = this.props.hideBackModal ? contentStyle : { 'left': 0 };
|
||||
return (
|
||||
<div className={Style.wrapper} onClick={this.onClose} style={modalStyle} >
|
||||
<div className={Style.contentWrapper} style={contentStyle} >
|
||||
<div className={Style.content} >
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={Style.dragBar}
|
||||
style={dragBarStyle}
|
||||
onMouseDown={this.onDragbarMoveDown}
|
||||
/>
|
||||
<div className={Style.closeIcon} title="Close, Esc" onClick={this.doClose} >
|
||||
<Icon type="close" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResizablePanel;
|
||||
39
web/src/component/resizable-panel.less
Normal file
39
web/src/component/resizable-panel.less
Normal file
@@ -0,0 +1,39 @@
|
||||
@import '../style/constant.less';
|
||||
.wrapper {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
width: 360px;
|
||||
height: 100%;
|
||||
-webkit-box-shadow: 10px 0px 21px 0px rgba(97,95,97,0.15);
|
||||
-moz-box-shadow: 10px 0px 21px 0px rgba(97,95,97,0.15);
|
||||
box-shadow: 10px 0px 21px 0px rgba(97,95,97,0.15);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.contentWrapper, .content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dragBar {
|
||||
width: 1px;
|
||||
background-color: @hint-color;
|
||||
z-index: 2000;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
i {
|
||||
font-size: @font-size-large;
|
||||
}
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
98
web/src/component/table-panel.jsx
Normal file
98
web/src/component/table-panel.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* A copoment for the request log table
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Table } from 'antd';
|
||||
import { formatDate } from 'common/CommonUtil';
|
||||
|
||||
import Style from './table-panel.less';
|
||||
import ClassBind from 'classnames/bind';
|
||||
import CommonStyle from '../style/common.less';
|
||||
|
||||
const StyleBind = ClassBind.bind(Style);
|
||||
|
||||
class TablePanel extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
active: true
|
||||
};
|
||||
}
|
||||
static propTypes = {
|
||||
data: PropTypes.array
|
||||
}
|
||||
|
||||
getTr () {
|
||||
|
||||
}
|
||||
render () {
|
||||
const httpsIcon = <i className="fa fa-lock" />;
|
||||
const columns = [
|
||||
{
|
||||
title: '#',
|
||||
width: 50,
|
||||
dataIndex: 'id'
|
||||
},
|
||||
{
|
||||
title: 'Method',
|
||||
width:100,
|
||||
dataIndex: 'method',
|
||||
render (text, item) {
|
||||
return <span>{text} {item.protocol === 'https' ? httpsIcon : null}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Code',
|
||||
width: 70,
|
||||
dataIndex: 'statusCode',
|
||||
render(text) {
|
||||
const className = StyleBind({ 'okStatus': text === '200' });
|
||||
return <span className={className} >{text}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Host',
|
||||
width: 200,
|
||||
dataIndex: 'host'
|
||||
},
|
||||
{
|
||||
title: 'Path',
|
||||
dataIndex: 'path'
|
||||
},
|
||||
{
|
||||
title: 'MIME',
|
||||
width: 150,
|
||||
dataIndex: 'mime'
|
||||
},
|
||||
{
|
||||
title: 'Start',
|
||||
width: 100,
|
||||
dataIndex: 'startTime',
|
||||
render (text) {
|
||||
const timeStr = formatDate(text, 'hh:mm:ss');
|
||||
return <span>{timeStr}</span>;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function rowClassFunc (record, index) {
|
||||
return StyleBind('row', { 'lightBackgroundColor': index % 2 === 1 });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Style.tableWrapper} >
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={this.props.data || []}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
rowClassName={rowClassFunc}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TablePanel;
|
||||
31
web/src/component/table-panel.less
Normal file
31
web/src/component/table-panel.less
Normal file
@@ -0,0 +1,31 @@
|
||||
@import '../style/constant.less';
|
||||
|
||||
.tableWrapper {
|
||||
clear: both;
|
||||
margin-top: 30px;
|
||||
:global {
|
||||
th {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 5px !important;
|
||||
background: @background-color !important;
|
||||
border-top: 1px solid @hint-color;
|
||||
border-bottom: 1px solid @hint-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.row {
|
||||
cursor: pointer;
|
||||
font-size: @font-size-sm;
|
||||
td {
|
||||
padding-top: 5px !important;
|
||||
padding-bottom: 5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lightBackgroundColor {
|
||||
background: @light-background-color;
|
||||
}
|
||||
|
||||
.okStatus {
|
||||
color: @ok-color;
|
||||
}
|
||||
62
web/src/component/title-bar.jsx
Normal file
62
web/src/component/title-bar.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* The panel to edit the filter
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Icon } from 'antd';
|
||||
import { getQueryParameter } from 'common/CommonUtil';
|
||||
|
||||
import Style from './title-bar.less';
|
||||
|
||||
class TitleBar extends React.Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
inMaxWindow: false,
|
||||
inApp: getQueryParameter('in_app_mode') // will only show the bar when in app
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (this.state.inApp !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the buttons with normal window size
|
||||
const normalButton = (
|
||||
<div className={Style.iconButtons} >
|
||||
<span className={Style.close} id="win-close-btn" >
|
||||
<Icon type="close" />
|
||||
</span>
|
||||
<span className={Style.minimize} id="win-min-btn" >
|
||||
<Icon type="minus" />
|
||||
</span>
|
||||
<span className={Style.maxmize} >
|
||||
<Icon type="arrows-alt" id="win-max-btn"/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const maxmizeButton = (
|
||||
<div className={Style.iconButtons} >
|
||||
<span className={Style.close} id="win-close-btn" >
|
||||
<Icon type="close" />
|
||||
</span>
|
||||
<span className={Style.disabled} />
|
||||
<span className={Style.maxmize} id="win-max-btn" >
|
||||
<Icon type="shrink" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return this.state.inMaxWindow ? maxmizeButton : normalButton;
|
||||
}
|
||||
}
|
||||
|
||||
export default TitleBar;
|
||||
45
web/src/component/title-bar.less
Normal file
45
web/src/component/title-bar.less
Normal file
@@ -0,0 +1,45 @@
|
||||
@import '../style/constant.less';
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.iconButtons {
|
||||
overflow: hidden;
|
||||
span {
|
||||
position: relative;
|
||||
display: block;
|
||||
float: left;
|
||||
width: 12.44px;
|
||||
height: 12.84px;
|
||||
border-radius: 50%;
|
||||
margin: 0 7px;
|
||||
}
|
||||
|
||||
.close {
|
||||
background-color: #EB3131;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.minimize {
|
||||
background-color: #FFBF11;
|
||||
}
|
||||
|
||||
.maxmize {
|
||||
background-color: #09CE26;
|
||||
}
|
||||
|
||||
i {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
i {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
web/src/component/ws-listener.jsx
Normal file
209
web/src/component/ws-listener.jsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* listen on the websocket event
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
import { message } from 'antd';
|
||||
import { initWs } from 'common/WsUtil';
|
||||
import { updateWholeRequest } from 'action/recordAction';
|
||||
import {
|
||||
updateShouldClearRecord,
|
||||
updateShowNewRecordTip
|
||||
} from 'action/globalStatusAction';
|
||||
const RecordWorker = require('worker-loader?inline!./record-worker.jsx');
|
||||
import { getJSON } from 'common/ApiUtil';
|
||||
|
||||
const myRecordWorker = new RecordWorker(window.URL.createObjectURL(new Blob([RecordWorker.toString()])));
|
||||
const fetchLatestLog = function () {
|
||||
getJSON('/latestLog')
|
||||
.then((data) => {
|
||||
const msg = {
|
||||
type: 'initRecord',
|
||||
data
|
||||
};
|
||||
myRecordWorker.postMessage(JSON.stringify(msg));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
message.error(error.errorMsg || 'Failed to load latest log');
|
||||
});
|
||||
};
|
||||
|
||||
class WsListener extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
wsInited: false
|
||||
}
|
||||
|
||||
this.initWs = this.initWs.bind(this);
|
||||
this.onWsMessage = this.onWsMessage.bind(this);
|
||||
this.loadNext = this.loadNext.bind(this);
|
||||
this.loadPrevious = this.loadPrevious.bind(this);
|
||||
this.stopPanelRefreshing = this.stopPanelRefreshing.bind(this);
|
||||
fetchLatestLog();
|
||||
|
||||
this.refreshing = true;
|
||||
this.loadingNext = false;
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
globalStatus: PropTypes.object
|
||||
}
|
||||
|
||||
loadPrevious() {
|
||||
this.stopPanelRefreshing();
|
||||
myRecordWorker.postMessage(JSON.stringify({
|
||||
type: 'loadMore',
|
||||
data: -500
|
||||
}));
|
||||
}
|
||||
|
||||
loadNext() {
|
||||
this.loadingNext = true;
|
||||
myRecordWorker.postMessage(JSON.stringify({
|
||||
type: 'loadMore',
|
||||
data: 500
|
||||
}));
|
||||
}
|
||||
|
||||
stopPanelRefreshing() {
|
||||
this.refreshing = false;
|
||||
myRecordWorker.postMessage(JSON.stringify({
|
||||
type: 'updateRefreshing',
|
||||
refreshing: false
|
||||
}));
|
||||
}
|
||||
|
||||
resumePanelRefreshing() {
|
||||
this.refreshing = true;
|
||||
this.loadingNext = false;
|
||||
this.props.dispatch(updateShowNewRecordTip(false));
|
||||
myRecordWorker.postMessage(JSON.stringify({
|
||||
type: 'updateRefreshing',
|
||||
refreshing: true
|
||||
}));
|
||||
}
|
||||
|
||||
onWsMessage(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
switch (data.type) {
|
||||
case 'update': {
|
||||
const record = data.content;
|
||||
|
||||
// stop update the record when it's turned off
|
||||
if (this.props.globalStatus.recording) {
|
||||
// this.props.dispatch(updateRecord(record));
|
||||
const msg = {
|
||||
type: 'updateSingle',
|
||||
data: record
|
||||
};
|
||||
myRecordWorker.postMessage(JSON.stringify(msg));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateMultiple': {
|
||||
const records = data.content;
|
||||
// stop update the record when it's turned off
|
||||
if (this.props.globalStatus.recording) {
|
||||
// // only in multiple mode we consider there are new records
|
||||
// if (!this.refreshing && !this.loadingNext) {
|
||||
// console.info(`==> this.loadingNext`, this.loadingNext)
|
||||
// const hasNew = records.some((item) => {
|
||||
// return (typeof item.id !== 'undefined');
|
||||
// });
|
||||
// hasNew && this.props.dispatch(updateShowNewRecordTip(true));
|
||||
// }
|
||||
|
||||
const msg = {
|
||||
type: 'updateMultiple',
|
||||
data: records
|
||||
};
|
||||
|
||||
myRecordWorker.postMessage(JSON.stringify(msg));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default : {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Failed to parse the websocket data with message: ', event.data);
|
||||
}
|
||||
}
|
||||
|
||||
initWs() {
|
||||
const { wsPort } = this.props.globalStatus;
|
||||
if (!wsPort || this.state.wsInited) {
|
||||
return;
|
||||
}
|
||||
this.state.wsInited = true;
|
||||
const wsClient = initWs(wsPort);
|
||||
wsClient.onmessage = this.onWsMessage;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
myRecordWorker.addEventListener('message', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.loadingNext = false;
|
||||
|
||||
switch (data.type) {
|
||||
case 'updateData': {
|
||||
if (data.shouldUpdateRecord) {
|
||||
this.props.dispatch(updateWholeRequest(data.recordList));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateTip': {
|
||||
this.props.dispatch(updateShowNewRecordTip(data.data));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {
|
||||
shouldClearAllRecord: nextShouldClearAllRecord
|
||||
} = nextProps.globalStatus;
|
||||
|
||||
|
||||
// if it's going to clear the record,
|
||||
if (nextShouldClearAllRecord) {
|
||||
const msg = {
|
||||
type: 'clear'
|
||||
};
|
||||
myRecordWorker.postMessage(JSON.stringify(msg));
|
||||
this.props.dispatch(updateShouldClearRecord(false));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.initWs();
|
||||
const { displayRecordLimit: limit, filterStr } = this.props.globalStatus;
|
||||
|
||||
const msg = {
|
||||
type: 'updateQuery',
|
||||
limit,
|
||||
filterStr
|
||||
};
|
||||
|
||||
myRecordWorker.postMessage(JSON.stringify(msg));
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default WsListener;
|
||||
Reference in New Issue
Block a user