You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by nj...@apache.org on 2017/12/02 17:24:04 UTC

[11/19] kylin git commit: APACHE-KYLIN-2726 Introduce a dashboard for showing kylin service related metrics, like query count, query latency, job count, etc

APACHE-KYLIN-2726 Introduce a dashboard for showing kylin service related metrics, like query count, query latency, job count, etc


Project: http://git-wip-us.apache.org/repos/asf/kylin/repo
Commit: http://git-wip-us.apache.org/repos/asf/kylin/commit/6236bfd1
Tree: http://git-wip-us.apache.org/repos/asf/kylin/tree/6236bfd1
Diff: http://git-wip-us.apache.org/repos/asf/kylin/diff/6236bfd1

Branch: refs/heads/master
Commit: 6236bfd1a9de325816241394d2b11249b469825c
Parents: c27455f
Author: liapan <li...@ebay.com>
Authored: Fri Nov 17 14:56:12 2017 +0800
Committer: Zhong <nj...@apache.org>
Committed: Sat Dec 2 23:43:43 2017 +0800

----------------------------------------------------------------------
 .../rest/controller/DashboardController.java    | 129 +++++++
 .../apache/kylin/rest/service/BasicService.java |   5 +
 .../kylin/rest/service/DashboardService.java    | 333 +++++++++++++++++++
 .../apache/kylin/rest/service/QueryService.java |   8 +
 webapp/app/index.html                           |   8 +-
 webapp/app/js/controllers/dashboard.js          | 296 +++++++++++++++++
 webapp/app/js/controllers/job.js                |   3 +
 webapp/app/js/controllers/query.js              |   3 +
 webapp/app/js/directives/directives.js          |  97 ++++++
 webapp/app/js/filters/filter.js                 |   9 +
 webapp/app/js/model/dashboardConfig.js          |  96 ++++++
 webapp/app/js/services/dashboard.js             |  95 ++++++
 webapp/app/less/app.less                        |  58 ++++
 webapp/app/partials/dashboard/dashboard.html    | 151 +++++++++
 webapp/app/partials/header.html                 |   3 +
 webapp/app/routes.json                          |   8 +
 webapp/bower.json                               |   8 +-
 webapp/grunt.json                               |   3 +
 18 files changed, 1309 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/server-base/src/main/java/org/apache/kylin/rest/controller/DashboardController.java
