update to 4.0
28
web/404.html
@@ -1,2 +0,0 @@
|
||||
jsx /src /build
|
||||
webpack --progress --colors
|
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
web socket util for AnyProxy
|
||||
https://github.com/alibaba/anyproxy
|
||||
*/
|
||||
|
||||
/*
|
||||
{
|
||||
baseUrl : ""
|
||||
}
|
||||
config
|
||||
config.baseUrl
|
||||
config.port
|
||||
config.onOpen
|
||||
config.onClose
|
||||
config.onError
|
||||
config.onGetData
|
||||
config.onGetUpdate
|
||||
config.onGetBody
|
||||
config.onError
|
||||
*/
|
||||
function anyproxy_wsUtil(config){
|
||||
config = config || {};
|
||||
if(!WebSocket){
|
||||
throw (new Error("webSocket is not available on this browser"));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var baseUrl = config.baseUrl || "127.0.0.1",
|
||||
socketPort = config.port || 8003;
|
||||
|
||||
var dataSocket = new WebSocket("ws://" + baseUrl + ":" + socketPort);
|
||||
|
||||
self.bodyCbMap = {};
|
||||
dataSocket.onmessage = function(event){
|
||||
config.onGetData && config.onGetData.call(self,event.data);
|
||||
|
||||
try{
|
||||
var data = JSON.parse(event.data),
|
||||
type = data.type,
|
||||
content = data.content,
|
||||
reqRef = data.reqRef;
|
||||
}catch(e){
|
||||
config.onError && config.onError.call(self, new Error("failed to parse socket data - " + e.toString()) );
|
||||
}
|
||||
|
||||
if(type == "update"){
|
||||
config.onGetUpdate && config.onGetUpdate.call(self, content);
|
||||
|
||||
}else if(type == "body"){
|
||||
config.onGetBody && config.onGetBody.call(self, content, reqRef);
|
||||
|
||||
if(data.reqRef && self.bodyCbMap[reqRef]){
|
||||
self.bodyCbMap[reqRef].call(self,content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataSocket.onopen = function(e){
|
||||
config.onOpen && config.onOpen.call(self,e);
|
||||
}
|
||||
dataSocket.onclose = function(e){
|
||||
config.onClose && config.onClose.call(self,e);
|
||||
}
|
||||
dataSocket.onerror = function(e){
|
||||
config.onError && config.onError.call(self,e);
|
||||
}
|
||||
|
||||
self.dataSocket = dataSocket;
|
||||
};
|
||||
|
||||
anyproxy_wsUtil.prototype.send = function(data){
|
||||
if(typeof data == "object"){
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
this.dataSocket.send(data);
|
||||
};
|
||||
|
||||
anyproxy_wsUtil.prototype.reqBody = function(id,callback){
|
||||
if(!id) return;
|
||||
|
||||
var payload = {
|
||||
type : "reqBody",
|
||||
id : id
|
||||
};
|
||||
if(callback){
|
||||
var reqRef = "r_" + Math.random()*100 + "_" + (new Date().getTime());
|
||||
payload.reqRef = reqRef;
|
||||
this.bodyCbMap[reqRef] = callback;
|
||||
}
|
||||
this.send(payload);
|
||||
};
|
||||
|
||||
module.exports = anyproxy_wsUtil;
|
@@ -1,27 +0,0 @@
|
||||
define(function(require,exports,module){
|
||||
var $ = require("$");
|
||||
|
||||
|
||||
|
||||
|
||||
// var cbMap = {};
|
||||
// function render(data,cb){
|
||||
// var resultEl = $(_.template(tpl, data)),
|
||||
// id = data._id;
|
||||
// try{
|
||||
// //if finished
|
||||
// var reqRef = "r" + Math.random() + "_" + new Date().getTime();
|
||||
// if(data.statusCode){
|
||||
// //fetch body
|
||||
// ws.reqBody(id,function(content){
|
||||
// $(".J_responseBody", resultEl).html(he.encode(content.body));
|
||||
// cb(resultEl);
|
||||
// });
|
||||
// }
|
||||
// }catch(e){
|
||||
// cb(resultEl);
|
||||
// };
|
||||
// }
|
||||
|
||||
module.exports = DetailPanel;
|
||||
});
|
@@ -1,120 +0,0 @@
|
||||
function init(React){
|
||||
|
||||
var DetailPanel = React.createClass({displayName: "DetailPanel",
|
||||
getInitialState : function(){
|
||||
return {
|
||||
body : {id : -1, content : null},
|
||||
left : "35%"
|
||||
};
|
||||
},
|
||||
loadBody:function(){
|
||||
var self = this,
|
||||
id = self.props.data.id;
|
||||
if(!id) return;
|
||||
|
||||
jQuery.get("/fetchBody?id=" + id ,function(resObj){
|
||||
if(resObj && resObj.id){
|
||||
if(resObj.type && resObj.type == "image" && resObj.ref){
|
||||
self.setState({
|
||||
body : {
|
||||
img : resObj.ref,
|
||||
id : resObj.id
|
||||
}
|
||||
});
|
||||
}else if(resObj.content){
|
||||
self.setState({
|
||||
body : {
|
||||
body : resObj.content,
|
||||
id : resObj.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
render : function(){
|
||||
var reqHeaderSection = [],
|
||||
resHeaderSection = [],
|
||||
summarySection,
|
||||
detailSection,
|
||||
bodyContent;
|
||||
|
||||
if(this.props.data.reqHeader){
|
||||
for(var key in this.props.data.reqHeader){
|
||||
reqHeaderSection.push(React.createElement("li", {key: "reqHeader_" + key}, React.createElement("strong", null, key), " : ", this.props.data.reqHeader[key]))
|
||||
}
|
||||
}
|
||||
|
||||
summarySection = (
|
||||
React.createElement("div", null,
|
||||
React.createElement("section", {className: "req"},
|
||||
React.createElement("h4", {className: "subTitle"}, "request"),
|
||||
React.createElement("div", {className: "detail"},
|
||||
React.createElement("ul", {className: "uk-list"},
|
||||
React.createElement("li", null, this.props.data.method, " ", React.createElement("span", {title: "{this.props.data.path}"}, this.props.data.path), " HTTP/1.1"),
|
||||
reqHeaderSection
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement("section", {className: "reqBody"},
|
||||
React.createElement("h4", {className: "subTitle"}, "request body"),
|
||||
React.createElement("div", {className: "detail"},
|
||||
React.createElement("p", null, this.props.data.reqBody)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if(this.props.data.statusCode){
|
||||
if(this.state.body.id == this.props.data.id){
|
||||
if(this.state.body.img){
|
||||
var imgEl = { __html : '<img src="'+ this.state.body.img +'" />'};
|
||||
bodyContent = (React.createElement("div", {dangerouslySetInnerHTML: imgEl}));
|
||||
}else{
|
||||
bodyContent = (React.createElement("pre", {className: "resBodyContent"}, this.state.body.body));
|
||||
}
|
||||
}else{
|
||||
bodyContent = null;
|
||||
this.loadBody();
|
||||
}
|
||||
|
||||
if(this.props.data.resHeader){
|
||||
for(var key in this.props.data.resHeader){
|
||||
resHeaderSection.push(React.createElement("li", {key: "resHeader_" + key}, React.createElement("strong", null, key), " : ", this.props.data.resHeader[key]))
|
||||
}
|
||||
}
|
||||
|
||||
detailSection = (
|
||||
React.createElement("div", null,
|
||||
React.createElement("section", {className: "resHeader"},
|
||||
React.createElement("h4", {className: "subTitle"}, "response header"),
|
||||
React.createElement("div", {className: "detail"},
|
||||
React.createElement("ul", {className: "uk-list"},
|
||||
React.createElement("li", null, "HTTP/1.1 ", React.createElement("span", {className: "http_status http_status_" + this.props.data.statusCode}, this.props.data.statusCode)),
|
||||
resHeaderSection
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement("section", {className: "resBody"},
|
||||
React.createElement("h4", {className: "subTitle"}, "response body"),
|
||||
React.createElement("div", {className: "detail"}, bodyContent)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("div", null,
|
||||
summarySection,
|
||||
detailSection
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return DetailPanel;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,43 +0,0 @@
|
||||
//Ref : http://jsfiddle.net/JxYca/3/
|
||||
var EventManager = function() {
|
||||
this.initialize();
|
||||
};
|
||||
EventManager.prototype = {
|
||||
initialize: function() {
|
||||
//declare listeners as an object
|
||||
this.listeners = {};
|
||||
},
|
||||
// public methods
|
||||
addListener: function(event, fn) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
if (fn instanceof Function) {
|
||||
this.listeners[event].push(fn);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
dispatchEvent: function(event, params) {
|
||||
// loop through listeners array
|
||||
for (var index = 0, l = this.listeners[event].length; index < l; index++) {
|
||||
// execute matching 'event' - loop through all indices and
|
||||
// when ev is found, execute
|
||||
this.listeners[event][index].call(window, params);
|
||||
}
|
||||
},
|
||||
removeListener: function(event, fn) {
|
||||
// split array 1 item after our listener
|
||||
// shorten to remove it
|
||||
// join the two arrays back together
|
||||
if (this.listeners[event]) {
|
||||
for (var i = 0, l = this.listners[event].length; i < l; i++) {
|
||||
if (this.listners[event][i] === fn) {
|
||||
this.listners[event].slice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EventManager;
|
@@ -1,43 +0,0 @@
|
||||
function init(React){
|
||||
|
||||
var Filter = React.createClass({displayName: "Filter",
|
||||
|
||||
dealChange:function(){
|
||||
var self = this,
|
||||
userInput = React.findDOMNode(self.refs.keywordInput).value;
|
||||
|
||||
self.props.onChangeKeyword && self.props.onChangeKeyword.call(null,userInput);
|
||||
},
|
||||
setFocus:function(){
|
||||
var self = this;
|
||||
React.findDOMNode(self.refs.keywordInput).focus();
|
||||
},
|
||||
componentDidUpdate:function(){
|
||||
this.setFocus();
|
||||
},
|
||||
render:function(){
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
React.createElement("div", null,
|
||||
React.createElement("h4", {className: "subTitle"}, "Log Filter"),
|
||||
React.createElement("div", {className: "filterSection"},
|
||||
React.createElement("div", {className: "uk-form"},
|
||||
React.createElement("input", {className: "uk-form-large", ref: "keywordInput", onChange: self.dealChange, type: "text", placeholder: "keywords or /^regExp$/", width: "300"})
|
||||
)
|
||||
),
|
||||
React.createElement("p", null,
|
||||
React.createElement("i", {className: "uk-icon-magic"}), " type ", React.createElement("strong", null, "/id=\\d", 3, "/"), " will give you all the logs containing ", React.createElement("strong", null, "id=123")
|
||||
)
|
||||
)
|
||||
);
|
||||
},
|
||||
componentDidMount:function(){
|
||||
this.setFocus();
|
||||
}
|
||||
});
|
||||
|
||||
return Filter;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,273 +0,0 @@
|
||||
window.$ = window.jQuery = require("../lib/jquery");
|
||||
|
||||
var EventManager = require('../lib/event'),
|
||||
Anyproxy_wsUtil = require("../lib/anyproxy_wsUtil"),
|
||||
React = require("../lib/react");
|
||||
|
||||
var WsIndicator = require("./wsIndicator").init(React),
|
||||
RecordPanel = require("./recordPanel").init(React),
|
||||
Popup = require("./popup").init(React);
|
||||
|
||||
var PopupContent = {
|
||||
map : require("./mapPanel").init(React),
|
||||
detail : require("./detailPanel").init(React),
|
||||
filter : require("./filter").init(React)
|
||||
};
|
||||
|
||||
var ifPause = false,
|
||||
recordSet = [];
|
||||
|
||||
//Event : wsGetUpdate
|
||||
//Event : recordSetUpdated
|
||||
//Event : wsOpen
|
||||
//Event : wsEnd
|
||||
var eventCenter = new EventManager();
|
||||
|
||||
//merge : right --> left
|
||||
function util_merge(left,right){
|
||||
for(var key in right){
|
||||
left[key] = right[key];
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
//invoke AnyProxy web socket util
|
||||
(function(){
|
||||
try{
|
||||
var ws = window.ws = new Anyproxy_wsUtil({
|
||||
baseUrl : document.getElementById("baseUrl").value,
|
||||
port : document.getElementById("socketPort").value,
|
||||
onOpen : function(){
|
||||
eventCenter.dispatchEvent("wsOpen");
|
||||
},
|
||||
onGetUpdate : function(record){
|
||||
eventCenter.dispatchEvent("wsGetUpdate",record);
|
||||
},
|
||||
onError : function(e){
|
||||
eventCenter.dispatchEvent("wsEnd");
|
||||
},
|
||||
onClose : function(e){
|
||||
eventCenter.dispatchEvent("wsEnd");
|
||||
}
|
||||
});
|
||||
window.ws = ws;
|
||||
|
||||
}catch(e){
|
||||
alert("failed to invoking web socket on this browser");
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
//websocket status indicator
|
||||
(function(){
|
||||
var wsIndicator = React.render(
|
||||
React.createElement(WsIndicator, null),
|
||||
document.getElementById("J_indicatorEl")
|
||||
);
|
||||
|
||||
eventCenter.addListener("wsOpen",function(){
|
||||
wsIndicator.setState({
|
||||
isValid : true
|
||||
});
|
||||
});
|
||||
|
||||
eventCenter.addListener("wsEnd",function(){
|
||||
wsIndicator.setState({
|
||||
isValid : false
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
//init popup
|
||||
var showPop;
|
||||
(function(){
|
||||
$("body").append('<div id="J_popOuter"></div>');
|
||||
var pop = React.render(
|
||||
React.createElement(Popup, null),
|
||||
document.getElementById("J_popOuter")
|
||||
);
|
||||
|
||||
showPop = function(popArg){
|
||||
var stateArg = util_merge({show : true },popArg);
|
||||
pop.setState(stateArg);
|
||||
};
|
||||
})();
|
||||
|
||||
//init record panel
|
||||
var recorder;
|
||||
(function(){
|
||||
function updateRecordSet(newRecord){
|
||||
if(ifPause) return;
|
||||
|
||||
if(newRecord && newRecord.id){
|
||||
if(!recordSet[newRecord.id]){
|
||||
recordSet[newRecord.id] = newRecord;
|
||||
}else{
|
||||
util_merge(recordSet[newRecord.id],newRecord);
|
||||
}
|
||||
|
||||
recordSet[newRecord.id]._justUpdated = true;
|
||||
// React.addons.Perf.start();
|
||||
eventCenter.dispatchEvent("recordSetUpdated");
|
||||
// React.addons.Perf.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function initRecordSet(){
|
||||
$.getJSON("/lastestLog",function(res){
|
||||
if(typeof res == "object"){
|
||||
res.map(function(item){
|
||||
if(item.id){
|
||||
recordSet[item.id] = item;
|
||||
}
|
||||
});
|
||||
eventCenter.dispatchEvent("recordSetUpdated");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
eventCenter.addListener("wsGetUpdate",updateRecordSet);
|
||||
|
||||
eventCenter.addListener('recordSetUpdated',function(){
|
||||
recorder.setState({
|
||||
list : recordSet
|
||||
});
|
||||
});
|
||||
|
||||
eventCenter.addListener("filterUpdated",function(newKeyword){
|
||||
recorder.setState({
|
||||
filter: newKeyword
|
||||
});
|
||||
});
|
||||
|
||||
function showDetail(data){
|
||||
showPop({left:"35%",content:React.createElement(PopupContent["detail"], {data:data})});
|
||||
}
|
||||
|
||||
//init recorder panel
|
||||
recorder = React.render(
|
||||
React.createElement(RecordPanel, {onSelect: showDetail}),
|
||||
document.getElementById("J_content")
|
||||
);
|
||||
|
||||
initRecordSet();
|
||||
})();
|
||||
|
||||
|
||||
//action bar
|
||||
(function(){
|
||||
|
||||
//clear log
|
||||
function clearLogs(){
|
||||
recordSet = [];
|
||||
eventCenter.dispatchEvent("recordSetUpdated");
|
||||
}
|
||||
|
||||
$(document).on("keyup",function(e){
|
||||
if(e.keyCode == 88 && e.ctrlKey){ // ctrl + x
|
||||
clearLogs();
|
||||
}
|
||||
});
|
||||
|
||||
var clearLogBtn = $(".J_clearBtn");
|
||||
clearLogBtn.on("click",function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
clearLogs();
|
||||
});
|
||||
|
||||
//start , pause
|
||||
var statusBtn = $(".J_statusBtn");
|
||||
statusBtn.on("click",function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
$(".J_statusBtn").removeClass("btn_disable");
|
||||
$(this).addClass("btn_disable");
|
||||
|
||||
if(/stop/i.test($(this).html()) ){
|
||||
ifPause = true;
|
||||
}else{
|
||||
ifPause = false;
|
||||
}
|
||||
});
|
||||
|
||||
//preset button
|
||||
(function (){
|
||||
var TopBtn = React.createClass({displayName: "TopBtn",
|
||||
getInitialState: function(){
|
||||
return {
|
||||
inUse : false
|
||||
};
|
||||
},
|
||||
render: function(){
|
||||
var self = this,
|
||||
iconClass = self.state.inUse ? "uk-icon-check" : self.props.icon,
|
||||
btnClass = self.state.inUse ? "topBtn topBtnInUse" : "topBtn";
|
||||
|
||||
return React.createElement("a", {href: "#"}, React.createElement("span", {className: btnClass, onClick: self.props.onClick}, React.createElement("i", {className: iconClass}), self.props.title))
|
||||
}
|
||||
});
|
||||
|
||||
// filter
|
||||
var filterBtn,
|
||||
FilterPanel = PopupContent["filter"],
|
||||
filterPanelEl;
|
||||
|
||||
filterBtn = React.render(React.createElement(TopBtn, {icon: "uk-icon-filter", title: "Filter", onClick: filterBtnClick}), document.getElementById("J_filterBtnContainer"));
|
||||
filterPanelEl = (React.createElement(FilterPanel, {onChangeKeyword: updateKeyword}) );
|
||||
|
||||
function updateKeyword(key){
|
||||
eventCenter.dispatchEvent("filterUpdated",key);
|
||||
filterBtn.setState({inUse : !!key});
|
||||
}
|
||||
function filterBtnClick(){
|
||||
showPop({ left:"60%", content:filterPanelEl });
|
||||
}
|
||||
|
||||
// map local
|
||||
var mapBtn,
|
||||
mapPanelEl;
|
||||
function onChangeMapConfig(cfg){
|
||||
mapBtn.setState({inUse : cfg && cfg.length});
|
||||
}
|
||||
|
||||
function mapBtnClick(){
|
||||
showPop({left:"60%", content:mapPanelEl });
|
||||
}
|
||||
|
||||
//detect whether to show the map btn
|
||||
require("./mapPanel").fetchConfig(function(initConfig){
|
||||
var MapPanel = PopupContent["map"];
|
||||
mapBtn = React.render(React.createElement(TopBtn, {icon: "uk-icon-shield", title: "Map Local", onClick: mapBtnClick}),document.getElementById("J_filterContainer"));
|
||||
mapPanelEl = (React.createElement(MapPanel, {onChange: onChangeMapConfig}));
|
||||
onChangeMapConfig(initConfig);
|
||||
});
|
||||
|
||||
var t = true;
|
||||
setInterval(function(){
|
||||
t = !t;
|
||||
// mapBtn && mapBtn.setState({inUse : t})
|
||||
},300);
|
||||
|
||||
|
||||
|
||||
})();
|
||||
|
||||
//other button
|
||||
(function(){
|
||||
$(".J_customButton").on("click",function(){
|
||||
var thisEl = $(this),
|
||||
iframeUrl = thisEl.attr("iframeUrl");
|
||||
|
||||
if(!iframeUrl){
|
||||
throw new Error("cannot find the url assigned for this button");
|
||||
}
|
||||
|
||||
var iframeEl = React.createElement("iframe",{src:iframeUrl,frameBorder:0});
|
||||
showPop({left:"35%", content: iframeEl });
|
||||
});
|
||||
})();
|
||||
|
||||
})();
|
@@ -1,120 +0,0 @@
|
||||
require("../lib/jstree");
|
||||
|
||||
function init(React){
|
||||
function fetchTree(root,cb){
|
||||
if(!root || root == "#"){
|
||||
root = "";
|
||||
}
|
||||
|
||||
$.getJSON("/filetree?root=" + root,function(resObj){
|
||||
var ret = [];
|
||||
try{
|
||||
$.each(resObj.directory, function(k,item){
|
||||
if(item.name.indexOf(".") == 0) return;
|
||||
ret.push({
|
||||
text : item.name,
|
||||
id : item.fullPath,
|
||||
icon : "uk-icon-folder",
|
||||
children : true
|
||||
});
|
||||
});
|
||||
|
||||
$.each(resObj.file, function(k,item){
|
||||
if(item.name.indexOf(".") == 0) return;
|
||||
ret.push({
|
||||
text : item.name,
|
||||
id : item.fullPath,
|
||||
icon : 'uk-icon-file-o',
|
||||
children : false
|
||||
});
|
||||
});
|
||||
}catch(e){}
|
||||
cb && cb.call(null,ret);
|
||||
});
|
||||
}
|
||||
|
||||
var MapForm = React.createClass({displayName: "MapForm",
|
||||
|
||||
submitData:function(){
|
||||
var self = this,
|
||||
result = {};
|
||||
|
||||
var filePathInput = React.findDOMNode(self.refs.localFilePath),
|
||||
filePath = filePathInput.value,
|
||||
keywordInput = React.findDOMNode(self.refs.keywordInput),
|
||||
keyword = keywordInput.value;
|
||||
|
||||
if(filePath && keyword){
|
||||
self.props.onSubmit.call(null,{
|
||||
keyword : keyword,
|
||||
local : filePath
|
||||
});
|
||||
|
||||
filePathInput.value = "";
|
||||
keywordInput.value = "";
|
||||
}
|
||||
},
|
||||
|
||||
render:function(){
|
||||
var self = this;
|
||||
return (
|
||||
React.createElement("div", null,
|
||||
React.createElement("form", {className: "uk-form uk-form-stacked mapAddNewForm"},
|
||||
React.createElement("fieldset", null,
|
||||
React.createElement("div", {className: "uk-form-row"},
|
||||
React.createElement("label", {className: "uk-form-label", htmlFor: "map_keywordInput"}, "keyword"),
|
||||
React.createElement("div", {className: "uk-form-controls"},
|
||||
React.createElement("input", {className: "mapConfigInputs", type: "text", id: "map_keywordInput", ref: "keywordInput", placeholder: "keyword"})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement("div", {className: "uk-form-row"},
|
||||
React.createElement("label", {className: "uk-form-label", htmlFor: "map_localFilePath"}, "local file"),
|
||||
React.createElement("div", {className: "uk-form-controls"},
|
||||
React.createElement("input", {className: "mapConfigInputs pathInput", type: "text", id: "map_localFilePath", ref: "localFilePath", placeholder: "local file path"})
|
||||
),
|
||||
React.createElement("div", {ref: "treeWrapper", className: "treeWrapper"})
|
||||
),
|
||||
|
||||
React.createElement("div", {className: "uk-form-row"},
|
||||
React.createElement("button", {type: "button", className: "uk-button", onClick: self.submitData}, "Add")
|
||||
)
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
componentDidMount :function(){
|
||||
var self = this;
|
||||
var wrapperEl = $(React.findDOMNode(self.refs.treeWrapper)),
|
||||
filePathInput = React.findDOMNode(self.refs.localFilePath);
|
||||
|
||||
wrapperEl.jstree({
|
||||
'core' : {
|
||||
'data' : function (node, cb) {
|
||||
fetchTree(node.id,cb);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapperEl.on("changed.jstree", function (e, data) {
|
||||
if(data && data.selected && data.selected.length){
|
||||
//is folder
|
||||
if(/folder/.test(data.node.icon)) return;
|
||||
|
||||
var item = data.selected[0];
|
||||
filePathInput.value = item;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
componentDidUpdate:function(){}
|
||||
});
|
||||
|
||||
return MapForm;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,86 +0,0 @@
|
||||
function fetchConfig(cb){
|
||||
return $.getJSON("/getMapConfig",cb);
|
||||
}
|
||||
|
||||
function init(React){
|
||||
var MapList = React.createClass({displayName: "MapList",
|
||||
getInitialState:function(){
|
||||
return {
|
||||
ruleList : []
|
||||
}
|
||||
},
|
||||
appendRecord:function(data){
|
||||
var self = this,
|
||||
newState = self.state.ruleList;
|
||||
|
||||
if(data && data.keyword && data.local){
|
||||
newState.push({
|
||||
keyword : data.keyword,
|
||||
local : data.local
|
||||
});
|
||||
|
||||
self.setState({
|
||||
ruleList: newState
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeRecord:function(index){
|
||||
var self = this,
|
||||
newList = self.state.ruleList;
|
||||
|
||||
newList.splice(index,1);
|
||||
self.setState({
|
||||
ruleList : newList
|
||||
});
|
||||
},
|
||||
render:function(){
|
||||
var self = this,
|
||||
collection = [];
|
||||
|
||||
collection = self.state.ruleList.map(function(item,index){
|
||||
return (
|
||||
React.createElement("li", null,
|
||||
React.createElement("strong", null, item.keyword), React.createElement("a", {className: "removeBtn", href: "#", onClick: self.removeRecord.bind(self,index)}, "remove"), React.createElement("br", null),
|
||||
React.createElement("span", null, item.local)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("ul", {className: "mapRuleList"},
|
||||
collection
|
||||
)
|
||||
);
|
||||
},
|
||||
componentDidMount :function(){
|
||||
var self = this;
|
||||
fetchConfig(function(data){
|
||||
self.setState({
|
||||
ruleList : data
|
||||
});
|
||||
});
|
||||
},
|
||||
componentDidUpdate:function(){
|
||||
var self = this;
|
||||
|
||||
//upload config to server
|
||||
var currentList = self.state.ruleList;
|
||||
$.ajax({
|
||||
method : "POST",
|
||||
url : "/setMapConfig",
|
||||
contentType :"application/json",
|
||||
data : JSON.stringify(currentList),
|
||||
dataType : "json",
|
||||
success :function(res){}
|
||||
});
|
||||
|
||||
self.props.onChange && self.props.onChange(self.state.ruleList);
|
||||
}
|
||||
});
|
||||
|
||||
return MapList;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
||||
module.exports.fetchConfig = fetchConfig;
|
@@ -1,33 +0,0 @@
|
||||
require("../lib/jstree");
|
||||
|
||||
function init(React){
|
||||
var MapForm = require("./mapForm").init(React),
|
||||
MapList = require("./mapList").init(React);
|
||||
|
||||
var MapPanel = React.createClass({displayName: "MapPanel",
|
||||
appendRecord : function(data){
|
||||
var self = this,
|
||||
listComponent = self.refs.list;
|
||||
|
||||
listComponent.appendRecord(data);
|
||||
},
|
||||
|
||||
render:function(){
|
||||
var self = this;
|
||||
return (
|
||||
React.createElement("div", {className: "mapWrapper"},
|
||||
React.createElement("h4", {className: "subTitle"}, "Current Config"),
|
||||
React.createElement(MapList, {ref: "list", onChange: self.props.onChange}),
|
||||
|
||||
React.createElement("h4", {className: "subTitle"}, "Add Map Rule"),
|
||||
React.createElement(MapForm, {onSubmit: self.appendRecord})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return MapPanel;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
||||
module.exports.fetchConfig = require("./mapList").fetchConfig;
|
@@ -1,94 +0,0 @@
|
||||
function init(React){
|
||||
|
||||
function dragableBar(initX,cb){
|
||||
var self = this,
|
||||
dragging = true;
|
||||
|
||||
var ghostbar = $('<div class="ghostbar"></div>').css("left",initX).prependTo('body');
|
||||
|
||||
$(document).mousemove(function(e){
|
||||
e.preventDefault();
|
||||
ghostbar.css("left",e.pageX + "px");
|
||||
});
|
||||
|
||||
$(document).mouseup(function(e){
|
||||
if(!dragging) return;
|
||||
|
||||
dragging = false;
|
||||
|
||||
var deltaPageX = e.pageX - initX;
|
||||
cb && cb.call(null,{
|
||||
delta : deltaPageX,
|
||||
finalX : e.pageX
|
||||
});
|
||||
|
||||
ghostbar.remove();
|
||||
$(document).unbind('mousemove');
|
||||
});
|
||||
}
|
||||
|
||||
var Popup = React.createClass({displayName: "Popup",
|
||||
getInitialState : function(){
|
||||
return {
|
||||
show : false,
|
||||
left : "35%",
|
||||
content : null
|
||||
};
|
||||
},
|
||||
componentDidMount:function(){
|
||||
var self = this;
|
||||
$(document).on("keyup",function(e){
|
||||
if(e.keyCode == 27){ //ESC
|
||||
self.setState({
|
||||
show : false
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setHide:function(){
|
||||
this.setState({
|
||||
show : false
|
||||
});
|
||||
},
|
||||
setShow:function(ifShow){
|
||||
this.setState({
|
||||
show : true
|
||||
});
|
||||
},
|
||||
dealDrag:function(){
|
||||
var self = this,
|
||||
leftVal = $(React.findDOMNode(this.refs.mainOverlay)).css("left");
|
||||
dragableBar(leftVal, function(data){
|
||||
if(data && data.finalX){
|
||||
if(window.innerWidth - data.finalX < 200){
|
||||
data.finalX = window.innerWidth - 200;
|
||||
}
|
||||
self.setState({
|
||||
left : data.finalX + "px"
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
componentDidUpdate:function(){
|
||||
|
||||
},
|
||||
render : function(){
|
||||
return (
|
||||
React.createElement("div", {style: {display:this.state.show ? "block" :"none"}},
|
||||
React.createElement("div", {className: "overlay_mask", onClick: this.setHide}),
|
||||
React.createElement("div", {className: "recordDetailOverlay", ref: "mainOverlay", style: {left: this.state.left}},
|
||||
React.createElement("div", {className: "dragbar", onMouseDown: this.dealDrag}),
|
||||
React.createElement("span", {className: "escBtn", onClick: this.setHide}, React.createElement("i", {className: "uk-icon-times"})),
|
||||
React.createElement("div", null,
|
||||
this.state.content
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Popup;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
21598
web/build/react.js
vendored
@@ -1,72 +0,0 @@
|
||||
function init(React){
|
||||
var RecordRow = require("./recordRow").init(React);
|
||||
|
||||
var RecordPanel = React.createClass({displayName: "RecordPanel",
|
||||
getInitialState : function(){
|
||||
return {
|
||||
list : [],
|
||||
filter: ""
|
||||
};
|
||||
},
|
||||
render : function(){
|
||||
var self = this,
|
||||
rowCollection = [],
|
||||
filterStr = self.state.filter,
|
||||
filter = filterStr;
|
||||
|
||||
//regexp
|
||||
if(filterStr[0]=="/" && filterStr[filterStr.length-1]=="/"){
|
||||
try{
|
||||
filter = new RegExp(filterStr.substr(1,filterStr.length-2));
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
for(var i = self.state.list.length-1 ; i >=0 ; i--){
|
||||
var item = self.state.list[i];
|
||||
if(item){
|
||||
if(filter && item){
|
||||
try{
|
||||
if(typeof filter == "object" && !filter.test(item.url)){
|
||||
continue;
|
||||
}else if(typeof filter == "string" && item.url.indexOf(filter) < 0){
|
||||
continue;
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
if(item._justUpdated){
|
||||
item._justUpdated = false;
|
||||
item._needRender = true;
|
||||
}else{
|
||||
item._needRender = false;
|
||||
}
|
||||
|
||||
rowCollection.push(React.createElement(RecordRow, {key: item.id, data: item, onSelect: self.props.onSelect.bind(self,item)}));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
React.createElement("table", {className: "uk-table uk-table-condensed uk-table-hover"},
|
||||
React.createElement("thead", null,
|
||||
React.createElement("tr", null,
|
||||
React.createElement("th", {className: "col_id"}, "#"),
|
||||
React.createElement("th", {className: "col_method"}, "method"),
|
||||
React.createElement("th", {className: "col_code"}, "code"),
|
||||
React.createElement("th", {className: "col_host"}, "host"),
|
||||
React.createElement("th", {className: "col_path"}, "path"),
|
||||
React.createElement("th", {className: "col_mime"}, "mime type"),
|
||||
React.createElement("th", {className: "col_time"}, "time")
|
||||
)
|
||||
),
|
||||
React.createElement("tbody", null,
|
||||
rowCollection
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return RecordPanel;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,65 +0,0 @@
|
||||
function init(React){
|
||||
function dateFormat(date,fmt) {
|
||||
var o = {
|
||||
"M+": date.getMonth() + 1, //月份
|
||||
"d+": date.getDate(), //日
|
||||
"h+": date.getHours(), //小时
|
||||
"m+": date.getMinutes(), //分
|
||||
"s+": date.getSeconds(), //秒
|
||||
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
|
||||
"S" : date.getMilliseconds() //毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
return fmt;
|
||||
}
|
||||
|
||||
var RecordRow = React.createClass({displayName: "RecordRow",
|
||||
getInitialState : function(){
|
||||
return null;
|
||||
},
|
||||
render : function(){
|
||||
var trClassesArr = [],
|
||||
trClasses,
|
||||
data = this.props.data || {};
|
||||
if(data.statusCode){
|
||||
trClassesArr.push("record_status_done");
|
||||
}
|
||||
|
||||
trClassesArr.push( ((Math.floor(data._id /2) - data._id /2) == 0)? "row_even" : "row_odd" );
|
||||
trClasses = trClassesArr.join(" ");
|
||||
|
||||
var dateStr = dateFormat(new Date(data.startTime),"hh:mm:ss");
|
||||
|
||||
var rowIcon = [];
|
||||
if(data.protocol == "https"){
|
||||
rowIcon.push(React.createElement("span", {className: "icon_record", title: "https"}, React.createElement("i", {className: "uk-icon-lock"})));
|
||||
}
|
||||
|
||||
if(data.ext && data.ext.map){
|
||||
rowIcon.push(React.createElement("span", {className: "icon_record", title: "mapped to local file"}, React.createElement("i", {className: "uk-icon-shield"})));
|
||||
}
|
||||
|
||||
return(
|
||||
React.createElement("tr", {className: trClasses, onClick: this.props.onSelect},
|
||||
React.createElement("td", {className: "data_id"}, data._id),
|
||||
React.createElement("td", null, data.method, " ", rowIcon, " "),
|
||||
React.createElement("td", {className: "http_status http_status_" + data.statusCode}, data.statusCode),
|
||||
React.createElement("td", {title: data.host}, data.host),
|
||||
React.createElement("td", {title: data.path}, data.path),
|
||||
React.createElement("td", null, data.mime),
|
||||
React.createElement("td", null, dateStr)
|
||||
)
|
||||
);
|
||||
},
|
||||
shouldComponentUpdate:function(nextPros){
|
||||
return nextPros.data._needRender;
|
||||
},
|
||||
componentDidUpdate:function(){},
|
||||
componentWillUnmount:function(){}
|
||||
});
|
||||
|
||||
return RecordRow;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,18 +0,0 @@
|
||||
function init(React){
|
||||
var WsIndicator = React.createClass({displayName: "WsIndicator",
|
||||
getInitialState:function(){
|
||||
return {
|
||||
isValid: false
|
||||
}
|
||||
},
|
||||
render:function(){
|
||||
return (
|
||||
React.createElement("img", {className: "logo_bottom anim_rotation", src: "https://t.alipayobjects.com/images/rmsweb/T1P_dfXa8oXXXXXXXX.png", width: "50", height: "50", style: {display: this.state.isValid ?"block" : "none"}})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return WsIndicator;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 1.8 KiB |
1
web/css/jstree_style/style.min.css
vendored
Before Width: | Height: | Size: 1.7 KiB |
360
web/css/page.css
@@ -1,360 +0,0 @@
|
||||
body{
|
||||
min-width: 1090px;
|
||||
}
|
||||
|
||||
body, html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topHead{
|
||||
height: 53px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.topHead .logoWrapper{
|
||||
float: left;
|
||||
width: 220px;
|
||||
height: 53px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.topHead .logoWrapper h1{
|
||||
color: #333;
|
||||
line-height: 53px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.topHead .logoWrapper .logo_bottom {
|
||||
position: absolute;
|
||||
left: -25px;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.anim_rotation{
|
||||
-webkit-animation: rotation 1.2s infinite cubic-bezier(.63,.33,.46,.71);
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotation {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
40% {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(359deg);
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.topHead .ctrlWrapper{
|
||||
height : 26px;
|
||||
line-height : 16px;
|
||||
overflow : hidden;
|
||||
text-overflow : ellipsis;
|
||||
white-space : nowrap;
|
||||
margin-top : 1px;
|
||||
}
|
||||
|
||||
.topHead .ctrlWrapper .sep{
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.topHead .topBtn{
|
||||
margin-right: 4px;
|
||||
transition:0.08s;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
min-width: 50px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.topHead .topBtn.topBtnInUse{
|
||||
color: #30B630;
|
||||
}
|
||||
|
||||
.topHead .topBtn:hover:not(.btn_disable){
|
||||
background: #07D;
|
||||
transition:0.08s;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.topHead a:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.topHead i{
|
||||
margin-right: 3px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.topHead .btn_disable{
|
||||
color: #777;
|
||||
}
|
||||
|
||||
|
||||
.mainTableWrapper{
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mainTableWrapper table{
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.mainTableWrapper thead{
|
||||
background: #F4F5F9;
|
||||
border-bottom: 1px solid #AAA;
|
||||
}
|
||||
|
||||
.mainTableWrapper td,
|
||||
.mainTableWrapper th{
|
||||
padding: 4px 12px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mainTableWrapper tbody tr{
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
.mainTableWrapper tbody tr.record_status_done{
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.mainTableWrapper .col_id{
|
||||
width: 25px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.mainTableWrapper .col_code{
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.mainTableWrapper .col_method{
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.mainTableWrapper .col_host{
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.mainTableWrapper .col_mime{
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.mainTableWrapper .col_time{
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.mainTableWrapper tr.row_odd{
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.mainTableWrapper tr.row_even{
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.uk-table-hover tbody tr:hover{
|
||||
cursor: pointer;
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
.resBody .resBodyContent{
|
||||
min-width: 200px;
|
||||
padding: 10px;
|
||||
border: 1px solid #99baca;
|
||||
background: #f5fbfe;
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
.resBody .resBodyContent img{
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.subTitle{
|
||||
padding-left: 6px;
|
||||
border-left: 3px solid #1FA2D6;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.detail{
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.overlay_mask{
|
||||
position: fixed;
|
||||
top:0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #EEE;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.recordDetailOverlay{
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
background: #FFF;
|
||||
border-left: 1px solid #CCC;
|
||||
top: 0;
|
||||
padding: 10px 20px 10px 10px;
|
||||
overflow-y:auto;
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.recordDetailOverlay .escBtn{
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
color: #777;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.recordDetailOverlay li{
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.recordDetailOverlay iframe{
|
||||
position: relative;
|
||||
height: 92vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data_id{
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.http_status{
|
||||
/*font-weight: 700;*/
|
||||
}
|
||||
|
||||
.http_status_200{
|
||||
color: #408E2F;
|
||||
}
|
||||
|
||||
.http_status_404,
|
||||
.http_status_500,
|
||||
.http_status_501,
|
||||
.http_status_502,
|
||||
.http_status_503,
|
||||
.http_status_504
|
||||
{
|
||||
color: #910A0A;
|
||||
}
|
||||
|
||||
.icon_record{
|
||||
display: inline;
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.dragbar{
|
||||
position:absolute;
|
||||
left:-5px;
|
||||
top:0px;
|
||||
height: 100%;
|
||||
width: 10px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.ghostbar{
|
||||
position:fixed;
|
||||
width:3px;
|
||||
height: 100vh;
|
||||
background-color:#000;
|
||||
opacity:0.5;
|
||||
cursor: col-resize;
|
||||
z-index:999
|
||||
}
|
||||
|
||||
/*filter*/
|
||||
|
||||
.filterSection{
|
||||
margin: 50px auto 0;
|
||||
}
|
||||
|
||||
.filterSection .uk-form{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.filterSection input{
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
/*filter end*/
|
||||
|
||||
|
||||
/*map panel*/
|
||||
|
||||
.jstree-icon.folder{
|
||||
background: url(http://img.alicdn.com/bao/uploaded/TB1TqJjIpXXXXcYXFXXSutbFXXX.jpg_q90.jpg) no-repeat !important;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.mapWrapper .form{
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mapWrapper .mapRuleList ul{
|
||||
list-style: none;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.mapWrapper .mapRuleList li{
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.mapWrapper .removeBtn{
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.mapWrapper .mapConfigInputs{
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.mapWrapper .mapConfigInputs.pathInput{
|
||||
border-radius: 4px 4px 0 0 ;
|
||||
}
|
||||
|
||||
.mapWrapper .treeWrapper{
|
||||
width: 398px;
|
||||
height: 350px;
|
||||
overflow: scroll;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.mapWrapper .mapAddNewForm{
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*map panel end*/
|
3
web/css/uikit.gradient.min.css
vendored
BIN
web/favico.png
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 423 KiB |
@@ -3,55 +3,18 @@
|
||||
<head>
|
||||
<title>AnyProxy</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="stylesheet" href="/css/uikit.gradient.min.css" />
|
||||
<link rel="stylesheet" href="/css/page.css" />
|
||||
<link rel="stylesheet" type="text/css" href="/css/jstree_style/style.min.css">
|
||||
|
||||
<link rel="icon" type="image/png" href="/favico.png" />
|
||||
<link rel="icon" type="image/png" href="/favico.png">
|
||||
<link rel="stylesheet" href="/dist/main.css" />
|
||||
<style type="text/css">
|
||||
#root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-width: 1050px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topHead">
|
||||
<div class="logoWrapper">
|
||||
<h1>AnyProxy</h1>
|
||||
<div id="J_indicatorEl"></div>
|
||||
</div>
|
||||
|
||||
<div class="ctrlWrapper">
|
||||
<a href="#"><span class="topBtn J_statusBtn"><i class="uk-icon-stop"></i>Stop</span></a>
|
||||
<a href="#"><span class="topBtn J_statusBtn btn_disable"><i class="uk-icon-play"></i>Resume</span></a>
|
||||
<a href="#"><span class="topBtn J_clearBtn"><i class="uk-icon-eraser"></i>Clear(Ctrl+X)</span></a>
|
||||
|
||||
<span class="sep">|</span>
|
||||
<a href="/fetchCrtFile" target="_blank"><span class="topBtn"><i class="uk-icon-download"></i>Download rootCA.crt</span></a>
|
||||
<a href="/qr_root" class="J_fetchRootQR" target="_blank"><span class="topBtn"><i class="uk-icon-qrcode"></i>QRCode of rootCA.crt</span></a>
|
||||
|
||||
<span class="sep">|</span>
|
||||
<a href="https://github.com/alibaba/anyproxy" target="_blank"><span class="topBtn"><i class="uk-icon-github"></i>Github</span></a>
|
||||
</div>
|
||||
|
||||
<div class="ctrlWrapper">
|
||||
<span id="J_filterBtnContainer"></span>
|
||||
<span id="J_filterContainer"></span>
|
||||
<span class="sep">|</span>
|
||||
|
||||
{@if customMenu.length}
|
||||
{@each customMenu as item}
|
||||
<a href="#"><span class="topBtn J_customButton" iframeUrl="${item.url}"><i class="${item.icon}"></i>${item.name}</span></a>
|
||||
{@/each}
|
||||
<span class="sep">|</span>
|
||||
{@/if}
|
||||
|
||||
<span title="${rule}"><i class="uk-icon-chain"></i>Rule : ${rule}</span>
|
||||
</div>
|
||||
|
||||
<div style="clear:both"></div>
|
||||
</div>
|
||||
|
||||
<div class="mainTableWrapper" id="J_content"></div>
|
||||
|
||||
<input type="hidden" id="socketPort" value="${wsPort}" />
|
||||
<input type="hidden" id="baseUrl" value="${ipAddress}" />
|
||||
|
||||
<script src="./page.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
<div id="root"></div>
|
||||
<script type="text/javascript" src="/dist/main.js"></script>
|
||||
</body>
|
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
web socket util for AnyProxy
|
||||
https://github.com/alibaba/anyproxy
|
||||
*/
|
||||
|
||||
/*
|
||||
{
|
||||
baseUrl : ""
|
||||
}
|
||||
config
|
||||
config.baseUrl
|
||||
config.port
|
||||
config.onOpen
|
||||
config.onClose
|
||||
config.onError
|
||||
config.onGetData
|
||||
config.onGetUpdate
|
||||
config.onGetBody
|
||||
config.onError
|
||||
*/
|
||||
function anyproxy_wsUtil(config){
|
||||
config = config || {};
|
||||
if(!WebSocket){
|
||||
throw (new Error("webSocket is not available on this browser"));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var baseUrl = config.baseUrl || "127.0.0.1",
|
||||
socketPort = config.port || 8003,
|
||||
dataSocket;
|
||||
|
||||
function initSocket(){
|
||||
self.bodyCbMap = {};
|
||||
dataSocket = new WebSocket("ws://" + baseUrl + ":" + socketPort);
|
||||
dataSocket.onmessage = function(event){
|
||||
config.onGetData && config.onGetData.call(self,event.data);
|
||||
|
||||
try{
|
||||
var data = JSON.parse(event.data),
|
||||
type = data.type,
|
||||
content = data.content,
|
||||
reqRef = data.reqRef;
|
||||
}catch(e){
|
||||
config.onError && config.onError.call(self, new Error("failed to parse socket data - " + e.toString()) );
|
||||
}
|
||||
|
||||
if(type == "update"){
|
||||
config.onGetUpdate && config.onGetUpdate.call(self, content);
|
||||
|
||||
}else if(type == "body"){
|
||||
config.onGetBody && config.onGetBody.call(self, content, reqRef);
|
||||
|
||||
if(data.reqRef && self.bodyCbMap[reqRef]){
|
||||
self.bodyCbMap[reqRef].call(self,content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dataSocket.onopen = function(e){
|
||||
config.onOpen && config.onOpen.call(self,e);
|
||||
}
|
||||
dataSocket.onclose = function(e){
|
||||
config.onClose && config.onClose.call(self,e);
|
||||
}
|
||||
dataSocket.onerror = function(e){
|
||||
config.onError && config.onError.call(self,e);
|
||||
}
|
||||
|
||||
self.dataSocket = dataSocket;
|
||||
}
|
||||
|
||||
initSocket();
|
||||
};
|
||||
|
||||
anyproxy_wsUtil.prototype.send = function(data){
|
||||
if(typeof data == "object"){
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
this.dataSocket.send(data);
|
||||
};
|
||||
|
||||
anyproxy_wsUtil.prototype.reqBody = function(id,callback){
|
||||
if(!id) return;
|
||||
|
||||
var payload = {
|
||||
type : "reqBody",
|
||||
id : id
|
||||
};
|
||||
if(callback){
|
||||
var reqRef = "r_" + Math.random()*100 + "_" + (new Date().getTime());
|
||||
payload.reqRef = reqRef;
|
||||
this.bodyCbMap[reqRef] = callback;
|
||||
}
|
||||
this.send(payload);
|
||||
};
|
||||
|
||||
if(typeof module != "undefined"){
|
||||
module.exports = anyproxy_wsUtil;
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
//Ref : http://jsfiddle.net/JxYca/3/
|
||||
var EventManager = function() {
|
||||
this.initialize();
|
||||
};
|
||||
EventManager.prototype = {
|
||||
initialize: function() {
|
||||
//declare listeners as an object
|
||||
this.listeners = {};
|
||||
},
|
||||
// public methods
|
||||
addListener: function(event, fn) {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
if (fn instanceof Function) {
|
||||
this.listeners[event].push(fn);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
dispatchEvent: function(event, params) {
|
||||
// loop through listeners array
|
||||
for (var index = 0, l = this.listeners[event].length; index < l; index++) {
|
||||
// execute matching 'event' - loop through all indices and
|
||||
// when ev is found, execute
|
||||
this.listeners[event][index].call(window, params);
|
||||
}
|
||||
},
|
||||
removeListener: function(event, fn) {
|
||||
// split array 1 item after our listener
|
||||
// shorten to remove it
|
||||
// join the two arrays back together
|
||||
if (this.listeners[event]) {
|
||||
for (var i = 0, l = this.listners[event].length; i < l; i++) {
|
||||
if (this.listners[event][i] === fn) {
|
||||
this.listners[event].slice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = EventManager;
|
4
web/lib/jquery.js
vendored
7420
web/lib/jstree.js
19602
web/lib/react.js
vendored
28220
web/page.js
217
web/src/action/globalStatusAction.js
Normal file
@@ -0,0 +1,217 @@
|
||||
export const STOP_RECORDING = 'STOP_RECORDING';
|
||||
export const RESUME_RECORDING = 'RESUME_RECORDING';
|
||||
export const SHOW_FILTER = 'SHOW_FILTER';
|
||||
export const HIDE_FILTER = 'HIDE_FILTER';
|
||||
export const UPDATE_FILTER = 'UPDATE_FILTER';
|
||||
export const SHOW_MAP_LOCAL = 'SHOW_MAP_LOCAL';
|
||||
export const HIDE_MAP_LOCAL = 'HIDE_MAP_LOCAL';
|
||||
export const FETCH_DIRECTORY = 'FETCH_DIRECTORY'; // fetch the directory
|
||||
export const UPDATE_LOCAL_DIRECTORY = 'UPDATE_LOCAL_DIRECTORY';
|
||||
export const FETCH_MAPPED_CONFIG = 'FETCH_MAPPED_CONFIG';
|
||||
export const UPDATE_LOCAL_MAPPED_CONFIG = 'UPDATE_LOCAL_MAPPED_CONFIG';
|
||||
export const UPDATE_REMOTE_MAPPED_CONFIG = 'UPDATE_REMOTE_MAPPED_CONFIG';
|
||||
export const UPDATE_ACTIVE_RECORD_ITEM = 'UPDATE_ACTIVE_RECORD_ITEM';
|
||||
export const UPDATE_GLOBAL_WSPORT = 'UPDATE_GLOBAL_WSPORT';
|
||||
|
||||
export const TOGGLE_REMOTE_INTERCEPT_HTTPS = 'TOGGLE_REMOTE_INTERCEPT_HTTPS';
|
||||
export const UPDATE_LOCAL_INTERCEPT_HTTPS_FLAG = 'UPDATE_LOCAL_INTERCEPT_HTTPS_FLAG';
|
||||
|
||||
export const TOGGLE_REMORE_GLOBAL_PROXY_FLAG = 'TOGGLE_REMORE_GLOBAL_PROXY_FLAG';
|
||||
export const UPDATE_LOCAL_GLOBAL_PROXY_FLAG = 'UPDATE_LOCAL_GLOBAL_PROXY_FLAG';
|
||||
|
||||
export const SHOW_ROOT_CA = 'SHOW_ROOT_CA';
|
||||
export const HIDE_ROOT_CA = 'HIDE_ROOT_CA';
|
||||
|
||||
export const UPDATE_CAN_LOAD_MORE = 'UPDATE_CAN_LOAD_MORE';
|
||||
export const INCREASE_DISPLAY_RECORD_LIST = 'INCREASE_DISPLAY_RECORD_LIST';
|
||||
export const UPDATE_SHOULD_CLEAR_RECORD = 'UPDATE_SHOULD_CLEAR_RECORD';
|
||||
export const UPDATE_APP_VERSION = 'UPDATE_APP_VERSION';
|
||||
export const UPDATE_IS_ROOTCA_EXISTS = 'UPDATE_IS_ROOTCA_EXISTS';
|
||||
|
||||
// should we display the tip for new record
|
||||
export const UPDATE_SHOW_NEW_RECORD_TIP = 'UPDATE_SHOW_NEW_RECORD_TIP';
|
||||
// update if currently loading the record from server
|
||||
export const UPDATE_FETCHING_RECORD_STATUS = 'UPDATE_FETCHING_RECORD_STATUS';
|
||||
|
||||
export function stopRecording() {
|
||||
return {
|
||||
type: STOP_RECORDING
|
||||
};
|
||||
}
|
||||
|
||||
export function resumeRecording() {
|
||||
return {
|
||||
type: RESUME_RECORDING
|
||||
};
|
||||
}
|
||||
|
||||
export function showFilter() {
|
||||
return {
|
||||
type: SHOW_FILTER
|
||||
};
|
||||
}
|
||||
|
||||
export function hideFilter() {
|
||||
return {
|
||||
type: HIDE_FILTER
|
||||
};
|
||||
}
|
||||
|
||||
export function updateFilter(filterStr) {
|
||||
return {
|
||||
type: UPDATE_FILTER,
|
||||
data: filterStr
|
||||
};
|
||||
}
|
||||
|
||||
export function showMapLocal() {
|
||||
return {
|
||||
type: SHOW_MAP_LOCAL
|
||||
};
|
||||
}
|
||||
|
||||
export function hideMapLocal() {
|
||||
return {
|
||||
type: HIDE_MAP_LOCAL
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchDirectory(path) {
|
||||
return {
|
||||
type: FETCH_DIRECTORY,
|
||||
data: path
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLocalDirectory(path, sub) {
|
||||
return {
|
||||
type: UPDATE_LOCAL_DIRECTORY,
|
||||
data: {
|
||||
path,
|
||||
sub
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMappedConfig() {
|
||||
return {
|
||||
type: FETCH_MAPPED_CONFIG
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLocalMappedConfig(config) {
|
||||
return {
|
||||
type: UPDATE_LOCAL_MAPPED_CONFIG,
|
||||
data: config
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRemoteMappedConfig(config) {
|
||||
return {
|
||||
type: UPDATE_REMOTE_MAPPED_CONFIG,
|
||||
data: config
|
||||
};
|
||||
}
|
||||
|
||||
export function updateActiveRecordItem(id) {
|
||||
return {
|
||||
type: UPDATE_ACTIVE_RECORD_ITEM,
|
||||
data: id
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLocalInterceptHttpsFlag(flag) {
|
||||
return {
|
||||
type: UPDATE_LOCAL_INTERCEPT_HTTPS_FLAG,
|
||||
data: flag
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleRemoteInterceptHttpsFlag(flag) {
|
||||
return {
|
||||
type: TOGGLE_REMOTE_INTERCEPT_HTTPS,
|
||||
data: flag
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleRemoteGlobalProxyFlag(flag) {
|
||||
return {
|
||||
type: TOGGLE_REMORE_GLOBAL_PROXY_FLAG,
|
||||
data: flag
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLocalGlobalProxyFlag(flag) {
|
||||
return {
|
||||
type: UPDATE_LOCAL_GLOBAL_PROXY_FLAG,
|
||||
data: flag
|
||||
};
|
||||
}
|
||||
|
||||
export function showRootCA() {
|
||||
return {
|
||||
type: SHOW_ROOT_CA
|
||||
};
|
||||
}
|
||||
|
||||
export function hideRootCA() {
|
||||
return {
|
||||
type: HIDE_ROOT_CA
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCanLoadMore(canLoadMore) {
|
||||
return {
|
||||
type: UPDATE_CAN_LOAD_MORE,
|
||||
data: canLoadMore
|
||||
};
|
||||
}
|
||||
|
||||
export function increaseDisplayRecordLimit(moreToAdd) {
|
||||
return {
|
||||
type: INCREASE_DISPLAY_RECORD_LIST,
|
||||
data: moreToAdd
|
||||
};
|
||||
}
|
||||
|
||||
export function updateShouldClearRecord(shouldClear) {
|
||||
return {
|
||||
type: UPDATE_SHOULD_CLEAR_RECORD,
|
||||
data: shouldClear
|
||||
};
|
||||
}
|
||||
|
||||
export function updateLocalAppVersion(version) {
|
||||
return {
|
||||
type: UPDATE_APP_VERSION,
|
||||
data: version
|
||||
};
|
||||
}
|
||||
|
||||
export function updateShowNewRecordTip(shouldShow) {
|
||||
return {
|
||||
type: UPDATE_SHOW_NEW_RECORD_TIP,
|
||||
data: shouldShow
|
||||
};
|
||||
}
|
||||
|
||||
export function updateIsRootCAExists(exists) {
|
||||
return {
|
||||
type: UPDATE_IS_ROOTCA_EXISTS,
|
||||
data: exists
|
||||
};
|
||||
}
|
||||
|
||||
export function updateGlobalWsPort(wsPort) {
|
||||
return {
|
||||
type: UPDATE_GLOBAL_WSPORT,
|
||||
data: wsPort
|
||||
}
|
||||
}
|
||||
|
||||
export function updateFechingRecordStatus(isFetching) {
|
||||
return {
|
||||
type: UPDATE_FETCHING_RECORD_STATUS,
|
||||
data: isFetching
|
||||
}
|
||||
}
|
69
web/src/action/recordAction.js
Normal file
@@ -0,0 +1,69 @@
|
||||
export const FETCH_REQUEST_LOG = 'FETCH_REQUEST_LOG';
|
||||
export const UPDATE_WHOLE_REQUEST = 'UPDATE_WHOLE_REQUEST';
|
||||
export const UPDATE_SINGLE_RECORD = 'UPDATE_SINGLE_RECORD';
|
||||
export const CLEAR_ALL_RECORD = 'CLEAR_ALL_RECORD';
|
||||
export const CLEAR_ALL_LOCAL_RECORD = 'CLEAR_ALL_LOCAL_RECORD';
|
||||
export const FETCH_RECORD_DETAIL = 'FETCH_RECORD_DETAIL';
|
||||
export const SHOW_RECORD_DETAIL = 'SHOW_RECORD_DETAIL';
|
||||
export const HIDE_RECORD_DETAIL = 'HIDE_RECORD_DETAIL';
|
||||
export const UPDATE_MULTIPLE_RECORDS = 'UPDATE_MULTIPLE_RECORDS';
|
||||
|
||||
export function fetchRequestLog() {
|
||||
return {
|
||||
type: FETCH_REQUEST_LOG
|
||||
};
|
||||
}
|
||||
|
||||
export function updateWholeRequest(data) {
|
||||
return {
|
||||
type: UPDATE_WHOLE_REQUEST,
|
||||
data: data
|
||||
};
|
||||
}
|
||||
|
||||
export function updateRecord(record) {
|
||||
return {
|
||||
type: UPDATE_SINGLE_RECORD,
|
||||
data: record
|
||||
};
|
||||
}
|
||||
|
||||
export function clearAllRecord () {
|
||||
return {
|
||||
type: CLEAR_ALL_RECORD
|
||||
};
|
||||
}
|
||||
|
||||
export function clearAllLocalRecord () {
|
||||
return {
|
||||
type: CLEAR_ALL_LOCAL_RECORD
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchRecordDetail (recordId) {
|
||||
return {
|
||||
type: FETCH_RECORD_DETAIL,
|
||||
data: recordId
|
||||
};
|
||||
}
|
||||
|
||||
export function showRecordDetail (record) {
|
||||
return {
|
||||
type: SHOW_RECORD_DETAIL,
|
||||
data: record
|
||||
};
|
||||
}
|
||||
|
||||
export function hideRecordDetail () {
|
||||
return {
|
||||
type: HIDE_RECORD_DETAIL
|
||||
};
|
||||
}
|
||||
|
||||
export function updateMultipleRecords (records) {
|
||||
return {
|
||||
type: UPDATE_MULTIPLE_RECORDS,
|
||||
data: records
|
||||
};
|
||||
}
|
||||
|
21
web/src/assets/clear.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="26px" height="23px" viewBox="0 0 26 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>clear</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<path d="M14.404298,23 L25.8072366,10.1478112 L14.7771384,0.0807951437 L0.573792401,16.0893076 L8.14561061,23 L14.404298,23 Z" id="eye-path-1"></path>
|
||||
</defs>
|
||||
<g id="clear-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="clear">
|
||||
<g id="clear-group1">
|
||||
<mask id="clear-mask-2" fill="white">
|
||||
<use xlink:href="#eye-path-1"></use>
|
||||
</mask>
|
||||
<use id="clear-Mask" fill="#3A3A3A" xlink:href="#eye-path-1"></use>
|
||||
<polygon id="clear-gray-rec" fill="#E0E0E0" mask="url(#clear-mask-2)" points="5.94833903 7.43670143 19.9092371 19.7626302 10.9693568 28.720266 -2.14582775 16.3594041"></polygon>
|
||||
</g>
|
||||
<polyline id="clear-bottom-line" stroke="#3A3A3A" stroke-width="2" stroke-linecap="square" points="3.3999939 22 8.17626453 22 18.5999939 22"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
12
web/src/assets/download.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="15px" height="15px" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="download-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.5">
|
||||
<g id="download-Artboard-Copy" transform="translate(-81.000000, -411.000000)" fill="#FFFFFF">
|
||||
<path d="M81,426 L81,424.774002 L81,423.548004 L81,422.322006 L82.6666931,422.322006 L82.6666931,423.548004 L94.3333069,423.548004 L94.3333069,422.322006 L96,422.322006 L96,423.548004 L96,424.774002 L96,426 L81,426 Z M85.5,416.566715 L82.0638717,416.516663 L88.5661122,423.259455 L94.9362596,416.516794 L91.5,416.518266 L91.5,411 L85.5,411 L85.5,416.566715 Z" id="download-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 982 B |
12
web/src/assets/filter.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="15px" height="16px" viewBox="0 0 15 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>filter</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="filter-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.5">
|
||||
<g id="filter-Artboard-Copy-5" transform="translate(-97.000000, -292.000000)" fill="#FFFFFF">
|
||||
<polygon id="filter" points="97 292 112 292 106.703713 297.939369 106.823828 308 102.26223 304.371582 102.26223 297.939369"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 728 B |
BIN
web/src/assets/https.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
12
web/src/assets/play.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="18px" viewBox="0 0 16 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>play</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="play-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="play-Artboard-Copy-5" transform="translate(-278.000000, -165.000000)" fill="#108EE9">
|
||||
<polygon id="play-play" points="278 165 293.494141 174 278 183"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 649 B |
15
web/src/assets/retweet.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="21px" height="13px" viewBox="0 0 21 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Group</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="retweet-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.5">
|
||||
<g id="retweet-Artboard-Copy" transform="translate(-78.000000, -354.000000)" fill="#FFFFFF">
|
||||
<g id="retweet-Group" transform="translate(78.000000, 354.000000)">
|
||||
<polygon id="retweet-Path-3-Copy-2" points="16.8367004 0 16.8367004 6.88540415 20.1927161 6.86020623 15.5159149 11.8396841 10.9366574 6.86020623 14.1340833 6.84578316 14.1340833 2.51778471 9.51127625 2.51778471 7 0"></polygon>
|
||||
<polygon id="retweet-Path-3-Copy-3" transform="translate(6.596358, 6.919842) scale(-1, -1) translate(-6.596358, -6.919842) " points="9.83670044 1 9.83670044 7.88540415 13.1927161 7.86020623 8.51591492 12.8396841 3.93665738 7.86020623 7.13408327 7.84578316 7.13408327 3.51778471 2.51127625 3.51778471 1.77635684e-15 1"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
12
web/src/assets/start.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="18px" viewBox="0 0 16 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>start</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Artboard-Copy-5" transform="translate(-278.000000, -165.000000)" fill="#108EE9">
|
||||
<polygon id="start" points="278 165 293.494141 174 278 183"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 636 B |
12
web/src/assets/stop.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>stop</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="stop-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Artboard" transform="translate(-278.000000, -165.000000)" fill="#3A3A3A">
|
||||
<rect id="stop" x="278" y="165" width="18" height="18"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 625 B |
12
web/src/assets/tip.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>tip-shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="tip-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="tip-Artboard" transform="translate(-1281.000000, -163.000000)" fill="#3A3A3A">
|
||||
<path d="M1291.85306,163.001003 C1285.77914,163.082217 1280.91979,168.072968 1281.001,174.148546 C1281.08222,180.219736 1286.07303,185.080693 1292.14695,184.998985 C1298.22086,184.917833 1303.08015,179.92702 1302.999,173.851504 C1302.91884,167.77914 1297.92715,162.91979 1291.85306,163.001003 L1291.85306,163.001003 L1291.85306,163.001003 Z M1293.07256,166.667055 C1294.19027,166.667055 1294.52025,167.315035 1294.52025,168.057269 C1294.52025,168.984366 1293.77759,169.841375 1292.5116,169.841375 C1291.45236,169.841375 1290.94846,169.30817 1290.97837,168.428108 C1290.97893,167.685935 1291.60064,166.667055 1293.07256,166.667055 L1293.07256,166.667055 Z M1290.20523,180.87507 C1289.44099,180.87507 1288.88152,180.411027 1289.41584,178.371289 L1290.29219,174.755609 C1290.44405,174.175741 1290.46921,173.943719 1290.29219,173.943719 C1290.06382,173.943719 1289.07127,174.344411 1288.48454,174.73886 L1288.10319,174.113378 C1289.9627,172.561417 1292.10034,171.650513 1293.01644,171.650513 C1293.78006,171.650513 1293.90689,172.554618 1293.52554,173.944708 L1292.52143,177.747105 C1292.34442,178.419066 1292.42093,178.651087 1292.59795,178.651087 C1292.82626,178.651087 1293.57851,178.373452 1294.3171,177.792533 L1294.74857,178.372464 C1292.94202,180.179006 1290.97046,180.87507 1290.20523,180.87507 L1290.20523,180.87507 L1290.20523,180.87507 Z" id="tip-shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
12
web/src/assets/touchmeter.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="23px" height="20px" viewBox="0 0 23 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>touchmeta-shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="touchmeter-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="tip-Artboard" transform="translate(-612.000000, -163.000000)" fill="#3A3A3A">
|
||||
<path d="M634.089379,170.123912 C633.483028,168.686607 632.667555,167.4453 631.63885,166.404073 C630.613282,165.364106 629.388977,164.536851 627.96618,163.922555 C626.547467,163.308258 625.057677,163 623.499919,163 C621.943243,163 620.453587,163.308258 619.032684,163.922555 C617.614106,164.536851 616.388828,165.364106 615.361096,166.404073 C614.334446,167.444204 613.518,168.685511 612.911676,170.123912 C612.304244,171.564367 612,173.072612 612,174.650646 C612,176.263836 612.308436,177.789042 612.924253,179.228401 C613.539907,180.666692 614.395138,181.923892 615.491784,183 L631.508216,183 C632.604835,181.923919 633.460066,180.665624 634.075747,179.228401 C634.692592,177.787973 635,176.263836 635,174.650646 C635,173.073571 634.695729,171.564394 634.089379,170.123912 Z M622.911419,165.495383 L624.091692,165.495383 L624.091692,167.628556 L622.911419,167.628556 L622.911419,165.495383 Z M616.569632,174.182533 L614.491342,173.793826 L614.697218,172.623542 L616.775671,173.013235 L616.569632,174.182533 Z M618.572816,169.65788 L617.21163,168.018249 L618.110649,167.26514 L619.468724,168.903676 L618.572816,169.65788 Z M625.862127,177.618588 C625.736776,178.153862 625.443854,178.571659 624.984666,178.873086 C624.524266,179.173378 624.025132,179.260057 623.489782,179.135365 C622.952037,179.009568 622.539643,178.712778 622.253814,178.246375 C621.851997,177.566329 621.840329,176.876734 622.223385,176.177744 L620.009244,170.697984 L623.978368,175.185555 C624.688229,175.205636 625.234157,175.516673 625.618425,176.117194 C625.905437,176.583566 625.987509,177.083284 625.862127,177.618588 Z M628.429321,169.65788 L627.530221,168.903703 L628.89138,167.265168 L629.789398,168.018276 L628.429321,169.65788 Z M630.430341,174.182533 L630.226466,173.012248 L632.30481,172.621377 L632.50974,173.790675 L630.430341,174.182533 Z" id="touchmeta-shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
27
web/src/assets/view-eye.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="27px" height="20px" viewBox="0 0 27 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>eye-slash</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<polygon id="view-eye-path-1" points="21.995689 0.818019485 23.4099026 2.23223305 5.73223305 19.9099026 4.31801948 18.495689"></polygon>
|
||||
<mask id="eye-mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="-0.5" y="-0.5" width="20.0918831" height="20.0918831">
|
||||
<rect x="3.81801948" y="0.318019485" width="20.0918831" height="20.0918831" fill="white"></rect>
|
||||
<use xlink:href="#view-eye-path-1" fill="black"></use>
|
||||
</mask>
|
||||
</defs>
|
||||
<g id="eye-Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Artboard" transform="translate(-505.000000, -163.000000)">
|
||||
<g id="eye-slash" transform="translate(505.000000, 163.000000)">
|
||||
<g id="eys-slash-group1" transform="translate(0.000000, 1.000000)" fill="#3A3A3A">
|
||||
<path d="M27,9 C23.9831661,3.54935292 19.0604961,0 13.5,0 C7.93950391,0 3.01683393,3.54935292 0,9 C3.01683393,14.4506471 7.93950391,18 13.5,18 C19.0604961,18 23.9831661,14.4506471 27,9 Z M13.5,15 C16.8137085,15 19.5,12.3137085 19.5,9 C19.5,5.6862915 16.8137085,3 13.5,3 C10.1862915,3 7.5,5.6862915 7.5,9 C7.5,12.3137085 10.1862915,15 13.5,15 Z" id="Combined-Shape"></path>
|
||||
<ellipse id="eye-boll" cx="13.5" cy="9" rx="3" ry="3"></ellipse>
|
||||
</g>
|
||||
<g id="eye-slash-border">
|
||||
<use fill="#3A3A3A" fill-rule="evenodd" xlink:href="#view-eye-path-1"></use>
|
||||
<use stroke="#FFFFFF" mask="url(#eye-mask-2)" stroke-width="1" xlink:href="#view-eye-path-1"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
62
web/src/common/ApiUtil.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* AJAX操作工具类
|
||||
*/
|
||||
import PromiseUtil from './PromiseUtil';
|
||||
export function getJSON(url, data) {
|
||||
const d = PromiseUtil.defer();
|
||||
fetch(url + serializeQuery(data))
|
||||
.then((data) => {
|
||||
d.resolve(data.json());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
d.reject(error);
|
||||
});
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
export function postJSON(url, data) {
|
||||
const d = PromiseUtil.defer();
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then((data) => {
|
||||
|
||||
d.resolve(data.json());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
d.reject(error);
|
||||
});
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function serializeQuery (data = {}) {
|
||||
data['__t'] = Date.now();// disable the cache
|
||||
const queryArray = [];
|
||||
|
||||
for (let key in data) {
|
||||
queryArray.push(`${key}=${data[key]}`);
|
||||
}
|
||||
|
||||
const queryStr = queryArray.join('&');
|
||||
|
||||
return queryStr ? '?' + queryStr : '';
|
||||
}
|
||||
|
||||
export function isApiSuccess (response) {
|
||||
return response.status === 'success';
|
||||
}
|
||||
|
||||
const ApiUtil = {
|
||||
getJSON,
|
||||
postJSON,
|
||||
isApiSuccess
|
||||
};
|
||||
|
||||
export default ApiUtil;
|
9
web/src/common/Constant.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* define all Constant variables here
|
||||
*/
|
||||
|
||||
module.exports.MenuKeyMap = {
|
||||
RECORD_FILTER: 'RECORD_FILTER',
|
||||
MAP_LOCAL: 'MAP_LOCAL',
|
||||
ROOT_CA: 'ROOT_CA'
|
||||
};
|
33
web/src/common/WsUtil.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Utility for websocket
|
||||
*
|
||||
*/
|
||||
import { message } from 'antd';
|
||||
|
||||
export function initWs(wsPort = 8003, key = '') {
|
||||
if(!WebSocket){
|
||||
throw (new Error('WebSocket is not supportted on this browser'));
|
||||
}
|
||||
|
||||
const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${key}`);
|
||||
|
||||
wsClient.onerror = (error) => {
|
||||
console.error(error);
|
||||
message.error('error happened when setup websocket');
|
||||
};
|
||||
|
||||
wsClient.onopen = (e) => {
|
||||
console.info('websocket opened: ', e);
|
||||
};
|
||||
|
||||
wsClient.onclose = (e) => {
|
||||
console.info('websocket closed: ', e);
|
||||
};
|
||||
|
||||
return wsClient;
|
||||
}
|
||||
|
||||
export default {
|
||||
initWs: initWs
|
||||
};
|
||||
|
66
web/src/common/commonUtil.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 存放常用工具类
|
||||
*/
|
||||
|
||||
/*
|
||||
* 格式化日期
|
||||
* @param date Date or timestamp
|
||||
* @param formatter yyyyMMddHHmmss
|
||||
*/
|
||||
export function formatDate(date, formatter) {
|
||||
if (typeof date !== 'object') {
|
||||
date = new Date(date);
|
||||
}
|
||||
const transform = function(value) {
|
||||
return value < 10 ? '0' + value : value;
|
||||
};
|
||||
return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, function(match) {
|
||||
switch (match) {
|
||||
case 'YYYY':
|
||||
return transform(date.getFullYear());
|
||||
case 'MM':
|
||||
return transform(date.getMonth() + 1);
|
||||
case 'mm':
|
||||
return transform(date.getMinutes());
|
||||
case 'DD':
|
||||
return transform(date.getDate());
|
||||
case 'hh':
|
||||
return transform(date.getHours());
|
||||
case 'ss':
|
||||
return transform(date.getSeconds());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function selectText(element) {
|
||||
let range, selection;
|
||||
|
||||
if (window.getSelection) {
|
||||
selection = window.getSelection();
|
||||
range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
} else if (document.body.createTextRange) {
|
||||
range = document.body.createTextRange();
|
||||
range.moveToElementText(element);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryParameter (name) {
|
||||
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
|
||||
if (results == null) {
|
||||
return '';
|
||||
} else {
|
||||
return results[1] || '';
|
||||
}
|
||||
}
|
||||
|
||||
const CommonUtil = {
|
||||
formatDate,
|
||||
selectText,
|
||||
getQueryParameter
|
||||
};
|
||||
|
||||
export default CommonUtil;
|
37
web/src/common/curlUtil.js
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
export function curlify(recordDetail){
|
||||
let curlified = []
|
||||
let type = ''
|
||||
let headers = { ...recordDetail.reqHeader }
|
||||
curlified.push('curl')
|
||||
curlified.push('-X', recordDetail.method)
|
||||
curlified.push(`'${recordDetail.url}'`)
|
||||
|
||||
if (headers) {
|
||||
type = headers['Content-Type']
|
||||
delete headers['Accept-Encoding']
|
||||
|
||||
for(let k of Object.keys(headers)){
|
||||
let v = headers[k]
|
||||
curlified.push('-H')
|
||||
curlified.push(`'${k}: ${v}'`)
|
||||
}
|
||||
}
|
||||
|
||||
if (recordDetail.reqBody){
|
||||
|
||||
if(type === 'multipart/form-data' && recordDetail.method === 'POST') {
|
||||
let formDataBody = recordDetail.reqBody.split('&')
|
||||
|
||||
for(let data of formDataBody) {
|
||||
curlified.push('-F')
|
||||
curlified.push(data)
|
||||
}
|
||||
} else {
|
||||
curlified.push('-d')
|
||||
curlified.push(recordDetail.reqBody)
|
||||
}
|
||||
}
|
||||
|
||||
return curlified.join(' ')
|
||||
}
|
17
web/src/common/promiseUtil.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Promise的工具类
|
||||
*/
|
||||
|
||||
export function defer() {
|
||||
const d = {};
|
||||
d.promise = new Promise((resolve, reject) => {
|
||||
d.resolve = resolve;
|
||||
d.reject = reject;
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
export default {
|
||||
defer
|
||||
};
|
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
@@ -1,120 +0,0 @@
|
||||
function init(React){
|
||||
|
||||
var DetailPanel = React.createClass({
|
||||
getInitialState : function(){
|
||||
return {
|
||||
body : {id : -1, content : null},
|
||||
left : "35%"
|
||||
};
|
||||
},
|
||||
loadBody:function(){
|
||||
var self = this,
|
||||
id = self.props.data.id;
|
||||
if(!id) return;
|
||||
|
||||
jQuery.get("/fetchBody?id=" + id ,function(resObj){
|
||||
if(resObj && resObj.id){
|
||||
if(resObj.type && resObj.type == "image" && resObj.ref){
|
||||
self.setState({
|
||||
body : {
|
||||
img : resObj.ref,
|
||||
id : resObj.id
|
||||
}
|
||||
});
|
||||
}else if(resObj.content){
|
||||
self.setState({
|
||||
body : {
|
||||
body : resObj.content,
|
||||
id : resObj.id
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
render : function(){
|
||||
var reqHeaderSection = [],
|
||||
resHeaderSection = [],
|
||||
summarySection,
|
||||
detailSection,
|
||||
bodyContent;
|
||||
|
||||
if(this.props.data.reqHeader){
|
||||
for(var key in this.props.data.reqHeader){
|
||||
reqHeaderSection.push(<li key={"reqHeader_" + key}><strong>{key}</strong> : {this.props.data.reqHeader[key]}</li>)
|
||||
}
|
||||
}
|
||||
|
||||
summarySection = (
|
||||
<div>
|
||||
<section className="req">
|
||||
<h4 className="subTitle">request</h4>
|
||||
<div className="detail">
|
||||
<ul className="uk-list">
|
||||
<li>{this.props.data.method} <span title="{this.props.data.path}">{this.props.data.path}</span> HTTP/1.1</li>
|
||||
{reqHeaderSection}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="reqBody">
|
||||
<h4 className="subTitle">request body</h4>
|
||||
<div className="detail">
|
||||
<p>{this.props.data.reqBody}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
if(this.props.data.statusCode){
|
||||
if(this.state.body.id == this.props.data.id){
|
||||
if(this.state.body.img){
|
||||
var imgEl = { __html : '<img src="'+ this.state.body.img +'" />'};
|
||||
bodyContent = (<div dangerouslySetInnerHTML={imgEl}></div>);
|
||||
}else{
|
||||
bodyContent = (<pre className="resBodyContent">{this.state.body.body}</pre>);
|
||||
}
|
||||
}else{
|
||||
bodyContent = null;
|
||||
this.loadBody();
|
||||
}
|
||||
|
||||
if(this.props.data.resHeader){
|
||||
for(var key in this.props.data.resHeader){
|
||||
resHeaderSection.push(<li key={"resHeader_" + key}><strong>{key}</strong> : {this.props.data.resHeader[key]}</li>)
|
||||
}
|
||||
}
|
||||
|
||||
detailSection = (
|
||||
<div>
|
||||
<section className="resHeader">
|
||||
<h4 className="subTitle">response header</h4>
|
||||
<div className="detail">
|
||||
<ul className="uk-list">
|
||||
<li>HTTP/1.1 <span className={"http_status http_status_" + this.props.data.statusCode}>{this.props.data.statusCode}</span></li>
|
||||
{resHeaderSection}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="resBody">
|
||||
<h4 className="subTitle">response body</h4>
|
||||
<div className="detail">{bodyContent}</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{summarySection}
|
||||
{detailSection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return DetailPanel;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,43 +0,0 @@
|
||||
function init(React){
|
||||
|
||||
var Filter = React.createClass({
|
||||
|
||||
dealChange:function(){
|
||||
var self = this,
|
||||
userInput = React.findDOMNode(self.refs.keywordInput).value;
|
||||
|
||||
self.props.onChangeKeyword && self.props.onChangeKeyword.call(null,userInput);
|
||||
},
|
||||
setFocus:function(){
|
||||
var self = this;
|
||||
React.findDOMNode(self.refs.keywordInput).focus();
|
||||
},
|
||||
componentDidUpdate:function(){
|
||||
this.setFocus();
|
||||
},
|
||||
render:function(){
|
||||
var self = this;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className="subTitle">Log Filter</h4>
|
||||
<div className="filterSection">
|
||||
<div className="uk-form">
|
||||
<input className="uk-form-large" ref="keywordInput" onChange={self.dealChange} type="text" placeholder="keywords or /^regExp$/" width="300"/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<i className="uk-icon-magic"></i> type <strong>/id=\d{3}/</strong> will give you all the logs containing <strong>id=123</strong>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
componentDidMount:function(){
|
||||
this.setFocus();
|
||||
}
|
||||
});
|
||||
|
||||
return Filter;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
273
web/src/index.js
@@ -1,273 +0,0 @@
|
||||
window.$ = window.jQuery = require("../lib/jquery");
|
||||
|
||||
var EventManager = require('../lib/event'),
|
||||
Anyproxy_wsUtil = require("../lib/anyproxy_wsUtil"),
|
||||
React = require("../lib/react");
|
||||
|
||||
var WsIndicator = require("./wsIndicator").init(React),
|
||||
RecordPanel = require("./recordPanel").init(React),
|
||||
Popup = require("./popup").init(React);
|
||||
|
||||
var PopupContent = {
|
||||
map : require("./mapPanel").init(React),
|
||||
detail : require("./detailPanel").init(React),
|
||||
filter : require("./filter").init(React)
|
||||
};
|
||||
|
||||
var ifPause = false,
|
||||
recordSet = [];
|
||||
|
||||
//Event : wsGetUpdate
|
||||
//Event : recordSetUpdated
|
||||
//Event : wsOpen
|
||||
//Event : wsEnd
|
||||
var eventCenter = new EventManager();
|
||||
|
||||
//merge : right --> left
|
||||
function util_merge(left,right){
|
||||
for(var key in right){
|
||||
left[key] = right[key];
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
//invoke AnyProxy web socket util
|
||||
(function(){
|
||||
try{
|
||||
var ws = window.ws = new Anyproxy_wsUtil({
|
||||
baseUrl : document.getElementById("baseUrl").value,
|
||||
port : document.getElementById("socketPort").value,
|
||||
onOpen : function(){
|
||||
eventCenter.dispatchEvent("wsOpen");
|
||||
},
|
||||
onGetUpdate : function(record){
|
||||
eventCenter.dispatchEvent("wsGetUpdate",record);
|
||||
},
|
||||
onError : function(e){
|
||||
eventCenter.dispatchEvent("wsEnd");
|
||||
},
|
||||
onClose : function(e){
|
||||
eventCenter.dispatchEvent("wsEnd");
|
||||
}
|
||||
});
|
||||
window.ws = ws;
|
||||
|
||||
}catch(e){
|
||||
alert("failed to invoking web socket on this browser");
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
//websocket status indicator
|
||||
(function(){
|
||||
var wsIndicator = React.render(
|
||||
<WsIndicator />,
|
||||
document.getElementById("J_indicatorEl")
|
||||
);
|
||||
|
||||
eventCenter.addListener("wsOpen",function(){
|
||||
wsIndicator.setState({
|
||||
isValid : true
|
||||
});
|
||||
});
|
||||
|
||||
eventCenter.addListener("wsEnd",function(){
|
||||
wsIndicator.setState({
|
||||
isValid : false
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
//init popup
|
||||
var showPop;
|
||||
(function(){
|
||||
$("body").append('<div id="J_popOuter"></div>');
|
||||
var pop = React.render(
|
||||
<Popup />,
|
||||
document.getElementById("J_popOuter")
|
||||
);
|
||||
|
||||
showPop = function(popArg){
|
||||
var stateArg = util_merge({show : true },popArg);
|
||||
pop.setState(stateArg);
|
||||
};
|
||||
})();
|
||||
|
||||
//init record panel
|
||||
var recorder;
|
||||
(function(){
|
||||
function updateRecordSet(newRecord){
|
||||
if(ifPause) return;
|
||||
|
||||
if(newRecord && newRecord.id){
|
||||
if(!recordSet[newRecord.id]){
|
||||
recordSet[newRecord.id] = newRecord;
|
||||
}else{
|
||||
util_merge(recordSet[newRecord.id],newRecord);
|
||||
}
|
||||
|
||||
recordSet[newRecord.id]._justUpdated = true;
|
||||
// React.addons.Perf.start();
|
||||
eventCenter.dispatchEvent("recordSetUpdated");
|
||||
// React.addons.Perf.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function initRecordSet(){
|
||||
$.getJSON("/lastestLog",function(res){
|
||||
if(typeof res == "object"){
|
||||
res.map(function(item){
|
||||
if(item.id){
|
||||
recordSet[item.id] = item;
|
||||
}
|
||||
});
|
||||
eventCenter.dispatchEvent("recordSetUpdated");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
eventCenter.addListener("wsGetUpdate",updateRecordSet);
|
||||
|
||||
eventCenter.addListener('recordSetUpdated',function(){
|
||||
recorder.setState({
|
||||
list : recordSet
|
||||
});
|
||||
});
|
||||
|
||||
eventCenter.addListener("filterUpdated",function(newKeyword){
|
||||
recorder.setState({
|
||||
filter: newKeyword
|
||||
});
|
||||
});
|
||||
|
||||
function showDetail(data){
|
||||
showPop({left:"35%",content:React.createElement(PopupContent["detail"], {data:data})});
|
||||
}
|
||||
|
||||
//init recorder panel
|
||||
recorder = React.render(
|
||||
<RecordPanel onSelect={showDetail}/>,
|
||||
document.getElementById("J_content")
|
||||
);
|
||||
|
||||
initRecordSet();
|
||||
})();
|
||||
|
||||
|
||||
//action bar
|
||||
(function(){
|
||||
|
||||
//clear log
|
||||
function clearLogs(){
|
||||
recordSet = [];
|
||||
eventCenter.dispatchEvent("recordSetUpdated");
|
||||
}
|
||||
|
||||
$(document).on("keyup",function(e){
|
||||
if(e.keyCode == 88 && e.ctrlKey){ // ctrl + x
|
||||
clearLogs();
|
||||
}
|
||||
});
|
||||
|
||||
var clearLogBtn = $(".J_clearBtn");
|
||||
clearLogBtn.on("click",function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
clearLogs();
|
||||
});
|
||||
|
||||
//start , pause
|
||||
var statusBtn = $(".J_statusBtn");
|
||||
statusBtn.on("click",function(e){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
$(".J_statusBtn").removeClass("btn_disable");
|
||||
$(this).addClass("btn_disable");
|
||||
|
||||
if(/stop/i.test($(this).html()) ){
|
||||
ifPause = true;
|
||||
}else{
|
||||
ifPause = false;
|
||||
}
|
||||
});
|
||||
|
||||
//preset button
|
||||
(function (){
|
||||
var TopBtn = React.createClass({
|
||||
getInitialState: function(){
|
||||
return {
|
||||
inUse : false
|
||||
};
|
||||
},
|
||||
render: function(){
|
||||
var self = this,
|
||||
iconClass = self.state.inUse ? "uk-icon-check" : self.props.icon,
|
||||
btnClass = self.state.inUse ? "topBtn topBtnInUse" : "topBtn";
|
||||
|
||||
return <a href="#"><span className={btnClass} onClick={self.props.onClick}><i className={iconClass}></i>{self.props.title}</span></a>
|
||||
}
|
||||
});
|
||||
|
||||
// filter
|
||||
var filterBtn,
|
||||
FilterPanel = PopupContent["filter"],
|
||||
filterPanelEl;
|
||||
|
||||
filterBtn = React.render(<TopBtn icon="uk-icon-filter" title="Filter" onClick={filterBtnClick}/>, document.getElementById("J_filterBtnContainer"));
|
||||
filterPanelEl = (<FilterPanel onChangeKeyword={updateKeyword} /> );
|
||||
|
||||
function updateKeyword(key){
|
||||
eventCenter.dispatchEvent("filterUpdated",key);
|
||||
filterBtn.setState({inUse : !!key});
|
||||
}
|
||||
function filterBtnClick(){
|
||||
showPop({ left:"60%", content:filterPanelEl });
|
||||
}
|
||||
|
||||
// map local
|
||||
var mapBtn,
|
||||
mapPanelEl;
|
||||
function onChangeMapConfig(cfg){
|
||||
mapBtn.setState({inUse : cfg && cfg.length});
|
||||
}
|
||||
|
||||
function mapBtnClick(){
|
||||
showPop({left:"60%", content:mapPanelEl });
|
||||
}
|
||||
|
||||
//detect whether to show the map btn
|
||||
require("./mapPanel").fetchConfig(function(initConfig){
|
||||
var MapPanel = PopupContent["map"];
|
||||
mapBtn = React.render(<TopBtn icon="uk-icon-shield" title="Map Local" onClick={mapBtnClick} />,document.getElementById("J_filterContainer"));
|
||||
mapPanelEl = (<MapPanel onChange={onChangeMapConfig} />);
|
||||
onChangeMapConfig(initConfig);
|
||||
});
|
||||
|
||||
var t = true;
|
||||
setInterval(function(){
|
||||
t = !t;
|
||||
// mapBtn && mapBtn.setState({inUse : t})
|
||||
},300);
|
||||
|
||||
|
||||
|
||||
})();
|
||||
|
||||
//other button
|
||||
(function(){
|
||||
$(".J_customButton").on("click",function(){
|
||||
var thisEl = $(this),
|
||||
iframeUrl = thisEl.attr("iframeUrl");
|
||||
|
||||
if(!iframeUrl){
|
||||
throw new Error("cannot find the url assigned for this button");
|
||||
}
|
||||
|
||||
var iframeEl = React.createElement("iframe",{src:iframeUrl,frameBorder:0});
|
||||
showPop({left:"35%", content: iframeEl });
|
||||
});
|
||||
})();
|
||||
|
||||
})();
|
312
web/src/index.jsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import { Provider, connect } from 'react-redux';
|
||||
import { LocaleProvider } from 'antd';
|
||||
import enUS from 'antd/lib/locale-provider/en_US';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import rootSaga from 'saga/rootSaga';
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
import { getQueryParameter } from 'common/CommonUtil';
|
||||
|
||||
import reducer from 'reducer/rootReducer';
|
||||
import HeaderMenu from 'component/header-menu';
|
||||
import RecordPanel from 'component/record-panel';
|
||||
import RecordFilter from 'component/record-filter';
|
||||
import MapLocal from 'component/map-local';
|
||||
import WsListener from 'component/ws-listener';
|
||||
import RecordDetail from 'component/record-detail';
|
||||
import LeftMenu from 'component/left-menu';
|
||||
import DownloadRootCA from 'component/download-root-ca';
|
||||
|
||||
require('./style/antd-reset.global.less');
|
||||
import Style from './index.less';
|
||||
import CommonStyle from './style/common.less';
|
||||
|
||||
const {
|
||||
RECORD_FILTER: RECORD_FILTER_MENU_KEY,
|
||||
MAP_LOCAL: MAP_LOCAL_MENU_KEY,
|
||||
ROOT_CA: ROOT_CA_MENU_KEY
|
||||
} = MenuKeyMap;
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
class App extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
showResizePanel: false,
|
||||
panelIndex: '',
|
||||
inAppMode: getQueryParameter('in_app_mode'),
|
||||
refreshing: true
|
||||
};
|
||||
|
||||
this.onResizePanelClose = this.onResizePanelClose.bind(this);
|
||||
this.onRecordScroll = this.onRecordScroll.bind(this);
|
||||
this.stopRefresh = this.stopRefresh.bind(this);
|
||||
this.resumeFresh = this.resumeFresh.bind(this);
|
||||
this.detectIfToStopRefreshing = this.detectIfToStopRefreshing.bind(this);
|
||||
this.scrollHandler = this.scrollHandler.bind(this);
|
||||
this.initRecrodPanelWrapperRef = this.initRecrodPanelWrapperRef.bind(this);
|
||||
|
||||
this.recordTableRef = null;
|
||||
this.wsListenerRef = null;
|
||||
|
||||
this.lastScrollTop = 0;
|
||||
|
||||
this.scrollHandlerTimeout = null;
|
||||
this.stopRefreshTimout = null;
|
||||
this.stopRefreshTokenScrollTop = null; // the token used to decide the move distance
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
requestRecord: PropTypes.object,
|
||||
globalStatus: PropTypes.object
|
||||
}
|
||||
|
||||
stopRefresh() {
|
||||
this.wsListenerRef && this.wsListenerRef.stopPanelRefreshing();
|
||||
this.state.refreshing = false;
|
||||
this.setState({
|
||||
refreshing: false
|
||||
});
|
||||
}
|
||||
|
||||
resumeFresh() {
|
||||
this.wsListenerRef && this.wsListenerRef.resumePanelRefreshing();
|
||||
this.state.refreshing = true;
|
||||
this.setState({
|
||||
refreshing: true
|
||||
});
|
||||
}
|
||||
|
||||
onResizePanelClose() {
|
||||
this.setState({
|
||||
showResizePanel: false
|
||||
});
|
||||
}
|
||||
|
||||
// if is scrolling up during refresh, will stop the refresh
|
||||
detectIfToStopRefreshing(currentScrollTop) {
|
||||
if (!this.stopRefreshTokenScrollTop) {
|
||||
this.stopRefreshTokenScrollTop = currentScrollTop;
|
||||
}
|
||||
|
||||
this.stopRefreshTimout = setTimeout(() => {
|
||||
// if the scrollbar is scrolled up more than 50px, stop refreshing
|
||||
if ((this.stopRefreshTokenScrollTop - currentScrollTop) > 50) {
|
||||
this.stopRefresh();
|
||||
this.stopRefreshTokenScrollTop = null;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
initRecrodPanelWrapperRef(ref) {
|
||||
this.recordTableRef = ref;
|
||||
ref.addEventListener('wheel', this.onRecordScroll, { passive: true });
|
||||
}
|
||||
|
||||
scrollHandler() {
|
||||
if (!this.recordTableRef || !this.wsListenerRef) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
const scrollTop = this.recordTableRef.scrollTop;
|
||||
|
||||
if (scrollTop < this.lastScrollTop || (this.lastScrollTop === 0)) {
|
||||
this.detectIfToStopRefreshing(scrollTop);
|
||||
|
||||
// load more previous record when scrolled to top
|
||||
if (scrollTop < 10) {
|
||||
self.state.loadingPrev = true;
|
||||
self.setState({
|
||||
loadingPrev: true
|
||||
});
|
||||
|
||||
//TODO: hide the loading stauts after 1000 ms, a lazy way to hide it when there is no previous records
|
||||
setTimeout(() => {
|
||||
self.state.loadingPrev = false;
|
||||
self.setState({
|
||||
loadingPrev: false
|
||||
});
|
||||
}, 1000);
|
||||
this.wsListenerRef.loadPrevious();
|
||||
}
|
||||
} else if (scrollTop >= this.lastScrollTop) {
|
||||
const recordPanelHeight = this.recordTableRef.firstChild.clientHeight;
|
||||
const tableHeight = this.recordTableRef.clientHeight;
|
||||
|
||||
// when close to bottom in less than 30, load more next records
|
||||
if (scrollTop + tableHeight + 30 > recordPanelHeight) {
|
||||
this.state.loadNext = true;
|
||||
this.setState({
|
||||
loadingNext: true
|
||||
});
|
||||
this.wsListenerRef.loadNext();
|
||||
}
|
||||
}
|
||||
this.lastScrollTop = scrollTop;
|
||||
}
|
||||
|
||||
onRecordScroll() {
|
||||
this.scrollHandlerTimeout && clearTimeout(this.scrollHandlerTimeout);
|
||||
this.scrollHandlerTimeout = setTimeout(() => {
|
||||
this.scrollHandler();
|
||||
}, 60);
|
||||
}
|
||||
|
||||
getResumeFreshDiv() {
|
||||
if (!this.props.globalStatus.showNewRecordTip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={Style.resumeTip} onClick={this.resumeFresh} >
|
||||
<div className={CommonStyle.relativeWrapper}>
|
||||
<span>New Records Detected.</span>
|
||||
<span className={Style.arrowDown} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getMiddlePanel() {
|
||||
const { activeMenuKey } = this.props.globalStatus;
|
||||
let middlePanel = null;
|
||||
|
||||
// TODO: move the logic of resizepanel out to here, keep each panel component independant
|
||||
switch (activeMenuKey) {
|
||||
case RECORD_FILTER_MENU_KEY: {
|
||||
middlePanel = <RecordFilter />;
|
||||
break;
|
||||
}
|
||||
|
||||
case MAP_LOCAL_MENU_KEY: {
|
||||
middlePanel = <MapLocal />;
|
||||
break;
|
||||
}
|
||||
|
||||
case ROOT_CA_MENU_KEY: {
|
||||
middlePanel = <DownloadRootCA />;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
middlePanel = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return middlePanel;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { recordList: nextRecordList } = nextProps.requestRecord;
|
||||
const { recordList: currentRecordList } = this.props.requestRecord;
|
||||
|
||||
// if there are new data, reset the status of loadingNext and loadingPrev
|
||||
if (nextRecordList !== currentRecordList) {
|
||||
// scroll the window to last remembered position, when in loading pre mode
|
||||
if (this.state.loadingPrev) {
|
||||
const nextBeginId = nextRecordList[0].id;
|
||||
const currentBeginId = currentRecordList[0].id;
|
||||
|
||||
if (nextBeginId < currentBeginId) {
|
||||
// each line is limited to 29px
|
||||
const scrollPosition = 29 * (nextRecordList.length - currentRecordList.length);
|
||||
if (this.recordTableRef) {
|
||||
setTimeout(() => {
|
||||
this.recordTableRef.scrollTop = scrollPosition;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
this.state.loadingPrev = false;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingNext: false,
|
||||
loadingPrev: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.refreshing && this.recordTableRef && !this.state.loadingPrev) {
|
||||
this.recordTableRef.scrollTop = this.recordTableRef.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.state.refreshing && this.recordTableRef) {
|
||||
this.recordTableRef.scrollTop = this.recordTableRef.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { lastActiveRecordId, currentActiveRecordId } = this.props.globalStatus;
|
||||
const leftMenuDiv = (
|
||||
<div className={Style.leftPanel} >
|
||||
<LeftMenu />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={Style.indexWrapper} >
|
||||
{this.state.inAppMode ? null : leftMenuDiv}
|
||||
<div className={Style.middlePanel} >
|
||||
{this.getMiddlePanel()}
|
||||
</div>
|
||||
<div className={Style.rightPanel} >
|
||||
<div className={Style.headerWrapper} >
|
||||
<HeaderMenu resumeRefreshFunc={this.resumeFresh} />
|
||||
</div>
|
||||
<div
|
||||
className={Style.tableWrapper}
|
||||
ref={this.initRecrodPanelWrapperRef}
|
||||
>
|
||||
<RecordPanel
|
||||
data={this.props.requestRecord.recordList}
|
||||
lastActiveRecordId={lastActiveRecordId}
|
||||
currentActiveRecordId={currentActiveRecordId}
|
||||
dispatch={this.props.dispatch}
|
||||
loadingNext={this.state.loadingNext}
|
||||
loadingPrev={this.state.loadingPrev}
|
||||
stopRefresh={this.stopRefresh}
|
||||
/>
|
||||
</div>
|
||||
{this.getResumeFreshDiv()}
|
||||
</div>
|
||||
<WsListener
|
||||
ref={(ref) => { this.wsListenerRef = ref; }}
|
||||
dispatch={this.props.dispatch}
|
||||
globalStatus={this.props.globalStatus}
|
||||
/>
|
||||
<RecordDetail
|
||||
globalStatus={this.props.globalStatus}
|
||||
requestRecord={this.props.requestRecord}
|
||||
dispatch={this.props.dispatch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function select(state) {
|
||||
return {
|
||||
requestRecord: state.requestRecord,
|
||||
globalStatus: state.globalStatus
|
||||
};
|
||||
}
|
||||
|
||||
const ReduxApp = connect(select)(App);
|
||||
|
||||
ReactDOM.render(
|
||||
<LocaleProvider locale={enUS} >
|
||||
<Provider store={store} >
|
||||
<ReduxApp />
|
||||
</Provider>
|
||||
</LocaleProvider>, document.getElementById('root'));
|
79
web/src/index.less
Normal file
@@ -0,0 +1,79 @@
|
||||
@import './style/constant.less';
|
||||
|
||||
.indexWrapper {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.leftPanel {
|
||||
width: 158px;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
float: left;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.middlePanel {
|
||||
float: left;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rightPanel {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
border-bottom: 1px solid @hint-color;
|
||||
padding-bottom: 5px;
|
||||
padding-top: 25px;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
clear: both;
|
||||
position: absolute;
|
||||
top: 89px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.resumeTip {
|
||||
display: block;
|
||||
background-color: @primary-color;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 15px;
|
||||
padding: 10px 10px;
|
||||
opacity: 0.9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
-webkit-box-shadow: 1px 1px 9px 0px rgba(0,0,0,0.37);
|
||||
-moz-box-shadow: 1px 1px 9px 0px rgba(0,0,0,0.37);
|
||||
box-shadow: 1px 1px 9px 0px rgba(0,0,0,0.37);
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.arrowDown {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: -15px;
|
||||
right: 60px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid @primary-color;
|
||||
opacity: 0.9;
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
require("../lib/jstree");
|
||||
|
||||
function init(React){
|
||||
function fetchTree(root,cb){
|
||||
if(!root || root == "#"){
|
||||
root = "";
|
||||
}
|
||||
|
||||
$.getJSON("/filetree?root=" + root,function(resObj){
|
||||
var ret = [];
|
||||
try{
|
||||
$.each(resObj.directory, function(k,item){
|
||||
if(item.name.indexOf(".") == 0) return;
|
||||
ret.push({
|
||||
text : item.name,
|
||||
id : item.fullPath,
|
||||
icon : "uk-icon-folder",
|
||||
children : true
|
||||
});
|
||||
});
|
||||
|
||||
$.each(resObj.file, function(k,item){
|
||||
if(item.name.indexOf(".") == 0) return;
|
||||
ret.push({
|
||||
text : item.name,
|
||||
id : item.fullPath,
|
||||
icon : 'uk-icon-file-o',
|
||||
children : false
|
||||
});
|
||||
});
|
||||
}catch(e){}
|
||||
cb && cb.call(null,ret);
|
||||
});
|
||||
}
|
||||
|
||||
var MapForm = React.createClass({
|
||||
|
||||
submitData:function(){
|
||||
var self = this,
|
||||
result = {};
|
||||
|
||||
var filePathInput = React.findDOMNode(self.refs.localFilePath),
|
||||
filePath = filePathInput.value,
|
||||
keywordInput = React.findDOMNode(self.refs.keywordInput),
|
||||
keyword = keywordInput.value;
|
||||
|
||||
if(filePath && keyword){
|
||||
self.props.onSubmit.call(null,{
|
||||
keyword : keyword,
|
||||
local : filePath
|
||||
});
|
||||
|
||||
filePathInput.value = "";
|
||||
keywordInput.value = "";
|
||||
}
|
||||
},
|
||||
|
||||
render:function(){
|
||||
var self = this;
|
||||
return (
|
||||
<div>
|
||||
<form className="uk-form uk-form-stacked mapAddNewForm">
|
||||
<fieldset>
|
||||
<div className="uk-form-row">
|
||||
<label className="uk-form-label" htmlFor="map_keywordInput">keyword</label>
|
||||
<div className="uk-form-controls">
|
||||
<input className="mapConfigInputs" type="text" id="map_keywordInput" ref="keywordInput" placeholder="keyword" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="uk-form-row">
|
||||
<label className="uk-form-label" htmlFor="map_localFilePath">local file</label>
|
||||
<div className="uk-form-controls">
|
||||
<input className="mapConfigInputs pathInput" type="text" id="map_localFilePath" ref="localFilePath" placeholder="local file path" />
|
||||
</div>
|
||||
<div ref="treeWrapper" className="treeWrapper"></div>
|
||||
</div>
|
||||
|
||||
<div className="uk-form-row">
|
||||
<button type="button" className="uk-button" onClick={self.submitData}>Add</button>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
componentDidMount :function(){
|
||||
var self = this;
|
||||
var wrapperEl = $(React.findDOMNode(self.refs.treeWrapper)),
|
||||
filePathInput = React.findDOMNode(self.refs.localFilePath);
|
||||
|
||||
wrapperEl.jstree({
|
||||
'core' : {
|
||||
'data' : function (node, cb) {
|
||||
fetchTree(node.id,cb);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wrapperEl.on("changed.jstree", function (e, data) {
|
||||
if(data && data.selected && data.selected.length){
|
||||
//is folder
|
||||
if(/folder/.test(data.node.icon)) return;
|
||||
|
||||
var item = data.selected[0];
|
||||
filePathInput.value = item;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
componentDidUpdate:function(){}
|
||||
});
|
||||
|
||||
return MapForm;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,86 +0,0 @@
|
||||
function fetchConfig(cb){
|
||||
return $.getJSON("/getMapConfig",cb);
|
||||
}
|
||||
|
||||
function init(React){
|
||||
var MapList = React.createClass({
|
||||
getInitialState:function(){
|
||||
return {
|
||||
ruleList : []
|
||||
}
|
||||
},
|
||||
appendRecord:function(data){
|
||||
var self = this,
|
||||
newState = self.state.ruleList;
|
||||
|
||||
if(data && data.keyword && data.local){
|
||||
newState.push({
|
||||
keyword : data.keyword,
|
||||
local : data.local
|
||||
});
|
||||
|
||||
self.setState({
|
||||
ruleList: newState
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeRecord:function(index){
|
||||
var self = this,
|
||||
newList = self.state.ruleList;
|
||||
|
||||
newList.splice(index,1);
|
||||
self.setState({
|
||||
ruleList : newList
|
||||
});
|
||||
},
|
||||
render:function(){
|
||||
var self = this,
|
||||
collection = [];
|
||||
|
||||
collection = self.state.ruleList.map(function(item,index){
|
||||
return (
|
||||
<li>
|
||||
<strong>{item.keyword}</strong><a className="removeBtn" href="#" onClick={self.removeRecord.bind(self,index)}>remove</a><br />
|
||||
<span>{item.local}</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="mapRuleList">
|
||||
{collection}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
componentDidMount :function(){
|
||||
var self = this;
|
||||
fetchConfig(function(data){
|
||||
self.setState({
|
||||
ruleList : data
|
||||
});
|
||||
});
|
||||
},
|
||||
componentDidUpdate:function(){
|
||||
var self = this;
|
||||
|
||||
//upload config to server
|
||||
var currentList = self.state.ruleList;
|
||||
$.ajax({
|
||||
method : "POST",
|
||||
url : "/setMapConfig",
|
||||
contentType :"application/json",
|
||||
data : JSON.stringify(currentList),
|
||||
dataType : "json",
|
||||
success :function(res){}
|
||||
});
|
||||
|
||||
self.props.onChange && self.props.onChange(self.state.ruleList);
|
||||
}
|
||||
});
|
||||
|
||||
return MapList;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
||||
module.exports.fetchConfig = fetchConfig;
|
@@ -1,33 +0,0 @@
|
||||
require("../lib/jstree");
|
||||
|
||||
function init(React){
|
||||
var MapForm = require("./mapForm").init(React),
|
||||
MapList = require("./mapList").init(React);
|
||||
|
||||
var MapPanel = React.createClass({
|
||||
appendRecord : function(data){
|
||||
var self = this,
|
||||
listComponent = self.refs.list;
|
||||
|
||||
listComponent.appendRecord(data);
|
||||
},
|
||||
|
||||
render:function(){
|
||||
var self = this;
|
||||
return (
|
||||
<div className="mapWrapper">
|
||||
<h4 className="subTitle">Current Config</h4>
|
||||
<MapList ref="list" onChange={self.props.onChange}/>
|
||||
|
||||
<h4 className="subTitle">Add Map Rule</h4>
|
||||
<MapForm onSubmit={self.appendRecord}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return MapPanel;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
||||
module.exports.fetchConfig = require("./mapList").fetchConfig;
|
@@ -1,94 +0,0 @@
|
||||
function init(React){
|
||||
|
||||
function dragableBar(initX,cb){
|
||||
var self = this,
|
||||
dragging = true;
|
||||
|
||||
var ghostbar = $('<div class="ghostbar"></div>').css("left",initX).prependTo('body');
|
||||
|
||||
$(document).mousemove(function(e){
|
||||
e.preventDefault();
|
||||
ghostbar.css("left",e.pageX + "px");
|
||||
});
|
||||
|
||||
$(document).mouseup(function(e){
|
||||
if(!dragging) return;
|
||||
|
||||
dragging = false;
|
||||
|
||||
var deltaPageX = e.pageX - initX;
|
||||
cb && cb.call(null,{
|
||||
delta : deltaPageX,
|
||||
finalX : e.pageX
|
||||
});
|
||||
|
||||
ghostbar.remove();
|
||||
$(document).unbind('mousemove');
|
||||
});
|
||||
}
|
||||
|
||||
var Popup = React.createClass({
|
||||
getInitialState : function(){
|
||||
return {
|
||||
show : false,
|
||||
left : "35%",
|
||||
content : null
|
||||
};
|
||||
},
|
||||
componentDidMount:function(){
|
||||
var self = this;
|
||||
$(document).on("keyup",function(e){
|
||||
if(e.keyCode == 27){ //ESC
|
||||
self.setState({
|
||||
show : false
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setHide:function(){
|
||||
this.setState({
|
||||
show : false
|
||||
});
|
||||
},
|
||||
setShow:function(ifShow){
|
||||
this.setState({
|
||||
show : true
|
||||
});
|
||||
},
|
||||
dealDrag:function(){
|
||||
var self = this,
|
||||
leftVal = $(React.findDOMNode(this.refs.mainOverlay)).css("left");
|
||||
dragableBar(leftVal, function(data){
|
||||
if(data && data.finalX){
|
||||
if(window.innerWidth - data.finalX < 200){
|
||||
data.finalX = window.innerWidth - 200;
|
||||
}
|
||||
self.setState({
|
||||
left : data.finalX + "px"
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
componentDidUpdate:function(){
|
||||
|
||||
},
|
||||
render : function(){
|
||||
return (
|
||||
<div style={{display:this.state.show ? "block" :"none"}}>
|
||||
<div className="overlay_mask" onClick={this.setHide}></div>
|
||||
<div className="recordDetailOverlay" ref="mainOverlay" style={{left: this.state.left}}>
|
||||
<div className="dragbar" onMouseDown={this.dealDrag}></div>
|
||||
<span className="escBtn" onClick={this.setHide}><i className="uk-icon-times"></i></span>
|
||||
<div>
|
||||
{this.state.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Popup;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,72 +0,0 @@
|
||||
function init(React){
|
||||
var RecordRow = require("./recordRow").init(React);
|
||||
|
||||
var RecordPanel = React.createClass({
|
||||
getInitialState : function(){
|
||||
return {
|
||||
list : [],
|
||||
filter: ""
|
||||
};
|
||||
},
|
||||
render : function(){
|
||||
var self = this,
|
||||
rowCollection = [],
|
||||
filterStr = self.state.filter,
|
||||
filter = filterStr;
|
||||
|
||||
//regexp
|
||||
if(filterStr[0]=="/" && filterStr[filterStr.length-1]=="/"){
|
||||
try{
|
||||
filter = new RegExp(filterStr.substr(1,filterStr.length-2));
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
for(var i = self.state.list.length-1 ; i >=0 ; i--){
|
||||
var item = self.state.list[i];
|
||||
if(item){
|
||||
if(filter && item){
|
||||
try{
|
||||
if(typeof filter == "object" && !filter.test(item.url)){
|
||||
continue;
|
||||
}else if(typeof filter == "string" && item.url.indexOf(filter) < 0){
|
||||
continue;
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
if(item._justUpdated){
|
||||
item._justUpdated = false;
|
||||
item._needRender = true;
|
||||
}else{
|
||||
item._needRender = false;
|
||||
}
|
||||
|
||||
rowCollection.push(<RecordRow key={item.id} data={item} onSelect={self.props.onSelect.bind(self,item)}></RecordRow>);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="uk-table uk-table-condensed uk-table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="col_id">#</th>
|
||||
<th className="col_method">method</th>
|
||||
<th className="col_code">code</th>
|
||||
<th className="col_host">host</th>
|
||||
<th className="col_path">path</th>
|
||||
<th className="col_mime">mime type</th>
|
||||
<th className="col_time">time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rowCollection}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return RecordPanel;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
@@ -1,65 +0,0 @@
|
||||
function init(React){
|
||||
function dateFormat(date,fmt) {
|
||||
var o = {
|
||||
"M+": date.getMonth() + 1, //月份
|
||||
"d+": date.getDate(), //日
|
||||
"h+": date.getHours(), //小时
|
||||
"m+": date.getMinutes(), //分
|
||||
"s+": date.getSeconds(), //秒
|
||||
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
|
||||
"S" : date.getMilliseconds() //毫秒
|
||||
};
|
||||
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
|
||||
for (var k in o) if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
return fmt;
|
||||
}
|
||||
|
||||
var RecordRow = React.createClass({
|
||||
getInitialState : function(){
|
||||
return null;
|
||||
},
|
||||
render : function(){
|
||||
var trClassesArr = [],
|
||||
trClasses,
|
||||
data = this.props.data || {};
|
||||
if(data.statusCode){
|
||||
trClassesArr.push("record_status_done");
|
||||
}
|
||||
|
||||
trClassesArr.push( ((Math.floor(data._id /2) - data._id /2) == 0)? "row_even" : "row_odd" );
|
||||
trClasses = trClassesArr.join(" ");
|
||||
|
||||
var dateStr = dateFormat(new Date(data.startTime),"hh:mm:ss");
|
||||
|
||||
var rowIcon = [];
|
||||
if(data.protocol == "https"){
|
||||
rowIcon.push(<span className="icon_record" title="https"><i className="uk-icon-lock"></i></span>);
|
||||
}
|
||||
|
||||
if(data.ext && data.ext.map){
|
||||
rowIcon.push(<span className="icon_record" title="mapped to local file"><i className="uk-icon-shield"></i></span>);
|
||||
}
|
||||
|
||||
return(
|
||||
<tr className={trClasses} onClick={this.props.onSelect}>
|
||||
<td className="data_id">{data._id}</td>
|
||||
<td>{data.method} {rowIcon} </td>
|
||||
<td className={"http_status http_status_" + data.statusCode}>{data.statusCode}</td>
|
||||
<td title={data.host}>{data.host}</td>
|
||||
<td title={data.path}>{data.path}</td>
|
||||
<td>{data.mime}</td>
|
||||
<td>{dateStr}</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
shouldComponentUpdate:function(nextPros){
|
||||
return nextPros.data._needRender;
|
||||
},
|
||||
componentDidUpdate:function(){},
|
||||
componentWillUnmount:function(){}
|
||||
});
|
||||
|
||||
return RecordRow;
|
||||
}
|
||||
|
||||
module.exports.init = init;
|
230
web/src/reducer/globalStatusReducer.js
Normal file
@@ -0,0 +1,230 @@
|
||||
const defaultStatus = {
|
||||
recording: true,
|
||||
panelRefreshing: true, // indicate whether the record panel should be refreshing
|
||||
showFilter: false, // if the filter panel is showing
|
||||
showMapLocal: false,
|
||||
activeMenuKey: '',
|
||||
canLoadMore: false,
|
||||
interceptHttpsFlag: false,
|
||||
globalProxyFlag: false, // is global proxy now
|
||||
filterStr: '',
|
||||
directory: [],
|
||||
lastActiveRecordId: -1,
|
||||
currentActiveRecordId: -1,
|
||||
shouldClearAllRecord: false,
|
||||
appVersion: '',
|
||||
panelLoadingNext: false,
|
||||
panelLoadingPrev: false,
|
||||
showNewRecordTip: false,
|
||||
isRootCAFileExists: false,
|
||||
fetchingRecord: false,
|
||||
wsPort: null,
|
||||
mappedConfig:[] // configured map config
|
||||
};
|
||||
|
||||
import { MenuKeyMap } from 'common/Constant';
|
||||
|
||||
import {
|
||||
STOP_RECORDING,
|
||||
RESUME_RECORDING,
|
||||
SHOW_FILTER,
|
||||
HIDE_FILTER,
|
||||
UPDATE_FILTER,
|
||||
UPDATE_LOCAL_DIRECTORY,
|
||||
SHOW_MAP_LOCAL,
|
||||
HIDE_MAP_LOCAL,
|
||||
UPDATE_LOCAL_MAPPED_CONFIG,
|
||||
UPDATE_ACTIVE_RECORD_ITEM,
|
||||
UPDATE_LOCAL_INTERCEPT_HTTPS_FLAG,
|
||||
UPDATE_LOCAL_GLOBAL_PROXY_FLAG,
|
||||
HIDE_ROOT_CA,
|
||||
SHOW_ROOT_CA,
|
||||
UPDATE_CAN_LOAD_MORE,
|
||||
INCREASE_DISPLAY_RECORD_LIST,
|
||||
UPDATE_SHOULD_CLEAR_RECORD,
|
||||
UPDATE_APP_VERSION,
|
||||
UPDATE_IS_ROOTCA_EXISTS,
|
||||
UPDATE_SHOW_NEW_RECORD_TIP,
|
||||
UPDATE_GLOBAL_WSPORT,
|
||||
UPDATE_FETCHING_RECORD_STATUS
|
||||
} from 'action/globalStatusAction';
|
||||
|
||||
// The map to save the mapping relationships of the path and it's location in the tree node
|
||||
const directoryNodeMap = {};
|
||||
|
||||
// The map to store all the directory in a tree way
|
||||
let direcotryList = [];
|
||||
|
||||
function getTreeMap(path, sub) {
|
||||
|
||||
const children = [];
|
||||
sub.directory.forEach((item) => {
|
||||
if (!(item.name.indexOf('.') === 0)) {
|
||||
item.isLeaf = false;
|
||||
directoryNodeMap[item.fullPath] = item;
|
||||
children.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
sub.file.forEach((item) => {
|
||||
if (!(item.name.indexOf('.') === 0)) {
|
||||
item.isLeaf = true;
|
||||
directoryNodeMap[item.fullPath] = item;
|
||||
children.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
if (!path) {
|
||||
direcotryList = children;
|
||||
} else {
|
||||
directoryNodeMap[path].children = children;
|
||||
}
|
||||
|
||||
return direcotryList;
|
||||
}
|
||||
|
||||
function requestListReducer(state = defaultStatus, action) {
|
||||
switch (action.type) {
|
||||
case STOP_RECORDING: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.recording = false;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case RESUME_RECORDING: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.recording = true;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case SHOW_FILTER: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.activeMenuKey = MenuKeyMap.RECORD_FILTER;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case HIDE_FILTER: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.activeMenuKey = '';
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_FILTER: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.filterStr = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case SHOW_MAP_LOCAL: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.activeMenuKey = MenuKeyMap.MAP_LOCAL;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case HIDE_MAP_LOCAL: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.activeMenuKey = '';
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_LOCAL_DIRECTORY: {
|
||||
const newState = Object.assign({}, state);
|
||||
const { path, sub } = action.data;
|
||||
|
||||
newState.directory = getTreeMap(path, sub);
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_LOCAL_MAPPED_CONFIG: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.mappedConfig = action.data;
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_ACTIVE_RECORD_ITEM: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.lastActiveRecordId = state.currentActiveRecordId;
|
||||
newState.currentActiveRecordId = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_LOCAL_INTERCEPT_HTTPS_FLAG: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.interceptHttpsFlag = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_LOCAL_GLOBAL_PROXY_FLAG: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.globalProxyFlag = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case SHOW_ROOT_CA: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.activeMenuKey = MenuKeyMap.ROOT_CA;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case HIDE_ROOT_CA: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.activeMenuKey = '';
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_CAN_LOAD_MORE: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.canLoadMore = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_SHOULD_CLEAR_RECORD: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.shouldClearAllRecord = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case INCREASE_DISPLAY_RECORD_LIST: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.displayRecordLimit += action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_APP_VERSION: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.appVersion = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_SHOW_NEW_RECORD_TIP: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.showNewRecordTip = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_IS_ROOTCA_EXISTS: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.isRootCAFileExists = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_GLOBAL_WSPORT: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.wsPort = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_FETCHING_RECORD_STATUS: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.fetchingRecord = action.data;
|
||||
return newState;
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requestListReducer;
|
120
web/src/reducer/requestRecordReducer.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const defaultState = {
|
||||
recordList: [],
|
||||
recordDetail: null
|
||||
};
|
||||
|
||||
import {
|
||||
UPDATE_WHOLE_REQUEST,
|
||||
UPDATE_SINGLE_RECORD,
|
||||
CLEAR_ALL_LOCAL_RECORD,
|
||||
UPDATE_MULTIPLE_RECORDS,
|
||||
SHOW_RECORD_DETAIL,
|
||||
HIDE_RECORD_DETAIL
|
||||
} from 'action/recordAction';
|
||||
|
||||
const getRecordInList = function (recordId, recordList) {
|
||||
const newRecordList = recordList.slice();
|
||||
for (let i = 0; i< newRecordList.length ; i++) {
|
||||
const record = newRecordList[i];
|
||||
if (record.id === recordId) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function requestListReducer (state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
case UPDATE_WHOLE_REQUEST: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.recordList = action.data.slice();
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_SINGLE_RECORD: {
|
||||
|
||||
const newState = Object.assign({}, state);
|
||||
|
||||
const list = newState.recordList.slice();
|
||||
|
||||
list.forEach((item) => {
|
||||
item._render = false;
|
||||
});
|
||||
|
||||
const record = action.data;
|
||||
|
||||
const index = list.findIndex((item) => {
|
||||
return item.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
// set the mark to ensure the item get re-rendered
|
||||
record._render = true;
|
||||
list[index] = record;
|
||||
} else {
|
||||
list.push(record);
|
||||
}
|
||||
|
||||
newState.recordList = list;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case UPDATE_MULTIPLE_RECORDS: {
|
||||
const newState = Object.assign({}, state);
|
||||
const list = newState.recordList.slice();
|
||||
|
||||
list.forEach((item) => {
|
||||
item._render = false;
|
||||
});
|
||||
|
||||
const records = action.data;
|
||||
records.forEach((record) => {
|
||||
const index = list.findIndex((item) => {
|
||||
return item.id === record.id;
|
||||
});
|
||||
|
||||
if (index >= 0) {
|
||||
// set the mark to ensure the item get re-rendered
|
||||
record._render = true;
|
||||
list[index] = record;
|
||||
} else {
|
||||
list.push(record);
|
||||
}
|
||||
});
|
||||
|
||||
newState.recordList = list;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case CLEAR_ALL_LOCAL_RECORD: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.recordList = [];
|
||||
return newState;
|
||||
}
|
||||
|
||||
case SHOW_RECORD_DETAIL: {
|
||||
const newState = Object.assign({}, state);
|
||||
const responseBody = action.data;
|
||||
const originRecord = getRecordInList(responseBody.id, newState.recordList);
|
||||
// 只在id存在的时候,才更新, 否则取消
|
||||
if (originRecord) {
|
||||
newState.recordDetail = Object.assign(responseBody, originRecord);
|
||||
} else {
|
||||
newState.recordDetail = null;
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
case HIDE_RECORD_DETAIL: {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.recordDetail = null;
|
||||
return newState;
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requestListReducer;
|
13
web/src/reducer/rootReducer.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import requestRecordReducer from './requestRecordReducer';
|
||||
import globalStatusReducer from './globalStatusReducer';
|
||||
|
||||
const defaultState = {
|
||||
|
||||
};
|
||||
|
||||
export default function(state = defaultState, action) {
|
||||
return {
|
||||
requestRecord: requestRecordReducer(state.requestRecord, action),
|
||||
globalStatus: globalStatusReducer(state.globalStatus, action)
|
||||
};
|
||||
}
|
152
web/src/saga/rootSaga.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
take,
|
||||
put,
|
||||
call,
|
||||
fork
|
||||
} from 'redux-saga/effects';
|
||||
import { message } from 'antd';
|
||||
|
||||
import {
|
||||
FETCH_REQUEST_LOG,
|
||||
CLEAR_ALL_RECORD,
|
||||
FETCH_RECORD_DETAIL,
|
||||
clearAllLocalRecord,
|
||||
updateWholeRequest,
|
||||
showRecordDetail
|
||||
} from 'action/recordAction';
|
||||
|
||||
import {
|
||||
FETCH_DIRECTORY,
|
||||
FETCH_MAPPED_CONFIG,
|
||||
UPDATE_REMOTE_MAPPED_CONFIG,
|
||||
TOGGLE_REMOTE_INTERCEPT_HTTPS,
|
||||
TOGGLE_REMORE_GLOBAL_PROXY_FLAG,
|
||||
updateLocalDirectory,
|
||||
updateLocalMappedConfig,
|
||||
updateActiveRecordItem,
|
||||
updateLocalInterceptHttpsFlag,
|
||||
updateFechingRecordStatus,
|
||||
updateLocalGlobalProxyFlag
|
||||
} from 'action/globalStatusAction';
|
||||
|
||||
import { getJSON, postJSON, isApiSuccess } from 'common/ApiUtil';
|
||||
|
||||
function* doFetchRequestList() {
|
||||
const data = yield call(getJSON, '/latestLog');
|
||||
yield put(updateWholeRequest(data));
|
||||
}
|
||||
|
||||
function* doFetchDirectory(path = '') {
|
||||
const sub = yield call(getJSON, '/filetree', { root: path });
|
||||
yield put(updateLocalDirectory(path, sub));
|
||||
}
|
||||
|
||||
function* doFetchMappedConfig() {
|
||||
const config = yield call(getJSON, '/getMapConfig');
|
||||
yield put(updateLocalMappedConfig(config));
|
||||
}
|
||||
|
||||
function* doFetchRecordBody(recordId) {
|
||||
// const recordBody = { id: recordId };
|
||||
yield put(updateFechingRecordStatus(true));
|
||||
const recordBody = yield call(getJSON, '/fetchBody', { id: recordId });
|
||||
recordBody.id = parseInt(recordBody.id, 10);
|
||||
|
||||
yield put(updateFechingRecordStatus(false));
|
||||
yield put(updateActiveRecordItem(recordId));
|
||||
yield put(showRecordDetail(recordBody));
|
||||
}
|
||||
|
||||
function* doUpdateRemoteMappedConfig(config) {
|
||||
const newConfig = yield call(postJSON, '/setMapConfig', config);
|
||||
yield put(updateLocalMappedConfig(newConfig));
|
||||
}
|
||||
|
||||
|
||||
function * doToggleRemoteInterceptHttps(flag) {
|
||||
yield call(postJSON, '/api/toggleInterceptHttps', { flag: flag });
|
||||
yield put(updateLocalInterceptHttpsFlag(flag));
|
||||
}
|
||||
|
||||
function * doToggleRemoteGlobalProxy(flag) {
|
||||
const result = yield call(postJSON, '/api/toggleGlobalProxy', { flag: flag });
|
||||
const windowsMessage = 'Sucessfully turned on, it may take up to 1 min to take effect.';
|
||||
const linuxMessage = 'Sucessfully turned on.';
|
||||
const turnDownMessage = 'Global proxy has been turned down.';
|
||||
if (isApiSuccess(result)) {
|
||||
const tipMessage = result.isWindows ? windowsMessage : linuxMessage;
|
||||
message.success(flag ? tipMessage : turnDownMessage, 3);
|
||||
yield put(updateLocalGlobalProxyFlag(flag));
|
||||
} else {
|
||||
message.error(result.errorMsg, 3);
|
||||
}
|
||||
}
|
||||
|
||||
function * fetchRequestSaga() {
|
||||
while (true) {
|
||||
yield take(FETCH_REQUEST_LOG);
|
||||
yield fork(doFetchRequestList);
|
||||
}
|
||||
}
|
||||
|
||||
function * clearRequestRecordSaga() {
|
||||
while (true) {
|
||||
yield take(CLEAR_ALL_RECORD);
|
||||
yield put(clearAllLocalRecord());
|
||||
}
|
||||
}
|
||||
|
||||
function * fetchDirectorySaga() {
|
||||
while (true) {
|
||||
const action = yield take(FETCH_DIRECTORY);
|
||||
yield fork(doFetchDirectory, action.data);
|
||||
}
|
||||
}
|
||||
|
||||
function * fetchMappedConfigSaga() {
|
||||
while (true) {
|
||||
yield take(FETCH_MAPPED_CONFIG);
|
||||
yield fork(doFetchMappedConfig);
|
||||
}
|
||||
}
|
||||
|
||||
function * updateRemoteMappedConfigSaga() {
|
||||
while (true) {
|
||||
const action = yield take(UPDATE_REMOTE_MAPPED_CONFIG);
|
||||
|
||||
yield fork(doUpdateRemoteMappedConfig, action.data);
|
||||
}
|
||||
}
|
||||
|
||||
function * fetchRecordBodySaga() {
|
||||
while (true) {
|
||||
const action = yield take(FETCH_RECORD_DETAIL);
|
||||
|
||||
yield fork(doFetchRecordBody, action.data);
|
||||
}
|
||||
}
|
||||
|
||||
function * toggleRemoteInterceptHttpsSaga() {
|
||||
while (true) {
|
||||
const action = yield take(TOGGLE_REMOTE_INTERCEPT_HTTPS);
|
||||
yield fork(doToggleRemoteInterceptHttps, action.data);
|
||||
}
|
||||
}
|
||||
|
||||
function * toggleRemoteGlobalProxySaga() {
|
||||
while (true) {
|
||||
const action = yield take(TOGGLE_REMORE_GLOBAL_PROXY_FLAG);
|
||||
yield fork(doToggleRemoteGlobalProxy, action.data);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* root() {
|
||||
yield fork(fetchRequestSaga);
|
||||
yield fork(clearRequestRecordSaga);
|
||||
yield fork(fetchDirectorySaga);
|
||||
yield fork(fetchMappedConfigSaga);
|
||||
yield fork(updateRemoteMappedConfigSaga);
|
||||
yield fork(fetchRecordBodySaga);
|
||||
yield fork(toggleRemoteInterceptHttpsSaga);
|
||||
yield fork(toggleRemoteGlobalProxySaga);
|
||||
}
|