http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js 
b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js
new file mode 100644
index 0000000..d717ad1
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js
@@ -0,0 +1,571 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       'use strict';
+
+       var featureControllers = angular.module('featureControllers');
+       var feature = featureControllers.register("metrics");
+
+       // ==============================================================
+       // =                       Initialization                       =
+       // ==============================================================
+
+       // ==============================================================
+       // =                          Function                          =
+       // ==============================================================
+       // Format dashboard unit. Will adjust format with old version and add 
miss attributes.
+       feature.service("DashboardFormatter", function() {
+               return {
+                       parse: function(unit) {
+                               unit = unit || {};
+                               unit.groups = unit.groups || [];
+
+                               $.each(unit.groups, function (i, group) {
+                                       group.charts = group.charts || [];
+                                       $.each(group.charts, function (i, 
chart) {
+                                               if (!chart.metrics && 
chart.metric) {
+                                                       chart.metrics = [{
+                                                               aggregations: 
chart.aggregations,
+                                                               dataSource: 
chart.dataSource,
+                                                               metric: 
chart.metric
+                                                       }];
+
+                                                       delete 
chart.aggregations;
+                                                       delete chart.dataSource;
+                                                       delete chart.metric;
+                                               } else if (!chart.metrics) {
+                                                       chart.metrics = [];
+                                               }
+                                       });
+                               });
+
+                               return unit;
+                       }
+               };
+       });
+
+       // ==============================================================
+       // =                         Controller                         =
+       // ==============================================================
+
+       // ========================= Dashboard ==========================
+       feature.navItem("dashboard", "Metrics", "line-chart");
+
+       feature.controller('dashboard', function(PageConfig, $scope, $http, $q, 
UI, Site, Authorization, Application, Entities, DashboardFormatter) {
+               var _siteApp = Site.currentSiteApplication();
+               var _druidConfig = 
_siteApp.configObj.getValueByPath("web.druid");
+               var _refreshInterval;
+
+               var _menu_newChart;
+
+               $scope.lock = false;
+
+               $scope.dataSourceListReady = false;
+               $scope.dataSourceList = [];
+               $scope.dashboard = {
+                       groups: []
+               };
+               $scope.dashboardEntity = null;
+               $scope.dashboardReady = false;
+
+               $scope._newMetricFilter = "";
+               $scope._newMetricDataSrc = null;
+               $scope._newMetricDataMetric = null;
+
+               $scope.tabHolder = {};
+
+               $scope.endTime = app.time.now();
+               $scope.startTime = $scope.endTime.clone();
+
+               // =================== Initialization ===================
+               if(!_druidConfig || !_druidConfig.coordinator || 
!_druidConfig.broker) {
+                       $.dialog({
+                               title: "OPS",
+                               content: "Druid configuration can't be empty!"
+                       });
+                       return;
+               }
+
+               $scope.autoRefreshList = [
+                       {title: "Last 1 Month", timeDes: "day", getStartTime: 
function(endTime) {return endTime.clone().subtract(1, "month");}},
+                       {title: "Last 1 Day", timeDes: "thirty_minute", 
getStartTime: function(endTime) {return endTime.clone().subtract(1, "day");}},
+                       {title: "Last 6 Hour", timeDes: "fifteen_minute", 
getStartTime: function(endTime) {return endTime.clone().subtract(6, "hour");}},
+                       {title: "Last 2 Hour", timeDes: "fifteen_minute", 
getStartTime: function(endTime) {return endTime.clone().subtract(2, "hour");}},
+                       {title: "Last 1 Hour", timeDes: "minute", getStartTime: 
function(endTime) {return endTime.clone().subtract(1, "hour");}}
+               ];
+               $scope.autoRefreshSelect = $scope.autoRefreshList[2];
+
+               // ====================== Function ======================
+               $scope.setAuthRefresh = function(item) {
+                       $scope.autoRefreshSelect = item;
+                       $scope.refreshAllChart(true);
+               };
+
+               $scope.refreshTimeDisplay = function() {
+                       PageConfig.pageSubTitle = 
common.format.date($scope.startTime) + " ~ " + 
common.format.date($scope.endTime) + " [refresh interval: 30s]";
+               };
+               $scope.refreshTimeDisplay();
+
+               // ======================= Metric =======================
+               // Fetch metric data
+               $http.get(_druidConfig.coordinator + 
"/druid/coordinator/v1/metadata/datasources", {withCredentials: 
false}).then(function(data) {
+                       var _endTime = new moment();
+                       var _startTime = _endTime.clone().subtract(1, "day");
+                       var _intervals = _startTime.toISOString() + "/" + 
_endTime.toISOString();
+
+                       $scope.dataSourceList = $.map(data.data, 
function(dataSrc) {
+                               return {
+                                       dataSource: dataSrc,
+                                       metricList: []
+                               };
+                       });
+
+                       // List dataSource metrics
+                       var _metrixList_promiseList = 
$.map($scope.dataSourceList, function(dataSrc) {
+                               var _data = JSON.stringify({
+                                       "queryType": "groupBy",
+                                       "dataSource": dataSrc.dataSource,
+                                       "granularity": "all",
+                                       "dimensions": ["metric"],
+                                       "aggregations": [
+                                               {
+                                                       "type":"count",
+                                                       "name":"count"
+                                               }
+                                       ],
+                                       "intervals": [_intervals]
+                               });
+
+                               return $http.post(_druidConfig.broker + 
"/druid/v2", _data, {withCredentials: false}).then(function(response) {
+                                       dataSrc.metricList = 
$.map(response.data, function(entity) {
+                                               return entity.event.metric;
+                                       });
+                               });
+                       });
+
+                       $q.all(_metrixList_promiseList).finally(function() {
+                               $scope.dataSourceListReady = true;
+
+                               $scope._newMetricDataSrc = 
$scope.dataSourceList[0];
+                               $scope._newMetricDataMetric = 
common.getValueByPath($scope._newMetricDataSrc, "metricList.0");
+                       });
+               }, function() {
+                       $.dialog({
+                               title: "OPS",
+                               content: "Fetch data source failed. Please 
check Site Application Metrics configuration."
+                       });
+               });
+
+               // Filter data source
+               $scope.dataSourceMetricList = function(dataSrc, filter) {
+                       filter = (filter || 
"").toLowerCase().trim().split(/\s+/);
+                       return $.grep((dataSrc && dataSrc.metricList) || [], 
function(metric) {
+                               for(var i = 0 ; i < filter.length ; i += 1) {
+                                       
if(metric.toLowerCase().indexOf(filter[i]) === -1) return false;
+                               }
+                               return true;
+                       });
+               };
+
+               // New metric select
+               $scope.newMetricSelectDataSource = function(dataSrc) {
+                       if(dataSrc !== $scope._newMetricDataMetric) 
$scope._newMetricDataMetric = dataSrc.metricList[0];
+                       $scope._newMetricDataSrc = dataSrc;
+               };
+               $scope.newMetricSelectMetric = function(metric) {
+                       $scope._newMetricDataMetric = metric;
+               };
+
+               // Confirm new metric
+               $scope.confirmSelectMetric = function() {
+                       var group = $scope.tabHolder.selectedPane.data;
+                       var metric = {
+                               dataSource: $scope._newMetricDataSrc.dataSource,
+                               metric: $scope._newMetricDataMetric,
+                               aggregations: ["max"]
+                       };
+                       $("#metricMDL").modal('hide');
+
+                       if($scope.metricForConfigChart) {
+                               $scope.configPreviewChart.metrics.push(metric);
+                               $scope.refreshChart($scope.configPreviewChart, 
true, true);
+                       } else {
+                               group.charts.push({
+                                       chart: "line",
+                                       metrics: [metric]
+                               });
+                               $scope.refreshAllChart();
+                       }
+               };
+
+               // ======================== Menu ========================
+               function _checkGroupName(entity) {
+                       if(common.array.find(entity.name, 
$scope.dashboard.groups, "name")) {
+                               return "Group name conflict";
+                       }
+               }
+
+               $scope.newGroup = function() {
+                       if($scope.lock) return;
+
+                       UI.createConfirm("Group", {}, [{field: "name"}], 
_checkGroupName).then(null, null, function(holder) {
+                               $scope.dashboard.groups.push({
+                                       name: holder.entity.name,
+                                       charts: []
+                               });
+                               holder.closeFunc();
+
+                               setTimeout(function() {
+                                       
$scope.tabHolder.setSelect(holder.entity.name);
+                               }, 0);
+                       });
+               };
+
+               function renameGroup() {
+                       var group = $scope.tabHolder.selectedPane.data;
+                       UI.updateConfirm("Group", {}, [{field: "name", name: 
"New Name"}], _checkGroupName).then(null, null, function(holder) {
+                               group.name = holder.entity.name;
+                               holder.closeFunc();
+                       });
+               }
+
+               function deleteGroup() {
+                       var group = $scope.tabHolder.selectedPane.data;
+                       UI.deleteConfirm(group.name).then(null, null, 
function(holder) {
+                               common.array.remove(group, 
$scope.dashboard.groups);
+                               holder.closeFunc();
+                       });
+               }
+
+               _menu_newChart = {title: "Add Metric", func: function() 
{$scope.newChart();}};
+               Object.defineProperties(_menu_newChart, {
+                       icon: {
+                               get: function() {return 
$scope.dataSourceListReady ? 'plus' : 'refresh fa-spin';}
+                       },
+                       disabled: {
+                               get: function() {return 
!$scope.dataSourceListReady;}
+                       }
+               });
+
+               $scope.menu = Authorization.isRole('ROLE_ADMIN') ? [
+                       {icon: "cog", title: "Configuration", list: [
+                               _menu_newChart,
+                               {icon: "pencil", title: "Rename Group", func: 
renameGroup},
+                               {icon: "trash", title: "Delete Group", danger: 
true, func: deleteGroup}
+                       ]},
+                       {icon: "plus", title: "New Group", func: 
$scope.newGroup}
+               ] : [];
+
+               // ===================== Dashboard ======================
+               $scope.dashboardList = 
Entities.queryEntities("GenericResourceService", {
+                       site: Site.current().tags.site,
+                       application: Application.current().tags.application
+               });
+               $scope.dashboardList._promise.then(function(list) {
+                       $scope.dashboardEntity = list[0];
+                       $scope.dashboard = 
DashboardFormatter.parse(common.parseJSON($scope.dashboardEntity.value));
+                       $scope.refreshAllChart();
+               }).finally(function() {
+                       $scope.dashboardReady = true;
+               });
+
+               $scope.saveDashboard = function() {
+                       $scope.lock = true;
+
+                       if(!$scope.dashboardEntity) {
+                               $scope.dashboardEntity = {
+                                       tags: {
+                                               site: Site.current().tags.site,
+                                               application: 
Application.current().tags.application,
+                                               name: 
"/metric_dashboard/dashboard/default"
+                                       }
+                               };
+                       }
+                       $scope.dashboardEntity.value = 
common.stringify($scope.dashboard);
+
+                       Entities.updateEntity("GenericResourceService", 
$scope.dashboardEntity)._promise.then(function() {
+                               $.dialog({
+                                       title: "Done",
+                                       content: "Save success!"
+                               });
+                       }, function() {
+                               $.dialog({
+                                       title: "POS",
+                                       content: "Save failed. Please retry."
+                               });
+                       }).finally(function() {
+                               $scope.lock = false;
+                       });
+               };
+
+               // ======================= Chart ========================
+               $scope.configTargetChart = null;
+               $scope.configPreviewChart = null;
+               $scope.metricForConfigChart = false;
+               $scope.viewChart = null;
+
+               $scope.chartConfig = {
+                       xType: "time"
+               };
+
+               $scope.chartTypeList = [
+                       {icon: "line-chart", chart: "line"},
+                       {icon: "area-chart", chart: "area"},
+                       {icon: "bar-chart", chart: "column"},
+                       {icon: "pie-chart", chart: "pie"}
+               ];
+
+               $scope.chartSeriesList = [
+                       {name: "Min", series: "min"},
+                       {name: "Max", series: "max"},
+                       {name: "Avg", series: "avg"},
+                       {name: "Count", series: "count"},
+                       {name: "Sum", series: "sum"}
+               ];
+
+               $scope.newChart = function() {
+                       $scope.metricForConfigChart = false;
+                       $("#metricMDL").modal();
+               };
+
+               $scope.configPreviewChartMinimumCheck = function() {
+                       $scope.configPreviewChart.min = 
$scope.configPreviewChart.min === 0 ? undefined : 0;
+               };
+
+               $scope.seriesChecked = function(metric, series) {
+                       if(!metric) return false;
+                       return $.inArray(series, metric.aggregations || []) !== 
-1;
+               };
+               $scope.seriesCheckClick = function(metric, series, chart) {
+                       if(!metric || !chart) return;
+                       if($scope.seriesChecked(metric, series)) {
+                               common.array.remove(series, 
metric.aggregations);
+                       } else {
+                               metric.aggregations.push(series);
+                       }
+                       $scope.chartSeriesUpdate(chart);
+               };
+
+               $scope.chartSeriesUpdate = function(chart) {
+                       chart._data = $.map(chart._oriData, function(groupData, 
i) {
+                               var metric = chart.metrics[i];
+                               return $.map(groupData, function(series) {
+                                       if($.inArray(series._key, 
metric.aggregations) !== -1) return series;
+                               });
+                       });
+               };
+
+               $scope.configAddMetric = function() {
+                       $scope.metricForConfigChart = true;
+                       $("#metricMDL").modal();
+               };
+
+               $scope.configRemoveMetric = function(metric) {
+                       common.array.remove(metric, 
$scope.configPreviewChart.metrics);
+               };
+
+               $scope.getChartConfig = function(chart) {
+                       if(!chart) return null;
+
+                       var _config = chart._config = chart._config || 
$.extend({}, $scope.chartConfig);
+                       _config.yMin = chart.min;
+
+                       return _config;
+               };
+
+               $scope.configChart = function(chart) {
+                       $scope.configTargetChart = chart;
+                       $scope.configPreviewChart = $.extend({}, chart);
+                       $scope.configPreviewChart.metrics = 
$.map(chart.metrics, function(metric) {
+                               return $.extend({}, metric, {aggregations: 
(metric.aggregations || []).slice()});
+                       });
+                       delete $scope.configPreviewChart._config;
+                       $("#chartMDL").modal();
+                       setTimeout(function() {
+                               $(window).resize();
+                       }, 200);
+               };
+
+               $scope.confirmUpdateChart = function() {
+                       $("#chartMDL").modal('hide');
+                       $.extend($scope.configTargetChart, 
$scope.configPreviewChart);
+                       $scope.chartSeriesUpdate($scope.configTargetChart);
+                       if($scope.configTargetChart._holder) 
$scope.configTargetChart._holder.refreshAll();
+                       $scope.configPreviewChart = null;
+               };
+
+               $scope.deleteChart = function(group, chart) {
+                       UI.deleteConfirm(chart.metric).then(null, null, 
function(holder) {
+                               common.array.remove(chart, group.charts);
+                               holder.closeFunc();
+                               $scope.refreshAllChart(false, true);
+                       });
+               };
+
+               $scope.showChart = function(chart) {
+                       $scope.viewChart = chart;
+                       $("#chartViewMDL").modal();
+                       setTimeout(function() {
+                               $(window).resize();
+                       }, 200);
+               };
+
+               $scope.refreshChart = function(chart, forceRefresh, refreshAll) 
{
+                       var _intervals = $scope.startTime.toISOString() + "/" + 
$scope.endTime.toISOString();
+
+                       function _refreshChart() {
+                               if (chart._holder) {
+                                       if (refreshAll) {
+                                               chart._holder.refreshAll();
+                                       } else {
+                                               chart._holder.refresh();
+                                       }
+                               }
+                       }
+
+                       var _tmpData, _metricPromiseList;
+
+                       if (chart._data && !forceRefresh) {
+                               // Refresh chart without reload
+                               _refreshChart();
+                       } else {
+                               // Refresh chart with reload
+                               _tmpData = [];
+                               _metricPromiseList = $.map(chart.metrics, 
function (metric, k) {
+                                       // Each Metric
+                                       var _query = JSON.stringify({
+                                               "queryType": "groupBy",
+                                               "dataSource": metric.dataSource,
+                                               "granularity": 
$scope.autoRefreshSelect.timeDes,
+                                               "dimensions": ["metric"],
+                                               "filter": {"type": "selector", 
"dimension": "metric", "value": metric.metric},
+                                               "aggregations": [
+                                                       {
+                                                               "type": "max",
+                                                               "name": "max",
+                                                               "fieldName": 
"maxValue"
+                                                       },
+                                                       {
+                                                               "type": "min",
+                                                               "name": "min",
+                                                               "fieldName": 
"maxValue"
+                                                       },
+                                                       {
+                                                               "type": "count",
+                                                               "name": "count",
+                                                               "fieldName": 
"maxValue"
+                                                       },
+                                                       {
+                                                               "type": 
"longSum",
+                                                               "name": "sum",
+                                                               "fieldName": 
"maxValue"
+                                                       }
+                                               ],
+                                               "postAggregations" : [
+                                                       {
+                                                               "type": 
"javascript",
+                                                               "name": "avg",
+                                                               "fieldNames": 
["sum", "count"],
+                                                               "function": 
"function(sum, cnt) { return sum / cnt;}"
+                                                       }
+                                               ],
+                                               "intervals": [_intervals]
+                                       });
+
+                                       return $http.post(_druidConfig.broker + 
"/druid/v2", _query, {withCredentials: false}).then(function (response) {
+                                               var _data = 
nvd3.convert.druid([response.data]);
+                                               _tmpData[k] = _data;
+
+                                               // Process series name
+                                               $.each(_data, function(i, 
series) {
+                                                       series._key = 
series.key;
+                                                       if(chart.metrics.length 
> 1) {
+                                                               series.key = 
metric.metric.replace(/^.*\./, "") + "-" +series._key;
+                                                       }
+                                               });
+                                       });
+                               });
+
+                               $q.all(_metricPromiseList).then(function() {
+                                       chart._oriData = _tmpData;
+                                       $scope.chartSeriesUpdate(chart);
+                                       _refreshChart();
+                               });
+                       }
+               };
+
+               $scope.refreshAllChart = function(forceRefresh, refreshAll) {
+                       setTimeout(function() {
+                               $scope.endTime = app.time.now();
+                               $scope.startTime = 
$scope.autoRefreshSelect.getStartTime($scope.endTime);
+
+                               $scope.refreshTimeDisplay();
+
+                               $.each($scope.dashboard.groups, function (i, 
group) {
+                                       $.each(group.charts, function (j, 
chart) {
+                                               $scope.refreshChart(chart, 
forceRefresh, refreshAll);
+                                       });
+                               });
+
+                               $(window).resize();
+                       }, 0);
+               };
+
+               $scope.chartSwitchRefresh = function(source, target) {
+                       var _oriSize = source.size;
+                       source.size = target.size;
+                       target.size = _oriSize;
+
+                       if(source._holder) source._holder.refreshAll();
+                       if(target._holder) target._holder.refreshAll();
+
+               };
+
+               _refreshInterval = setInterval(function() {
+                       if(!$scope.dashboardReady) return;
+                       $scope.refreshAllChart(true);
+               }, 1000 * 30);
+
+               // > Chart UI
+               $scope.configChartSize = function(chart, sizeOffset) {
+                       chart.size = (chart.size || 6) + sizeOffset;
+                       if(chart.size <= 0) chart.size = 1;
+                       if(chart.size > 12) chart.size = 12;
+                       setTimeout(function() {
+                               $(window).resize();
+                       }, 1);
+               };
+
+               // ========================= UI =========================
+               $("#metricMDL").on('hidden.bs.modal', function () {
+                       if($(".modal-backdrop").length) {
+                               $("body").addClass("modal-open");
+                       }
+               });
+
+               $("#chartViewMDL").on('hidden.bs.modal', function () {
+                       $scope.viewChart = null;
+               });
+
+               // ====================== Clean Up ======================
+               $scope.$on('$destroy', function() {
+                       clearInterval(_refreshInterval);
+               });
+       });
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
 