----------------------------------------------------------------------
diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/DashboardController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/DashboardController.java
new file mode 100644
index 0000000..35ba615
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/controller/DashboardController.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+*/
+
+
+package org.apache.kylin.rest.controller;
+
+import org.apache.kylin.cube.CubeInstance;
+import org.apache.kylin.metadata.project.ProjectInstance;
+import org.apache.kylin.metrics.MetricsManager;
+import org.apache.kylin.rest.request.SQLRequest;
+import org.apache.kylin.rest.response.MetricsResponse;
+import org.apache.kylin.rest.response.SQLResponse;
+import org.apache.kylin.rest.service.CubeService;
+import org.apache.kylin.rest.service.DashboardService;
+import org.apache.kylin.rest.service.QueryService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.List;
+
+@Controller
+@RequestMapping(value = "/dashboard")
+public class DashboardController extends BasicController {
+    private static final Logger logger = LoggerFactory.getLogger(DashboardController.class);
+
+    @Autowired
+    private DashboardService dashboardService;
+
+    @Autowired
+    private QueryService queryService;
+
+    @Autowired
+    private CubeService cubeService;
+
+    @RequestMapping(value = "/metric/cube", method = { RequestMethod.GET })
+    @ResponseBody
+    public MetricsResponse getCubeMetrics(@RequestParam(value = "projectName", required = false) String projectName, @RequestParam(value = "cubeName", required = false) String cubeName) {
+        checkAuthorization(projectName);
+        return dashboardService.getCubeMetrics(projectName, cubeName);
+    }
+
+    @RequestMapping(value = "/metric/query", method = RequestMethod.GET)
+    @ResponseBody
+    public MetricsResponse getQueryMetrics(@RequestParam(value = "projectName", required = false) String projectName, @RequestParam(value = "cubeName", required = false) String cubeName, @RequestParam(value = "startTime") String startTime, @RequestParam(value = "endTime") String endTime) {
+        checkAuthorization(projectName);
+        MetricsResponse queryMetrics = new MetricsResponse();
+        SQLRequest sqlRequest = new SQLRequest();
+        sqlRequest.setProject(MetricsManager.SYSTEM_PROJECT);
+        String sql = dashboardService.getQueryMetricsSQL(startTime, endTime, projectName, cubeName);
+        sqlRequest.setSql(sql);
+        SQLResponse sqlResponse = queryService.queryWithoutSecure(sqlRequest);
+        if(!sqlResponse.getIsException()){
+            queryMetrics.increase("queryCount", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(0)));
+            queryMetrics.increase("avgQueryLatency", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(1)));
+            queryMetrics.increase("maxQueryLatency", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(2)));
+            queryMetrics.increase("minQueryLatency", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(3)));
+        }
+        return queryMetrics;
+    }
+
+    @RequestMapping(value = "/metric/job", method = RequestMethod.GET)
+    @ResponseBody
+    public MetricsResponse getJobMetrics(@RequestParam(value = "projectName", required = false) String projectName, @RequestParam(value = "cubeName", required = false) String cubeName, @RequestParam(value = "startTime") String startTime, @RequestParam(value = "endTime") String endTime) {
+        checkAuthorization(projectName);
+        MetricsResponse jobMetrics = new MetricsResponse();
+        SQLRequest sqlRequest = new SQLRequest();
+        sqlRequest.setProject(MetricsManager.SYSTEM_PROJECT);
+        String sql = dashboardService.getJobMetricsSQL(startTime, endTime, projectName, cubeName);
+        sqlRequest.setSql(sql);
+        SQLResponse sqlResponse = queryService.queryWithoutSecure(sqlRequest);
+        if(!sqlResponse.getIsException()){
+            jobMetrics.increase("jobCount", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(0)));
+            jobMetrics.increase("avgJobBuildTime", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(1)));
+            jobMetrics.increase("maxJobBuildTime", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(2)));
+            jobMetrics.increase("minJobBuildTime", dashboardService.getMetricValue(sqlResponse.getResults().get(0).get(3)));
+        }
+        return jobMetrics;
+    }
+
+    @RequestMapping(value = "/chart/{category}/{metric}/{dimension}", method = RequestMethod.GET)
+    @ResponseBody
+    public MetricsResponse getChartData(@PathVariable String dimension, @PathVariable String metric, @PathVariable String category, @RequestParam(value = "projectName", required = false) String projectName, @RequestParam(value = "cubeName", required = false) String cubeName, @RequestParam(value = "startTime") String startTime, @RequestParam(value = "endTime") String endTime) {
+        checkAuthorization(projectName);
+        SQLRequest sqlRequest = new SQLRequest();
+        sqlRequest.setProject(MetricsManager.SYSTEM_PROJECT);
+        String sql = dashboardService.getChartSQL(startTime, endTime, projectName, cubeName, dimension, metric, category);
+        sqlRequest.setSql(sql);
+        return dashboardService.transformChartData(queryService.queryWithoutSecure(sqlRequest));
+    }
+
+    private void checkAuthorization(String projectName){
+        if (projectName!=null && !projectName.isEmpty()) {
+            ProjectInstance project = dashboardService.getProjectManager().getProject(projectName);
+            try {
+                dashboardService.checkAuthorization(project);
+            } catch (AccessDeniedException e) {
+                List<CubeInstance> cubes = cubeService.listAllCubes(null, projectName, null, true);
+                if (cubes.isEmpty()) {
+                    throw new AccessDeniedException("Access is denied");
+                }
+            }
+        } else {
+            dashboardService.checkAuthorization();
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
----------------------------------------------------------------------
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
index 0e9ee7a..f8f50f3 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
@@ -31,6 +31,7 @@ import org.apache.kylin.metadata.draft.DraftManager;
 import org.apache.kylin.metadata.model.DataModelManager;
 import org.apache.kylin.metadata.project.ProjectManager;
 import org.apache.kylin.metadata.streaming.StreamingManager;
+import org.apache.kylin.metrics.MetricsManager;
 import org.apache.kylin.source.kafka.KafkaConfigManager;
 import org.apache.kylin.storage.hybrid.HybridManager;
 
@@ -94,4 +95,8 @@ public abstract class BasicService {
         return TableACLManager.getInstance(getConfig());
     }
 
+    public MetricsManager getMetricsManager() {
+        return MetricsManager.getInstance();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/server-base/src/main/java/org/apache/kylin/rest/service/DashboardService.java
----------------------------------------------------------------------
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/DashboardService.java b/server-base/src/main/java/org/apache/kylin/rest/service/DashboardService.java
new file mode 100644
index 0000000..f1084f3
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/DashboardService.java
@@ -0,0 +1,333 @@
+/*
+ * 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.
+*/
+
+
+package org.apache.kylin.rest.service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.Lists;
+import org.apache.kylin.cube.CubeInstance;
+import org.apache.kylin.metadata.project.ProjectInstance;
+import org.apache.kylin.metadata.project.RealizationEntry;
+import org.apache.kylin.metadata.realization.RealizationType;
+import org.apache.kylin.metrics.MetricsManager;
+import org.apache.kylin.metrics.lib.impl.TimePropertyEnum;
+import org.apache.kylin.metrics.property.JobPropertyEnum;
+import org.apache.kylin.metrics.property.QueryPropertyEnum;
+import org.apache.kylin.rest.constant.Constant;
+import org.apache.kylin.rest.exception.BadRequestException;
+import org.apache.kylin.rest.response.MetricsResponse;
+import org.apache.kylin.rest.response.SQLResponse;
+import org.apache.kylin.storage.hybrid.HybridInstance;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Component;
+
+import com.google.common.base.Strings;
+
+@Component("dashboardService")
+public class DashboardService extends BasicService {
+
+    private static final Logger logger = LoggerFactory.getLogger(DashboardService.class);
+
+    @Autowired
+    private CubeService cubeService;
+
+    private enum CategoryEnum {QUERY, JOB}
+
+    private enum QueryDimensionEnum {
+        PROJECT(QueryPropertyEnum.PROJECT.toString()),
+        CUBE(QueryPropertyEnum.REALIZATION.toString()),
+        DAY(TimePropertyEnum.DAY_DATE.toString()),
+        WEEK(TimePropertyEnum.WEEK_BEGIN_DATE.toString()),
+        MONTH(TimePropertyEnum.MONTH.toString());
+        private final String sql;
+
+        QueryDimensionEnum(String sql) {
+            this.sql = sql;
+        }
+
+        public String toSQL() {
+            return this.sql;
+        }
+    };
+
+    private enum JobDimensionEnum {
+        PROJECT(JobPropertyEnum.PROJECT.toString()),
+        CUBE(JobPropertyEnum.CUBE.toString()),
+        DAY(TimePropertyEnum.DAY_DATE.toString()),
+        WEEK(TimePropertyEnum.WEEK_BEGIN_DATE.toString()),
+        MONTH(TimePropertyEnum.MONTH.toString());
+        private final String sql;
+
+        JobDimensionEnum(String sql) {
+            this.sql = sql;
+        }
+
+        public String toSQL() {
+            return this.sql;
+        }
+    };
+
+    private enum QueryMetricEnum {
+        QUERY_COUNT("count(*)"),
+        AVG_QUERY_LATENCY("sum(" + QueryPropertyEnum.TIME_COST.toString() + ")/(count(" + QueryPropertyEnum.TIME_COST.toString() + "))"),
+        MAX_QUERY_LATENCY("max(" + QueryPropertyEnum.TIME_COST.toString() + ")"),
+        MIN_QUERY_LATENCY("min(" + QueryPropertyEnum.TIME_COST.toString() + ")");
+
+        private final String sql;
+
+        QueryMetricEnum(String sql) {
+            this.sql = sql;
+        }
+
+        public String toSQL() {
+            return this.sql;
+        }
+    }
+
+    private enum JobMetricEnum {
+        JOB_COUNT("count(*)"),
+        AVG_JOB_BUILD_TIME("sum(" + JobPropertyEnum.PER_BYTES_TIME_COST.toString() + ")/count(" + JobPropertyEnum.PER_BYTES_TIME_COST + ")"),
+        MAX_JOB_BUILD_TIME("max(" + JobPropertyEnum.PER_BYTES_TIME_COST.toString() + ")"),
+        MIN_JOB_BUILD_TIME("min(" + JobPropertyEnum.PER_BYTES_TIME_COST.toString() + ")");
+
+        private final String sql;
+
+        JobMetricEnum(String sql) {
+            this.sql = sql;
+        }
+
+        public String toSQL() {
+            return this.sql;
+        }
+    }
+
+    public MetricsResponse getCubeMetrics(String projectName, String cubeName) {
+        MetricsResponse cubeMetrics = new MetricsResponse();
+        Float totalCubeSize = 0f;
+        long totalRecoadSize = 0;
+        List<CubeInstance> cubeInstances = cubeService.listAllCubes(cubeName, projectName, null, true);
+        Integer totalCube = cubeInstances.size();
+        if (projectName == null) {
+            totalCube += getHybridManager().listHybridInstances().size();
+        } else {
+            ProjectInstance project = getProjectManager().getProject(projectName);
+            totalCube +=  project.getRealizationCount(RealizationType.HYBRID);
+        }
+        Float minCubeExpansion = Float.POSITIVE_INFINITY;
+        Float maxCubeExpansion = Float.NEGATIVE_INFINITY;
+        cubeMetrics.increase("totalCube", totalCube.floatValue());
+        for (CubeInstance cubeInstance : cubeInstances) {
+            if (cubeInstance.getInputRecordSize() > 0) {
+                totalCubeSize += cubeInstance.getSizeKB();
+                totalRecoadSize += cubeInstance.getInputRecordSize();
+                Float cubeExpansion = new Float(cubeInstance.getSizeKB()) * 1024 / cubeInstance.getInputRecordSize();
+                if (cubeExpansion > maxCubeExpansion) {
+                    maxCubeExpansion = cubeExpansion;
+                }
+                if (cubeExpansion < minCubeExpansion) {
+                    minCubeExpansion = cubeExpansion;
+                }
+            }
+        }
+        Float avgCubeExpansion = 0f;
+        if (totalRecoadSize != 0) {
+            avgCubeExpansion = totalCubeSize * 1024 / totalRecoadSize;
+        }
+        cubeMetrics.increase("avgCubeExpansion", avgCubeExpansion);
+        cubeMetrics.increase("maxCubeExpansion", maxCubeExpansion == Float.NEGATIVE_INFINITY ? 0 : maxCubeExpansion);
+        cubeMetrics.increase("minCubeExpansion", minCubeExpansion == Float.POSITIVE_INFINITY ? 0 : minCubeExpansion);
+        return cubeMetrics;
+    }
+
+    private List<CubeInstance> getCubeByHybrid(HybridInstance hybridInstance) {
+        List<CubeInstance> cubeInstances = Lists.newArrayList();
+        List<RealizationEntry> realizationEntries = hybridInstance.getRealizationEntries();
+        for (RealizationEntry realizationEntry : realizationEntries) {
+            String reName = realizationEntry.getRealization();
+            if (RealizationType.CUBE == realizationEntry.getType()) {
+                CubeInstance cubeInstance = getCubeManager().getCube(reName);
+                cubeInstances.add(cubeInstance);
+            } else if (RealizationType.HYBRID == realizationEntry.getType()) {
+                HybridInstance innerHybridInstance = getHybridManager().getHybridInstance(reName);
+                cubeInstances.addAll(getCubeByHybrid(innerHybridInstance));
+            }
+        }
+        return cubeInstances;
+    }
+
+    public String getQueryMetricsSQL(String startTime, String endTime, String projectName, String cubeName) {
+        String[] metrics = new String[] {QueryMetricEnum.QUERY_COUNT.toSQL(), QueryMetricEnum.AVG_QUERY_LATENCY.toSQL(), QueryMetricEnum.MAX_QUERY_LATENCY.toSQL(), QueryMetricEnum.MIN_QUERY_LATENCY.toSQL()};
+        List<String> filters = getBaseFilters(CategoryEnum.QUERY, projectName, startTime, endTime);
+        filters = addCubeFilter(filters, CategoryEnum.QUERY, cubeName);
+        return createSql(null, metrics, getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectQuery()), filters.toArray(new String[filters.size()]));
+    }
+
+    public String getJobMetricsSQL(String startTime, String endTime, String projectName, String cubeName) {
+        String[] metrics = new String[] {JobMetricEnum.JOB_COUNT.toSQL(), JobMetricEnum.AVG_JOB_BUILD_TIME.toSQL(), JobMetricEnum.MAX_JOB_BUILD_TIME.toSQL(), JobMetricEnum.MIN_JOB_BUILD_TIME.toSQL()};
+        List<String> filters = getBaseFilters(CategoryEnum.JOB, projectName, startTime, endTime);
+        filters = addCubeFilter(filters, CategoryEnum.JOB, cubeName);
+        return createSql(null, metrics, getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectJob()), filters.toArray(new String[filters.size()]));
+    }
+
+    public String getChartSQL(String startTime, String endTime, String projectName, String cubeName, String dimension, String metric, String category) {
+        try{
+            CategoryEnum categoryEnum = CategoryEnum.valueOf(category);
+            String table = "";
+            String[] dimensionSQL = null;
+            String[] metricSQL = null;
+
+            if(categoryEnum == CategoryEnum.QUERY) {
+                dimensionSQL = new String[] {QueryDimensionEnum.valueOf(dimension).toSQL()};
+                metricSQL = new String[] {QueryMetricEnum.valueOf(metric).toSQL()};
+                table = getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectQuery());
+            } else if (categoryEnum == CategoryEnum.JOB) {
+                dimensionSQL = new String[] {JobDimensionEnum.valueOf(dimension).toSQL()};
+                metricSQL = new String[] {JobMetricEnum.valueOf(metric).toSQL()};
+                table = getMetricsManager().getSystemTableFromSubject(getConfig().getKylinMetricsSubjectJob());
+            }
+
+            List<String> filters = getBaseFilters(categoryEnum, projectName, startTime, endTime);
+            filters = addCubeFilter(filters, categoryEnum, cubeName);
+
+            return createSql(dimensionSQL, metricSQL, table, filters.toArray(new String[filters.size()]));
+        } catch (IllegalArgumentException e) {
+            String message = "Generate dashboard chart sql failed. Please double check the input parameter: dimension, metric or category.";
+            logger.error(message, e);
+            throw new BadRequestException(message + " Caused by: " + e.getMessage(), null, e.getCause());
+        }
+    }
+
+    public MetricsResponse transformChartData(SQLResponse sqlResponse) {
+        if(!sqlResponse.getIsException()){
+            MetricsResponse metrics = new MetricsResponse();
+            List<List<String>> results = sqlResponse.getResults();
+            for (List<String> result : results) {
+                String dimension = result.get(0);
+                if (dimension !=null && !dimension.isEmpty()) {
+                    String metric = result.get(1);
+                    metrics.increase(dimension, getMetricValue(metric));
+                }
+            }
+            return  metrics;
+        }
+        return null;
+    }
+
+    public Float getMetricValue(String value) {
+        if (value == null || value.isEmpty()) {
+            return 0f;
+        } else {
+            return Float.valueOf(value);
+        }
+    }
+
+    @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN + " or hasPermission(#project, 'ADMINISTRATION')")
+    public void checkAuthorization(ProjectInstance project) throws AccessDeniedException {
+    }
+
+    @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN)
+    public void checkAuthorization() throws AccessDeniedException{
+    }
+
+    private List<String> getBaseFilters(CategoryEnum category, String projectName, String startTime, String endTime) {
+        List<String> filters = new ArrayList<String>();
+        String project = "";
+        if (category == CategoryEnum.QUERY) {
+            project = QueryDimensionEnum.PROJECT.toSQL();
+        } else {
+            project = JobDimensionEnum.PROJECT.toSQL();
+        }
+        filters.add(TimePropertyEnum.DAY_DATE.toString() + " >= '" + startTime + "'");
+        filters.add(TimePropertyEnum.DAY_DATE.toString() + " <= '" + endTime + "'");
+        if (!Strings.isNullOrEmpty(projectName)) {
+            filters.add(project + " ='" + ProjectInstance.getNormalizedProjectName(projectName) + "'");
+        } else {
+            filters.add(project + " <> '" + MetricsManager.SYSTEM_PROJECT + "'");
+        }
+        return filters;
+    }
+
+    private List<String> addCubeFilter(List<String> baseFilter, CategoryEnum category, String cubeName) {
+        if (category == CategoryEnum.QUERY) {
+            baseFilter.add(QueryPropertyEnum.EXCEPTION.toString() + " = 'NULL'");
+            if (!Strings.isNullOrEmpty(cubeName)) {
+                baseFilter.add(QueryPropertyEnum.REALIZATION + " = '" + cubeName + "'");
+            }
+        } else if (category == CategoryEnum.JOB && !Strings.isNullOrEmpty(cubeName)) {
+            HybridInstance hybridInstance = getHybridManager().getHybridInstance(cubeName);
+            if (null != hybridInstance) {
+                StringBuffer cubeNames = new StringBuffer();
+                for (CubeInstance cube:getCubeByHybrid(hybridInstance)) {
+                    cubeNames.append(",'" + cube.getName() + "'");
+                }
+                baseFilter.add(JobPropertyEnum.CUBE.toString() + " IN (" + cubeNames.substring(1) + ")");
+            } else {
+                baseFilter.add(JobPropertyEnum.CUBE.toString() + " ='" + cubeName + "'");
+            }
+        }
+        return baseFilter;
+    }
+
+    private String createSql(String[] dimensions, String[] metrics, String category, String[] filters) {
+        StringBuffer baseSQL = new StringBuffer("select ");
+        StringBuffer groupBy = new StringBuffer("");
+        if (dimensions != null && dimensions.length > 0) {
+            groupBy.append(" group by ");
+            StringBuffer dimensionSQL = new StringBuffer("");
+            for (String dimension : dimensions) {
+                dimensionSQL.append(",");
+                dimensionSQL.append(dimension);
+            }
+            baseSQL.append(dimensionSQL.substring(1));
+            groupBy.append(dimensionSQL.substring(1));
+        }
+        if (metrics != null && metrics.length > 0) {
+            StringBuffer metricSQL = new StringBuffer("");
+            for (String metric : metrics) {
+                metricSQL.append(",");
+                metricSQL.append(metric);
+            }
+            if (groupBy.length() > 0) {
+                baseSQL.append(metricSQL);
+            } else {
+                baseSQL.append(metricSQL.substring(1));
+            }
+        }
+        baseSQL.append(" from ");
+        baseSQL.append(category);
+        if (filters != null && filters.length > 0) {
+            StringBuffer filterSQL = new StringBuffer(" where ");
+            filterSQL.append(filters[0]);
+            for(int i = 1; i < filters.length; i++) {
+                filterSQL.append(" and ");
+                filterSQL.append(filters[i]);
+            }
+            baseSQL.append(filterSQL.toString());
+        }
+        baseSQL.append(groupBy);
+
+        return baseSQL.toString();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
----------------------------------------------------------------------
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
index 26b25d2..3f16646 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
@@ -371,7 +371,15 @@ public class QueryService extends BasicService {
         }
     }
 
