update to 4.0

This commit is contained in:
Otto Mao
2017-12-01 21:30:49 +08:00
parent e392fefc64
commit 4be5aa8954
267 changed files with 27008 additions and 84482 deletions

View 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);

View 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;
}
}

View 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)

View 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;
}
}
}

View 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;

View File

@@ -0,0 +1,8 @@
@import '../style/constant.less';
.wrapper {
border: 1px solid #d9d9d9;
}
.contentDiv {
padding: 20px 25px;
background: #fff;
}

View 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);

View 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;
}
}

View 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));

View 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;
}
}

View 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;

View 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;
}

View 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;

View 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;
}

View 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);

View 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;
}
}

View 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
}));
});

View 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;

View 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;
}
}

View 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;

View 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;

View 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;

View 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;
}

View 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;
}
}
});

View 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;

View 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;
}
}

View 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;

View 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;
}

View 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;

View 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;
}
}
}

View 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;