b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
new file mode 100644
index 0000000..2acb954
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
@@ -0,0 +1,250 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<div class="page-fixed">
+       <div class="dropdown inline">
+               <button data-toggle="dropdown" class="btn btn-primary">
+                       <span class="fa fa-clock-o"></span> 
{{autoRefreshSelect.title}}
+               </button>
+               <ul class="dropdown-menu left">
+                       <li ng-repeat="item in autoRefreshList track by $index">
+                               <a ng-click="setAuthRefresh(item)">
+                                       <span class="fa fa-clock-o"></span>
+                                       <span ng-class="{'text-bold': item === 
autoRefreshSelect}">{{item.title}}</span>
+                               </a>
+                       </li>
+               </ul>
+       </div>
+
+       <button class="btn btn-primary" ng-if="Auth.isRole('ROLE_ADMIN')"
+                       ng-click="saveDashboard()" ng-disabled="lock">
+               <span class="fa fa-floppy-o"></span> Save
+       </button>
+</div>
+
+<div class="box box-default" ng-if="!dashboard.groups.length">
+       <div class="box-header with-border">
+               <h3 class="box-title">Empty Dashboard</h3>
+       </div>
+       <div class="box-body">
+               <div ng-show="!dashboardReady">
+                       Loading...
+               </div>
+               <div ng-show="dashboardReady">
+                       Current dashboard is empty.
+                       <span ng-if="Auth.isRole('ROLE_ADMIN')">Click <a 
ng-click="newGroup()">here</a> to create a new group.</span>
+               </div>
+       </div>
+       <div class="overlay" ng-show="!dashboardReady">
+               <i class="fa fa-refresh fa-spin"></i>
+       </div>
+</div>
+
+<div tabs menu="menu" holder="tabHolder" ng-show="dashboard.groups.length" 
data-sortable-model="Auth.isRole('ROLE_ADMIN') ? dashboard.groups : null">
+       <pane ng-repeat="group in dashboard.groups" data-data="group" 
data-title="{{group.name}}">
+               <div uie-sortable ng-model="group.charts" class="row narrow" 
sortable-update-func="chartSwitchRefresh" ng-show="group.charts.length">
+                       <div ng-repeat="chart in group.charts track by $index" 
class="col-md-{{chart.size || 6}}">
+                               <div class="nvd3-chart-wrapper">
+                                       <div nvd3="chart._data" 
data-holder="chart._holder" data-title="{{chart.title || 
chart.metrics[0].metric}}" data-watching="false"
+                                                data-chart="{{chart.chart || 
'line'}}" data-config="getChartConfig(chart)" class="nvd3-chart-cntr"></div>
+                                       <div class="nvd3-chart-config">
+                                               <a class="fa fa-expand" 
ng-click="showChart(chart, -1)"></a>
+                                               <span 
ng-if="Auth.isRole('ROLE_ADMIN')">
+                                                       <a class="fa fa-minus" 
ng-click="configChartSize(chart, -1)"></a>
+                                                       <a class="fa fa-plus" 
ng-click="configChartSize(chart, 1)"></a>
+                                                       <a class="fa fa-cog" 
ng-click="configChart(chart)"></a>
+                                                       <a class="fa fa-trash" 
ng-click="deleteChart(group, chart)"></a>
+                                               </span>
+                                       </div>
+                               </div>
+                       </div>
+               </div>
+
+               <p ng-if="!group.charts.length">
+                       Empty group.
+                       <span ng-if="Auth.isRole('ROLE_ADMIN')">
+                               Click
+                               <span ng-hide="dataSourceListReady" class="fa 
fa-refresh fa-spin"></span>
+                               <a ng-show="dataSourceListReady" 
ng-click="newChart()">here</a>
+                               to add metric.
+                       </span>
+               </p>
+       </pane>
+</div>
+
+
+
+<!-- Modal: Chart configuration -->
+<div class="modal fade" id="chartMDL" tabindex="-1" role="dialog">
+       <div class="modal-dialog modal-lg" role="document">
+               <div class="modal-content">
+                       <div class="modal-header">
+                               <button type="button" class="close" 
data-dismiss="modal" aria-label="Close">
+                                       <span aria-hidden="true">&times;</span>
+                               </button>
+                               <h4 class="modal-title">Chart Configuration</h4>
+                       </div>
+                       <div class="modal-body">
+                               <div class="row">
+                                       <div class="col-md-6">
+                                               <div class="nvd3-chart-wrapper">
+                                                       <div 
nvd3="configPreviewChart._data" data-title="{{configPreviewChart.title || 
configPreviewChart.metrics[0].metric}}"
+                                                                
data-watching="true" data-chart="{{configPreviewChart.chart || 'line'}}" 
data-config="getChartConfig(configPreviewChart)" class="nvd3-chart-cntr"></div>
+                                               </div>
+                                       </div>
+                                       <div class="col-md-6">
+                                               <!-- Chart Configuration -->
+                                               <table class="table">
+                                                       <tbody>
+                                                       <tr>
+                                                               <th 
width="100">Name</th>
+                                                               <td><input 
type="text" class="form-control input-xs" ng-model="configPreviewChart.title" 
placeholder="Default: {{configPreviewChart.metrics[0].metric}}" /></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <th>Chart 
Type</th>
+                                                               <td>
+                                                                       <div 
class="btn-group" data-toggle="buttons">
+                                                                               
<label class="btn btn-default btn-xs" ng-class="{active: 
(configPreviewChart.chart || 'line') === type.chart}"
+                                                                               
           ng-repeat="type in chartTypeList track by $index" 
