diff --git a/src/.bowerrc b/src/.bowerrc
new file mode 100644
index 000000000..d528c255c
--- /dev/null
+++ b/src/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "main/webapp/components"
+}
diff --git a/src/bower.json b/src/bower.json
new file mode 100644
index 000000000..f1d9e9881
--- /dev/null
+++ b/src/bower.json
@@ -0,0 +1,29 @@
+{
+ "name": "pinpoint",
+ "version": "0.0.1",
+ "dependencies": {
+ "angular": "~1.0.7",
+ "json3": "~3.2.4",
+ "jquery": "~1.9.1",
+ "es5-shim": "~2.0.8",
+ "angular-resource": "~1.0.7",
+ "angular-cookies": "~1.0.7",
+ "select2": "~3.4.1",
+ "underscore": "~1.5.1",
+ "angular-bootstrap": "~0.5.0",
+ "angular-bootstrap-datetimepicker": "~0.1.2",
+ "moment": "*",
+ "bootstrap-datetimepicker": "~0.0.11",
+ "modernizr": "~2.6.2",
+ "jquery-ui": "~1.10.3",
+ "datejs": "*",
+ "nvd3": "~1.0.0-beta",
+ "d3": "~3.3.1"
+ },
+ "devDependencies": {
+ "angular-mocks": "~1.0.7",
+ "angular-scenario": "~1.0.7"
+ },
+ "appPath": "main/webapp",
+ "testPath": "test/webapp"
+}
diff --git a/src/karma.conf.js b/src/karma.conf.js
new file mode 100644
index 000000000..4afda8620
--- /dev/null
+++ b/src/karma.conf.js
@@ -0,0 +1,56 @@
+// Karma configuration
+
+// base path, that will be used to resolve files and exclude
+basePath = '';
+
+// list of files / patterns to load in the browser
+files = [
+ JASMINE,
+ JASMINE_ADAPTER,
+ 'main/webapp/components/angular/angular.js',
+ 'main/webapp/components/angular-mocks/angular-mocks.js',
+ 'main/webapp/components/angular-resource/angular-resource.js',
+ 'main/webapp/scripts/*.js',
+ 'main/webapp/scripts/**/*.js',
+ 'test/webapp/**/*.js'
+];
+
+// list of files to exclude
+exclude = [];
+
+// test results reporter to use
+// possible values: dots || progress || growl
+reporters = ['progress'];
+
+// web server port
+port = 8080;
+
+// cli runner port
+runnerPort = 9100;
+
+// enable / disable colors in the output (reporters and logs)
+colors = true;
+
+// level of logging
+// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
+logLevel = LOG_INFO;
+
+// enable / disable watching file and executing tests whenever any file changes
+autoWatch = true;
+
+// Start these browsers, currently available:
+// - Chrome
+// - ChromeCanary
+// - Firefox
+// - Opera
+// - Safari (only Mac)
+// - PhantomJS
+// - IE (only Windows)
+browsers = ['Chrome'];
+
+// If browser does not capture in given timeout [ms], kill it
+captureTimeout = 5000;
+
+// Continuous Integration mode
+// if true, it capture browsers, run tests and exit
+singleRun = false;
diff --git a/src/main/webapp/components_v1/pinpoint/chart-servermap.js b/src/main/webapp/components_v1/pinpoint/chart-servermap.js
index ea6a6fd58..0400b0cf6 100644
--- a/src/main/webapp/components_v1/pinpoint/chart-servermap.js
+++ b/src/main/webapp/components_v1/pinpoint/chart-servermap.js
@@ -11,8 +11,8 @@ var showApplicationStatisticsSummary = function(data) {
var chart = nv.models.discreteBarChart().x(function(d) {
return d.label;
}).y(function(d) {
- return d.value;
- }).staggerLabels(false).tooltips(false).showValues(true);
+ return d.value;
+ }).staggerLabels(false).tooltips(false).showValues(true);
chart.xAxis.tickFormat(function(d) {
if($.isNumeric(d)) {
@@ -20,17 +20,17 @@ var showApplicationStatisticsSummary = function(data) {
}
return d;
});
-
+
chart.yAxis.tickFormat(function(d) {
return d;
});
-
+
chart.valueFormat(function(d) {
return d;
});
-
+
chart.color(myColors);
-
+
d3.select('#linkInfoDetails .linkInfoBarChart svg')
.datum(data)
.transition()
diff --git a/src/main/webapp/favicon.ico b/src/main/webapp/favicon.ico
new file mode 100644
index 000000000..dc1a5003a
Binary files /dev/null and b/src/main/webapp/favicon.ico differ
diff --git a/src/main/webapp/fonts/avenir55.eot b/src/main/webapp/fonts/avenir55.eot
new file mode 100644
index 000000000..ec6eeafc5
Binary files /dev/null and b/src/main/webapp/fonts/avenir55.eot differ
diff --git a/src/main/webapp/fonts/avenir55.woff b/src/main/webapp/fonts/avenir55.woff
new file mode 100644
index 000000000..2371699e6
Binary files /dev/null and b/src/main/webapp/fonts/avenir55.woff differ
diff --git a/src/main/webapp/fonts/avenir85.eot b/src/main/webapp/fonts/avenir85.eot
new file mode 100644
index 000000000..db45193fb
Binary files /dev/null and b/src/main/webapp/fonts/avenir85.eot differ
diff --git a/src/main/webapp/fonts/avenir85.woff b/src/main/webapp/fonts/avenir85.woff
new file mode 100644
index 000000000..c38bb02a3
Binary files /dev/null and b/src/main/webapp/fonts/avenir85.woff differ
diff --git a/src/main/webapp/fonts/nanumgothic.eot b/src/main/webapp/fonts/nanumgothic.eot
new file mode 100644
index 000000000..4d0e88567
Binary files /dev/null and b/src/main/webapp/fonts/nanumgothic.eot differ
diff --git a/src/main/webapp/fonts/nanumgothic.woff b/src/main/webapp/fonts/nanumgothic.woff
new file mode 100644
index 000000000..0ce939f07
Binary files /dev/null and b/src/main/webapp/fonts/nanumgothic.woff differ
diff --git a/src/main/webapp/fonts/nanummyeongjo.eot b/src/main/webapp/fonts/nanummyeongjo.eot
new file mode 100644
index 000000000..2811d11e7
Binary files /dev/null and b/src/main/webapp/fonts/nanummyeongjo.eot differ
diff --git a/src/main/webapp/fonts/nanummyeongjo.woff b/src/main/webapp/fonts/nanummyeongjo.woff
new file mode 100644
index 000000000..04339d37e
Binary files /dev/null and b/src/main/webapp/fonts/nanummyeongjo.woff differ
diff --git a/src/main/webapp/fonts/trajanpro.eot b/src/main/webapp/fonts/trajanpro.eot
new file mode 100644
index 000000000..8d7fb77f1
Binary files /dev/null and b/src/main/webapp/fonts/trajanpro.eot differ
diff --git a/src/main/webapp/fonts/trajanpro.woff b/src/main/webapp/fonts/trajanpro.woff
new file mode 100644
index 000000000..bb7b24213
Binary files /dev/null and b/src/main/webapp/fonts/trajanpro.woff differ
diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
index 437dee2d5..67a9f4a5b 100644
--- a/src/main/webapp/index.html
+++ b/src/main/webapp/index.html
@@ -1,383 +1,98 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
Show all transaction
-
-
-
-
-
-
-
-
-
-
-
-
- Response histogram
-
-
-
- Response histogram summary
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/scripts/app.js b/src/main/webapp/scripts/app.js
new file mode 100644
index 000000000..1789b88f9
--- /dev/null
+++ b/src/main/webapp/scripts/app.js
@@ -0,0 +1,16 @@
+'use strict';
+
+var pinpointApp = angular.module('pinpointApp', [ 'ngResource' ]);
+
+pinpointApp.config(function($routeProvider, $locationProvider) {
+ $locationProvider.html5Mode(false).hashPrefix(''); // 해쉬뱅을 사용 안할 수 있
+ $routeProvider.when('/main', {
+ templateUrl : 'views/ready.html',
+ controller : 'MainCtrl',
+ }).when('/main/:application/:period/:queryEndTime', {
+ templateUrl : 'views/main.html',
+ controller : 'MainCtrl',
+ }).otherwise({
+ redirectTo : '/main'
+ });
+});
diff --git a/src/main/webapp/scripts/controllers/main.js b/src/main/webapp/scripts/controllers/main.js
new file mode 100644
index 000000000..39a511f0d
--- /dev/null
+++ b/src/main/webapp/scripts/controllers/main.js
@@ -0,0 +1,20 @@
+'use strict';
+
+pinpointApp.controller('MainCtrl', [ '$scope', '$routeParams', function($scope, $routeParams) {
+
+ $scope.main = "";
+ $scope.sidebar = "";
+
+ $scope.$on("dividerOpen", function(event) {
+ $scope.main = "";
+ $scope.sidebar = "";
+ $(window).trigger('resize');
+ });
+
+ $scope.$on("dividerClosed", function(event) {
+ $scope.main = "fullsize";
+ $scope.sidebar = "smallsize";
+ $(window).trigger('resize');
+ });
+
+} ]);
diff --git a/src/main/webapp/scripts/directives/divider.js b/src/main/webapp/scripts/directives/divider.js
new file mode 100644
index 000000000..6742e4bb5
--- /dev/null
+++ b/src/main/webapp/scripts/directives/divider.js
@@ -0,0 +1,22 @@
+'use strict';
+
+pinpointApp
+ .directive('divider', ['$rootScope', function ($rootScope) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ templateUrl: 'views/divider.html',
+ link: function postLink(scope, element, attrs) {
+ scope.onoff = 'divider-open';
+ scope.toggle = function(){
+ if(scope.onoff === 'divider-open'){
+ scope.onoff = 'divider-closed';
+ $rootScope.$broadcast('dividerClosed');
+ }else{
+ scope.onoff = 'divider-open';
+ $rootScope.$broadcast('dividerOpen');
+ }
+ };
+ }
+ };
+ }]);
diff --git a/src/main/webapp/scripts/directives/linkInfoDetails.js b/src/main/webapp/scripts/directives/linkInfoDetails.js
new file mode 100644
index 000000000..30e0cda46
--- /dev/null
+++ b/src/main/webapp/scripts/directives/linkInfoDetails.js
@@ -0,0 +1,169 @@
+'use strict';
+
+pinpointApp.constant('linkInfoDetailsConfig', {
+ linkStatisticsUrl: '/linkStatistics.pinpoint',
+ myColors : ["#008000", "#4B72E3", "#A74EA7", "#BB5004", "#FF0000"]
+});
+
+pinpointApp
+ .directive('linkInfoDetails', [ 'linkInfoDetailsConfig', function (config) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ templateUrl: 'views/linkInfoDetails.html',
+ link: function postLink(scope, element, attrs) {
+
+ var reset = function () {
+ scope.linkCategory = null;
+ scope.rawdata = null;
+ scope.query = null;
+ scope.targetinfo = null;
+ scope.sourceinfo = null;
+ scope.showLinkInfoChart = false;
+ scope.showLinkInfoBarChart = false;
+ scope.$digest();
+ };
+
+ var showDetailInformation = function (query, data) {
+ if (data.rawdata) {
+ scope.linkCategory = 'UnknownLinkInfoBox';
+ } else {
+ scope.linkCategory = 'LinkInfoBox';
+ showApplicationStatistics(query.from,
+ query.to,
+ data.sourceinfo.serviceTypeCode,
+ data.sourceinfo.applicationName,
+ data.targetinfo.serviceTypeCode,
+ data.targetinfo.applicationName);
+ }
+
+ scope.rawdata = data.rawdata;
+ scope.query = data.query;
+ scope.targetinfo = data.targetinfo;
+ scope.sourceinfo = data.sourceinfo;
+ scope.$digest();
+ };
+
+ var getLinkStatisticsData = function (query, callback) {
+ jQuery.ajax({
+ type : 'GET',
+ url : config.linkStatisticsUrl,
+ cache : false,
+ dataType: 'json',
+ data : {
+ from : query.from,
+ to : query.to,
+ srcServiceType : query.srcServiceType,
+ srcApplicationName : query.srcApplicationName,
+ destServiceType : query.destServiceType,
+ destApplicationName : query.destApplicationName
+ },
+ success : function (result) {
+ callback(query, result);
+ },
+ error : function (xhr, status, error) {
+ console.log("ERROR", status, error);
+ }
+ });
+ };
+
+ var renderStatisticsTimeseriesHistogram = function (data) {
+ nv.addGraph(function() {
+ var chart = nv.models.multiBarChart().x(function (d) {
+ return d[0];
+ }).y(function(d) {
+ return d[1];
+ }).clipEdge(true).showControls(false);
+
+ chart.stacked(true);
+
+ chart.xAxis.tickFormat(function (d) {
+ return d3.time.format('%H:%M')(new Date(d));
+ });
+
+ chart.yAxis.tickFormat(function (d) {
+ return d;
+ });
+
+ chart.color(config.myColors);
+
+ d3.select('.linkInfoDetails .infoChart svg')
+ .datum(data)
+ .transition()
+ .duration(0)
+ .call(chart);
+
+ nv.utils.windowResize(chart.update);
+
+ return chart;
+ });
+ };
+
+ var renderStatisticsSummary = function (data) {
+ nv.addGraph(function() {
+ var chart = nv.models.discreteBarChart().x(function (d) {
+ return d.label;
+ }).y(function (d) {
+ return d.value;
+ }).staggerLabels(false).tooltips(false).showValues(true);
+
+ chart.xAxis.tickFormat(function (d) {
+ if(angular.isNumber(d)) {
+ return (d >= 1000) ? d / 1000 + "s" : d + "ms";
+ }
+ return d;
+ });
+
+ chart.yAxis.tickFormat(function (d) {
+ return d;
+ });
+
+ chart.valueFormat(function (d) {
+ return d;
+ });
+
+ chart.color(config.myColors);
+
+ d3.select('.linkInfoDetails .infoBarChart svg')
+ .datum(data)
+ .transition()
+ .duration(0)
+ .call(chart);
+
+ nv.utils.windowResize(chart.update);
+
+ return chart;
+ });
+ }
+
+ var showApplicationStatistics = function (begin, end, srcServiceType, srcApplicationName, destServiceType, destApplicationName) {
+ var params = {
+ "from" : begin,
+ "to" : end,
+ "srcServiceType" : srcServiceType,
+ "srcApplicationName" : srcApplicationName,
+ "destServiceType" : destServiceType,
+ "destApplicationName" : destApplicationName
+ };
+
+ getLinkStatisticsData(params, function(query, result) {
+ scope.showLinkInfoChart = true;
+ scope.showLinkInfoBarChart = true;
+ scope.$digest();
+ renderStatisticsTimeseriesHistogram(result.timeseriesHistogram);
+ renderStatisticsSummary(result.histogramSummary);
+ });
+ }
+
+ scope.$on('servermap.nodeClicked', function (event, e, query, data, containerId) {
+ reset();
+ });
+ scope.$on('servermap.linkClicked', function (event, e, query, data, containerId) {
+ reset();
+ showDetailInformation(query, data);
+ });
+
+
+ }
+ };
+ } ]);
diff --git a/src/main/webapp/scripts/directives/navbar.js b/src/main/webapp/scripts/directives/navbar.js
new file mode 100644
index 000000000..15bd55c7b
--- /dev/null
+++ b/src/main/webapp/scripts/directives/navbar.js
@@ -0,0 +1,232 @@
+'use strict';
+
+pinpointApp.constant('navbarConfig', {
+ applicationUrl: '/applications.pinpoint'
+});
+
+pinpointApp.directive('navbar', [ 'navbarConfig', '$rootScope', '$http',
+ '$document', '$timeout', '$location', '$routeParams',
+ function (navbarConfig, $rootScope, $http, $document, $timeout, $location, $routeParams) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ templateUrl: 'views/navbar.html',
+ link: function (scope, element, attrs) {
+
+ /**
+ * initialize
+ */
+ scope.applications = [
+ {
+ text: 'Loading...',
+ value: ''
+ }
+ ];
+
+ scope.application = '';
+ scope.disableApplication = true;
+ scope.period = '';
+ var applicationElement = element.find('.application');
+ applicationElement.width(200);
+ $http.defaults.useXDomain = true;
+
+ $timeout(function () {
+ if ($routeParams.period) {
+ scope.period = $routeParams.period;
+ }
+ });
+
+ /**
+ * date time picker
+ */
+ var elDatetimepicker = element.find('#datetimepicker');
+ elDatetimepicker.datetimepicker({
+ pick12HourFormat: false,
+ pickSeconds: false
+ });
+ $timeout(function () {
+ setDateTime($routeParams.queryEndTime);
+ });
+
+ /**
+ * set DateTime
+ */
+ var setDateTime = function (time) {
+ var elDatetimepicker = element.find('#datetimepicker');
+ var picker = elDatetimepicker.data('datetimepicker');
+ var date = new Date();
+ if (time) {
+ date.setTime(date);
+ }
+ picker.setDate(date);
+ };
+
+ /**
+ * now
+ */
+ scope.now = function() {
+ setDateTime();
+ broadcast();
+ };
+
+ /**
+ * _boardcast as applicationChanged with args
+ */
+ var broadcast = scope.broadcast = function () {
+ if (!scope.application || !scope.period || !getQueryEndTime()) {
+ $location.path('/main');
+ return;
+ }
+ var splitedApp = scope.application.split('@'),
+ data = {
+ application: scope.application,
+ applicationName: splitedApp[0],
+ serviceType: splitedApp[1],
+ period: scope.period,
+ queryPeriod: getQueryPeriod(),
+ queryEndTime: getQueryEndTime()
+ },
+ url = '/main/' + scope.application + '/' + scope.period + '/' + getQueryEndTime();
+
+ $location.path(url);
+
+ $timeout(function () {
+ $rootScope.$broadcast('navbar.applicationChanged', data);
+ }, 100);
+
+ };
+
+ /**
+ * get query period
+ */
+ var getQueryPeriod = function () {
+ return scope.period * 1000 * 60;
+ };
+
+ var getQueryEndTime = function () {
+ var date = elDatetimepicker.data('datetimepicker');
+ var time = new Date(date.getDate());
+ return time.getTime();
+ };
+
+ /**
+ * get Application List
+ */
+ var getApplicationList = function () {
+ $http.get(navbarConfig.applicationUrl).success(function (data, status) {
+
+ if (angular.isArray(data) === false || data.length === 0) {
+ scope.applications[0].text = 'Application not found.';
+ } else {
+ parseApplicationList(data, function () {
+ scope.disableApplication = false;
+ $timeout(function () { // it should be apply after pushing data, so
+ // it should work like nextTick
+ applySelect2Plugnin();
+ });
+ });
+ }
+
+ }).error(function (data, status) {
+ scope.applications[0].text = 'Application error.';
+ });
+ };
+
+ /**
+ * parse Application List
+ */
+ var parseApplicationList = function (data, cb) {
+ scope.applications = [
+ {
+ text: '',
+ value: ''
+ }
+ ];
+ angular.forEach(data, function (value, key) {
+ scope.applications.push({
+ text: value.applicationName + "@" + value.serviceType,
+ value: value.applicationName + "@" + value.code
+ });
+ });
+ if (angular.isFunction(cb))
+ cb.apply(scope);
+ };
+
+
+ /**
+ * apply Select2 Plugin
+ */
+ var applySelect2Plugnin = function () {
+ /**
+ * formatOptionText
+ */
+ var formatOptionText = function (state) {
+ if (!state.id)
+ return state.text;
+ var chunk = state.text.split("@");
+ if (chunk.length > 1) {
+ var img = $document.get(0).createElement("img");
+ img.src = "/images/icons/" + chunk[1] + ".png";
+ return img.outerHTML + chunk[0];
+ } else {
+ return state.text;
+ }
+ };
+
+ applicationElement.select2({
+ placeholder: "Select an application.",
+ allowClear: false,
+ formatResult: formatOptionText,
+ formatSelection: formatOptionText,
+ escapeMarkup: function (m) {
+ return m;
+ }
+ });
+ if ($routeParams.application) {
+ applicationElement.select2('val', $routeParams.application);
+ scope.application = $routeParams.application;
+ broadcast();
+ }
+ applicationElement.on("change", function (e) {
+ scope.application = e.val;
+ broadcast();
+ // 참고1 : http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
+ // 참고2 : http://jsfiddle.net/CDvGy/2/
+ scope.$digest();
+ });
+ };
+
+ /**
+ * period click
+ * @param val
+ */
+ scope.periodClick = function (val) {
+ if (scope.period !== val) {
+ scope.period = val;
+ broadcast();
+ }
+ };
+
+ /**
+ * watches
+ */
+ scope.$watch('useAnchorDate', function (newVal, oldVal) {
+ scope.classAnchorDate = newVal ? 'input-append' : '';
+// if(newVal === false && oldVal === true){
+// var now = new Date();
+ // 초단위 무시.
+// now.setSeconds(0);
+ // 5분 단위로 조회
+// now.setMinutes(Math.floor(now.getMinutes() / 5 + 0.9) * 5);
+// elDatetimepicker.datetimepicker('setLocalDate', now);
+// broadcast();
+// }
+ });
+
+ /**
+ * executes
+ */
+ getApplicationList();
+ }
+ };
+ } ]);
diff --git a/src/main/webapp/scripts/directives/nodeInfoDetails.js b/src/main/webapp/scripts/directives/nodeInfoDetails.js
new file mode 100644
index 000000000..e2a799aff
--- /dev/null
+++ b/src/main/webapp/scripts/directives/nodeInfoDetails.js
@@ -0,0 +1,124 @@
+'use strict';
+
+pinpointApp.constant('nodeInfoDetailsConfig', {
+ applicationStatisticsUrl: '/applicationStatistics.pinpoint',
+ myColors : ["#008000", "#4B72E3", "#A74EA7", "#BB5004", "#FF0000"]
+});
+
+pinpointApp
+ .directive('nodeInfoDetails', [ 'nodeInfoDetailsConfig', '$rootScope', '$templateCache', function (config, $rootScope, $templateCache) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ templateUrl: 'views/nodeInfoDetails.html',
+ link: function postLink(scope, element, attrs) {
+
+ var reset = function () {
+ scope.nodeCategory = null;
+ scope.unknownGroup = null;
+ scope.hosts = null;
+ scope.showHosts = false;
+ scope.agents = null;
+ scope.showAgents = false;
+ scope.showNodeInfoBarChart = false;
+ scope.$digest();
+ };
+
+ var showDetailInformation = function (query, data) {
+ scope.nodeCategory = data.category;
+ scope.unknownGroup = data.textArr;
+ scope.hosts = data.hosts;
+ scope.showHosts = (scope.hosts.length > 0) ? true : false;
+ scope.agents = data.agents;
+ scope.showAgents = (scope.agents.length > 0) ? true : false;
+ scope.$digest();
+ };
+
+ var getApplicationStatisticsData = function (query, callback) {
+ jQuery.ajax({
+ type : 'GET',
+ url : config.applicationStatisticsUrl,
+ cache : false,
+ dataType: 'json',
+ data : {
+ from : query.from,
+ to : query.to,
+ applicationName : query.applicationName,
+ serviceType : query.serviceType
+ },
+ success : function (result) {
+ callback(query, result);
+ },
+ error : function (xhr, status, error) {
+ console.log("ERROR", status, error);
+ }
+ });
+ };
+
+ var renderApplicationStatistics = function (data) {
+ scope.showInfoBarChart = true;
+ nv.addGraph(function () {
+ var chart = nv.models.discreteBarChart().x(function (d) {
+ return d.label;
+ }).y(function(d) {
+ return d.value;
+ }).staggerLabels(false).tooltips(false).showValues(true);
+
+ chart.xAxis.tickFormat(function (d) {
+ if(angular.isNumber(d)) {
+ return (d >= 1000) ? d / 1000 + "s" : d + "ms";
+ }
+ return d;
+ });
+
+ chart.yAxis.tickFormat(function (d) {
+ return d;
+ });
+
+ chart.valueFormat(function (d) {
+ return d;
+ });
+
+ chart.color(config.myColors);
+
+ d3.select('.nodeInfoDetails .infoBarChart svg')
+ .datum(data)
+ .transition()
+ .duration(0)
+ .call(chart);
+
+ nv.utils.windowResize(chart.update);
+
+ return chart;
+ });
+ };
+
+ var showApplicationStatisticsSummary = function (begin, end, applicationName, serviceType) {
+ var params = {
+ "from" : begin,
+ "to" : end,
+ "applicationName" : applicationName,
+ "serviceType" : serviceType
+ };
+ getApplicationStatisticsData(params, function (query, result) {
+ scope.showNodeInfoBarChart = true;
+ scope.$digest();
+ renderApplicationStatistics(result.histogramSummary);
+ });
+ };
+
+ scope.$on('servermap.nodeClicked', function (event, e, query, data, containerId) {
+ reset();
+ showDetailInformation(query, data);
+ if (!data.rawdata && data.category !== "USER" && data.category !== "UNKNOWN_GROUP") {
+ showApplicationStatisticsSummary(query.from, query.to, data.text, data.serviceTypeCode);
+ }
+ });
+ scope.$on('servermap.linkClicked', function (event, e, query, data, containerId) {
+ reset();
+ });
+
+
+ }
+ };
+ }]);
diff --git a/src/main/webapp/scripts/directives/scatter.js b/src/main/webapp/scripts/directives/scatter.js
new file mode 100644
index 000000000..7c2b5b677
--- /dev/null
+++ b/src/main/webapp/scripts/directives/scatter.js
@@ -0,0 +1,235 @@
+'use strict';
+
+pinpointApp.constant('scatterConfig', {
+ get: {
+ scatterData: '/getScatterData.pinpoint',
+ lastScatterData: '/getLastScatterData.pinpoint'
+ }
+});
+
+pinpointApp.directive('scatter',
+ [ 'scatterConfig', '$rootScope', '$timeout', function (scatterConfig, $rootScope, $timeout) {
+ return {
+ template: '
',
+ restrict: 'EA',
+ replace: true,
+ link: function (scope, element, attrs) {
+
+ var oScatterChart = null;
+ var selectdTracesBox = {};
+
+ scope.$on('navbar.applicationChanged', function (event, data) {
+ console.log('got navbar.applicationChanged from scatter : ', data);
+
+ scope.data = data;
+
+ makeScatter(data.applicationName, data.queryEndTime - data.queryPeriod, data.queryEndTime, 'scatterchart', data.queryPeriod);
+ });
+
+ var showScatter = function (applicationName, from, to, period, filter, w, h) {
+ if (oScatterChart) {
+ console.log('clear');
+// oScatterChart.clear();
+ }
+
+ selectdTracesBox = {};
+// var fullscreenButton = $("#scatterChartContainer I.icon-fullscreen");
+// fullscreenButton.data("applicationName", applicationName);
+// fullscreenButton.data("from", from);
+// fullscreenButton.data("to", to);
+// fullscreenButton.data("period", period);
+// fullscreenButton.data("usePeriod", usePeriod);
+// fullscreenButton.data("filter", filter);
+
+// var downloadButton = $("#scatterChartContainer A");
+
+// var imageFileName = applicationName +
+// "_" +
+// new Date(from).toString("yyyyMMdd_HHmm") +
+// "~" +
+// new Date(to).toString("yyyyMMdd_HHmm") +
+// "_response_scatter.png";
+//
+// downloadButton.attr("download", imageFileName);
+// downloadButton.unbind("click");
+// downloadButton.bind("click", function() {
+// var sImageUrl = oScatterChart.getChartAsPNG();
+// $(this).attr('href', sImageUrl);
+// });
+
+// $("#scatterChartContainer SPAN").unbind("click");
+// $("#scatterChartContainer SPAN").bind("click", function() {
+// showRequests(applicationName, from, to, period, usePeriod, filter);
+// });
+
+// $("#scatterChartContainer").show();
+
+ var bDrawOnceAll = false,
+ nInterval = 2000,
+ fetchLimit = 2000;
+
+ var htDataSource = {
+ sUrl: function (nFetchIndex) {
+// if (!usePeriod) {
+// return scatterConfig.get.scatterData;
+// }
+
+// if (nFetchIndex === 0) {
+// return scatterConfig.get.lastScatterData;
+// } else {
+ return scatterConfig.get.scatterData;
+// }
+ },
+ htParam: function (nFetchIndex, htLastFetchParam, htLastFetchedData) {
+ // calculate parameter
+ var htData;
+// console.log("htParam", nFetchIndex, htLastFetchParam, htLastFetchedData);
+
+// if (nFetchIndex === 0 && !usePeriod) {
+ if (nFetchIndex === 0) {
+ return {
+ 'application': applicationName,
+ 'from': from,
+ 'to': to,
+ 'limit': fetchLimit,
+ 'filter': filter
+ };
+ }
+
+ // period만큼 먼저 조회해본다.
+ if (nFetchIndex === 0 /*|| typeof(htLastFetchParam) === 'undefined' || typeof(htLastFetchedData) === 'undefined'*/) {
+ htData = {
+ 'application': applicationName,
+ 'period': period,
+ 'limit': fetchLimit,
+ 'filter': filter
+ };
+ } else {
+ if (bDrawOnceAll || htLastFetchedData.scatter.length == 0) {
+ htData = {
+ 'application': applicationName,
+ 'from': htLastFetchParam.to + 1,
+ 'to': htLastFetchParam.to + 2000,
+ 'limit': fetchLimit,
+ 'filter': filter
+ };
+ } else {
+ htData = {
+ 'application': applicationName,
+ // array[0] 이 최근 값, array[len]이 오래된 이다.
+ 'from': from,
+ 'to': htLastFetchedData.scatter[htLastFetchedData.scatter.length - 1].x - 1,
+ 'limit': fetchLimit,
+ 'filter': filter
+ };
+ }
+ }
+
+ return htData;
+ },
+ nFetch: function (htLastFetchParam, htLastFetchedData) {
+ // -1 : stop, n = 0 : immediately, n > 0 : interval
+ var useInterval = false;
+
+// console.log("nFetch", htLastFetchedData);
+
+ if (useInterval && htLastFetchedData.scatter.length === 0) {
+// console.log("2A");
+ bDrawOnceAll = true;
+ return nInterval;
+ }
+
+ if (htLastFetchedData.scatter.length !== 0) {
+ // array[0] 이 최근 값, array[len]이 오래된 이다.
+ if (htLastFetchedData.scatter[0].x > from) {
+ // TO THE NEXT
+ return 0;
+ } else {
+ // STOP
+ return -1;
+ }
+ }
+
+ if (htLastFetchedData.scatter[htLastFetchedData.scatter.length - 1] &&
+ htLastFetchedData.scatter[htLastFetchedData.scatter.length - 1].x < date.getTime()) {
+ if (useInterval) {
+ bDrawOnceAll = true;
+ return nInterval;
+ }
+ // TO THE NEXT
+ return 0;
+ }
+
+ // STOP
+ return -1;
+ },
+ htOption: {
+ dataType: 'jsonp',
+ jsonp: '_callback'
+ }
+ };
+ oScatterChart.drawWithDataSource(htDataSource);
+ };
+ var makeScatter = function (title, start, end, targetId, period, w, h) {
+ if(!Modernizr.canvas) {
+ alert("Can't draw scatter. Not supported browser.");
+ }
+
+ var yAxisMAX = 10000;
+ var date = new Date();
+
+ if (oScatterChart !== null) {
+ console.log('destory');
+// oScatterChart.destroy();
+ // oScatterChart.updateXYAxis(start, end, 0, yAxisMAX);
+ // oScatterChart.clear();
+ // return;
+ }
+
+ var options = {
+ sContainerId : targetId,
+ nWidth : w ? w : 400,
+ nHeight : h ? h : 250,
+ // nXMin: date.getTime() - 86400000, nXMax: date.getTime(),
+ nXMin: start, nXMax: end,
+ nYMin: 0, nYMax: yAxisMAX,
+ nZMin: 0, nZMax: 5,
+ nBubbleSize: 3,
+ sXLabel : '',
+ sYLabel : '(ms)',
+ sTitle : title,
+ htTypeAndColor : {
+ // type name : color
+ 'Success' : '#2ca02c',
+ // 'Warning' : '#f5d025',
+ 'Failed' : '#d62728'
+ },
+ fOnSelect : function(htPosition, htXY){
+ var traces = this.getDataByXY(htXY.nXFrom, htXY.nXTo, htXY.nYFrom, htXY.nYTo);
+
+ if (traces.length === 0) {
+ return;
+ }
+
+ if (traces.length === 1) {
+// openTrace(traces[0].traceId, traces[0].x);
+ return;
+ }
+
+ var token = Math.random() * 10000 + 1;
+ selectdTracesBox[token] = traces;
+
+ var popupwindow = window.open("/selectedScatter.pinpoint", token);
+ }
+ };
+
+ $timeout(function() {
+ oScatterChart = new BigScatterChart(options);
+ showScatter(scope.data.applicationName, start,end, period);
+ }, 100);
+
+ };
+
+ }
+ };
+ } ]);
diff --git a/src/main/webapp/scripts/directives/servermap.js b/src/main/webapp/scripts/directives/servermap.js
new file mode 100644
index 000000000..121b83b29
--- /dev/null
+++ b/src/main/webapp/scripts/directives/servermap.js
@@ -0,0 +1,459 @@
+'use strict';
+
+pinpointApp.constant('servermapConfig', {
+ applicationUrl: '/applications.pinpoint',
+ options: {
+ "sContainerId": 'servermap',
+ "sImageDir": '/images/icons/',
+ "sBigFont": "12pt calibri, Helvetica, Arial, sans-serif",
+ "sSmallFont": "11pt calibri, Helvetica, Arial, sans-serif",
+ "htIcons": {
+ 'APACHE': 'APACHE.png',
+ 'ARCUS': 'ARCUS.png',
+ 'CUBRID': 'CUBRID.png',
+ 'ETC': 'ETC.png',
+ 'MEMCACHED': 'MEMCACHED.png',
+ 'MYSQL': 'MYSQL.png',
+ 'QUEUE': 'QUEUE.png',
+ 'TOMCAT': 'TOMCAT.png',
+ 'UNKNOWN_CLOUD': 'UNKNOWN_CLOUD.png',
+ 'UNKNOWN_GROUP': 'UNKNOWN_CLOUD.png',
+ 'USER': 'USER.png',
+ 'ORACLE': 'ORACLE.png'
+ },
+ "htLinkTheme": {
+ "default": {
+ "background": { 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 1)"},
+ "border": "gray",
+ "font": "10pt calibri, helvetica, arial, sans-serif",
+ "color": "#000000", // "#919191",
+ "align": "center",
+ "margin": 1
+ },
+ "good": {
+ // "background" : { 0: "rgb(240, 1, 240)", 0.3: "rgb(240, 1,
+ // 240)", 1: "rgba(240, 1, 240, 1)"},
+ "background": { 0: "#2CA02C"},
+ "border": "green",
+ "font": "10pt calibri, helvetica, arial, sans-serif",
+ "color": "#919191",
+ "align": "center",
+ "margin": 1
+ },
+ "bad": {
+ // "background" : { 0: "rgb(214, 27, 28)", 0.3: "rgb(214, 27,
+ // 28)", 1: "rgba(214, 27, 28, 1)"},
+ // "background" : { 0: "#D62728" },
+ "background": { 0: "rgba(200, 27, 28, 1)" },
+ "border": "#FFFFFF",
+ "font": "10pt calibri, helvetica, arial, sans-serif",
+ "color": "#FFFFFF",
+ "align": "center",
+ "margin": 1
+ }
+ }
+ }
+});
+
+pinpointApp.directive('servermap', [ 'servermapConfig', '$rootScope', '$templateCache', '$compile', '$timeout', function (servermapConfig, $rootScope, $templateCache, $compile, $timeout) {
+ return {
+ restrict: 'EA',
+ replace: true,
+ templateUrl: 'views/servermap.html',
+ link: function postLink(scope, element, attrs) {
+ var serverMapCachedQuery, serverMapCachedData;
+ var oServerMap = null;
+ var FILTER_DELIMETER = "^";
+ var FILTER_ENTRY_DELIMETER = "|";
+ var SERVERMAP_METHOD_CACHE = {};
+ var myColors = ["#008000", "#4B72E3", "#A74EA7", "#BB5004", "#FF0000"];
+
+ /**
+ * loading
+ */
+ var startLoading = function () {
+ setLoading(0);
+ $timeout(function () {
+ $('.servermap .progress').show();
+ });
+ };
+ var stopLoading = function () {
+ $timeout(function () {
+ $('.servermap .progress').hide();
+ }, 300);
+ };
+ var setLoading = function (p) {
+ $('.servermap .progress .bar').width(p + '%');
+ };
+
+ var showWarning = function (msg) {
+ $timeout(function () {
+ $('.servermap .warning').show();
+ $('.servermap .warning .msg').text(msg);
+ }, 300);
+ };
+
+ var showServerMap = function (applicationName, serviceType, to, period, filterText, hideIndirectAccess) {
+ startLoading();
+ if (oServerMap) {
+ oServerMap.clear();
+ }
+ setLoading(10);
+
+ var query = {
+ applicationName: applicationName,
+ serviceType: serviceType,
+ from: to - period,
+ to: to,
+ period: period,
+ filter: filterText,
+ hideIndirectAccess: hideIndirectAccess
+ };
+
+ if (filterText) {
+// getFilteredServerMapData(query, function(query, result) {
+// if (cb) {
+// cb(query, result);
+// }
+// serverMapCallback(query, result);
+// });
+ } else {
+ getServerMapData2(query, function (query, result) {
+ serverMapCallback(query, result);
+ });
+ }
+ };
+
+ var getServerMapData2 = function (query, callback) {
+ setLoading(50);
+ jQuery.ajax({
+ type: 'GET',
+ url: '/getServerMapData2.pinpoint',
+ cache: false,
+ dataType: 'json',
+ data: {
+ application: query.applicationName,
+ serviceType: query.serviceType,
+ from: query.from,
+ to: query.to,
+ hideIndirectAccess: query.hideIndirectAccess
+ },
+ success: function (result) {
+ setLoading(30);
+ callback(query, result);
+ },
+ error: function (xhr, status, error) {
+ console.log("ERROR", status, error);
+ stopLoading();
+ showWarning('There is some error.');
+ }
+ });
+ };
+
+// var getFilteredServerMapData = function (query, callback) {
+// jQuery.ajax({
+// type: 'GET',
+// url: '/getFilteredServerMapData2.pinpoint',
+// cache: false,
+// dataType: 'json',
+// data: {
+// application: query.applicationName,
+// serviceType: query.serviceType,
+// from: query.from,
+// to: query.to,
+// filter: query.filter
+// },
+// success: function (result) {
+// callback(query, result);
+// },
+// error: function (xhr, status, error) {
+// console.log("ERROR", status, error);
+// }
+// });
+// };
+
+ var serverMapCallback = function (query, data, ignoreCache) {
+ var containerId = servermapConfig.options.sContainerId;
+
+ serverMapCachedQuery = angular.copy(query);
+ serverMapCachedData = angular.copy(data);
+ setLoading(80);
+ if (data.applicationMapData.nodeDataArray.length === 0) {
+ stopLoading();
+ showWarning('There is no data.');
+// warning("NO DATA", "");
+ return;
+ } else {
+// clearAllWarnings();
+// $("#" + containerId).show();
+ }
+
+// if ($('#mergeUnknown').data('selected')) {
+// // if ($('#mergeUnknown').is(':checked')) {
+// mergeUnknown(query, data);
+// }
+ mergeUnknown(query, data);
+ replaceClientToUser(data);
+ setLoading(90);
+
+ var options = servermapConfig.options;
+ options.fOnNodeContextClick = function (e, d) {
+ // nodeContextClickHandler(e, query, d, "#" + containerId);
+ console.log("servermap.nodeContextClicked", e, query, d, "#" + containerId);
+ $rootScope.$broadcast("servermap.nodeContextClicked", e, query, d, "#" + containerId);
+ };
+ options.fOnLinkContextClick = function (e, d) {
+ // linkContextClickHandler(e, query, d, "#" + containerId);
+ console.log("servermap.linkContextClicked", e, query, d, "#" + containerId);
+ $rootScope.$broadcast("servermap.linkContextClicked", e, query, d, "#" + containerId);
+ };
+ options.fOnLinkClick = function (e, d) {
+// linkClickHandler(e, query, d, data, "#" + containerId);
+ console.log("servermap.linkClicked", e, query, d, "#" + containerId);
+ $rootScope.$broadcast("servermap.linkClicked", e, query, d, "#" + containerId);
+ };
+ options.fOnNodeClick = function (e, d) {
+// nodeClickHandler(e, query, d, data, "#" + containerId);
+ console.log("servermap.nodeClicked", e, query, d, "#" + containerId);
+ $rootScope.$broadcast("servermap.nodeClicked", e, query, d, "#" + containerId);
+ };
+
+ setLoading(100);
+ oServerMap = new ServerMap(options);
+ oServerMap.load(data.applicationMapData);
+ stopLoading();
+
+ try {
+ var selectedNode = (function () {
+ for (var i = 0; i < data.applicationMapData.nodeDataArray.length; i++) {
+ var e = data.applicationMapData.nodeDataArray[i];
+ if (e.text === query.applicationName && e.serviceTypeCode === query.serviceType) {
+ return e;
+ }
+ }
+ })();
+ oServerMap.highlightNodeByKey(selectedNode.key);
+// nodeClickHandler(null, query, selectedNode);
+ $rootScope.$broadcast("servermap.nodeClicked", null, query, selectedNode);
+ } catch (e) {
+ console.log(e);
+ }
+ ;
+ };
+
+ // TODO 임시코드로 나중에 USER와 backend를 구분할 예정.
+ var replaceClientToUser = function (data) {
+ var nodes = data.applicationMapData.nodeDataArray;
+ nodes.forEach(function (node) {
+ if (node.category == "CLIENT") {
+ node.category = "USER";
+ node.text = "USER";
+ }
+ });
+ };
+
+ var mergeUnknown = function (query, data) {
+ SERVERMAP_METHOD_CACHE = {};
+ var nodes = data.applicationMapData.nodeDataArray;
+ var links = data.applicationMapData.linkDataArray;
+
+ var inboundCountMap = {};
+ nodes.forEach(function (node) {
+ if (!inboundCountMap[node.key]) {
+ inboundCountMap[node.key] = {
+ "sourceCount": 0,
+ "totalCallCount": 0
+ };
+ }
+
+ links.forEach(function (link) {
+ if (link.to == node.key) {
+ inboundCountMap[node.key].sourceCount++;
+ inboundCountMap[node.key].totalCallCount += link.text;
+ }
+ });
+ });
+
+ var newNodeList = [];
+ var newLinkList = [];
+
+ var removeNodeIdSet = {};
+ var removeLinkIdSet = {};
+
+ nodes.forEach(function (node, nodeIndex) {
+ if (node.category == "UNKNOWN_CLOUD") {
+ 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_CLOUD" &&
+ 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_CLOUD") {
+ return;
+ }
+ if (inboundCountMap[link.to] && inboundCountMap[link.to].sourceCount > 1) {
+ return;
+ }
+
+ // branch out from current node.
+ if (link.from == node.key) {
+ if (!newNode) {
+ newNode = {
+ "id": newNodeKey,
+ "key": newNodeKey,
+ "textArr": [],
+ "text": "",
+ "hosts": [],
+ "category": "UNKNOWN_GROUP",
+ "terminal": "true",
+ "agents": [],
+ "fig": "FramedRectangle"
+ }
+ }
+ if (!newLink) {
+ newLink = {
+ "id": node.key + "-" + newNodeKey,
+ "from": node.key,
+ "to": newNodeKey,
+ "sourceinfo": [],
+ "targetinfo": [],
+ "text": 0,
+ "error": 0,
+ "slow": 0,
+ "rawdata": [],
+ "histogram": {}
+ };
+ }
+
+ // fill the new node/link informations.
+ newNode.textArr.push({ 'count': link.text, 'applicationName': link.targetinfo.applicationName});
+
+ newLink.text += link.text;
+ newLink.error += link.error;
+ newLink.slow += link.slow;
+ newLink.sourceinfo.push(link.sourceinfo);
+ newLink.targetinfo.push(link.targetinfo);
+
+ var newRawData = {
+ "id": link.id,
+ "from": link.from,
+ "to": link.to,
+ "sourceinfo": link.sourceinfo,
+ "targetinfo": link.targetinfo,
+ "text": 0,
+ "count": link.text,
+ "error": link.error,
+ "slow": link.slow,
+ "histogram": link.histogram
+ };
+ newLink.rawdata[link.targetinfo.applicationName] = newRawData;
+
+ /*
+ * group된 노드에서 개별 노드의 정보를 조회할 때 사용됨.
+ * onclick="SERVERMAP_METHOD_CACHE['{{=
+ * value.applicationName}}']();" 으로 호출함.
+ */
+ SERVERMAP_METHOD_CACHE[link.targetinfo.applicationName] = function () {
+ linkClickHandler(null, query, newRawData);
+ }
+
+ $.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.id] = null;
+ }
+ });
+
+ if (newNode) {
+ newNode.textArr.sort(function (e1, e2) {
+ return e2.count - e1.count;
+ });
+
+ var nodeCount = newNode.textArr.length - 1;
+ $.each(newNode.textArr, function (i, e) {
+ newNode.text += e.applicationName + " (" + e.count + ")" + (i < nodeCount ? "\n" : "");
+ });
+
+// console.log("newNode", newNode);
+ newNodeList.push(newNode);
+ }
+
+ if (newLink) {
+ if ((newLink.error / newLink.text * 100) > 10) {
+ newLink.category = "bad";
+ } else {
+ newLink.category = "default";
+ }
+
+ // targetinfo 에러를 우선으로, 요청수 내림차순 정렬.
+ newLink.targetinfo.sort(function (e1, e2) {
+ var err1 = newLink.rawdata[e1.applicationName].error;
+ var err2 = newLink.rawdata[e2.applicationName].error;
+
+ if (err1 + err2 > 0) {
+ return err2 - err1;
+ } else {
+ return newLink.rawdata[e2.applicationName].count - newLink.rawdata[e1.applicationName].count;
+ }
+ });
+
+// console.log("newLink", newLink);
+ newLinkList.push(newLink);
+ }
+ });
+
+ newNodeList.forEach(function (newNode) {
+ data.applicationMapData.nodeDataArray.push(newNode);
+ });
+
+ newLinkList.forEach(function (newLink) {
+ data.applicationMapData.linkDataArray.push(newLink);
+ });
+
+ $.each(removeNodeIdSet, function (key, val) {
+ nodes.forEach(function (node, i) {
+ if (node.id == key) {
+ nodes.splice(i, 1);
+ }
+ });
+ });
+
+ $.each(removeLinkIdSet, function (key, val) {
+ links.forEach(function (link, i) {
+ if (link.id === key) {
+ links.splice(i, 1);
+ }
+ });
+ });
+ };
+
+ scope.$on('navbar.applicationChanged', function (event, data) {
+ console.log('got navbar.applicationChanged from servermap : ', data);
+ showServerMap(data.applicationName, data.serviceType, data.queryEndTime, data.queryPeriod, '', false);
+ });
+
+
+ }
+ }
+}]);
diff --git a/src/main/webapp/styles/main.css b/src/main/webapp/styles/main.css
new file mode 100644
index 000000000..880e765ce
--- /dev/null
+++ b/src/main/webapp/styles/main.css
@@ -0,0 +1,136 @@
+@font-face{font-family:ng;src:url(../fonts/nanumgothic.eot);src:local('※'),url(../fonts/nanumgothic.woff) format('woff')}
+@font-face{font-family:nm;src:url(../fonts/nanummyeongjo.eot);src:local('※'),url(../fonts/nanummyeongjo.woff) format('woff')}
+@font-face{font-family:tjp;src:url(../fonts/trajanpro.eot);src:local('※'),url(../fonts/trajanpro.woff) format('woff')}
+@font-face{font-family:avn55;src:url(../fonts/avenir55.eot);src:local('※'),url(../fonts/avenir55.woff) format('woff')}
+@font-face{font-family:avn85;src:url(../fonts/avenir85.eot);src:local('※'),url(../fonts/avenir85.woff) format('woff')}
+
+html, body {
+ height: 100%;
+}
+body,input,textarea,select,button,table{font-family:'나눔고딕',NanumGothic,ng,'돋움',dotum,AppleGothic,avn55,sans-serif}
+canvas {outline:none}
+
+#wrapper {
+ padding: 40px 0 0 0;
+ position:absolute;
+ top:0;
+ bottom:0;
+ left:0;
+ right:0;
+}
+
+.navbar {
+ position: absolute;
+ margin: -40px 0 0 0;
+ height: 40px;
+ background-color: #252a3a;
+ width: 100%;
+ border-radius: 0;
+}
+.navbar .brand {
+ margin-left: 20px;
+}
+.navbar .nav>li {
+ margin-top: 5px;
+}
+.navbar .divider-vertical {
+ min-height: 1px;
+ margin: 0 9px;
+ border: 0;
+}
+.navbar-form {
+ margin-bottom: 0;
+}
+.navbar-form .datetimepicker {
+ margin: 0;
+}
+.navbar-form .datetimepicker input[type="text"]{
+ width: 120px;
+}
+.navbar-form .datetimepicker .checkbox input[type="checkbox"]{
+ float: none;
+}
+.navbar-form .datetimepicker .icon-calendar {
+ margin-top: 3px;
+}
+
+.mt8 {
+ margin-top: 8px;
+}
+
+.navbar .btn-group > .btn > input[type="radio"] {
+ display: none;
+}
+
+.main-container {
+ width: 100%;
+ height: 100%;
+ position: relative;
+}
+
+.main {
+ margin-right: 430px;
+ height: 100%;
+}
+.main.fullsize {
+ margin-right: 12px;
+}
+
+/* sidebar */
+.sidebar {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding-left: 12px;
+ width: 410px;
+ height: 100%;
+}
+.sidebar.smallsize {
+ width: 0;
+}
+
+.divider {
+ position: absolute;
+ top: 45%;
+ left: 0;
+}
+
+.onoff-ico {
+ display: inline-block;
+ width: 12px;
+ height: 54px;
+ background-repeat: no-repeat;
+ background-image: url('../images/onoff.png');
+ vertical-align: middle;
+}
+
+
+/* divider */
+.divider-open {
+ background-position: 0 0;
+}
+.onoff-ico.divider-open:hover,
+.onoff-ico.divider-open:focus {
+ background-position: 0 -56px;
+}
+
+.divider-closed {
+ background-position: -12px 0;
+}
+.onoff-ico.divider-closed:hover,
+.onoff-ico.divider-closed:focus {
+ background-position: -12px -56px;
+}
+
+/* info-details */
+.info-details {
+ border-top: 1px solid #ddd;
+ border-left: 1px solid #ddd;
+ position: absolute;
+ top: 250px;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ overflow: auto;
+ padding: 5px;
+}
\ No newline at end of file
diff --git a/src/main/webapp/views/divider.html b/src/main/webapp/views/divider.html
new file mode 100644
index 000000000..6ef426567
--- /dev/null
+++ b/src/main/webapp/views/divider.html
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/views/linkInfoDetails.html b/src/main/webapp/views/linkInfoDetails.html
new file mode 100644
index 000000000..9b663ec0d
--- /dev/null
+++ b/src/main/webapp/views/linkInfoDetails.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
{{sourceinfo[0].applicationName}}
+
+
{{target.applicationName}}
+
+
+
+
+ request count : {{rawdata[target.applicationName].count}} /
+ {{rawdata[target.applicationName].error}}
+
+
+
+
+
+
+
+
+
+
+
+ Response histogram
+
+
+
+
+ Response histogram summary
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/views/main.html b/src/main/webapp/views/main.html
new file mode 100644
index 000000000..7261f14c8
--- /dev/null
+++ b/src/main/webapp/views/main.html
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/views/navbar.html b/src/main/webapp/views/navbar.html
new file mode 100644
index 000000000..c648c8444
--- /dev/null
+++ b/src/main/webapp/views/navbar.html
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
diff --git a/src/main/webapp/views/nodeInfoDetails.html b/src/main/webapp/views/nodeInfoDetails.html
new file mode 100644
index 000000000..a4d4bcff5
--- /dev/null
+++ b/src/main/webapp/views/nodeInfoDetails.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ {{node.applicationName}}
+
+
+
+
+
+
USER
+
+
+
+
+
+
+ Server instances
+
+
+ OK
+ {{agent.agentId}}
+
+
+
+
+
+
+
+
+
+
+ Response histogram summary
+
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/views/ready.html b/src/main/webapp/views/ready.html
new file mode 100644
index 000000000..40b3828b7
--- /dev/null
+++ b/src/main/webapp/views/ready.html
@@ -0,0 +1,8 @@
+
+
+
+
+ Please select an application and period above.
+
+
+
\ No newline at end of file
diff --git a/src/main/webapp/views/servermap.html b/src/main/webapp/views/servermap.html
new file mode 100644
index 000000000..c1a029a0d
--- /dev/null
+++ b/src/main/webapp/views/servermap.html
@@ -0,0 +1,6 @@
+
\ No newline at end of file