Merge pull request #304 from denzelsN/master

modify some ui
This commit is contained in:
denzelsN
2015-04-06 16:09:32 +09:00
23 changed files with 397 additions and 221 deletions
@@ -20,7 +20,8 @@
* @method $init
* @param {Hash Table} options
*/
$init: function (htOption) {
$init: function (htOption, $location) {
this._query = "";
this.option({
"sContainerId": '',
"sOverviewId": '',
@@ -39,7 +40,9 @@
'MEMCACHED': 'MEMCACHED.png',
'MONGODB': 'MONGODB.png',
'MSSQLSERVER': 'MSSQLSERVER.png',
'MSSQLSERVER_GROUP': 'MSSQLSERVER.png',
'MYSQL': 'MYSQL.png',
'MYSQL_GROUP': 'MYSQL.png',
'NBASE': 'NBASE.png',
'NGINX': 'NGINX.png',
'ORACLE': 'ORACLE.png',
@@ -47,6 +50,7 @@
'STAND_ALONE': 'STAND_ALONE.png',
'TOMCAT': 'TOMCAT.png',
'UNDEFINED': 'UNDEFINED.png',
'UNDEFINED_GROUP': 'UNDEFINED.png',
'UNKNOWN': 'UNKNOWN.png',
'UNKNOWN_GROUP': 'UNKNOWN_GROUP.png',
'REDIS': 'REDIS.png',
@@ -58,12 +62,13 @@
"backgroundColor": "#ffffff",
"borderColor": "#C5C5C5",
"borderWidth": 1,
"fontColor": "#1F1F21"
"fontColor": "#000000"
},
"bold": {
"backgroundColor": "#f2f2f2",
"borderColor": "#666975",
"borderWidth": 2
"borderWidth": 2,
"fontColor": "#000000"
}
},
"htLinkType": {
@@ -94,7 +99,8 @@
//"borderColor": "#25AFF4",
"borderColor": "#53069B",
"backgroundColor": "#289E1D",
"fontColor": "#ffffff"
//"fontColor": "#5cb85c"
"fontColor": "#53069B"
},
"htHighlightLink": {
"fontFamily": "bold 12pt avn55,NanumGothic,ng,dotum,AppleGothic,sans-serif",
@@ -130,7 +136,7 @@
this._initNodeTemplates();
this._initLinkTemplates();
this._initDiagramEnvironment();
this._initOverview();
this._initOverview( $location );
},
/**
@@ -202,7 +208,7 @@
click: function (e, obj) {
self._onNodeClicked(e, obj);
},
contextClick: self._onNodeContextClicked.bind(this)
contextClick: self._onNodeContextClicked.bind(self)
},
self.$(
go.Shape,
@@ -388,6 +394,7 @@
self.$(
go.TextBlock,
{
name: "NODE_APPLICATION_NAME",
margin: new go.Margin(1, 2),
column: 2,
font: self.option('sSmallFont'),
@@ -430,7 +437,7 @@
self._onNodeClicked(e, obj);
}
},
contextClick: self._onNodeContextClicked.bind(this)
contextClick: self._onNodeContextClicked.bind(self)
},
self.$(
go.Shape,
@@ -452,11 +459,13 @@
go.Panel,
go.Panel.Spot,
{
name: "NODE_1SUB_PANEL",
},
self.$(
go.Panel,
go.Panel.Vertical,
{
name: "NODE_2SUB_PANEL",
alignment: go.Spot.TopLeft,
alignmentFocus: go.Spot.TopLeft,
minSize: new go.Size(130, NaN)
@@ -483,12 +492,51 @@
new go.Binding("itemArray", "unknownNodeGroup")
)
)
),
self.$(
go.Panel,
go.Panel.Auto,
{
alignment: go.Spot.TopRight,
alignmentFocus: go.Spot.TopRight,
visible: false
},
new go.Binding("visible", "instanceCount", function (v) {
return v > 1 ? true : false;
}),
self.$(
go.Shape,
{
figure: "RoundedRectangle",
fill: "#848484",
strokeWidth: 1,
stroke: "#848484"
}
),
self.$(
go.Panel,
go.Panel.Auto,
{
margin: new go.Margin(0, 3, 0, 3)
},
self.$(
go.TextBlock,
new go.Binding("text", "instanceCount"),
{
stroke: "#FFFFFF",
textAlign: "center",
height: 16,
font: self.option('sSmallFont'),
editable: false
}
)
)
)
);
};
_.each(htIcons, function (sVal, sKey) {
if (sKey === "UNKNOWN_GROUP") {
if ( sKey.indexOf( "_GROUP" ) != -1 ) {
this._oDiagram.nodeTemplateMap.add(sKey, getUnknownGroupTemplate(sVal));
} else {
this._oDiagram.nodeTemplateMap.add(sKey, getNodeTemplate(sVal));
@@ -711,14 +759,19 @@
*
* @method _initOverview
*/
_initOverview: function () {
this._oOverview = this.$( go.Overview,
this.option('sOverviewId'),
{ observed: this._oDiagram }
);
this._oOverview.box.elt(0).figure = "RoundedRectangle";
this._oOverview.box.elt(0).stroke = "#53069B";
this._oOverview.box.elt(0).strokeWidth = 4;
_initOverview: function ( $location ) {
if ( /^\/main/.test( $location.path() ) ) {
this._oOverview = this.$( go.Overview,
this.option('sOverviewId'),
{ observed: this._oDiagram }
);
this._oOverview.box.elt(0).figure = "RoundedRectangle";
this._oOverview.box.elt(0).stroke = "#53069B";
this._oOverview.box.elt(0).strokeWidth = 4;
} else {
$( "#" + this.option('sOverviewId') ).hide();
}
},
/**
@@ -787,7 +840,7 @@
// nodes, including groups
while (allNodes.next()) {
this._hightlightNode(allNodes.value.findObject("NODE_SHAPE"), allNodes.value.findObject("NODE_TEXT"), allNodes.value.highlight);
this._highlightNode(allNodes.value.findObject("NODE_SHAPE"), allNodes.value.findObject("NODE_TEXT"), allNodes.value.highlight);
}
// links
while (allLinks.next()) {
@@ -853,18 +906,43 @@
* @param theme
* @private
*/
_hightlightNode: function (nodeShape, nodeText, theme) {
_highlightNode: function (nodeShape, nodeText, theme) {
if (nodeShape === null || nodeText === null) {
return;
}
if (theme) {
nodeShape.stroke = this.option('htHighlightNode').borderColor;
nodeShape.strokeWidth = 2;
nodeShape.part.isShadowed = true;
if ( this._query != "" ) {
if ( nodeText.type === go.Panel.Table ) {
for( var i = 0 ; i < nodeText.rowCount ; i++ ) {
if ( new RegExp( this._query, "i" ).test( nodeText.elt(i).elt(1).text ) ) {
nodeText.elt(i).elt(1).stroke = this.option('htHighlightNode').fontColor;
}
}
} else {
if ( new RegExp( this._query, "i" ).test( nodeText.text ) ) {
nodeText.stroke = this.option('htHighlightNode').fontColor;
}
}
}
// nodeText.stroke = this.option('htHighlightNode')[theme].fontColor;
} else {
var type = (nodeShape.key === this.option('sBoldKey')) ? 'bold' : 'default';
nodeShape.stroke = this.option('htNodeTheme')[type].borderColor;
nodeShape.strokeWidth = 1;
nodeShape.part.isShadowed = false;
if ( nodeText.type === go.Panel.Table ) {
for( var i = 0 ; i < nodeText.rowCount ; i++ ) {
nodeText.elt(i).elt(1).stroke = this.option('htNodeTheme')[type].fontColor;
}
} else {
nodeText.stroke = this.option('htNodeTheme')[type].fontColor;
}
// nodeText.stroke = this.option('htNodeTheme').default.fontColor;
}
},
@@ -987,12 +1065,12 @@
* @param {ojb} ojb
* @param {String} unknownKey
*/
_onNodeClicked: function (e, obj, unknownKey) {
_onNodeClicked: function (e, obj, unknownKey, query) {
var node = obj.part,
htData = node.data,
fOnNodeClicked = this.option('fOnNodeClicked');
if (_.isFunction(fOnNodeClicked)) {
fOnNodeClicked.call(this, e, htData, unknownKey);
fOnNodeClicked.call(this, e, htData, unknownKey, query);
}
// node.diagram.startTransaction("onNodeClick");
// node.diagram.commitTransaction("onNodeClick");
@@ -1070,10 +1148,14 @@
this._oDiagram.contentAlignment = go.Spot.Center;
this._oDiagram.contentAlignment = go.Spot.None;
},
clearQuery: function() {
this._query = "";
},
/**
* search node
*/
searchNode: function( nodeName, nodeServiceType ) {
searchNode: function( query, nodeServiceType ) {
this._query = query;
var allNodes = this._oDiagram.nodes,
selectedNode = null,
selectedIndex = 0,
@@ -1082,17 +1164,17 @@
while (allNodes.next()) {
var node = allNodes.value;
if ( node.data.serviceType == this.option("unknownGroupName") && ( nodeServiceType == this.option("unknownGroupName") || nodeServiceType == null ) ) {
if ( node.data.unknownNodeGroup ) {
var unknownNodeGroup = node.data.unknownNodeGroup;
for( var i = 0; i < unknownNodeGroup.length ; i++ ) {
if ( new RegExp( nodeName, "i" ).test( unknownNodeGroup[i].applicationName ) ) {
if ( new RegExp( query, "i" ).test( unknownNodeGroup[i].applicationName ) ) {
this._addNodeToTemporaryList( similarNodeList, returnNodeDataList, node, unknownNodeGroup[i], this.option("unknownGroupName") );
break;
}
}
} else {
if ( !angular.isUndefined( node.data.applicationName ) ) {
if ( new RegExp( nodeName, "i" ).test( node.data.applicationName ) ) {
if ( new RegExp( query, "i" ).test( node.data.applicationName ) ) {
this._addNodeToTemporaryList( similarNodeList, returnNodeDataList, node, node.data );
if ( new RegExp( nodeServiceType, "i" ).test( node.data.serviceType ) ) {
selectedIndex = similarNodeList.length - 1;
@@ -1119,7 +1201,7 @@
_selectAndHighlight : function( selectedNode ) {
this._oDiagram.select( selectedNode );
this._oDiagram.centerRect( selectedNode.actualBounds );
this._onNodeClicked(null, selectedNode );
this._onNodeClicked(null, selectedNode, null, this._query );
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+1
View File
@@ -88,6 +88,7 @@
<script src="components/slickgrid/slick.editors.js?v=${buildTime}"></script>
<script src="components/slickgrid/slick.grid.js?v=${buildTime}"></script>
<script src="components/slickgrid/slick.dataview.js?v=${buildTime}"></script>
<script src="components/slickgrid/plugins/slick.rowselectionmodel.js?v=${buildTime}"></script>
<script src="components/angular-base64/angular-base64.min.js?v=${buildTime}"></script>
<script src="components/amcharts/amcharts.js?v=${buildTime}"></script>
<script src="components/amcharts/serial.js?v=${buildTime}"></script>
@@ -185,7 +185,7 @@ pinpointApp.controller('FilteredMapCtrl', [ 'filterConfig', '$scope', '$routePar
oSidebarTitleVo.setTitle(node.applicationName);
$scope.$broadcast('scatter.showByNode', node);
} else if (node.unknownNodeGroup) {
oSidebarTitleVo.setTitle('Unknown Group');
oSidebarTitleVo.setTitle( node.serviceType.replace( "_", " " ) );
$scope.hasScatter = false;
} else {
oSidebarTitleVo.setTitle(node.applicationName);
@@ -146,7 +146,7 @@ pinpointApp.controller('MainCtrl', [ 'filterConfig', '$scope', '$timeout', '$rou
/**
* scope event on serverMap.nodeClicked
*/
$scope.$on('serverMap.nodeClicked', function (event, e, query, node, data) {
$scope.$on('serverMap.nodeClicked', function (event, e, query, node, data, searchQuery) {
bNodeSelected = true;
var oSidebarTitleVo = new SidebarTitleVo;
oSidebarTitleVo.setImageType(node.serviceType);
@@ -156,7 +156,7 @@ pinpointApp.controller('MainCtrl', [ 'filterConfig', '$scope', '$timeout', '$rou
oSidebarTitleVo.setTitle(node.applicationName);
$scope.$broadcast('scatter.initializeWithNode', node);
} else if (node.unknownNodeGroup) {
oSidebarTitleVo.setTitle('Unknown Group');
oSidebarTitleVo.setTitle( node.serviceType.replace( "_", " " ) );
$scope.hasScatter = false;
} else {
oSidebarTitleVo.setTitle(node.applicationName);
@@ -164,7 +164,7 @@ pinpointApp.controller('MainCtrl', [ 'filterConfig', '$scope', '$timeout', '$rou
}
$scope.hasFilter = false;
$scope.$broadcast('sidebarTitle.initialize.forMain', oSidebarTitleVo);
$scope.$broadcast('nodeInfoDetails.initialize', e, query, node, data, oNavbarVo);
$scope.$broadcast('nodeInfoDetails.initialize', e, query, node, data, oNavbarVo, null, searchQuery);
$scope.$broadcast('linkInfoDetails.hide');
$scope.refreshHelpIcons();
@@ -261,7 +261,7 @@ pinpointApp.controller('MainCtrl', [ 'filterConfig', '$scope', '$timeout', '$rou
oSidebarTitleVo
.setImageType(node.serviceType);
if (node.unknownNodeGroup) {
oSidebarTitleVo.setTitle('Unknown Group');
oSidebarTitleVo.setTitle( node.serviceType.replace( "_", " " ) );
$scope.hasScatter = false;
} else {
oSidebarTitleVo.setTitle(node.applicationName);
@@ -96,6 +96,10 @@ pinpointApp.controller('TransactionDetailCtrl', ['TransactionDetailConfig', '$sc
$scope.openTransactionView = function () {
$window.open('/#/transactionView/' + $scope.transactionDetail.agentId + '/' + $scope.transactionDetail.transactionId + '/' + $scope.transactionDetail.callStackStart);
};
$scope.$on("transactionDetail.selectDistributedCallFlowRow", function( event, rowId ) {
$("#traceTabs li:nth-child(1) a").trigger("click");
$scope.$broadcast('distributedCallFlow.selectRow.forTransactionDetail', rowId);
});
// events binding
$("#traceTabs li a").bind("click", function (e) {
@@ -54,7 +54,7 @@ pinpointApp.controller('TransactionListCtrl', ['TransactionListConfig', '$scope'
$timeout(function () {
$("#main-container").layout({
north__minSize: 20,
north__size: 200,
north__size: (window.innerHeight - 40) / 2,
// north__spacing_closed: 20,
// north__togglerLength_closed: 100,
// north__togglerAlign_closed: "top",
@@ -23,11 +23,11 @@ pinpointApp.directive('distributedCallFlow', [ '$filter', '$timeout',
/**
* get color by string
* @param str
* @param idx
* @returns {string}
*/
getColorByString = function(str) {
// str to hash
// str to hash
for (var i = 0, hash = 0; i < str.length; hash = str.charCodeAt(i++) + ((hash << 5) - hash));
// int/hash to hex
for (var i = 0, colour = "#"; i < 3; colour += ("00" + ((hash >> i++ * 8) & 0xFF).toString(16)).slice(-2));
@@ -59,8 +59,10 @@ pinpointApp.directive('distributedCallFlow', [ '$filter', '$timeout',
} else if (!item.isMethod) {
divClass += ' not-method';
}
html.push('<div class="'+divClass+'" data-container=".grid-canvas" data-toggle="popover" data-trigger="manual" data-placement="right" data-content="'+value+'">');
html.push("<div style='position:absolute;top:0;left:0;bottom:0;width:5px;background-color:"+leftBarColor+"'></div>");
html.push("<div style='position:absolute;top:0;left:0;bottom:0;width:5px;background-color:"+ leftBarColor +"'></div>");
html.push("<span style='display:inline-block;height:1px;width:" + (15 * dataContext["indent"]) + "px'></span>");
if (window.callStacks[idx + 1] && window.callStacks[idx + 1].indent > window.callStacks[idx].indent) {
@@ -210,6 +212,7 @@ pinpointApp.directive('distributedCallFlow', [ '$filter', '$timeout',
isMethod: val[index['isMethod']] ,
logLink : val[index['logPageUrl']],
logButtonName : val[index['logButtonName']],
isFocused : val[index['isFocused']]
});
});
return result;
@@ -248,6 +251,17 @@ pinpointApp.directive('distributedCallFlow', [ '$filter', '$timeout',
// };
// }
// };
dataView.getItemMetadata = function( row ) {
var item = dataView.getItemByIdx(row);
var o = { cssClasses: "" };
if ( item.isFocused === true ) {
o.cssClasses += " entry-point";
}
if ( item.execTime !== null ) {
o.cssClasses += " id_" + (row+1);
}
return o;
};
dataView.endUpdate();
var columns = [
@@ -268,6 +282,7 @@ pinpointApp.directive('distributedCallFlow', [ '$filter', '$timeout',
}
grid = new Slick.Grid(element.get(0), dataView, columns, options);
grid.setSelectionModel(new Slick.RowSelectionModel());
var isSingleClick = true, clickTimeout = false;
grid.onClick.subscribe(function (e, args) {
@@ -376,6 +391,11 @@ pinpointApp.directive('distributedCallFlow', [ '$filter', '$timeout',
scope.$on('distributedCallFlow.resize.' + scope.namespace, function (event) {
grid.resizeCanvas();
});
scope.$on("distributedCallFlow.selectRow." + scope.namespace, function( event, rowId ) {
var gridRow = rowId - 1;
grid.setSelectedRows( [gridRow] );
grid.scrollRowToTop( gridRow );
});
}
};
}
@@ -29,7 +29,7 @@ pinpointApp
scope.htLastUnknownNode = false;
angular.element($window).bind('resize',function(e) {
if (bShown && htLastNode.category === 'UNKNOWN_GROUP') {
if (bShown && /_GROUP$/.test( htLastNode.category ) ) {
renderAllChartWhichIsVisible(htLastNode);
}
});
@@ -84,13 +84,13 @@ pinpointApp
scope.showNodeServers = _.isEmpty(scope.serverList) ? false : true;
scope.agentHistogram = node.agentHistogram;
if (node.serviceType !== "UNKNOWN_GROUP") {
if ( /_GROUP$/.test( node.serviceType ) === false ) {
scope.showNodeResponseSummary = true;
scope.showNodeLoad = true;
renderResponseSummary('forNode', node.applicationName, node.histogram, '100%', '150px');
renderLoad('forNode', node.applicationName, node.timeSeriesHistogram, '100%', '220px', true);
} else if (node.serviceType === 'UNKNOWN_GROUP'){
} else if ( /_GROUP$/.test( node.serviceType ) ){
scope.showNodeResponseSummaryForUnknown = (scope.oNavbarVo.getPeriod() <= cfg.maxTimeToShowLoadAsDefaultForUnknown) ? false : true;
renderAllChartWhichIsVisible(node);
// scope.htLastUnknownNode = angular.copy(node);
@@ -308,7 +308,7 @@ pinpointApp
/**
* scope event on nodeInfoDetails.initialize
*/
scope.$on('nodeInfoDetails.initialize', function (event, e, query, node, mapData, navbarVo, reloadOnly) {
scope.$on('nodeInfoDetails.initialize', function (event, e, query, node, mapData, navbarVo, reloadOnly, searchQuery) {
show();
// DISABLE node Cache
//if (angular.equals(sLastKey, node.key) && !reloadOnly) {
@@ -323,6 +323,7 @@ pinpointApp
htLastNode = node;
scope.htLastUnknownNode = false;
scope.oNavbarVo = navbarVo;
scope.nodeSearch = searchQuery || "";
htServermapData = mapData;
showDetailInformation(node);
});
@@ -34,8 +34,8 @@ pinpointApp.constant('serverMapConfig', {
}
});
pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts', 'ProgressBar', 'SidebarTitleVo', '$filter', 'ServerMapFilterVo', 'encodeURIComponentFilter', 'filteredMapUtil', '$base64', 'ServerMapHintVo', '$timeout',
function (cfg, ServerMapDao, Alerts, ProgressBar, SidebarTitleVo, $filter, ServerMapFilterVo, encodeURIComponentFilter, filteredMapUtil, $base64, ServerMapHintVo, $timeout) {
pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts', 'ProgressBar', 'SidebarTitleVo', '$filter', 'ServerMapFilterVo', 'encodeURIComponentFilter', 'filteredMapUtil', '$base64', 'ServerMapHintVo', '$timeout', '$location',
function (cfg, ServerMapDao, Alerts, ProgressBar, SidebarTitleVo, $filter, ServerMapFilterVo, encodeURIComponentFilter, filteredMapUtil, $base64, ServerMapHintVo, $timeout, $location) {
return {
restrict: 'EA',
replace: true,
@@ -50,7 +50,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
// define private variables of methods
var showServerMap, setNodeContextMenuPosition, reset, emitDataExisting,
setLinkContextMenuPosition, setBackgroundContextMenuPosition, serverMapCallback, setLinkOption,
zoomToFit, updateLastSelection, openFilterWizard, searchNode;
zoomToFit, updateLastSelection, openFilterWizard, searchNode, getMergeArray, extractMergeTypeList;
// bootstrap
@@ -73,12 +73,12 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
htLastLink = {};
htLastNode = {};
scope.oNavbarVo = null;
scope.mergeUnknowns = true;
//scope.mergeUnknowns = true;
scope.totalRequestCount = true;
scope.bShowServerMapStatus = false;
scope.linkRouting = cfg.options.htLinkType.sRouting;
scope.linkCurve = cfg.options.htLinkType.sCurve;
scope.searchNodeName = "";
scope.searchNodeQuery = "";
scope.searchNodeIndex = 0;
scope.searchNodeList = [];
$fromAgentName = element.find('.fromAgentName');
@@ -86,7 +86,23 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
$fromAgentName.select2();
$toAgentName.select2();
bIsFilterWizardLoaded = false;
scope.mergeTypeList = [];//[ 'APACHE', 'ARCUS', 'BACKEND', 'BLOC', 'CASSANDRA', 'CUBRID', 'JAVA', 'MEMCACHED', 'MONGODB', 'MSSQLSERVER', 'MYSQL', 'NBASE', 'NGINX', 'ORACLE', 'QUEUE', 'STAND_ALONE', 'TOMCAT', 'UNKNOWN', 'REDIS', 'NBASE_ARC' ];
scope.mergeStatus = {};
/**
* extract node serviceType info
* @param serverMapData
*/
extractMergeTypeList = function( serverMapData ) {
serverMapData.nodeDataArray.forEach( function( o ) {
if ( o.isWas == false && o.serviceType !== "USER" ) {
if ( angular.isUndefined( scope.mergeStatus[o.serviceType] ) ) {
scope.mergeTypeList.push( o.serviceType );
scope.mergeStatus[o.serviceType] = true;
}
}
});
};
/**
* reset
*/
@@ -117,11 +133,10 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
* @param period
* @param filterText
* @parma hintText
* @param mergeUnknowns
* @param linkRouting
* @param linkCurve
*/
showServerMap = function (applicationName, serviceTypeCode, to, period, filterText, hintText, mergeUnknowns, linkRouting, linkCurve) {
showServerMap = function (applicationName, serviceTypeCode, to, period, filterText, hintText, linkRouting, linkCurve) {
oProgressBar.startLoading();
oAlert.hideError();
oAlert.hideWarning();
@@ -160,9 +175,14 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
htLastMapData.applicationMapData = ServerMapDao.mergeFilteredMapData(htLastMapData.applicationMapData, result.applicationMapData);
var serverMapData = ServerMapDao.extractDataFromApplicationMapData(htLastMapData.applicationMapData);
serverMapData = ServerMapDao.addFilterProperty(filters, serverMapData);
if (filteredMapUtil.doFiltersHaveUnknownNode(filters)) scope.mergeUnknowns = mergeUnknowns = false;
extractMergeTypeList( serverMapData );
if (filteredMapUtil.doFiltersHaveUnknownNode(filters)) {
for( var key in scope.mergeStatus ) {
scope.mergeStatus[key] = false;
}
}
emitDataExisting(htLastMapData);
serverMapCallback(query, serverMapData, mergeUnknowns, linkRouting, linkCurve);
serverMapCallback(query, serverMapData, linkRouting, linkCurve);
});
} else {
ServerMapDao.getServerMapData(htLastQuery, function (err, query, mapData) {
@@ -176,7 +196,8 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
emitDataExisting(mapData);
htLastMapData = mapData;
var serverMapData = ServerMapDao.extractDataFromApplicationMapData(mapData.applicationMapData);
serverMapCallback(query, serverMapData, mergeUnknowns, linkRouting, linkCurve);
extractMergeTypeList( serverMapData );
serverMapCallback(query, serverMapData, linkRouting, linkCurve);
});
}
};
@@ -237,21 +258,31 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
};
scope.$digest();
};
getMergeArray = function() {
var a = [];
for( var key in scope.mergeStatus ) {
if ( scope.mergeStatus[key] === true ) {
a.push( key );
}
}
return a;
};
/**
* server map callback
* @param query
* @param applicationMapData
* @param mergeUnknowns
* @param linkRouting
* @param linkCurve
*/
serverMapCallback = function (query, applicationMapData, mergeUnknowns, linkRouting, linkCurve) {
serverMapCallback = function (query, applicationMapData, linkRouting, linkCurve) {
console.log( applicationMapData );
var lastCopiedData = applicationMapData;
if (mergeUnknowns) {
lastCopiedData = ServerMapDao.mergeUnknown(applicationMapData);
}
//var lastCopiedData = applicationMapData;
//if (mergeUnknowns) {
//lastCopiedData = ServerMapDao.mergeUnknown(applicationMapData, mergeTypeList);
var lastCopiedData = ServerMapDao.mergeGroup(applicationMapData, getMergeArray());
//}
console.log( lastCopiedData );
// ServerMapDao.removeNoneNecessaryDataForHighPerformance(lastCopiedData);
@@ -290,7 +321,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
oProgressBar.setLoading(90);
var options = cfg.options;
options.fOnNodeClicked = function (e, node, unknownKey) {
options.fOnNodeClicked = function (e, node, unknownKey, searchQuery) {
var originalNode;
if (angular.isDefined(node.unknownNodeGroup) && !unknownKey) {
node.unknownNodeGroup = ServerMapDao.getUnknownNodeDataByUnknownNodeGroup(htLastMapData.applicationMapData, node.unknownNodeGroup);
@@ -302,7 +333,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
}
sLastSelection = 'node';
htLastNode = node;
scope.$emit("serverMap.nodeClicked", e, htLastQuery, node, lastCopiedData);
scope.$emit("serverMap.nodeClicked", e, htLastQuery, node, lastCopiedData, searchQuery);
reset();
};
options.fOnNodeContextClicked = function (e, node) {
@@ -391,7 +422,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
oProgressBar.setLoading(100);
if (oServerMap === null) {
oServerMap = new ServerMap(options);
oServerMap = new ServerMap(options, $location);
} else {
oServerMap.option(options);
}
@@ -609,7 +640,16 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
*/
scope.toggleMergeUnknowns = function () {
scope.mergeUnknowns = (scope.mergeUnknowns) ? false : true;
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.linkRouting, scope.linkCurve);
reset();
};
/**
* toggle merge group
*/
scope.toggleMergeGroup = function ( mergeType ) {
scope.mergeStatus[ mergeType ] = !scope.mergeStatus[ mergeType ];
//scope.mergeUnknowns = (scope.mergeUnknowns) ? false : true;
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.linkRouting, scope.linkCurve);
reset();
};
@@ -620,7 +660,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
scope.toggleLinkLableTextType = function (type) {
scope.totalRequestCount = (type !== 'tps') ? true : false;
scope.tps = (type === 'tps') ? true : false;
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.linkRouting, scope.linkCurve);
reset();
};
@@ -630,7 +670,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
*/
scope.toggleLinkRouting = function (type) {
scope.linkRouting = cfg.options.htLinkType.sRouting = type;
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.linkRouting, scope.linkCurve);
reset();
};
@@ -640,7 +680,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
*/
scope.toggleLinkCurve = function (type) {
scope.linkCurve = cfg.options.htLinkType.sCurve = type;
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.linkRouting, scope.linkCurve);
reset();
};
@@ -665,14 +705,14 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
scope.bShowServerMapStatus = true;
bUseLinkContextMenu = bUseBackgroundContextMenu = true;
bUseNodeContextMenu = false;
showServerMap(navbarVo.getApplicationName(), navbarVo.getServiceTypeCode(), navbarVo.getQueryEndTime(), navbarVo.getQueryPeriod(), navbarVo.getFilter(), navbarVo.getHint(), scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
showServerMap(navbarVo.getApplicationName(), navbarVo.getServiceTypeCode(), navbarVo.getQueryEndTime(), navbarVo.getQueryPeriod(), navbarVo.getFilter(), navbarVo.getHint(), scope.linkRouting, scope.linkCurve);
});
/**
* scope event on serverMap.fetch
*/
scope.$on('serverMap.fetch', function (event, queryPeriod, queryEndTime) {
showServerMap(scope.oNavbarVo.getApplicationName(), scope.oNavbarVo.getServiceTypeCode(), queryEndTime, queryPeriod, scope.oNavbarVo.getFilter(), scope.oNavbarVo.getHint(), scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
showServerMap(scope.oNavbarVo.getApplicationName(), scope.oNavbarVo.getServiceTypeCode(), queryEndTime, queryPeriod, scope.oNavbarVo.getFilter(), scope.oNavbarVo.getHint(), scope.linkRouting, scope.linkCurve);
});
/**
@@ -687,7 +727,7 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
applicationName: mapData.applicationId
};
htLastMapData = mapData;
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.mergeUnknowns, scope.linkRouting, scope.linkCurve);
serverMapCallback(htLastQuery, htLastMapData.applicationMapData, scope.linkRouting, scope.linkCurve);
});
/**
@@ -714,14 +754,15 @@ pinpointApp.directive('serverMap', [ 'serverMapConfig', 'ServerMapDao', 'Alerts'
}
}
scope.searchNode = function() {
if (oServerMap && scope.searchNodeName !== "" ) {
if (oServerMap && scope.searchNodeQuery !== "" ) {
scope.searchNodeIndex = 0;
scope.searchNodeList = oServerMap.searchNode( scope.searchNodeName );
scope.searchNodeList = oServerMap.searchNode( scope.searchNodeQuery );
}
};
scope.clearSearchNode = function() {
oServerMap.clearQuery();
scope.searchNodeIndex = 0;
scope.searchNodeName = "";
scope.searchNodeQuery = "";
scope.searchNodeList = [];
}
}
@@ -28,6 +28,9 @@ pinpointApp.directive('timeline', function () {
scope.$on('timeline.initialize', function (event, transactionDetail) {
initialize(transactionDetail);
});
scope.moveCallFlow = function( item ) {
scope.$emit( "transactionDetail.selectDistributedCallFlowRow", item[6] );
};
}
};
});
@@ -32,6 +32,9 @@ pinpointApp.directive('transactionTable', ['$window', function ($window) {
scope.transactionReverse = false;
element.find('table tbody tr:last-child')[0].scrollIntoView(true);
// $(".transaction-table_wrapper").animate({ scrollTop: element.find('table tbody tr:last-child').offset().top }, 500);
} else {
scope.transactionOrderBy = 'elapsed';
scope.transactionReverse = true;
}
scope.transactionList = scope.transactionList.concat(transactionList);
resetIndexToTransactionList();
@@ -447,35 +447,30 @@ pinpointApp.service('ServerMapDao', [ 'serverMapDaoConfig', function ServerMapDa
* @param applicationMapData
* @returns {*}
*/
this.mergeUnknown = function (mapData) {
this.mergeGroup = function (mapData, mergeTypeList) {
// SERVERMAP_METHOD_CACHE = {};
var applicationMapData = angular.copy(mapData);
var nodes = applicationMapData.nodeDataArray;
var links = applicationMapData.linkDataArray;
var inboundCountMap = {};
// 1. 각 노드 별로 인입되는 노드의 갯수와 request 합을 저장해 둠.
// > sourceCount == 0 인 경우 root node 라고 생각 할 수 있습니다.
// > sourceCount == 1 인 경우 아마도 merging을 대상이 아님.
nodes.forEach(function (node) {
if (!inboundCountMap[node.key]) {
inboundCountMap[node.key] = {
"sourceCount": 0,
"totalCallCount": 0
};
}
links.forEach(function (link) {
var sourceCount = 0;
var totalCallCount = 0;
links.forEach(function (link) {
if (link.to === node.key) {
inboundCountMap[node.key].sourceCount++;
inboundCountMap[node.key].totalCallCount += link.totalCount;
sourceCount++;
totalCallCount += link.totalCount;
}
});
inboundCountMap[node.key] = {
"sourceCount": sourceCount,
"totalCallCount": totalCallCount
};
});
var newNodeList = [];
var newLinkList = [];
var removeNodeIdSet = {};
var removeLinkIdSet = {};
function getNodeByApplicationName(applicationName) {
for(var k in nodes) {
if (applicationName === nodes[k].applicationName) {
@@ -485,128 +480,150 @@ pinpointApp.service('ServerMapDao', [ 'serverMapDaoConfig', function ServerMapDa
return false;
}
nodes.forEach(function (node, nodeIndex) {
if (node.category === "UNKNOWN") {
return;
}
var newNode;
var newLink;
var newNodeKey = "UNKNOWN_GROUP_" + node.key;
var unknownCount = 0;
links.forEach(function (link, linkIndex) {
if (link.from == node.key &&
link.targetInfo.serviceType == "UNKNOWN" &&
inboundCountMap[link.to] && inboundCountMap[link.to].sourceCount == 1) {
unknownCount++;
}
});
if (unknownCount < 2) {
return;
}
// for each children.
links.forEach(function (link, linkIndex) {
if (link.targetInfo.serviceType != "UNKNOWN") {
return;
}
if (inboundCountMap[link.to] && inboundCountMap[link.to].sourceCount > 1) {
return;
}
// branch out from current node.
if (link.from == node.key) {
if (!newNode) {
newNode = {
"key": newNodeKey,
"unknownNodeGroup": [],
"serviceType": "UNKNOWN_GROUP",
"category": "UNKNOWN_GROUP"
};
}
if (!newLink) {
newLink = {
"key": node.key + "-" + newNodeKey,
"from": node.key,
"to": newNodeKey,
"sourceInfo": {},
"targetInfo": [],
"totalCount": 0,
"errorCount": 0,
"slowCount": 0,
"hasAlert": false,
"unknownLinkGroup": [],
"histogram": {}
};
}
var thisNode = getNodeByApplicationName(link.targetInfo.applicationName);
delete thisNode.category;
newNode.unknownNodeGroup.push(thisNode);
link.targetInfo['totalCount'] = link.totalCount;
newLink.totalCount += link.totalCount;
newLink.errorCount += link.errorCount;
newLink.slowCount += link.slowCount;
newLink.sourceInfo = link.sourceInfo;
if (link.hasAlert) {
newLink.hasAlert = link.hasAlert;
}
newLink.unknownLinkGroup.push(link);
// $.each(link.histogram, function (key, value) {
// if (newLink.histogram[key]) {
// newLink.histogram[key] += value;
// } else {
// newLink.histogram[key] = value;
// }
// });
removeNodeIdSet[link.to] = null;
removeLinkIdSet[link.key] = null;
}
});
if (newNode) {
newNode.unknownNodeGroup.sort(function (e1, e2) {
return e2.totalCount - e1.totalCount;
});
newNodeList.push(newNode);
}
if (newLink) {
newLink.unknownLinkGroup.sort(function (e1, e2) {
return e2.totalCount - e1.totalCount;
});
newLinkList.push(newLink);
}
mergeTypeList.forEach( function( mergeType ) {
var mergeTypeGroup = mergeType + "_GROUP";
var newNodeList = [];
var newLinkList = [];
var removeNodeIdSet = {};
var removeLinkIdSet = {};
nodes.forEach(function (node, nodeIndex) {
if (node.category === mergeType) {
return;
}
var newNode;
var newLink;
var newNodeKey = mergeTypeGroup + "_" + node.key;
var unknownCount = 0;
// 현재 확인 중인 node가 "UNKNOWN"으로 요청을 보내는 Node인지 확인하고 그렇다면 갯수를 count 함.
// 또한 "UNKNOWN"의 sourceCount는 1인 것 만 대상으로 함.
// > 즉 두 개 이상의 노드로 부터 request를 받는 "UNKNOWN"은 mergeing 대상이 아님.
// from은 같고 to가 두 개 이상인 노드와 unknown을 골라 냄.
links.forEach(function (link, linkIndex) {
if (link.from == node.key &&
link.targetInfo.serviceType == mergeType &&
inboundCountMap[link.to] && inboundCountMap[link.to].sourceCount == 1) {
unknownCount++;
}
});
// merging을 해야 하닌 2개 이하는 의미없음.
if (unknownCount < 2) {
return;
}
// @@여기까지 오면 현재 노드는 두 개 이상의 UNKNOWN 으로 요청을 보내는 노드가 됨.( 것두 혼자서 보냄 )
// 고로 현재 노드의 to 가 되는 Unknown 노드들은 merging을 대상이 됨.
links.forEach(function (link, linkIndex) {
// 검증
if (link.targetInfo.serviceType != mergeType) {
return;
}
// 검증
if (inboundCountMap[link.to] && inboundCountMap[link.to].sourceCount > 1) {
return;
}
// 다시 현재 노드로 부터 나가는 링크들을 골라냄.
if (link.from == node.key) {
if (!newNode) {
newNode = {
"key": newNodeKey,
"unknownNodeGroup": [],
"serviceType": mergeTypeGroup,
"category": mergeTypeGroup,
"instanceCount": 0
};
}
if (!newLink) {
newLink = {
"key": node.key + "-" + newNodeKey,
"from": node.key,
"to": newNodeKey,
"sourceInfo": {},
"targetInfo": [],
"totalCount": 0,
"errorCount": 0,
"slowCount": 0,
"hasAlert": false,
"unknownLinkGroup": [],
"histogram": {}
};
}
// 자 링크의 target이 되는 unknown 노드를 가져옴.
var thisNode = getNodeByApplicationName(link.targetInfo.applicationName);
delete thisNode.category;
newNode.instanceCount += thisNode.instanceCount;
newNode.unknownNodeGroup.push(thisNode);
// 각종 count 정보를 합산함.
link.targetInfo['totalCount'] = link.totalCount;
newLink.totalCount += link.totalCount;
newLink.errorCount += link.errorCount;
newLink.slowCount += link.slowCount;
newLink.sourceInfo = link.sourceInfo;
if (link.hasAlert) {
newLink.hasAlert = link.hasAlert;
}
newLink.unknownLinkGroup.push(link);
// $.each(link.histogram, function (key, value) {
// if (newLink.histogram[key]) {
// newLink.histogram[key] += value;
// } else {
// newLink.histogram[key] = value;
// }
// });
// merging으로 인해 삭제할 원 노드와 원 링크의 정보를 저장해 둠.( 나중에 몰아서 삭제하도록 ).
removeNodeIdSet[link.to] = null;
removeLinkIdSet[link.key] = null;
}
});
if (newNode) {
newNode.unknownNodeGroup.sort(function (e1, e2) {
return e2.totalCount - e1.totalCount;
});
newNodeList.push(newNode);
}
if (newLink) {
newLink.unknownLinkGroup.sort(function (e1, e2) {
return e2.totalCount - e1.totalCount;
});
newLinkList.push(newLink);
}
});
newNodeList.forEach(function (newNode) {
applicationMapData.nodeDataArray.push(newNode);
});
newLinkList.forEach(function (newLink) {
applicationMapData.linkDataArray.push(newLink);
});
$.each(removeNodeIdSet, function (key, val) {
nodes.forEach(function (node, i) {
if (node.key == key) {
nodes.splice(i, 1);
}
});
});
$.each(removeLinkIdSet, function (key, val) {
links.forEach(function (link, i) {
if (link.key === key) {
links.splice(i, 1);
}
});
});
});
newNodeList.forEach(function (newNode) {
applicationMapData.nodeDataArray.push(newNode);
});
newLinkList.forEach(function (newLink) {
applicationMapData.linkDataArray.push(newLink);
});
$.each(removeNodeIdSet, function (key, val) {
nodes.forEach(function (node, i) {
if (node.key == key) {
nodes.splice(i, 1);
}
});
});
$.each(removeLinkIdSet, function (key, val) {
links.forEach(function (link, i) {
if (link.key === key) {
links.splice(i, 1);
}
});
});
return applicationMapData;
};
@@ -60,6 +60,11 @@
.slick-row.active {
background: #DFE8F6;
}
.slick-row.entry-point {
background: #DFF0D8;
color: #1469EB;
}
.slick-cell {
padding: 0 4px;
+3 -3
View File
@@ -61,9 +61,9 @@
width: 15%;
height: 15%;
position: absolute;
left: 0px;
right: 16px;
bottom: 16px;
background-color: rgba(255, 203, 39, 0.5);
background-color: rgba(44, 160, 44, 0.2);
z-index: 2;
border: 1px solid rgb(255, 203, 39);
border: 1px solid rgb(44, 160, 44);
}
@@ -1,6 +1,6 @@
<div class="linkInfoDetails" ng-show="showLinkInfoDetails">
<div class="unknown-group" ng-show="link.toNode.serviceType == 'UNKNOWN_GROUP'">
<div class="unknown-group" ng-show="link.toNode.serviceType.indexOf( '_GROUP' ) != -1">
<div class="search-n-order" style="padding-left:15px;">
<div style="float:left;width:265px;padding-right:5px">
@@ -65,7 +65,7 @@
</div>
<div class="known-link" ng-hide="link.toNode.serviceType == 'UNKNOWN_GROUP'">
<div class="known-link" ng-hide="link.toNode.serviceType.indexOf( '_GROUP' ) != -1">
<div class='response-summary' ng-show="showLinkResponseSummary">
<h5>Response Summary</h5>
<response-time-chart namespace="forLink"></response-time-chart>
@@ -1,6 +1,6 @@
<div class="nodeInfoDetails" ng-init="nodeIcon='USER'" ng-show="showNodeInfoDetails">
<div class="unknown-group" ng-show="node.serviceType == 'UNKNOWN_GROUP'">
<div class="unknown-group" ng-show="node.serviceType.indexOf('_GROUP') != -1">
<div class="search-n-order" style="padding-left:15px;">
<div style="float:left;width:265px;padding-right:5px">
@@ -58,7 +58,7 @@
</div>
<div class="known-node" ng-hide="node.serviceType == 'UNKNOWN_GROUP'">
<div class="known-node" ng-hide="node.serviceType.indexOf( '_GROUP' ) != -1">
<div class='response-summary' ng-show="showNodeResponseSummary">
<h5>Response Summary</h5>
@@ -102,8 +102,8 @@
</ul>
</div>
<div ng-show="node.serviceType=='UNKNOWN' && htLastUnknownNode">
<button type="button" class="btn btn-default btn-xs" ng-click="goBackToUnknownNode()"><span class="glyphicon glyphicon-arrow-left"></span> Go back to Unknown Group</button>
<div ng-show="htLastUnknownNode.serviceType.indexOf( '_GROUP' ) != -1 && htLastUnknownNode">
<button type="button" class="btn btn-default btn-xs" ng-click="goBackToUnknownNode()"><span class="glyphicon glyphicon-arrow-left"></span> Go back to Group</button>
</div>
</div>
+3 -4
View File
@@ -6,7 +6,7 @@
</div>
<div class="servermap-status" ng-show="bShowServerMapStatus" style="right:290px">
<form class="form-inline">
<input type="text" class="form-control" ng-keydown="searchNodeByEnter($event)" ng-model="searchNodeName" placeholder="Enter node name"/>
<input type="text" class="form-control" ng-keydown="searchNodeByEnter($event)" ng-model="searchNodeQuery" placeholder="Enter node name"/>
<button class="btn btn-info" style="margin-left:-10px" ng-click="searchNode()"><span class="glyphicon glyphicon-search"></span></button>
<button class="btn btn-info" ng-click="clearSearchNode()"><span class="glyphicon glyphicon-trash"></span></button>
</form>
@@ -131,9 +131,8 @@
<li class="dropdown-submenu">
<a tabindex="-1" href>Node Setting</a>
<ul class="dropdown-menu">
<li><a href ng-click="toggleMergeUnknowns()"><span class="glyphicon" style="width:14px"
ng-class="{'glyphicon-ok': mergeUnknowns}"></span>
Merge Unknowns</a></li>
<!--li><a href ng-click="toggleMergeUnknowns()"><span class="glyphicon" style="width:14px" ng-class="{'glyphicon-ok': mergeUnknowns}"></span>Merge Unknowns</a></li-->
<li ng-repeat="mergeType in mergeTypeList"><a href ng-click="toggleMergeGroup(mergeType)"><span class="glyphicon" style="width:14px" ng-class="{'glyphicon-ok': mergeStatus[mergeType]}"></span> Merge {{mergeType}}</a></li>
</ul>
</li>
<li class="dropdown-submenu">
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="timeline" style="background-color:#E8E8E8;width:100%;height:100%;overflow:auto;font-size:11px;">
<div ng-repeat="stack in timeline.callStack | timeline:key">
<div style="width:{{stack | timelineWidth:key:barRatio}}px; background-color:#69B2E9; margin-left:{{stack | timelineMarginLeft:key:barRatio:timeline.callStackStart}}px; margin-top:3px;" title="{{stack | json}}">
<div style="cursor:pointer;width:{{stack | timelineWidth:key:barRatio}}px; background-color:#69B2E9; margin-left:{{stack | timelineMarginLeft:key:barRatio:timeline.callStackStart}}px; margin-top:3px;" title="{{stack | json}}" ng-click="moveCallFlow( stack )">
<div style="width:200px;">{{stack[key.applicationName]}}/{{stack[key.apiType]}} ({{stack[key.end] - stack[key.begin]}}ms)</div>
</div>
</div>