ng-click="configPreviewChart.chart = type.chart;">
+                                                                               
        <input type="radio" name="chartType" autocomplete="off"
+                                                                               
                   ng-checked="(configPreviewChart.chart || 'line') === 
type.chart">
+                                                                               
        <span class="fa fa-{{type.icon}}"></span>
+                                                                               
</label>
+                                                                       </div>
+                                                               </td>
+                                                       </tr>
+                                                       <tr>
+                                                               <th>Minimum</th>
+                                                               <td><input 
type="checkbox" ng-checked="configPreviewChart.min === 0" 
ng-disabled="configPreviewChart.chart === 'area' || configPreviewChart.chart 
=== 'pie'"
+                                                                               
   ng-click="configPreviewChartMinimumCheck()" /></td>
+                                                       </tr>
+                                                       <tr>
+                                                               <th>Metrics</th>
+                                                               <td>
+                                                                       <div 
ng-repeat="metric in configPreviewChart.metrics" class="box inner-box">
+                                                                               
<div class="box-tools">
+                                                                               
        <button class="btn btn-box-tool" ng-click="configRemoveMetric(metric)">
+                                                                               
                <span class="fa fa-times"></span>
+                                                                               
        </button>
+                                                                               
</div>
+
+                                                                               
<h3 class="box-title">{{metric.metric}}</h3>
+                                                                               
<div class="checkbox noMargin" ng-repeat="series in chartSeriesList track by 
$index">
+                                                                               
        <label>
+                                                                               
                <input type="checkbox" ng-checked="seriesChecked(metric, 
series.series)"
+                                                                               
                           ng-click="seriesCheckClick(metric, series.series, 
configPreviewChart)" />
+                                                                               
                {{series.name}}
+                                                                               
        </label>
