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">×</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">×</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">×</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">×</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">×</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