+    public SQLResponse queryWithoutSecure(SQLRequest sqlRequest) {
+        return doQueryWithCache(sqlRequest, false);
+    }
+
     public SQLResponse doQueryWithCache(SQLRequest sqlRequest) {
+        return doQueryWithCache(sqlRequest, true);
+    }
+
+    public SQLResponse doQueryWithCache(SQLRequest sqlRequest, boolean secureEnabled) {
         Message msg = MsgPicker.getMsg();
         sqlRequest.setUsername(getUserName());
 

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/index.html
----------------------------------------------------------------------
diff --git a/webapp/app/index.html b/webapp/app/index.html
index d235b06..791aa4a 100644
--- a/webapp/app/index.html
+++ b/webapp/app/index.html
@@ -53,6 +53,7 @@
   <link rel="stylesheet" type="text/css" href="components/angular-toggle-switch/angular-toggle-switch.css">
   <link rel="stylesheet" type="text/css" href="components/angular-ui-select/dist/select.css">
   <link rel="stylesheet" type="text/css" href="components/angular-bootstrap-datetimepicker/src/css/datetimepicker.css">
+  <link rel="stylesheet" type="text/css" href="components/bootstrap-daterangepicker/daterangepicker-bs3.css" />
 
   <link rel="stylesheet/less" href="less/build.less">
   <!-- endref -->
@@ -111,7 +112,9 @@
 <script src="components/angular-ui-sortable/sortable.js"></script>
 <script src="components/angular-toggle-switch/angular-toggle-switch.js"></script>
 <script src="components/angular-sanitize/angular-sanitize.js"></script>
-<script src="components/angular-nvd3/dist/angular-nvd3.min.js"></script>
+<script src="components/angular-nvd3/dist/angular-nvd3.js"></script>
+<script src="components/moment-timezone/builds/moment-timezone-with-data.js"></script>
+<script src="components/bootstrap-daterangepicker/daterangepicker.js"></script>
 
 <script src="js/app.js"></script>
 <script src="js/config.js"></script>
@@ -145,6 +148,7 @@
 <script src="js/services/ngLoading.js"></script>
 <!--New GUI-->
 <script src="js/services/models.js"></script>
+<script src="js/services/dashboard.js"></script>
 
 <script src="js/model/cubeConfig.js"></script>
 <script src="js/model/jobConfig.js"></script>
@@ -163,6 +167,7 @@
 <script src="js/model/jobListModel.js"></script>
 <script src="js/model/cubesManager.js"></script>
 <script src="js/model/queryConfig.js"></script>
+<script src="js/model/dashboardConfig.js"></script>
 
 <!--New GUI-->
 <script src="js/model/modelsManager.js"></script>
@@ -202,6 +207,7 @@
 
 <!--New GUI-->
 <script src="js/controllers/models.js"></script>
+<script src="js/controllers/dashboard.js"></script>
 
 <!-- endref -->
 

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/controllers/dashboard.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/controllers/dashboard.js b/webapp/app/js/controllers/dashboard.js
new file mode 100644
index 0000000..3114919
--- /dev/null
+++ b/webapp/app/js/controllers/dashboard.js
@@ -0,0 +1,296 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+KylinApp.controller('DashboardCtrl', function ($scope, $location, storage, kylinConfig, dashboardConfig, DashboardService, MessageService, SweetAlert, loadingRequest, UserService, ProjectModel, $filter) {
+
+  $scope.init = function(){
+    $scope.timezone = 'GMT';
+
+    // Init date range
+    storage.bind($scope, 'dateRange', {defaultValue: {
+      startDate: moment().subtract(7, 'days').clone().tz($scope.timezone).startOf('day').format('x'),
+      endDate: moment().subtract(1, 'days').clone().tz($scope.timezone).endOf('day').format('x')
+    }, storeName: 'dashboard.dateRange'});
+
+    storage.bind($scope, 'barchartDimension', {defaultValue: dashboardConfig.dimensions[0], storeName: 'dashboard.barchart.dimension'});
+    storage.bind($scope, 'barchartCategory', {defaultValue: dashboardConfig.categories[0], storeName: 'dashboard.barchart.category'});
+    storage.bind($scope, 'barchartMetric', {defaultValue: dashboardConfig.metrics[0], storeName: 'dashboard.barchart.metric'});
+
+    storage.bind($scope, 'linechartDimension', {defaultValue: dashboardConfig.dimensions[2], storeName: 'dashboard.linechart.dimension'});
+    storage.bind($scope, 'linechartCategory', {defaultValue: dashboardConfig.categories[0], storeName: 'dashboard.linechart.category'});
+    storage.bind($scope, 'linechartMetric', {defaultValue: dashboardConfig.metrics[0], storeName: 'dashboard.linechart.metric'});
+
+    storage.bind($scope, 'currentSquare', 'queryCount');
+    $scope.initReady = true;
+  };
+
+  $scope.formatDatetime = function(dateTime) {
+    return moment(dateTime, 'x').tz($scope.timezone).format('YYYY-MM-DD');
+  };
+
+  $scope.refreshCubeMetric = function() {
+    $scope.cubeMetricsRefreshTime = moment().tz($scope.timezone).format('YYYY-MM-DD');
+    DashboardService.getCubeMetrics({projectName: ProjectModel.getSelectedProject(), cubeName: $scope.selectedCube}, {}, function (data) {
+      $scope.cubeMetrics = data;
+    }, function(e) {
+        SweetAlert.swal('Oops...', 'Failed to load cube metrics', 'error');
+        console.error('cube metrics error', e.data);
+    });
+  };
+
+  // Need to change name for this function
+  $scope.refreshOtherMetrics = function(){
+    DashboardService.getQueryMetrics({projectName: ProjectModel.getSelectedProject(), cubeName: $scope.selectedCube, startTime: $scope.formatDatetime($scope.dateRange.startDate), endTime: $scope.formatDatetime($scope.dateRange.endDate)}, {}, function (data) {
+      $scope.queryMetrics = data;
+    }, function(e) {
+        SweetAlert.swal('Oops...', 'Failed to load query metrics.', 'error');
+        console.error('query metrics error:', e.data);
+    });
+
+    DashboardService.getJobMetrics({projectName: ProjectModel.getSelectedProject(), cubeName: $scope.selectedCube, startTime: $scope.formatDatetime($scope.dateRange.startDate), endTime: $scope.formatDatetime($scope.dateRange.endDate)}, {}, function (data) {
+      $scope.jobMetrics = data;
+    }, function(e) {
+        SweetAlert.swal('Oops...', 'Failed to load job metrics.', 'error');
+        console.error('job metrics error:', e.data);
+    });
+
+    $scope.createCharts();
+  };
+
+  // Daterangepicker call back
+  $scope.changeDateRange = function(start, end) {
+    console.log("start time:", start);
+    console.log("end time:", end);
+    $scope.dateRange.startDate = start;
+    $scope.dateRange.endDate = end;
+    $scope.refreshOtherMetrics();
+  };
+
+  // Create chart option and data
+  $scope.createChart = function(dataQuery, chartType) {
+    var chartObj = {
+      queryObj: dataQuery
+    };
+
+    // get base options
+    var baseOptions = dashboardConfig.baseChartOptions;
+
+    var title = $filter('startCase')(dataQuery.metric.name) + ' by ' + $filter('startCase')(dataQuery.dimension.name);
+
+    // set title to options
+    chartObj.options = angular.copy(baseOptions);
+    chartObj.options.chart.xAxis.axisLabel = dataQuery.dimension.name;
+    chartObj.options.title.text = title;
+
+    var groupByOptions = [];
+    angular.forEach(dashboardConfig.granularityFilter, function(option) {
+      groupByOptions.push(option.value);
+    });
+    if (groupByOptions.indexOf(dataQuery.dimension.name) > -1) {
+      var formatPattern = '%Y-%m-%d';
+      if (dataQuery.dimension.name === dashboardConfig.granularityFilter[2].value) {
+        formatPattern = '%Y-%m';
+      }
+      chartObj.options.chart.xAxis.tickFormat = function (d) {
+        return d3.time.format(formatPattern)(moment.unix(d/1000).toDate());
+      };
+      chartObj.options.chart.tooltip.contentGenerator = function (d) {
+        return '<table><tr><td class="legend-color-guide"><div style="background-color: '+d.point.color+';"></div></td><td class="key">' + d3.time.format(formatPattern)(moment.unix(d.point.label/1000).toDate()) + '</td><td class="value">'+d.point.value.toFixed(2)+'</td></tr></table>';
+      };
+
+      // chartObj.options.chart.interpolate = 'cardinal';
+
+      chartObj.options.chart.legend = {
+        margin: {
+          left: 15
+        }
+      };
+
+      // Add filter for change month
+      chartObj.dimension = {};
+      chartObj.dimension.options = dashboardConfig.granularityFilter;
+      chartObj.dimension.selected = dataQuery.dimension;
+      angular.forEach(chartObj.dimension.options, function(option, ind) {
+        if (dataQuery.dimension.name.indexOf(option.value) > -1) {
+          chartObj.dimension.selected = chartObj.dimension.options[ind];
+        }
+      });
+    }
+
+    chartObj.data = [];
+
+    if (chartType === 'line') {
+      chartObj.options.chart.type = 'lineChart';
+      $scope.lineChart = chartObj;
+      DashboardService.getChartData({category: dataQuery.category, metric: dataQuery.metric.value, dimension: dataQuery.dimension.value, projectName: ProjectModel.getSelectedProject(), cubeName: $scope.selectedCube, startTime: $scope.formatDatetime($scope.dateRange.startDate), endTime: $scope.formatDatetime($scope.dateRange.endDate)}, {}, function (data) {
+        if (data.length > 6) {
+          $scope.lineChart.options.chart.xAxis.rotateLabels = -50;
+          $scope.lineChart.options.chart.xAxis.axisLabel = '';
+        }
+
+        $scope.lineChart.data = [{key: dataQuery.category, values: _.sortBy(data, 'label')}];
+      }, function(e) {
+          SweetAlert.swal('Oops...', 'Failed to load line chart.', 'error');
+          console.error('line chart error:', e.data);
+      });
+    } else  if (chartType === 'bar'){
+      chartObj.options.chart.type = 'discreteBarChart';
+      chartObj.options.chart.discretebar = {
+        dispatch: {
+          elementClick: function(el) {
+            if (ProjectModel.getSelectedProject()) {
+              $scope.selectedCube = el.data.label;
+            } else {
+              var project = el.data.label;
+              ProjectModel.projects.forEach(function(pro) {
+                if (pro.name.toLowerCase() === project.toLowerCase()) {
+                  project = pro.name;
+                }
+              });
+              ProjectModel.setSelectedProject(project);
+            }
+            $scope.$apply();
+          }
+        }
+      };
+      $scope.barChart = chartObj;
+      DashboardService.getChartData({category: dataQuery.category, metric: dataQuery.metric.value, dimension: dataQuery.dimension.value, projectName: ProjectModel.getSelectedProject(), cubeName: $scope.selectedCube, startTime: $scope.formatDatetime($scope.dateRange.startDate), endTime: $scope.formatDatetime($scope.dateRange.endDate)}, {}, function (data) {
+        if (data.length > 6) {
+          $scope.barChart.options.chart.xAxis.rotateLabels = -50;
+          $scope.barChart.options.chart.xAxis.axisLabel = '';
+        }
+        $scope.barChart.data = [{key: dataQuery.category, values: data}];
+        if ($scope.selectedCube) {
+          angular.forEach($scope.barChart.data[0].values, function (value, index){
+            if (value.label != $scope.selectedCube) {
+              value.color = '#ddd';
+            }
+          });
+        } 
+      }, function(e) {
+          SweetAlert.swal('Oops...', 'Failed to load bar chart.', 'error');
+          console.error('bar chart error:', e.data);
+      });
+    }    
+  };
+
+  // Clean and remove chart
+  $scope.removeChart = function(chartType) {
+    if (chartType === 'all') {
+      $scope.barChart = undefined;
+      $scope.lineChart = undefined;
+    } else if (chartType == 'bar') {
+      $scope.barChart = undefined;
+    } else if (chartType == 'line') {
+      $scope.lineChart = undefined;
+    }
+  };
+
+  $scope.createCharts = function() {
+    $scope.createChart({dimension: $scope.barchartDimension, category: $scope.barchartCategory, metric: $scope.barchartMetric}, 'bar');
+    $scope.createChart({dimension: $scope.linechartDimension, category: $scope.linechartCategory, metric: $scope.linechartMetric}, 'line');
+  };
+
+  // Click query count square
+  $scope.queryCountChart = function() {
+    $scope.currentSquare = 'queryCount'; 
+    $scope.barchartCategory = dashboardConfig.categories[0];
+    $scope.barchartMetric = dashboardConfig.metrics[0];
+    $scope.linechartCategory = dashboardConfig.categories[0];
+    $scope.linechartMetric = dashboardConfig.metrics[0];
+
+    $scope.removeChart('all');
+    $scope.createCharts();
+  };
+
+  // Click avg query latency
+  $scope.queryAvgChart = function() {
+    $scope.currentSquare = 'queryAvg';
+    $scope.barchartCategory = dashboardConfig.categories[0];
+    $scope.barchartMetric = dashboardConfig.metrics[1];
+    $scope.linechartCategory = dashboardConfig.categories[0];
+    $scope.linechartMetric = dashboardConfig.metrics[1];
+
+    $scope.removeChart('all');
+    $scope.createCharts();
+  };
+
+  // Click job count
+  $scope.jobCountChart = function() {
+    $scope.currentSquare = 'jobCount';
+    $scope.barchartCategory = dashboardConfig.categories[1];
+    $scope.barchartMetric = dashboardConfig.metrics[2];
+    $scope.linechartCategory = dashboardConfig.categories[1];
+    $scope.linechartMetric = dashboardConfig.metrics[2];
+
+    $scope.removeChart('all');
+    $scope.createCharts();
+  };
+
+  // Click job count
+  $scope.jobBuildTimeChart = function() {
+    $scope.currentSquare = 'jobBuildTime';
+    $scope.barchartCategory = dashboardConfig.categories[1];
+    $scope.barchartMetric = dashboardConfig.metrics[3];
+    $scope.linechartCategory = dashboardConfig.categories[1];
+    $scope.linechartMetric = dashboardConfig.metrics[3];
+
+    $scope.removeChart('all');
+    $scope.createCharts();
+  };
+
+  // Line chart granularity change.
+  $scope.changeDimensionFilter = function(chartType) {
+    if (chartType === 'line') {
+      var dataQuery = $scope.lineChart.queryObj;
+      angular.forEach(dashboardConfig.dimensions, function(dimension, ind) {
+        if (dimension.name === $scope.lineChart.dimension.selected.value) {
+          dataQuery.dimension = dashboardConfig.dimensions[ind];
+          $scope.linechartDimension = dashboardConfig.dimensions[ind];
+        }
+      });
+      $scope.removeChart(chartType);
+      $scope.createChart(dataQuery, chartType);
+    }
+  };
+
+  // watch the project or cube change
+  $scope.$watch('projectModel.selectedProject +"~"+ selectedCube', function (newValues, oldValues) {
+    if ($scope.initReady) {
+      if (ProjectModel.getSelectedProject() != null) {
+        $scope.barchartDimension = dashboardConfig.dimensions[1];
+      } else {
+        $scope.barchartDimension = dashboardConfig.dimensions[0];
+      }
+      if (newValues.split('~')[0] != oldValues.split('~')[0]) {
+        $scope.selectedCube = undefined;
+      }
+      $scope.refreshCubeMetric();
+      $scope.refreshOtherMetrics();
+    }
+  });
+
+  $scope.init();
+
+  $scope.cleanSelectedCube = function() {
+    $scope.selectedCube = undefined;
+  };
+
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/controllers/job.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/controllers/job.js b/webapp/app/js/controllers/job.js
index abb3bed..8e460fa 100644
--- a/webapp/app/js/controllers/job.js
+++ b/webapp/app/js/controllers/job.js
@@ -28,6 +28,9 @@ KylinApp
         //$scope.projects = [];
         $scope.action = {};
         $scope.timeFilter = jobConfig.timeFilter[1];
+        if ($routeParams.jobTimeFilter) {
+            $scope.timeFilter = jobConfig.timeFilter[$routeParams.jobTimeFilter];
+        }
 
         $scope.status = [];
         $scope.toggleSelection = function toggleSelection(current) {

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/controllers/query.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/controllers/query.js b/webapp/app/js/controllers/query.js
index 4014093..90699df 100644
--- a/webapp/app/js/controllers/query.js
+++ b/webapp/app/js/controllers/query.js
@@ -21,6 +21,9 @@
 KylinApp
     .controller('QueryCtrl', function ($scope, storage, $base64, $q, $location, $anchorScroll, $routeParams, QueryService, $modal, MessageService, $domUtilityService, $timeout, TableService, SweetAlert, VdmUtil) {
         $scope.mainPanel = 'query';
+        if ($routeParams.queryPanel) {
+            $scope.mainPanel = $routeParams.queryPanel;
+        }
         $scope.rowsPerPage = 50000;
         $scope.base64 = $base64;
         $scope.queryString = "";

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/directives/directives.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/directives/directives.js b/webapp/app/js/directives/directives.js
index bca3b03..d6ed304 100644
--- a/webapp/app/js/directives/directives.js
+++ b/webapp/app/js/directives/directives.js
@@ -430,4 +430,101 @@ KylinApp.directive('kylinPagination', function ($parse, $q) {
       });
     }
   }
+}).directive('kylinDaterangepicker', function() {
+  return {
+    restrict: 'E',
+    scope: {
+      startDate:'=',
+      endDate:'=',
+      minDate:'=',
+      maxDate:'=',
+      timezone: '=',
+      ranges: '=',
+      callbackHandler:'&callback',
+    },
+    template: '<button class="btn btn-default" style="background-color:#ffffff">' +
+                '<i class="fa fa-calendar"></i> ' +
+                ' <span></span> ' +
+                '<b class="caret"></b>' +
+              '</button>',
+    link: function(scope, element, attrs) {
+      // Init
+      var timezone = scope.timezone || 'GMT';
+      var format = attrs.format || 'YYYY-MM-DD';
+      var separator = ' - ';
+      var callback = scope.callbackHandler ? scope.callbackHandler() : function() {};
+
+      function startOfToday() {
+        return moment().tz(timezone).startOf('day');
+      }
+      function endOfToday() {
+        return moment().tz(timezone).endOf('day');
+      }
+
+      function getOption() {
+        var ranges = {
+          'Last 7 Days': [
+            startOfToday().subtract(1, 'weeks'),
+            endOfToday().subtract(1, 'days')
+          ],
+          'This Month': [
+            startOfToday().startOf('month'),
+            endOfToday()
+          ],
+          'Last Month': [
+            startOfToday().subtract(1, 'month').startOf('month'),
+            endOfToday().subtract(1, 'month').endOf('month')
+          ]
+        };
+
+        // Create datepicker, full list of options at https://github.com/dangrossman/bootstrap-daterangepicker
+        var maxDate = moment.tz(moment().tz(timezone).format(format), timezone);
+        var minDate = maxDate.clone().subtract(18, 'month');
+        return {
+          maxDate: scope.maxDate || maxDate,
+          minDate: scope.minDate || minDate,
+          format: format,
+          showDropdowns: true,
+          opens: attrs.opens || 'left',
+          ranges: scope.ranges || ranges
+        };
+      }
+
+      function _refresh() {
+        element.daterangepicker(getOption(), function(start, end, label) {
+          scope.startDate = moment.tz(start.startOf('day').format('YYYY-MM-DD HH:mm:ss'), timezone).format('x');
+          scope.endDate = moment.tz(end.endOf('day').format('YYYY-MM-DD HH:mm:ss'), timezone).format('x');
+          callback(scope.startDate, scope.endDate);
+          scope.$apply();
+        });
+      }
+
+      if (timezone) {
+        _refresh();
+      }
+
+      // Use $watch, update the view if either start or end change. (angular version 1.2 not support $watchGroup)
+      scope.$watch('startDate + "~" + endDate + "~" + timezone', function(newValues) {
+        var valueArr = newValues.split('~');
+
+        if (valueArr[2]) {
+          timezone = scope.timezone;
+          _refresh();
+        }
+
+        if (timezone) {
+          var startDate = valueArr[0] ? moment(valueArr[0], 'x').tz(timezone).format(format) : null;
+          var endDate = valueArr[1]  ? moment(valueArr[1], 'x').tz(timezone).format(format) : null;
+        }
+
+        if (startDate && endDate) {
+          var val = startDate + separator + endDate;
+          element.find('span').html(val);
+          element.data('daterangepicker').setStartDate(startDate);
+          element.data('daterangepicker').setEndDate(endDate);
+        }
+      });
+
+    }
+  };
 });

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/filters/filter.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/filters/filter.js b/webapp/app/js/filters/filter.js
index 8b6cffa..0d7eb5b 100755
--- a/webapp/app/js/filters/filter.js
+++ b/webapp/app/js/filters/filter.js
@@ -242,5 +242,14 @@ KylinApp
       });
       return out;
     }