+                                                                               
</div>
+                                                                       </div>
+                                                                       <a 
ng-click="configAddMetric()">+ Add Metric</a>
+                                                               </td>
+                                                       </tr>
+                                                       </tbody>
+                                               </table>
+                                       </div>
+                               </div>
+                       </div>
+                       <div class="modal-footer">
+                               <button type="button" class="btn btn-default" 
data-dismiss="modal">
+                                       Close
+                               </button>
+                               <button type="button" class="btn btn-primary" 
ng-click="confirmUpdateChart()">
+                                       Confirm
+                               </button>
+                       </div>
+               </div>
+       </div>
+</div>
+
+
+
+<!-- Modal: Metric selector -->
+<div class="modal fade" id="metricMDL" tabindex="-1" role="dialog">
+       <div class="modal-dialog modal-lg" role="document">
+               <div class="modal-content">
+                       <div class="modal-header">
+                               <button type="button" class="close" 
data-dismiss="modal" aria-label="Close">
+                                       <span aria-hidden="true">&times;</span>
+                               </button>
+                               <h4 class="modal-title">Select Metric</h4>
+                       </div>
+                       <div class="modal-body">
+                               <div class="input-group" style="margin-bottom: 
10px;">
+                                       <input type="text" class="form-control" 
placeholder="Filter..." ng-model="_newMetricFilter" />
+                                       <span class="input-group-btn">
+                                               <button class="btn btn-default 
btn-flat" ng-click="_newMetricFilter = '';"><span class="fa 
fa-times"></span></button>
+                                       </span>
+                               </div>
+
+                               <div class="row">
+                                       <div class="col-md-4">
+                                               <ul class="nav nav-pills 
nav-stacked fixed-height">
+                                                       <li 
class="disabled"><a>Data Source</a></li>
+                                                       <li ng-repeat="dataSrc 
in dataSourceList track by $index" ng-class="{active: _newMetricDataSrc === 
dataSrc}">
+                                                               <a 
ng-click="newMetricSelectDataSource(dataSrc)">{{dataSrc.dataSource}}</a>
+                                                       </li>
+                                               </ul>
+                                       </div>
+                                       <div class="col-md-8">
+                                               <ul class="nav nav-pills 
nav-stacked fixed-height">
+                                                       <li 
class="disabled"><a>Metric</a></li>
+                                                       <li ng-repeat="metric 
in dataSourceMetricList(_newMetricDataSrc, _newMetricFilter) track by $index" 
ng-class="{active: _newMetricDataMetric === metric}">
+                                                               <a 
ng-click="newMetricSelectMetric(metric)">{{metric}}</a>
+                                                       </li>
+                                               </ul>
+                                       </div>
+                               </div>
+                       </div>
+                       <div class="modal-footer">
+                               <span class="text-primary 
pull-left">{{_newMetricDataSrc.dataSource}} <span class="fa 
fa-arrow-right"></span> {{_newMetricDataMetric}}</span>
+                               <button type="button" class="btn btn-default" 
data-dismiss="modal">
+                                       Close
+                               </button>
+                               <button type="button" class="btn btn-primary" 
ng-click="confirmSelectMetric()">
+                                       Select
+                               </button>
+                       </div>
+               </div>
+       </div>
+</div>
+
+
+
+<!-- Modal: Chart View -->
+<div class="modal fade" id="chartViewMDL" tabindex="-1" role="dialog">
+       <div class="modal-dialog modal-lg" role="document">
+               <div class="modal-content">
+                       <div class="modal-header">
+                               <button type="button" class="close" 
data-dismiss="modal" aria-label="Close">
+                                       <span aria-hidden="true">&times;</span>
+                               </button>
+                               <h4 class="modal-title">{{viewChart.title || 
viewChart.metrics[0].metric}}</h4>
+                       </div>
+                       <div class="modal-body">
+                               <div nvd3="viewChart._data" 
data-title="{{viewChart.title || viewChart.metrics[0].metric}}"
+                                        data-watching="true" 
data-chart="{{viewChart.chart || 'line'}}" 
data-config="getChartConfig(viewChart)" class="nvd3-chart-cntr lg"></div>
+                       </div>
+                       <div class="modal-footer">
+                               <button type="button" class="btn btn-default" 
data-dismiss="modal">
+                                       Close
+                               </button>
+                       </div>
+               </div>
+       </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js 
b/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js
new file mode 100644
index 0000000..94886c9
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       'use strict';
+
+       var featureControllers = angular.module('featureControllers');
+       var feature = featureControllers.register("topology", {
+               global: true    // Global Feature needn't add to applications
+       });
+
+       // ==============================================================
+       // =                       Initialization                       =
+       // ==============================================================
+
+       // ==============================================================
+       // =                          Function                          =
+       // ==============================================================
+       //feature.service("DashboardFormatter", function() {
+       //});
+
+       // ==============================================================
+       // =                         Controller                         =
+       // ==============================================================
+       feature.configNavItem("monitoring", "Topology", "usb");
+
+       // ========================= Monitoring =========================
+       feature.configController('monitoring', function(PageConfig, $scope, 
$interval, Entities, UI, Site, Application) {
+               var topologyRefreshInterval;
+
+               PageConfig.hideApplication = true;
+               PageConfig.hideSite = true;
+               PageConfig.pageTitle = "Topology Execution";
+
+               $scope.topologyExecutionList = null;
+
+               $scope.currentTopologyExecution = null;
+               $scope.currentTopologyExecutionOptList = [];
+
+               // ======================= Function =======================
+               function refreshExecutionList() {
+                       var _list = 
Entities.queryEntities("TopologyExecutionService");
+                       _list._promise.then(function () {
+                               $scope.topologyExecutionList = _list;
+                       });
+               }
+
+               $scope.showTopologyDetail = function (topologyExecution) {
+                       $scope.currentTopologyExecution = topologyExecution;
+                       $("#topologyMDL").modal();
+
+                       $scope.currentTopologyExecutionOptList = 
Entities.queryEntities("TopologyOperationService", {
+                               site: topologyExecution.tags.site,
+                               application: topologyExecution.tags.application,
+                               topology: topologyExecution.tags.topology,
+                               _pageSize: 10,
+                               _duration: 1000 * 60 * 60 * 24 * 30
+                       });
+               };
+
+               $scope.getStatusClass = function (status) {
+                       switch (status) {
+                               case "NEW":
+                                       return "info";
+                               case "STARTING":
+                               case "STOPPING":
+                                       return "warning";
+                               case "STARTED":
+                                       return "success";
+                               case "STOPPED":
+                                       return "danger";
+                               default:
+                                       return "default";
+                       }
+               };
+
+               // ==================== Initialization ====================
+               refreshExecutionList();
+               topologyRefreshInterval = $interval(refreshExecutionList, 10 * 
1000);
+
+               $scope.topologyList = 
Entities.queryEntities("TopologyDescriptionService");
+               $scope.topologyList._promise.then(function () {
+                       $scope.topologyList = $.map($scope.topologyList, 
function (topology) {
+                               return topology.tags.topology;
+                       });
+               });
+
+               $scope.applicationList = $.map(Application.list, function 
(application) {
+                       return application.tags.application;
+               });
+
+               $scope.siteList = $.map(Site.list, function (site) {
+                       return site.tags.site;
+               });
+
+               // ================== Topology Execution ==================
+               $scope.newTopologyExecution = function () {
+                       UI.createConfirm("Topology", {}, [
+                               {field: "site", type: "select", valueList: 
$scope.siteList},
+                               {field: "application", type: "select", 
valueList: $scope.applicationList},
+                               {field: "topology", type: "select", valueList: 
$scope.topologyList}
+                       ], function (entity) {
+                               for(var i = 0 ; i < 
$scope.topologyExecutionList.length; i += 1) {
+                                       var _entity = 
$scope.topologyExecutionList[i].tags;
+                                       if(_entity.site === entity.site && 
_entity.application === entity.application && _entity.topology === 
entity.topology) {
+                                               return "Topology already 
exist!";
+                                       }
+                               }
+                       }).then(null, null, function(holder) {
+                               var _entity = {
+                                       tags: {
+                                               site: holder.entity.site,
+                                               application: 
holder.entity.application,
+                                               topology: holder.entity.topology
+                                       },
+                                       status: "NEW"
+                               };
+                               
Entities.updateEntity("TopologyExecutionService", 
_entity)._promise.then(function() {
+                                       holder.closeFunc();
+                                       
$scope.topologyExecutionList.push(_entity);
+                                       refreshExecutionList();
+                               });
+                       });
+               };
+
+               $scope.deleteTopologyExecution = function (topologyExecution) {
+                       
UI.deleteConfirm(topologyExecution.tags.topology).then(null, null, 
function(holder) {
+                               
Entities.deleteEntities("TopologyExecutionService", 
topologyExecution.tags)._promise.then(function() {
+                                       holder.closeFunc();
+                                       common.array.remove(topologyExecution, 
$scope.topologyExecutionList);
+                               });
+                       });
+               };
+
+               // ================== Topology Operation ==================
+               $scope.doTopologyOperation = function (topologyExecution, 
operation) {
+                       $.dialog({
+                               title: operation + " Confirm",
+                               content: "Do you want to " + operation + " '" + 
topologyExecution.tags.topology + "'?",
+                               confirm: true
+                       }, function (ret) {
+                               if(!ret) return;
+
+                               var list = 
Entities.queryEntities("TopologyOperationService", {
+                                       site: topologyExecution.tags.site,
+                                       application: 
topologyExecution.tags.application,
+                                       topology: 
topologyExecution.tags.topology,
+                                       _pageSize: 20
+                               });
+
+                               list._promise.then(function () {
+                                       var lastOperation = 
common.array.find(operation, list, "tags.operation");
+                                       if(lastOperation && 
(lastOperation.status === "INITIALIZED" || lastOperation.status === "PENDING")) 
{
+                                               refreshExecutionList();
+                                               return;
+                                       }
+
+                                       
Entities.updateEntity("rest/app/operation", {
+                                               tags: {
+                                                       site: 
topologyExecution.tags.site,
+                                                       application: 
topologyExecution.tags.application,
+                                                       topology: 
topologyExecution.tags.topology,
+                                                       operation: operation
+                                               },
+                                               status: "INITIALIZED"
+                                       }, {timestamp: false, hook: true});
+                               });
+                       });
+               };
+
+               $scope.startTopologyOperation = function (topologyExecution) {
+                       $scope.doTopologyOperation(topologyExecution, "START");
+               };
+               $scope.stopTopologyOperation = function (topologyExecution) {
+                       $scope.doTopologyOperation(topologyExecution, "STOP");
+               };
+
+               // ======================= Clean Up =======================
+               $scope.$on('$destroy', function() {
+                       $interval.cancel(topologyRefreshInterval);
+               });
+       });
+
+       // ========================= Management =========================
+       feature.configController('management', function(PageConfig, $scope, 
Entities, UI) {
+               PageConfig.hideApplication = true;
+               PageConfig.hideSite = true;
+               PageConfig.pageTitle = "Topology";
+
+               var typeList = ["CLASS", "DYNAMIC"];
+               var topologyDefineAttrs = [
+                       {field: "topology", name: "name"},
+                       {field: "type", type: "select", valueList: typeList},
+                       {field: "exeClass", name: "execution entry", 
description: function (entity) {
+                               if(entity.type === "CLASS") return "Class 
implements interface TopologyExecutable";
+                               if(entity.type === "DYNAMIC") return "DSL based 
topology definition";
+                       }, type: "blob", rows: 5},
+                       {field: "version", optional: true},
+                       {field: "description", optional: true, type: "blob"}
+               ];
+               var topologyUpdateAttrs = 
$.extend(topologyDefineAttrs.concat(), [{field: "topology", name: "name", 
readonly: true}]);
+
+               $scope.topologyList = 
Entities.queryEntities("TopologyDescriptionService");
+
+               $scope.newTopology = function () {
+                       UI.createConfirm("Topology", {}, topologyDefineAttrs, 
function (entity) {
+                               if(common.array.find(entity.topology, 
$scope.topologyList, "tags.topology", false, false)) {
+                                       return "Topology name conflict!";
+                               }
+                       }).then(null, null, function(holder) {
+                               holder.entity.tags = {
+                                       topology: holder.entity.topology
+                               };
+                               
Entities.updateEntity("TopologyDescriptionService", holder.entity, {timestamp: 
false})._promise.then(function() {
+                                       holder.closeFunc();
+                                       $scope.topologyList.push(holder.entity);
+                               });
+                       });
+               };
+
+               $scope.updateTopology = function (topology) {
+                       UI.updateConfirm("Topology", $.extend({}, topology, 
{topology: topology.tags.topology}), topologyUpdateAttrs).then(null, null, 
function(holder) {
+                               holder.entity.tags = {
+                                       topology: holder.entity.topology
+                               };
+                               
Entities.updateEntity("TopologyDescriptionService", holder.entity, {timestamp: 
false})._promise.then(function() {
+                                       holder.closeFunc();
+                                       $.extend(topology, holder.entity);
+                               });
+                       });
+               };
+
+               $scope.deleteTopology = function (topology) {
+                       UI.deleteConfirm(topology.tags.topology).then(null, 
null, function(holder) {
+                               Entities.delete("TopologyDescriptionService", 
{topology: topology.tags.topology})._promise.then(function() {
+                                       holder.closeFunc();
+                                       common.array.remove(topology, 
$scope.topologyList);
+                               });
+                       });
+               };
+       });
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
 
b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
new file mode 100644
index 0000000..9e22c84
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
@@ -0,0 +1,52 @@
+<div class="box box-primary">
+       <div class="box-header with-border">
+               <i class="fa fa-cog"></i>
+               <a class="pull-right" href="#/config/topology/monitoring"><span 
class="fa fa-angle-right"></span> Monitoring</a>
+               <h3 class="box-title">
+                       Management
+               </h3>
+       </div>
+       <div class="box-body">
+               <table class="table table-bordered">
+                       <thead>
+                               <tr>
+                                       <th>Name</th>
+                                       <th width="20%">Execution Class</th>
+                                       <th>Type</th>
+                                       <th width="50%">Description</th>
+                                       <th>Version</th>
+                                       <th width="70"></th>
+                               </tr>
+                       </thead>
+                       <tbody>
+                               <tr ng-repeat="item in topologyList">
+                                       <td>{{item.tags.topology}}</td>
+                                       <td><pre 
class="noWrap">{{item.exeClass}}</pre></td>
+                                       <td>{{item.type}}</td>
+                                       <td>{{item.description}}</td>
+                                       <td>{{item.version}}</td>
+                                       <td class="text-center">
+                                               <button class="rm fa fa-pencil 
btn btn-default btn-xs" uib-tooltip="Edit" tooltip-animation="false" 
ng-click="updateTopology(item)"> </button>
+                                               <button class="rm fa fa-trash-o 
btn btn-danger btn-xs" uib-tooltip="Delete" tooltip-animation="false" 
ng-click="deleteTopology(item)"> </button>
+                                       </td>
+                               </tr>
+                               <tr ng-if="topologyList.length === 0">
+                                       <td colspan="6">
+                                               <span class="text-muted">Empty 
list</span>
+                                       </td>
+                               </tr>
+                       </tbody>
+               </table>
+       </div>
+
+       <div class="box-footer">
+               <button class="btn btn-primary pull-right" 
ng-click="newTopology()">
+                       New Topology
+                       <i class="fa fa-plus-circle"> </i>
+               </button>
+       </div>
+
+       <div class="overlay" ng-hide="topologyList._promise.$$state.status === 
1;">
+               <i class="fa fa-refresh fa-spin"></i>
+       </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
 
b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
new file mode 100644
index 0000000..0acb2c1
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
@@ -0,0 +1,151 @@
+<div class="box box-primary">
+       <div class="box-header with-border">
+               <i class="fa fa-eye"></i>
+               <a class="pull-right" href="#/config/topology/management"><span 
class="fa fa-angle-right"></span> Management</a>
+               <h3 class="box-title">
+                       Monitoring
+               </h3>
+       </div>
+       <div class="box-body">
+               <div sorttable source="topologyExecutionList">
+                       <table class="table table-bordered" ng-non-bindable>
+                               <thead>
+                               <tr>
+                                       <th width="70" 
sortpath="status">Status</th>
+                                       <th width="90" 
sortpath="tags.topology">Topology</th>
+                                       <th width="60" 
sortpath="tags.site">Site</th>
+                                       <th width="100" 
sortpath="tags.application">Application</th>
+                                       <th width="60" sortpath="mode">Mode</th>
+                                       <th 
sortpath="description">Description</th>
+                                       <th width="70" style="min-width: 
70px;"></th>
+                               </tr>
+                               </thead>
+                               <tbody>
+                               <tr>
+                                       <td class="text-center">
+                                               <span class="label 
label-{{_parent.getStatusClass(item.status)}}">{{item.status}}</span>
+                                       </td>
+                                       <td><a 
ng-click="_parent.showTopologyDetail(item)">{{item.tags.topology}}</a></td>
+                                       <td>{{item.tags.site}}</td>
+                                       <td>{{item.tags.application}}</td>
+                                       <td>{{item.mode}}</td>
+                                       <td>{{item.description}}</td>
+                                       <td class="text-center">
+                                               <button ng-if="item.status === 
'NEW' || item.status === 'STOPPED'" class="fa fa-play sm btn btn-default 
btn-xs" uib-tooltip="Start" tooltip-animation="false" 
ng-click="_parent.startTopologyOperation(item)"> </button>
+                                               <button ng-if="item.status === 
'STARTED'" class="fa fa-stop sm btn btn-default btn-xs" uib-tooltip="Stop" 
tooltip-animation="false" ng-click="_parent.stopTopologyOperation(item)"> 
</button>
+                                               <button ng-if="item.status !== 
'NEW' && item.status !== 'STARTED' && item.status !== 'STOPPED'" class="fa 
fa-ban sm btn btn-default btn-xs" disabled="disabled"> </button>
+                                               <button class="rm fa fa-trash-o 
btn btn-danger btn-xs" uib-tooltip="Delete" tooltip-animation="false" 
ng-click="_parent.deleteTopologyExecution(item)"> </button>
+                                       </td>
+                               </tr>
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+
+       <div class="box-footer">
+               <button class="btn btn-primary pull-right" 
ng-click="newTopologyExecution()">
+                       New Topology Execution
+                       <i class="fa fa-plus-circle"> </i>
+               </button>
+       </div>
+
+       <div class="overlay" 
ng-hide="topologyExecutionList._promise.$$state.status === 1;">
+               <i class="fa fa-refresh fa-spin"></i>
+       </div>
+</div>
+
+
+
+
+<!-- Modal: Topology Detail -->
+<div class="modal fade" id="topologyMDL" tabindex="-1" role="dialog">
+       <div class="modal-dialog modal-lg" role="document">
+               <div class="modal-content">
+                       <div class="modal-header">
+                               <button type="button" class="close" 
data-dismiss="modal" aria-label="Close">
+                                       <span aria-hidden="true">&times;</span>
+                               </button>
+                               <h4 class="modal-title">Topology Detail</h4>
+                       </div>
+                       <div class="modal-body">
+                               <h3>Detail</h3>
+                               <table class="table">
+                                       <tbody>
+                                               <tr>
+                                                       <th>Site</th>
+                                                       
<td>{{currentTopologyExecution.tags.site}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Application</th>
+                                                       
<td>{{currentTopologyExecution.tags.application}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Topology</th>
+                                                       
<td>{{currentTopologyExecution.tags.topology}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Full Name</th>
+                                                       
<td>{{currentTopologyExecution.fullName || "-"}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Status</th>
+                                                       <td>
+                                                               <span 
class="label 
label-{{getStatusClass(currentTopologyExecution.status)}}">{{currentTopologyExecution.status}}</span>
+                                                       </td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Mode</th>
+                                                       
<td>{{currentTopologyExecution.mode || "-"}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Environment</th>
+                                                       
<td>{{currentTopologyExecution.environment || "-"}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Url</th>
+                                                       <td>
+                                                               <a 
ng-if="currentTopologyExecution.url" href="{{currentTopologyExecution.url}}" 
target="_blank">{{currentTopologyExecution.url}}</a>
+                                                               <span 
ng-if="!currentTopologyExecution.url">-</span>
+                                                       </td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Description</th>
+                                                       
<td>{{currentTopologyExecution.description || "-"}}</td>
+                                               </tr>
+                                               <tr>
+                                                       <th>Last Modified 
Date</th>
+                                                       
<td>{{common.format.date(currentTopologyExecution.lastModifiedDate) || 
"-"}}</td>
+                                               </tr>
+                                       </tbody>
+                               </table>
+
+                               <h3>Latest Operations</h3>
+                               <div class="table-responsive">
+                                       <table class="table table-bordered 
table-sm margin-bottom-none">
+                                               <thead>
+                                                       <tr>
+                                                               <th>Date 
Time</th>
+                                                               
<th>Operation</th>
+                                                               <th>Status</th>
+                                                               <th>Message</th>
+                                                       </tr>
+                                               </thead>
+                                               <tbody>
+                                                       <tr ng-repeat="action 
in currentTopologyExecutionOptList track by $index">
+                                                               
<td>{{common.format.date(action.lastModifiedDate) || "-"}}</td>
+                                                               
<td>{{action.tags.operation}}</td>
+                                                               
<td>{{action.status}}</td>
+                                                               <td><pre 
class="noWrap">{{action.message}}</pre></td>
+                                                       </tr>
+                                               </tbody>
+                                       </table>
+                               </div>
+                       </div>
+                       <div class="modal-footer">
+                               <button type="button" class="btn btn-default" 
data-dismiss="modal">
+                                       Close
+                               </button>
+                       </div>
+               </div>
+       </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
 
b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
new file mode 100644
index 0000000..ed619d3
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       'use strict';
+
+       var featureControllers = angular.module('featureControllers');
+       var feature = featureControllers.register("userProfile");
+
+       // ==============================================================
+       // =                          Function                          =
+       // ==============================================================
+
+       // ==============================================================
+       // =                        User Profile                        =
+       // ==============================================================
+
+       // ======================== Profile List ========================
+       //feature.navItem("list", "User Profiles", "graduation-cap");
+       feature.controller('list', function(PageConfig, Site, $scope, 
$interval, Entities) {
+               PageConfig.pageSubTitle = Site.current().tags.site;
+
+               $scope.common = common;
+               $scope.algorithms = [];
+
+               // ======================================== Algorithms 
========================================
+               $scope.algorithmEntity = {};
+               Entities.queryEntities("AlertDefinitionService", {site: 
Site.current().tags.site, application: 
"userProfile"})._promise.then(function(data) {
+                       $scope.algorithmEntity = common.getValueByPath(data, 
"obj[0]");
+                       $scope.algorithmEntity.policy = 
common.parseJSON($scope.algorithmEntity.policyDef);
+               });
+
+               // ======================================= User profile 
=======================================
+               $scope.profileList = Entities.queryEntities("MLModelService", 
{site: Site.current().tags.site}, ["user", "algorithm", "content", "version"]);
+               $scope.profileList._promise.then(function() {
+                       var _algorithms = {};
+                       var _users = {};
+
+                       // Map user
+                       $.each($scope.profileList, function(i, unit) {
+                               _algorithms[unit.tags.algorithm] = 
unit.tags.algorithm;
+                               var _user = _users[unit.tags.user] = 
_users[unit.tags.user] || {user: unit.tags.user};
+                               _user[unit.tags.algorithm] = {
+                                       version: unit.version
+                               };
+
+                               // DE
+                               if(unit.tags.algorithm === "DE") {
+                                       var _statistics = 
common.parseJSON(unit.content);
+                                       _statistics = 
common.getValueByPath(_statistics, "statistics", []);
+                                       _user[unit.tags.algorithm].topCommands 
= $.map(common.array.top(_statistics, "mean"), function(command) {
+                                               return command.commandName;
+                                       });
+                               }
+                       });
+
+                       // Map algorithms
+                       $scope.algorithms = $.map(_algorithms, 
function(algorithm) {
+                               return algorithm;
+                       }).sort();
+
+                       $scope.profileList.splice(0);
+                       $scope.profileList.push.apply($scope.profileList, 
common.map.toArray(_users));
+               });
+
+               // =========================================== Task 
===========================================
+               $scope.tasks = [];
+               function _loadTasks() {
+                       var _tasks = 
Entities.queryEntities("ScheduleTaskService", {
+                               site: Site.current().tags.site,
+                               _pageSize: 100,
+                               _duration: 1000 * 60 * 60 * 24 * 14,
+                               __ETD: 1000 * 60 * 60 * 24
+                       });
+                       _tasks._promise.then(function() {
+                               $scope.tasks.splice(0);
+                               $scope.tasks.push.apply($scope.tasks, _tasks);
+
+                               // Duration
+                               $.each($scope.tasks, function(i, data) {
+                                       if(data.timestamp && data.updateTime) {
+                                               var _ms = (new 
moment(data.updateTime)).diff(new moment(data.timestamp));
+                                               var _d = moment.duration(_ms);
+                                               data._duration = 
Math.floor(_d.asHours()) + moment.utc(_ms).format(":mm:ss");
+                                               data.duration = _ms;
+                                       } else {
+                                               data._duration = "--";
+                                       }
+                               });
+                       });
+               }
+
+               $scope.runningTaskCount = function () {
+                       return common.array.count($scope.tasks, "INITIALIZED", 
"status") +
+                               common.array.count($scope.tasks, "PENDING", 
"status") +
+                               common.array.count($scope.tasks, "EXECUTING", 
"status");
+               };
+
+               // Create task
+               $scope.updateTask = function() {
+                       $.dialog({
+                               title: "Confirm",
+                               content: "Do you want to update now?",
+                               confirm: true
+                       }, function(ret) {
+                               if(!ret) return;
+
+                               var _entity = {
+                                       status: "INITIALIZED",
+                                       detail: "Newly created command",
+                                       tags: {
+                                               site: Site.current().tags.site,
+                                               type: "USER_PROFILE_TRAINING"
+                                       },
+                                       timestamp: +new Date()
+                               };
+                               Entities.updateEntity("ScheduleTaskService", 
_entity, {timestamp: false})._promise.success(function(data) {
+                                       if(!Entities.dialog(data)) {
+                                               _loadTasks();
+                                       }
+                               });
+                       });
+               };
+
+               // Show detail
+               $scope.showTaskDetail = function(task) {
+                       var _content = $("<pre>").text(task.detail);
+
+                       var $mdl = $.dialog({
+                               title: "Detail",
+                               content: _content
+                       });
+
+                       _content.click(function(e) {
+                               if(!e.ctrlKey) return;
+
+                               $.dialog({
+                                       title: "Confirm",
+                                       content: "Remove this task?",
+                                       confirm: true
+                               }, function(ret) {
+                                       if(!ret) return;
+
+                                       $mdl.modal('hide');
+                                       
Entities.deleteEntity("ScheduleTaskService", task)._promise.then(function() {
+                                               _loadTasks();
+                                       });
+                               });
+                       });
+               };
+
+               _loadTasks();
+               var _loadInterval = $interval(_loadTasks, 
app.time.refreshInterval);
+               $scope.$on('$destroy',function(){
+                       $interval.cancel(_loadInterval);
+               });
+       });
+
+       // ======================= Profile Detail =======================
+       feature.controller('detail', function(PageConfig, Site, $scope, 
$wrapState, Entities) {
+               PageConfig.pageTitle = "User Profile";
+               PageConfig.pageSubTitle = Site.current().tags.site;
+               PageConfig
+                       .addNavPath("User Profile", "/userProfile/list")
+                       .addNavPath("Detail");
+
+               $scope.user = $wrapState.param.filter;
+
+               // User profile
+               $scope.profiles = {};
+               $scope.profileList = Entities.queryEntities("MLModelService", 
{site: Site.current().tags.site, user: $scope.user});
+               $scope.profileList._promise.then(function() {
+                       $.each($scope.profileList, function(i, unit) {
+                               unit._content = common.parseJSON(unit.content);
+                               $scope.profiles[unit.tags.algorithm] = unit;
+                       });
+
+                       // DE
+                       if($scope.profiles.DE) {
+                               console.log($scope.profiles.DE);
+
+                               $scope.profiles.DE._chart = {};
+
+                               $scope.profiles.DE.estimates = {};
+                               $.each($scope.profiles.DE._content, 
function(key, value) {
+                                       if(key !== "statistics") {
+                                               
$scope.profiles.DE.estimates[key] = value;
+                                       }
+                               });
+
+                               var _meanList = [];
+                               var _stddevList = [];
+
+                               $.each($scope.profiles.DE._content.statistics, 
function(i, unit) {
+                                       _meanList[i] = {
+                                               x: unit.commandName,
+                                               y: unit.mean
+                                       };
+                                       _stddevList[i] = {
+                                               x: unit.commandName,
+                                               y: unit.stddev
+                                       };
+                               });
+                               $scope.profiles.DE._chart.series = [
+                                       {
+                                               key: "mean",
+                                               values: _meanList
+                                       },
+                                       {
+                                               key: "stddev",
+                                               values: _stddevList
+                                       }
+                               ];
+
+                               // Percentage table list
+                               $scope.profiles.DE.meanList = [];
+                               var _total = 
common.array.sum($scope.profiles.DE._content.statistics, "mean");
+                               $.each($scope.profiles.DE._content.statistics, 
function(i, unit) {
+                                       $scope.profiles.DE.meanList.push({
+                                               command: unit.commandName,
+                                               percentage: unit.mean / _total
+                                       });
+                               });
+                       }
+
+                       // EigenDecomposition
+                       if($scope.profiles.EigenDecomposition && 
$scope.profiles.EigenDecomposition._content.principalComponents) {
+                               $scope.profiles.EigenDecomposition._chart = {
+                                       series: [],
+                               };
+
+                               
$.each($scope.profiles.EigenDecomposition._content.principalComponents, 
function(z, grp) {
+                                       var _line = [];
+                                       $.each(grp, function(x, y) {
+                                               _line.push([x,y,z]);
+                                       });
+
+                                       
$scope.profiles.EigenDecomposition._chart.series.push({
+                                               data: _line
+                                       });
+                               });
+                       }
+               });
+
+               // UI
+               $scope.showRawData = function(content) {
+                       $.dialog({
+                               title: "Raw Data",
+                               content: $("<pre>").text(content)
+                       });
+               };
+       });
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
 
b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
new file mode 100644
index 0000000..0f94e03
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
@@ -0,0 +1,87 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<div class="box box-primary">
+       <div class="box-header with-border">
+               <i class="fa fa-user"> </i>
+               <h3 class="box-title">
+                       {{user}}
+               </h3>
+       </div>
+       <div class="box-body">
+               <div>
+                       <div class="inline-group">
+                               <dl><dt>User</dt><dd>{{user}}</dd></dl>
+                               
<dl><dt>Site</dt><dd>{{Site.current().tags.site}}</dd></dl>
+                       </div>
+                       <div class="inline-group">
+                               <dl><dt>Other Info</dt><dd 
class="text-muted">N/A</dd></dl>
+                       </div>
+               </div>
+
+               <div class="overlay" 
ng-hide="profileList._promise.$$state.status === 1;">
+                       <span class="fa fa-refresh fa-spin"></span>
+               </div>
+       </div>
+</div>
+
+<!-- Analysis -->
+<div class="nav-tabs-custom">
+       <ul class="nav nav-tabs">
+               <li class="active">
+                       <a href="[data-id='DE']" data-toggle="tab" ng-click=" 
currentTab='DE'">DE</a>
+               </li>
+               <li>
+                       <a href="[data-id='EigenDecomposition']" 
data-toggle="tab" ng-click=" 
currentTab='EigenDecomposition'">EigenDecomposition</a>
+               </li>
+               <li class="pull-right">
+                       <button class="btn btn-primary" 
ng-click="showRawData(currentTab === 'EigenDecomposition' ? 
profiles.EigenDecomposition.content : profiles.DE.content)">Raw Data</button>
+               </li>
+       </ul>
+       <div class="tab-content">
+               <div class="tab-pane active" data-id="DE">
+                       <div class="row">
+                               <div class="col-md-9">
+                                       <div nvd3="profiles.DE._chart.series" 
data-config="{chart: 'column', xType: 'text', height: 400}" 
class="nvd3-chart-cntr" height="400"></div>
+                                       <div class="inline-group text-center">
+                                               <dl ng-repeat="(key, value) in 
profiles.DE.estimates"><dt>{{key}}</dt><dd>{{value}}</dd></dl>
+                                       </div>
+                               </div>
+
+                               <div class="col-md-3">
+                                       <table class="table table-bordered">
+                                               <thead>
+                                                       <tr>
+                                                               <th>Command</th>
+                                                               
<th>Percentage</th>
+                                                       </tr>
+                                               </thead>
+                                               <tbody>
+                                                       <tr ng-repeat="unit in 
profiles.DE.meanList">
+                                                               
<td>{{unit.command}}</td>
+                                                               <td 
class="text-right">{{(unit.percentage*100).toFixed(2)}}%</td>
+                                                       </tr>
+                                               </tbody>
+                                       </table>
+                               </div>
+                       </div>
+               </div><!-- /.tab-pane -->
+               <div class="tab-pane" data-id="EigenDecomposition">
+                       <div line3d-chart height="400" 
data="profiles.EigenDecomposition._chart.series"> </div>
+               </div><!-- /.tab-pane -->
+       </div><!-- /.tab-content -->
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
 
b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
new file mode 100644
index 0000000..2f14479
--- /dev/null
+++ 
b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
@@ -0,0 +1,138 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<div class="box box-primary">
+       <div class="box-header with-border">
+               <i class="fa fa-list-alt"> </i>
+               <h3 class="box-title">
+                       User Profiles
+                       <small><a data-toggle="collapse" 
href="[data-id='algorithms']">Detail</a></small>
+               </h3>
+               <div class="pull-right">
+                       <a class="label label-primary" 
ng-class="runningTaskCount() ? 'label-primary' : 'label-default'" 
data-toggle="modal" data-target="#taskMDL">Update</a>
+               </div>
+       </div>
+       <div class="box-body">
+               <!-- Algorithms -->
+               <div data-id="algorithms" class="collapse">
+                       <table class="table table-bordered">
+                               <thead>
+                                       <tr>
+                                               <th>Name</th>
+                                               <td>Feature</td>
+                                       </tr>
+                               </thead>
+                               <tbody>
+                                       <tr ng-repeat="algorithm in 
algorithmEntity.policy.algorithms">
+                                               <td>{{algorithm.name}}</td>
+                                               <td>{{algorithm.features}}</td>
+                                       </tr>
+                               </tbody>
+                       </table>
+                       <hr/>
+               </div>
+
+               <!-- User Profile List -->
+               <p ng-show="profileList._promise.$$state.status !== 1">
+                       <span class="fa fa-refresh fa-spin"> </span>
+                       Loading...
+               </p>
+
+               <div sorttable source="profileList" 
ng-show="profileList._promise.$$state.status === 1">
+                       <table class="table table-bordered" ng-non-bindable>
+                               <thead>
+                                       <tr>
+                                               <th width="10%" 
sortpath="user">User</th>
+                                               <th>Most Predominat Feature</th>
+                                               <th width="10"></th>
+                                       </tr>
+                               </thead>
+                               <tbody>
+                                       <tr>
+                                               <td>
+                                                       <a 
href="#/userProfile/detail/{{item.user}}">{{item.user}}</a>
+                                               </td>
+                                               <td>
+                                                       
{{item.DE.topCommands.slice(0,3).join(", ")}}
+                                               </td>
+                                               <td>
+                                                       <a 
href="#/userProfile/detail/{{item.user}}">Detail</a>
+                                               </td>
+                                       </tr>
+                               </tbody>
+                       </table>
+               </div>
+       </div>
+</div>
+
+<!-- Modal: User profile Schedule Task -->
+<div class="modal fade" id="taskMDL" tabindex="-1" role="dialog">
+       <div class="modal-dialog modal-lg" role="document">
+               <div class="modal-content">
+                       <div class="modal-header">
+                               <button type="button" class="close" 
data-dismiss="modal" aria-label="Close">
+                                       <span aria-hidden="true">&times;</span>
+                               </button>
+                               <h4 class="modal-title">Training History</h4>
+                       </div>
+                       <div class="modal-body">
+                               <div sorttable source="tasks">
+                                       <table class="table table-bordered" 
ng-non-bindable>
+                                               <thead>
+                                                       <tr>
+                                                               <th 
sortpath="tags.type">Command</th>
+                                                               <th 
sortpath="timestamp">Start Time</th>
+                                                               <th 
sortpath="updateTime">Update Time</th>
+                                                               <th 
sortpath="duration">Duration</th>
+                                                               <th 
sortpath="status">Status</th>
+                                                               <th width="10"> 
</th>
+                                                       </tr>
+                                               </thead>
+                                               <tbody>
+                                                       <tr>
+                                                               
<td>{{item.tags.type}}</td>
+                                                               
<td>{{common.format.date(item.timestamp) || "--"}}</td>
+                                                               
<td>{{common.format.date(item.updateTime) || "--"}}</td>
+                                                               
<td>{{item._duration}}</td>
+                                                               <td 
class="text-nowrap">
+                                                                       <span 
class="fa fa-hourglass-start text-muted" ng-show="item.status === 
'INITIALIZED'"></span>
+                                                                       <span 
class="fa fa-hourglass-half text-info" ng-show="item.status === 
'PENDING'"></span>
+                                                                       <span 
class="fa fa-circle-o-notch text-primary" ng-show="item.status === 
'EXECUTING'"></span>
+                                                                       <span 
class="fa fa-check-circle text-success" ng-show="item.status === 
'SUCCEEDED'"></span>
+                                                                       <span 
class="fa fa-exclamation-circle text-danger" ng-show="item.status === 
'FAILED'"></span>
+                                                                       <span 
class="fa fa-ban text-muted" ng-show="item.status === 'CANCELED'"></span>
+                                                                       
{{item.status}}
+                                                               </td>
+                                                               <td>
+                                                                       <a 
ng-click="_parent.showTaskDetail(item)">Detail</a>
+                                                               </td>
+                                                       </tr>
+                                               </tbody>
+                                       </table>
+                               </div>
+                       </div>
+                       <div class="modal-footer">
+                               <button type="button" class="btn btn-primary 
pull-left" ng-click="updateTask()" ng-show="Auth.isRole('ROLE_ADMIN')">
+                                       Update Now
+                               </button>
+                               <button type="button" class="btn btn-default" 
data-dismiss="modal">
+                                       Close
+                               </button>
+                       </div>
+               </div>
+       </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/images/favicon.png
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/images/favicon.png 
b/eagle-webservice/src/main/webapp/_app/public/images/favicon.png
new file mode 100644
index 0000000..3bede2a
Binary files /dev/null and 
b/eagle-webservice/src/main/webapp/_app/public/images/favicon.png differ

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png
----------------------------------------------------------------------
diff --git 
a/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png 
b/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png
new file mode 100644
index 0000000..9879e92
Binary files /dev/null and 
b/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png differ

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/app.config.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/app.config.js 
b/eagle-webservice/src/main/webapp/_app/public/js/app.config.js
new file mode 100644
index 0000000..d7c4be9
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/app.config.js
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+       'use strict';
+
+       app.config = {
+               // 
============================================================================
+               // =                                   URLs                     
              =
+               // 
============================================================================
+               urls: {
+                       HOST: '..',
+
+                       updateEntity: 
'rest/entities?serviceName=${serviceName}',
+                       queryEntity: 
'rest/entities/rowkey?serviceName=${serviceName}&value=${encodedRowkey}',
+                       queryEntities: 
'rest/entities?query=${serviceName}[${condition}]{${values}}&pageSize=100000',
+                       deleteEntity: 
'rest/entities/delete?serviceName=${serviceName}&byId=true',
+                       deleteEntities: 
'rest/entities?query=${serviceName}[${condition}]{*}&pageSize=100000',
+
+                       queryGroup: 
'rest/entities?query=${serviceName}[${condition}]<${groupBy}>{${values}}&pageSize=100000',
+                       querySeries: 
'rest/entities?query=${serviceName}[${condition}]<${groupBy}>{${values}}&pageSize=100000&timeSeries=true&intervalmin=${intervalmin}',
+
+                       query: 'rest/',
+
+                       userProfile: 'rest/authentication',
+                       logout: 'logout',
+
+                       maprNameResolver: '../rest/maprNameResolver',
+
+                       DELETE_HOOK: {
+                               FeatureDescService: 
'rest/module/feature?feature=${feature}',
+                               ApplicationDescService: 
'rest/module/application?application=${application}',
+                               SiteDescService: 
'rest/module/site?site=${site}',
+                               TopologyDescriptionService: 
'rest/app/topology?topology=${topology}'
+                       },
+                       UPDATE_HOOK: {
+                               SiteDescService: 'rest/module/siteApplication'
+                       }
+               },
+       };
+
+       // 
============================================================================
+       // =                                   URLs                             
      =
+       // 
============================================================================
+       app.getURL = function(name, kvs) {
+               var _path = app.config.urls[name];
+               if(!_path) throw "URL:'" + name + "' not exist!";
+               var _url = app.packageURL(_path);
+               if(kvs !== undefined) {
+                       _url = common.template(_url, kvs);
+               }
+               return _url;
+       };
+
+       app.getMapRNameResolverURL = function(name,value, site) {
+               var key = "maprNameResolver";
+               var _path = app.config.urls[key];
+               if(!_path) throw "URL:'" + name + "' not exist!";
+               var _url = _path;
+               if(name == "fNameResolver") {
+                       _url +=  "/" + name + "?fName=" + value + "&site=" + 
site;
+               } else if(name == "sNameResolver") {
+                       _url +=  "/" + name + "?sName=" + value + "&site=" + 
site;
+               } else if (name == "vNameResolver") {
+                       _url += "/" + name + "?vName=" + value + "&site=" + 
site;
+               } else{
+                       throw "resolver:'" + name + "' not exist!";
+               }
+               return _url;
+       };
+
+       function getHookURL(hookType, serviceName) {
+               var _path = app.config.urls[hookType][serviceName];
+               if(!_path) return null;
+
+               return app.packageURL(_path);
+       }
+
+       /***
+        * Eagle support delete function to process special entity delete. 
Which will delete all the relative entity.
+        * @param serviceName
+        */
+       app.getDeleteURL = function(serviceName) {
+               return getHookURL('DELETE_HOOK', serviceName);
+       };
+
+       /***
+        * Eagle support update function to process special entity update. 
Which will update all the relative entity.
+        * @param serviceName
+        */
+       app.getUpdateURL = function(serviceName) {
+               return getHookURL('UPDATE_HOOK', serviceName);
+       };
+
+       app.packageURL = function (path) {
+               var _host = localStorage.getItem("HOST") || 
app.config.urls.HOST;
+               return (_host ? _host + "/" : '') + path;
+       };
+
+       app._Host = function(host) {
+               if(host) {
+                       localStorage.setItem("HOST", host);
+                       return app;
+               }
+               return localStorage.getItem("HOST");
+       };
+       app._Host.clear = function() {
+               localStorage.removeItem("HOST");
+       };
+       app._Host.sample = "http://localhost:9099/eagle-service";;
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/app.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/app.js 
b/eagle-webservice/src/main/webapp/_app/public/js/app.js
new file mode 100644
index 0000000..70b4afe
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/app.js
@@ -0,0 +1,499 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var app = {};
+
+(function() {
+       'use strict';
+
+       /* App Module */
+       var eagleApp = angular.module('eagleApp', ['ngRoute', 'ngAnimate', 
'ui.router', 'eagleControllers', 'featureControllers', 'eagle.service']);
+
+       // GRUNT REPLACEMENT: eagleApp.buildTimestamp = TIMESTAMP
+       eagleApp._TRS = function() {
+               return eagleApp.buildTimestamp || Math.random();
+       };
+
+       // 
======================================================================================
+       // =                                   Feature Module                   
                =
+       // 
======================================================================================
+       var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
+       var FN_ARG_SPLIT = /,/;
+       var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
+       var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
+
+       var featureControllers = angular.module('featureControllers', 
['ui.bootstrap', 'eagle.components']);
+       var featureControllerCustomizeHtmlTemplate = {};
+       var featureControllerProvider;
+       var featureProvider;
+
+       featureControllers.config(function ($controllerProvider, $provide) {
+               featureControllerProvider = $controllerProvider;
+               featureProvider = $provide;
+       });
+
+       featureControllers.service("Feature", function($wrapState, PageConfig, 
ConfigPageConfig, FeaturePageConfig) {
+               var _features = {};
+               var _services = {};
+
+               var Feature = function(name, config) {
+                       this.name = name;
+                       this.config = config || {};
+                       this.features = {};
+               };
+
+               /***
+                * Inner function. Replace the dependency of constructor.
+                * @param constructor
+                * @private
+                */
+               Feature.prototype._replaceDependencies = function(constructor) {
+                       var i, srvName;
+                       var _constructor, _$inject;
+                       var fnText, argDecl;
+
+                       if($.isArray(constructor)) {
+                               _constructor = constructor[constructor.length - 
1];
+                               _$inject = constructor.slice(0, -1);
+                       } else if(constructor.$inject) {
+                               _constructor = constructor;
+                               _$inject = constructor.$inject;
+                       } else {
+                               _$inject = [];
+                               _constructor = constructor;
+                               fnText = 
constructor.toString().replace(STRIP_COMMENTS, '');
+                               argDecl = fnText.match(FN_ARGS);
+                               $.each(argDecl[1].split(FN_ARG_SPLIT), 
function(i, arg) {
+                                       arg.replace(FN_ARG, function(all, 
underscore, name) {
+                                               _$inject.push(name);
+                                       });
+                               });
+                       }
+                       _constructor.$inject = _$inject;
+
+                       for(i = 0 ; i < _$inject.length ; i += 1) {
+                               srvName = _$inject[i];
+                               _$inject[i] = this.features[srvName] || 
_$inject[i];
+                       }
+
+                       return _constructor;
+               };
+
+               /***
+                * Register a common service for feature usage. Common service 
will share between the feature. If you are coding customize feature, use 
'Feature.service' is the better way.
+                * @param name
+                * @param constructor
+                */
+               Feature.prototype.commonService = function(name, constructor) {
+                       if(!_services[name]) {
+                               featureProvider.service(name, constructor);
+                               _services[name] = this.name;
+                       } else {
+                               throw "Service '" + name + "' has already be 
registered by feature '" + _services[name] + "'";
+                       }
+               };
+
+               /***
+                * Register a service for feature usage.
+                * @param name
+                * @param constructor
+                */
+               Feature.prototype.service = function(name, constructor) {
+                       var _serviceName;
+                       if(!this.features[name]) {
+                               _serviceName = "__FEATURE_" + this.name + "_" + 
name;
+                               featureProvider.service(_serviceName, 
this._replaceDependencies(constructor));
+                               this.features[name] = _serviceName;
+                       } else {
+                               console.warn("Service '" + name + "' has 
already be registered.");
+                       }
+               };
+
+               /***
+                * Create an navigation item in left navigation bar
+                * @param path
+                * @param title
+                * @param icon use Font Awesome. Needn't with 'fa fa-'.
+                */
+               Feature.prototype.navItem = function(path, title, icon) {
+                       title = title || path;
+                       icon = icon || "question";
+
+                       FeaturePageConfig.addNavItem(this.name, {
+                               icon: icon,
+                               title: title,
+                               url: "#/" + this.name + "/" + path
+                       });
+               };
+
+               /***
+                * Register a controller.
+                * @param name
+                * @param constructor
+                */
+               Feature.prototype.controller = function(name, constructor, 
htmlTemplatePath) {
+                       var _name = this.name + "_" + name;
+
+                       // Replace feature registered service
+                       constructor = this._replaceDependencies(constructor);
+
+                       // Register controller
+                       featureControllerProvider.register(_name, constructor);
+                       if(htmlTemplatePath) {
+                               featureControllerCustomizeHtmlTemplate[_name] = 
htmlTemplatePath;
+                       }
+
+                       return _name;
+               };
+
+               /***
+                * Register a configuration controller for admin usage.
+                * @param name
+                * @param constructor
+                */
+               Feature.prototype.configController = function(name, 
constructor, htmlTemplatePath) {
+                       var _name = "config_" + this.name + "_" + name;
+
+                       // Replace feature registered service
+                       constructor = this._replaceDependencies(constructor);
+
+                       // Register controller
+                       featureControllerProvider.register(_name, constructor);
+                       if(htmlTemplatePath) {
+                               featureControllerCustomizeHtmlTemplate[_name] = 
htmlTemplatePath;
+                       }
+
+                       return _name;
+               };
+
+               /***
+                * Create an navigation item in left navigation bar for admin 
configuraion page
+                * @param path
+                * @param title
+                * @param icon use Font Awesome. Needn't with 'fa fa-'.
+                */
+               Feature.prototype.configNavItem = function(path, title, icon) {
+                       title = title || path;
+                       icon = icon || "question";
+
+                       ConfigPageConfig.addNavItem(this.name, {
+                               icon: icon,
+                               title: title,
+                               url: "#/config/" + this.name + "/" + path
+                       });
+               };
+
+               // Register
+               featureControllers.register = Feature.register = 
function(featureName, config) {
+                       _features[featureName] = _features[featureName] || new 
Feature(featureName, config);
+                       return _features[featureName];
+               };
+
+               // Page go
+               Feature.go = function(feature, page, filter) {
+                       if(!filter) {
+                               $wrapState.go("page", {
+                                       feature: feature,
+                                       page: page
+                               }, 2);
+                       } else {
+                               $wrapState.go("pageFilter", {
+                                       feature: feature,
+                                       page: page,
+                                       filter: filter
+                               }, 2);
+                       }
+               };
+
+               // Get feature by name
+               Feature.get = function (featureName) {
+                       return _features[featureName];
+               };
+
+               return Feature;
+       });
+
+       // 
======================================================================================
+       // =                                   Router config                    
                =
+       // 
======================================================================================
+       eagleApp.config(function ($stateProvider, $urlRouterProvider, 
$animateProvider) {
+               // Resolve
+               function _resolve(config) {
+                       config = config || {};
+
+                       var resolve = {
+                               Site: function (Site) {
+                                       return Site._promise();
+                               },
+                               Authorization: function (Authorization) {
+                                       if(!config.roleType) {
+                                               return Authorization._promise();
+                                       } else {
+                                               return 
Authorization.rolePromise(config.roleType);
+                                       }
+                               },
+                               Application: function (Application) {
+                                       return Application._promise();
+                               }
+                       };
+
+                       if(config.featureCheck) {
+                               resolve._navigationCheck = function($q, 
$wrapState, Site, Application) {
+                                       var _deferred = $q.defer();
+
+                                       $q.all(Site._promise(), 
Application._promise()).then(function() {
+                                               var _match, i, tmpApp;
+                                               var _site = Site.current();
+                                               var _app = 
Application.current();
+
+                                               // Check application
+                                               if(_site && (
+                                                       !_app ||
+                                                       
!_site.applicationList.set[_app.tags.application] ||
+                                                       
!_site.applicationList.set[_app.tags.application].enabled
+                                                       )
+                                               ) {
+                                                       _match = false;
+
+                                                       for(i = 0 ; i < 
_site.applicationGroupList.length ; i += 1) {
+                                                               tmpApp = 
_site.applicationGroupList[i].enabledList[0];
+                                                               if(tmpApp) {
+                                                                       _app = 
Application.current(tmpApp);
+                                                                       _match 
= true;
+                                                                       break;
+                                                               }
+                                                       }
+
+                                                       if(!_match) {
+                                                               _app = null;
+                                                               
Application.current(null);
+                                                       }
+                                               }
+                                       }).finally(function() {
+                                               _deferred.resolve();
+                                       });
+
+                                       return _deferred.promise;
+                               };
+                       }
+
+                       return resolve;
+               }
+
+               // Router
+               var _featureBase = {
+                       templateUrl: function ($stateParams) {
+                               var _htmlTemplate = 
featureControllerCustomizeHtmlTemplate[$stateParams.feature + "_" + 
$stateParams.page];
+                               return  "public/feature/" + 
$stateParams.feature + "/page/" + (_htmlTemplate ||  $stateParams.page) + 
".html?_=" + eagleApp._TRS();
+                       },
+                       controllerProvider: function ($stateParams) {
+                               return $stateParams.feature + "_" + 
$stateParams.page;
+                       },
+                       resolve: _resolve({featureCheck: true}),
+                       pageConfig: "FeaturePageConfig"
+               };
+
+               $urlRouterProvider.otherwise("/landing");
+               $stateProvider
+                       // =================== Landing ===================
+                       .state('landing', {
+                               url: "/landing",
+                               templateUrl: "partials/landing.html?_=" + 
eagleApp._TRS(),
+                               controller: "landingCtrl",
+                               resolve: _resolve({featureCheck: true})
+                       })
+
+                       // ================ Authorization ================
+                       .state('login', {
+                               url: "/login",
+                               templateUrl: "partials/login.html?_=" + 
eagleApp._TRS(),
+                               controller: "authLoginCtrl",
+                               access: {skipCheck: true}
+                       })
+
+                       // ================ Configuration ================
+                       // Site
+                       .state('configSite', {
+                               url: "/config/site",
+                               templateUrl: "partials/config/site.html?_=" + 
eagleApp._TRS(),
+                               controller: "configSiteCtrl",
+                               pageConfig: "ConfigPageConfig",
+                               resolve: _resolve({roleType: 'ROLE_ADMIN'})
+                       })
+
+                       // Application
+                       .state('configApplication', {
+                               url: "/config/application",
+                               templateUrl: 
"partials/config/application.html?_=" + eagleApp._TRS(),
+                               controller: "configApplicationCtrl",
+                               pageConfig: "ConfigPageConfig",
+                               resolve: _resolve({roleType: 'ROLE_ADMIN'})
+                       })
+
+                       // Feature
+                       .state('configFeature', {
+                               url: "/config/feature",
+                               templateUrl: "partials/config/feature.html?_=" 
+ eagleApp._TRS(),
+                               controller: "configFeatureCtrl",
+                               pageConfig: "ConfigPageConfig",
+                               resolve: _resolve({roleType: 'ROLE_ADMIN'})
+                       })
+
+                       // Feature configuration page
+                       .state('configFeatureDetail', $.extend({url: 
"/config/:feature/:page"}, {
+                               templateUrl: function ($stateParams) {
+                                       var _htmlTemplate = 
featureControllerCustomizeHtmlTemplate[$stateParams.feature + "_" + 
$stateParams.page];
+                                       return  "public/feature/" + 
$stateParams.feature + "/page/" + (_htmlTemplate ||  $stateParams.page) + 
".html?_=" + eagleApp._TRS();
+                               },
+                               controllerProvider: function ($stateParams) {
+                                       return "config_" + $stateParams.feature 
+ "_" + $stateParams.page;
+                               },
+                               pageConfig: "ConfigPageConfig",
+                               resolve: _resolve({roleType: 'ROLE_ADMIN'})
+                       }))
+
+                       // =================== Feature ===================
+                       // Dynamic feature page
+                       .state('page', $.extend({url: "/:feature/:page"}, 
_featureBase))
+                       .state('pageFilter', $.extend({url: 
"/:feature/:page/:filter"}, _featureBase))
+               ;
+
+               // Animation
+               $animateProvider.classNameFilter(/^((?!(fa-spin)).)*$/);
+               $animateProvider.classNameFilter(/^((?!(tab-pane)).)*$/);
+       });
+
+       eagleApp.filter('parseJSON', function () {
+               return function (input, defaultVal) {
+                       return common.parseJSON(input, defaultVal);
+               };
+       });
+
+       eagleApp.filter('split', function () {
+               return function (input, regex) {
+                       return input.split(regex);
+               };
+       });
+
+       eagleApp.filter('reverse', function () {
+               return function (items) {
+                       return items.slice().reverse();
+               };
+       });
+
+       // 
======================================================================================
+       // =                                   Main Controller                  
                =
+       // 
======================================================================================
+       eagleApp.controller('MainCtrl', function ($scope, $wrapState, $http, 
$injector, ServiceError, PageConfig, FeaturePageConfig, Site, Authorization, 
Entities, nvd3, Application, Feature, UI) {
+               window.serviceError = $scope.ServiceError = ServiceError;
+               window.site = $scope.Site = Site;
+               window.auth = $scope.Auth = Authorization;
+               window.entities = $scope.Entities = Entities;
+               window.application = $scope.Application = Application;
+               window.pageConfig = $scope.PageConfig = PageConfig;
+               window.featurePageConfig = $scope.FeaturePageConfig = 
FeaturePageConfig;
+               window.feature = $scope.Feature = Feature;
+               window.ui = $scope.UI = UI;
+               window.nvd3 = nvd3;
+               $scope.app = app;
+               $scope.common = common;
+
+               Object.defineProperty(window, "scope",{
+                       get: function() {
+                               return angular.element("[ui-view]").scope();
+                       }
+               });
+
+               // Clean up
+               $scope.$on('$stateChangeStart', function (event, next, 
nextParam, current, currentParam) {
+                       console.log("[Switch] current ->", current, 
currentParam);
+                       console.log("[Switch] next ->", next, nextParam);
+                       // Page initialization
+                       PageConfig.reset();
+
+                       // Dynamic navigation list
+                       if(next.pageConfig) {
+                               $scope.PageConfig.navConfig = 
$injector.get(next.pageConfig);
+                       } else {
+                               $scope.PageConfig.navConfig = {};
+                       }
+
+                       // Authorization
+                       // > Login check
+                       if (!common.getValueByPath(next, "access.skipCheck", 
false)) {
+                               if (!Authorization.isLogin) {
+                                       console.log("[Authorization] Need 
access. Redirect...");
+                                       $wrapState.go("login");
+                               }
+                       }
+
+                       // > Role control
+                       /*var _roles = common.getValueByPath(next, 
"access.roles", []);
+                       if (_roles.length && Authorization.userProfile.roles) {
+                               var _roleMatch = false;
+                               $.each(_roles, function (i, roleName) {
+                                       if (Authorization.isRole(roleName)) {
+                                               _roleMatch = true;
+                                               return false;
+                                       }
+                               });
+
+                               if (!_roleMatch) {
+                                       $wrapState.path("/dam");
+                               }
+                       }*/
+               });
+
+               $scope.$on('$stateChangeError', function (event, next, 
nextParam, current, currentParam, error) {
+                       console.error("[Switch] Error", arguments);
+               });
+
+               // Get side bar navigation item class
+               $scope.getNavClass = function (page) {
+                       var path = page.url.replace(/^#/, '');
+
+                       if ($wrapState.path() === path) {
+                               PageConfig.pageTitle = PageConfig.pageTitle || 
page.title;
+                               return "active";
+                       } else {
+                               return "";
+                       }
+               };
+
+               // Get side bar navigation item class visible
+               $scope.getNavVisible = function (page) {
+                       if (!page.roles) return true;
+
+                       for (var i = 0; i < page.roles.length; i += 1) {
+                               var roleName = page.roles[i];
+                               if (Authorization.isRole(roleName)) {
+                                       return true;
+                               }
+                       }
+
+                       return false;
+               };
+
+               // Authorization
+               $scope.logout = function () {
+                       console.log("[Authorization] Logout. Redirect...");
+                       Authorization.logout();
+                       $wrapState.go("login");
+               };
+       });
+})();
\ No newline at end of file

Reply via email to