+  }).filter('startCase', function($filter) {
+    return function (item) {
+      var words = item.split(' ');
+      var formatWord = '';
+      angular.forEach(words, function(word, ind) {
+        formatWord += ' ' + word.charAt(0).toUpperCase() + word.slice(1);
+      })
+      return formatWord.slice(1);
+    };
   });
 

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/model/dashboardConfig.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/model/dashboardConfig.js b/webapp/app/js/model/dashboardConfig.js
new file mode 100644
index 0000000..1620d1d
--- /dev/null
+++ b/webapp/app/js/model/dashboardConfig.js
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+KylinApp.constant('dashboardConfig', {
+  granularityFilter: [
+    {name: 'Daily', value: 'day'},
+    {name: 'Weekly', value: 'week'},
+    {name: 'Monthly', value: 'month'}
+  ],
+  metrics: [
+    {name: 'query count', value: 'QUERY_COUNT'},
+    {name: 'avg query latency', value: 'AVG_QUERY_LATENCY'},
+    {name: 'job count', value: 'JOB_COUNT'},
+    {name: 'avg build time', value: 'AVG_JOB_BUILD_TIME'}
+  ],
+  dimensions: [
+    {name: 'project', value: 'PROJECT'},
+    {name: 'cube', value: 'CUBE'},
+    {name: 'day', value: 'DAY'},
+    {name: 'week', value: 'WEEK'},
+    {name: 'month', value: 'MONTH'}
+  ],
+  categories: [
+    'QUERY', 'JOB'
+  ],
+  baseChartOptions: {
+    chart: {
+      height: 272,
+      margin : {
+        top: 20,
+        right: 40,
+        bottom: 60,
+        left: 45
+      },
+      useInteractiveGuideline: false,
+      x: function(d){return d.label;},
+      y: function(d){return d.value;},
+      xAxis: {
+        axisLabelDistance: 50,
+        staggerLabels: false,
+        tickFormat: function(d) {
+          if (d.length > 10) {
+            return d.substring(0,10) + '...';
+          } else {
+            return d;
+          }
+        }
+      },
+      yAxis: {
+        tickFormat: function(d) {
+          if (d < 1000) {
+            if (parseFloat(d) === d) {
+              return d3.format('.1')(d);
+            } else {
+              return d3.format('.2f')(d);
+            }
+          } else {
+            var prefix = d3.formatPrefix(d);
+            return prefix.scale(d) + prefix.symbol;
+          } 
+        },
+        showMaxMin: false
+      },
+      valueFormat: function(d){
+        return d3.format('.1')(d);
+      },
+      transitionDuration: 500,
+      tooltip: {
+        contentGenerator: function (d) {
+          return '<table><tr><td class="legend-color-guide"><div style="background-color: '+d.color+';"></div></td><td class="key">' + d.data.label + '</td><td class="value">'+d.data.value.toFixed(2)+'</td></tr></table>';
+        }
+      }
+    },
+    title: {
+      enable: true,
+      css: {
+        'margin-top': '20px'
+      }
+    }
+  }
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/js/services/dashboard.js
----------------------------------------------------------------------
diff --git a/webapp/app/js/services/dashboard.js b/webapp/app/js/services/dashboard.js
new file mode 100644
index 0000000..4340119
--- /dev/null
+++ b/webapp/app/js/services/dashboard.js
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+KylinApp.factory('DashboardService', ['$resource', '$location', function ($resource, $location, config) {
+  return $resource(Config.service.url + 'dashboard/:type/:category/:metric/:dimension', {}, {
+      getCubeMetrics: {
+        method: 'GET',
+        params: {type: 'metric', category: 'cube'},
+        isArray: false,
+        interceptor: {
+          response: function(response) {
+            var data = response.data;
+            var cubeMetrics;
+            if (data) {
+              cubeMetrics = {totalCubes: data.totalCube, expansionRate: {avg: data.avgCubeExpansion, max: data.maxCubeExpansion, min: data.minCubeExpansion}};
+            }
+            return cubeMetrics;
+          }
+        }
+      },
+      getQueryMetrics: {
+        method: 'GET',
+        params: {type: 'metric', category: 'query'},
+        isArray: false,
+        interceptor: {
+          response: function(response) {
+            var data = response.data;
+            var queryMetrics;
+            if (data) {
+              queryMetrics =  {queryCount: data.queryCount, queryLatency: {avg: data.avgQueryLatency/1000, max: data.maxQueryLatency/1000, min: data.minQueryLatency/1000}};
+            }
+            return queryMetrics;
+          }
+        }
+      },
+      getJobMetrics: {
+        method: 'GET', 
+        params: {type: 'metric', category: 'job'},
+        isArray: false,
+        interceptor: {
+          response: function(response) {
+            var data = response.data;
+            var jobMetrics;
+            if (data) {
+              jobMetrics = {jobCount: data.jobCount, buildingTime: {avg: data.avgJobBuildTime*1024*1024/1000, max: data.maxJobBuildTime*1024*1024/1000, min: data.minJobBuildTime*1024*1024/1000}};
+            }
+            return jobMetrics;
+          }
+        }
+      },
+      getChartData: {
+        method: 'GET',
+        params: {type: 'chart'},
+        isArray: false,
+        interceptor: {
+          response: function(response) {
+            var data = response.data;
+            var chartValues = [];
+            if (data) {
+              angular.forEach(Object.keys(data), function(key) {
+                var label = key;
+                var regEx = /^\d{4}-\d{1,2}-\d{1,2}$/;
+                if(key.match(regEx)) {
+                  label = moment(key, 'YYYY-MM-DD').format('x');
+                }
+                var value = data[key];
+                if (response.config.url.indexOf('/JOB/AVG_JOB_BUILD_TIME') > -1) { // AVG Job Build time format ms to sec
+                  value = value*1024*1024/1000;
+                } else if (response.config.url.indexOf('/QUERY/AVG_QUERY_LATENCY') > -1) { // AVG Query Latency format ms to sec
+                  value = value/1000;
+                }
+                chartValues.push({'label': label, 'value': value});
+              });
+            }
+            return chartValues;
+          }
+        }
+      }
+    });
+}]);

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/less/app.less
----------------------------------------------------------------------
diff --git a/webapp/app/less/app.less b/webapp/app/less/app.less
index 85ad937..fcba436 100644
--- a/webapp/app/less/app.less
+++ b/webapp/app/less/app.less
@@ -842,3 +842,61 @@ pre {
   white-space: -o-pre-wrap;
   word-wrap: break-word;
 }
+/* dashboard page style*/
+.square {
+  border: 2px solid #ddd;
+  text-align: center;
+  width: 215px;
+  height: 170px;
+  cursor: zoom-in;
+  padding-top: 15px;
+  padding-bottom: 15px;
+  .title {
+    font-size: 22px;
+    height: 60px;
+  }
+  .metric {
+    font-size: 35px ;
+    font-weight: bolder;
+    color: #8b1;
+    display: inline-block;
+    white-space: nowrap;
+    .unit {
+      font-size: 16px;
+    }
+  }
+  .description {
+    font-size: 15px;
+    color: #6a6a6a;
+    display: block;
+  }
+}
+.square-active {
+  border: 2px solid #6a6a6a;
+}
+.square-big {
+  border: 5px solid #ddd;
+  text-align: center;
+  width: 165px;
+  height: 240px;
+  padding: 20px 0;
+  margin-bottom: 20px;
+  .title {
+    font-size: 24px;
+    height: 55px;
+  }
+  .metric {
+    padding: 15px 0;
+    font-size: 50px ;
+    font-weight: bolder;
+    height: 100px;
+    color: #06d;
+    .unit {
+      font-size: 35px;
+    }
+  }
+  .description {
+    font-size: 18px;
+    color: #6a6a6a;
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/partials/dashboard/dashboard.html
----------------------------------------------------------------------
diff --git a/webapp/app/partials/dashboard/dashboard.html b/webapp/app/partials/dashboard/dashboard.html
new file mode 100644
index 0000000..891b4b9
--- /dev/null
+++ b/webapp/app/partials/dashboard/dashboard.html
@@ -0,0 +1,151 @@
+<!--
+* 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="container" style="min-width: 1140px;">
+  <div class="row" style="margin-top:22px">
+    <div class="col-sm-12">
+      <ol class="breadcrumb" style="display: inline-block; padding: 7px 0px; background-color: #ffffff;" ng-if="projectModel.selectedProject">
+        <li ng-if="selectedCube" ><a href="javascript:void(0)" ng-click="cleanSelectedCube()"><i class="fa fa-dashboard"></i> {{projectModel.selectedProject}}</a></li>
+        <li ng-if="!selectedCube"><i class="fa fa-dashboard"></i> {{projectModel.selectedProject}}</li>
+        <li class="active" ng-if="selectedCube">{{selectedCube}}</li>
+      </ol>
+      <div class="input-group pull-right" style="margin-bottom: 20px;">
+        <kylin-daterangepicker start-date="dateRange.startDate" end-date="dateRange.endDate" callback="changeDateRange"></kylin-daterangepicker>
+      </div>
+    </div>
+  </div>
+  <!-- metric boxs-->
+  <div class="row">
+    <div class="col-sm-2">
+      <div class="square-big" tooltip-placement="bottom" tooltip="As of {{cubeMetricsRefreshTime}}">
+        <div class="title">
+          TOTAL CUBE COUNT
+        </div>
+        <div class="metric" ng-if="cubeMetrics.totalCubes || (cubeMetrics.totalCubes === 0)">
+          {{cubeMetrics.totalCubes | number:0}}
+        </div>
+        <div class="metric" ng-if="!cubeMetrics.totalCubes && (cubeMetrics.totalCubes !== 0)">
+          --
+        </div>
+        <a class="description" ng-href="model">
+          More Details
+        </a>
+      </div>
+      <div class="square-big" tooltip-placement="bottom" tooltip-html-unsafe="Max: {{cubeMetrics.expansionRate.max | number:2}} | Min: {{cubeMetrics.expansionRate.min | number:2}}</br> As of {{cubeMetricsRefreshTime}}">
+        <div class="title">
+          AVG CUBE EXPANSION
+        </div>
+        <div class="metric" ng-if="cubeMetrics.expansionRate.avg || (cubeMetrics.expansionRate.avg === 0)">
+          {{cubeMetrics.expansionRate.avg | number:2}}
+        </div>
+        <div class="metric" ng-if="!cubeMetrics.expansionRate.avg && (cubeMetrics.expansionRate.avg !== 0)">
+          --
+        </div>
+        <a class="description" ng-href="model">
+          More Details
+        </a>
+      </div>
+    </div>
+    <div class="col-sm-10">
+      <div class="row">
+        <div class="col-sm-3">
+          <div class="square" ng-class="{'square-active': currentSquare ==='queryCount'}"  ng-click="queryCountChart()">
+            <div class="title">
+              QUERY<br/>COUNT
+            </div>
+            <div class="metric" ng-if="queryMetrics.queryCount || (queryMetrics.queryCount === 0)">
+              {{queryMetrics.queryCount | number:0}}
+            </div>
+            <div class="metric" ng-if="!queryMetrics.queryCount && (queryMetrics.queryCount !== 0)">
+              --
+            </div>
+            <a class="description" ng-href="query?queryPanel=cached" ng-click="$event.stopPropagation();">
+              More Details
+            </a>
+          </div>
+        </div>
+        <div class="col-sm-3">
+          <div class="square" ng-class="{'square-active': currentSquare ==='queryAvg'}"  tooltip-placement="bottom" tooltip="Max: {{queryMetrics.queryLatency.max ? (queryMetrics.queryLatency.max | number:2) : '--'}} sec | Min: {{queryMetrics.queryLatency.min ? (queryMetrics.queryLatency.min| number:2) : '--'}} sec" ng-click="queryAvgChart()">
+            <div class="title">
+              AVG QUERY LATENCY
+            </div>
+            <div class="metric" ng-if="queryMetrics.queryLatency.avg || (queryMetrics.queryLatency.avg === 0)">
+              {{queryMetrics.queryLatency.avg | number:2}}<span class="unit"> sec</span>
+            </div>
+            <div class="metric" ng-if="!queryMetrics.queryLatency.avg && (queryMetrics.queryLatency.avg !== 0)">
+              --
+            </div>
+            <a class="description" ng-href="query?queryPanel=cached" ng-click="$event.stopPropagation();">
+              More Details
+            </a>
+          </div>
+        </div>
+        <div class="col-sm-3">
+          <div class="square" ng-class="{'square-active': currentSquare ==='jobCount'}"  ng-click="jobCountChart()">
+            <div class="title">
+              JOB<br/>COUNT
+            </div>
+            <div class="metric" ng-if="jobMetrics.jobCount || (jobMetrics.jobCount === 0)">
+              {{jobMetrics.jobCount | number:0}}
+            </div>
+            <div class="metric" ng-if="!jobMetrics.jobCount && (jobMetrics.jobCount !== 0)">
+              --
+            </div>
+            <a class="description" ng-href="jobs" ng-click="$event.stopPropagation();">
+              More Details
+            </a>
+          </div>
+        </div>
+        <div class="col-sm-3" >
+          <div class="square" ng-class="{'square-active': currentSquare ==='jobBuildTime'}" tooltip-placement="bottom" tooltip="Max: {{jobMetrics.buildingTime.max ? (jobMetrics.buildingTime.max | number:2) : '--'}} sec | Min: {{jobMetrics.buildingTime.min ? ( jobMetrics.buildingTime.min | number:2) : '--'}} sec" ng-click="jobBuildTimeChart()">
+            <div class="title">
+              AVG BUILD TIME PER MB
+            </div>
+            <div class="metric" ng-if="jobMetrics.buildingTime.avg || jobMetrics.buildingTime.avg === 0">
+              {{jobMetrics.buildingTime.avg | number:2}}<span class="unit"> sec</span>
+            </div>
+            <div class="metric" ng-if="!jobMetrics.buildingTime.avg && (jobMetrics.buildingTime.avg !== 0)">
+               --
+            </div>
+            <a class="description" ng-href="jobs" ng-click="$event.stopPropagation();">
+              More Details
+            </a>
+          </div>
+        </div>
+      </div>
+      <div class="row charts">
+        <div class="col-sm-6" ng-if="barChart">
+          <div style="border: 2px solid #ddd; margin-top:15px;">
+            <div class="form-group" style="width: 90px; position: absolute; right: 15px; bottom: 265px;">
+              showValue: <input type="checkbox" ng-model="barChart.options.chart.showValues">
+            </div>
+            <nvd3 options="barChart.options" data="barChart.data"></nvd3>
+          </div>
+        </div>
+        <div class="col-sm-6" ng-if="lineChart">
+          <div style="border: 2px solid #ddd; margin-top:15px;">
+            <div class="form-group">
+              <select class="form-control" style="width: 80px; position: absolute; right: 20px; bottom: 270px;" ng-options="option.name for option in lineChart.dimension.options track by option.value" ng-model="lineChart.dimension.selected" ng-change="changeDimensionFilter('line')">
+              </select>
+            </div>
+            <nvd3 options="lineChart.options" data="lineChart.data"></nvd3>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/partials/header.html
----------------------------------------------------------------------
diff --git a/webapp/app/partials/header.html b/webapp/app/partials/header.html
index 0e37e63..fd22f9c 100644
--- a/webapp/app/partials/header.html
+++ b/webapp/app/partials/header.html
@@ -53,6 +53,9 @@
           <li class="{{activeTab=='admin'?'purple':'green'}}" ng-if="userService.hasRole('ROLE_ADMIN')">
             <a href="admin">System</a>
           </li>
+          <li class="{{activeTab=='dashboard'?'purple':'green'}}" ng-if="userService.isAuthorized()">
+            <a href="dashboard">Dashboard</a>
+          </li>
         </ul>
         <ul class="nav navbar-nav navbar-right">
           <li class="dropdown light-blue" ng-if="config.documents.length > 0">

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/app/routes.json
----------------------------------------------------------------------
diff --git a/webapp/app/routes.json b/webapp/app/routes.json
index 6729daf..65140a0 100644
--- a/webapp/app/routes.json
+++ b/webapp/app/routes.json
@@ -112,5 +112,13 @@
       "tab": "models",
       "controller": "ModelEditCtrl"
     }
+  },
+  {
+    "url": "/dashboard",
+    "params": {
+      "templateUrl": "partials/dashboard/dashboard.html",
+      "tab": "dashboard",
+      "controller": "DashboardCtrl"
+    }
   }
 ]

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/bower.json
----------------------------------------------------------------------
diff --git a/webapp/bower.json b/webapp/bower.json
index eac2cb3..f108862 100755
--- a/webapp/bower.json
+++ b/webapp/bower.json
@@ -17,7 +17,7 @@
     "angular-tree-control": "0.1.4",
     "angularLocalStorage": "~0.3.0",
     "messenger": "1.4.1",
-    "moment": "2.5.1",
+    "moment": "2.10.6",
     "d3": "3.4.4",
     "nvd3": "1.8.4",
     "angular-sweetalert": "~1.0.3",
@@ -35,7 +35,9 @@
     "angular-sanitize": "1.2.18",
     "angular-tree-control": "0.2.8",
     "angular-bootstrap-datetimepicker": "0.3.15",
-    "angular-nvd3": "1.0.9"
+    "angular-nvd3": "1.0.9",
+    "bootstrap-daterangepicker": "~1.3.23",
+    "moment-timezone" : "~0.5.5"
   },
   "devDependencies": {
     "less.js": "~1.4.0",
@@ -45,7 +47,7 @@
     "angular": "1.2.29",
     "nvd3": "1.8.4",
     "d3": "3.4.4",
-    "moment": "2.4.0",
+    "moment": "2.10.6",
     "angular-resource": "1.2.15",
     "angularLocalStorage": "0.1.7",
     "angular-cookies": "~1.2.0-rc.2",

http://git-wip-us.apache.org/repos/asf/kylin/blob/6236bfd1/webapp/grunt.json
----------------------------------------------------------------------
diff --git a/webapp/grunt.json b/webapp/grunt.json
index 06720b2..b86f60b 100755
--- a/webapp/grunt.json
+++ b/webapp/grunt.json
@@ -48,6 +48,8 @@
         "app/components/angular-ui-select/dist/select.js",
         "app/components/angular-sanitize/angular-sanitize.js",
         "app/components/angular-nvd3/dist/angular-nvd3.js",
+        "app/components/moment-timezone/builds/moment-timezone-with-data.js",
+        "app/components/bootstrap-daterangepicker/daterangepicker.js",
         "tmp/js/scripts.js"
       ],
       "dest": "tmp/js/scripts.min.js"
@@ -73,6 +75,7 @@
         "app/components/angular-toggle-switch/angular-toggle-switch.css",
         "app/components/angular-ui-select/dist/select.css",
         "app/components/angular-bootstrap-datetimepicker/src/css/datetimepicker.css",
+        "app/components/bootstrap-daterangepicker/daterangepicker-bs3.css",
         "tmp/css/styles.css"
       ],
       "dest": "tmp/css/styles.min.css"