You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by sh...@apache.org on 2018/07/26 10:58:25 UTC

[kylin] branch master updated: KYLIN-3418 User interface for hybrid model - Frontend

This is an automated email from the ASF dual-hosted git repository.

shaofengshi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kylin.git


The following commit(s) were added to refs/heads/master by this push:
     new f5e28fb  KYLIN-3418 User interface for hybrid model - Frontend
f5e28fb is described below

commit f5e28fb60ef42551deb58e81b08499e725afdf74
Author: Emiya0306 <wo...@qq.com>
AuthorDate: Thu Jul 26 09:59:18 2018 +0800

    KYLIN-3418 User interface for hybrid model - Frontend
---
 webapp/app/fonts/kylin.eot                        | Bin 0 -> 1600 bytes
 webapp/app/fonts/kylin.svg                        |  13 +
 webapp/app/fonts/kylin.ttf                        | Bin 0 -> 1436 bytes
 webapp/app/fonts/kylin.woff                       | Bin 0 -> 1512 bytes
 webapp/app/image/checkbox+.svg                    |  15 +
 webapp/app/image/checkbox-.svg                    |  17 +
 webapp/app/index.html                             |  22 ++
 webapp/app/js/controllers/hybridInstance.js       | 110 ++++++
 webapp/app/js/controllers/hybridInstanceSchema.js | 404 ++++++++++++++++++++++
 webapp/app/js/directives/directives.js            |   4 +-
 webapp/app/js/model/hybridInstanceManager.js      |  60 ++++
 webapp/app/js/services/hybridInstance.js          |  28 ++
 webapp/app/less/app.less                          | 106 ++++++
 webapp/app/less/build.less                        |   1 +
 webapp/app/less/font.less                         |  36 ++
 webapp/app/partials/cubes/hybrid_edit.html        | 176 ++++++++++
 webapp/app/partials/directives/pagination.html    |   2 +-
 webapp/app/partials/models/models_tree.html       | 113 +++---
 webapp/app/routes.json                            |  14 +
 19 files changed, 1076 insertions(+), 45 deletions(-)

diff --git a/webapp/app/fonts/kylin.eot b/webapp/app/fonts/kylin.eot
new file mode 100755
index 0000000..4cb39d1
Binary files /dev/null and b/webapp/app/fonts/kylin.eot differ
diff --git a/webapp/app/fonts/kylin.svg b/webapp/app/fonts/kylin.svg
new file mode 100755
index 0000000..df507bb
--- /dev/null
+++ b/webapp/app/fonts/kylin.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Generated by IcoMoon</metadata>
+<defs>
+<font id="icomoon" horiz-adv-x="1024">
+<font-face units-per-em="1024" ascent="960" descent="-64" />
+<missing-glyph horiz-adv-x="1024" />
+<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
+<glyph unicode="&#xe900;" glyph-name="hybrid" d="M538.389 557.895h282.93v-140.317l199.184-107.26 2.914-2.914 0.925-2.867v-232.435c0-3.237-1.295-5.457-3.838-6.752l-200.617-108.032h-0.971c-0.647-0.647-1.619-0.971-2.914-0.971s-2.266 0.324-2.914 0.971h-0.925l-200.617 108.032c-2.59 1.295-3.885 3.515-3.885 6.752v232.435c0 0.601 0.324 1.295 0.971 1.942v0.971l2.914 2.914 156.668 84.329v110.095l-541.185 0.276v-94.685l185.815-100.061 2.914-2.914 0.925-2.867v-232.435c0-3.237-1.295-5.457-3.838-6.752 [...]
+<glyph unicode="&#xe901;" glyph-name="arrows_right" d="M761.077 449.819l-456.626 392.050c-16.091 13.815-17.935 38.059-4.12 54.149s38.059 17.935 54.149 4.12l490.56-421.184c17.847-15.323 17.847-42.946 0-58.27l-490.56-421.184c-16.091-13.815-40.334-11.97-54.149 4.12s-11.97 40.334 4.12 54.149l456.626 392.050z" />
+<glyph unicode="&#xe902;" glyph-name="arrows_left" d="M242.824 449.819l456.626-392.050c16.091-13.815 17.935-38.059 4.12-54.149s-38.059-17.935-54.149-4.12l-490.56 421.184c-17.847 15.323-17.847 42.946 0 58.27l490.56 421.184c16.091 13.815 40.334 11.97 54.149-4.12s11.97-40.334-4.12-54.149l-456.626-392.050z" />
+</font></defs></svg>
\ No newline at end of file
diff --git a/webapp/app/fonts/kylin.ttf b/webapp/app/fonts/kylin.ttf
new file mode 100755
index 0000000..8294ac9
Binary files /dev/null and b/webapp/app/fonts/kylin.ttf differ
diff --git a/webapp/app/fonts/kylin.woff b/webapp/app/fonts/kylin.woff
new file mode 100755
index 0000000..4b6f197
Binary files /dev/null and b/webapp/app/fonts/kylin.woff differ
diff --git a/webapp/app/image/checkbox+.svg b/webapp/app/image/checkbox+.svg
new file mode 100644
index 0000000..b8630a3
--- /dev/null
+++ b/webapp/app/image/checkbox+.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 49.2 (51160) - http://www.bohemiancoding.com/sketch -->
+    <title>Group Copy 2</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Hybrid-add-cubes" transform="translate(-34.000000, -1331.000000)">
+            <g id="Group-Copy-2" transform="translate(34.000000, 1331.000000)">
+                <rect id="bg" fill="#0988DE" x="0" y="0" width="16" height="16" rx="2"></rect>
+                <path d="M4.51040764,8.01040764 L13.5104076,8.01040764 L13.5104076,10.0104076 L2.51040764,10.0104076 L2.51040764,8.01040764 L2.51040764,4.01040764 L4.51040764,4.01040764 L4.51040764,8.01040764 Z" id="check" fill="#FFFFFF" transform="translate(8.010408, 7.010408) rotate(-45.000000) translate(-8.010408, -7.010408) "></path>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/webapp/app/image/checkbox-.svg b/webapp/app/image/checkbox-.svg
new file mode 100644
index 0000000..6de232a
--- /dev/null
+++ b/webapp/app/image/checkbox-.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 49.2 (51160) - http://www.bohemiancoding.com/sketch -->
+    <title>bg copy 2</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <rect id="path-1" x="34" y="395" width="16" height="16" rx="2"></rect>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Hybrid-add-cubes" transform="translate(-34.000000, -395.000000)">
+            <g id="bg-copy-2">
+                <use fill="#FFFFFF" fill-rule="evenodd" xlink:href="#path-1"></use>
+                <rect stroke="#CFD8DC" stroke-width="1" x="34.5" y="395.5" width="15" height="15" rx="2"></rect>
+            </g>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/webapp/app/index.html b/webapp/app/index.html
index 12daaa2..8f7e954 100644
--- a/webapp/app/index.html
+++ b/webapp/app/index.html
@@ -149,6 +149,7 @@
 <script src="js/services/acl.js"></script>
 <!--New GUI-->
 <script src="js/services/models.js"></script>
+<script src="js/services/hybridInstance.js"></script>
 <script src="js/services/dashboard.js"></script>
 
 <script src="js/model/cubeConfig.js"></script>
@@ -172,6 +173,7 @@
 
 <!--New GUI-->
 <script src="js/model/modelsManager.js"></script>
+<script src="js/model/hybridInstanceManager.js"></script>
 <script src="js/services/badQuery.js"></script>
 <script src="js/utils/utils.js"></script>
 <script src="js/controllers/page.js"></script>
@@ -210,6 +212,8 @@
 
 <!--New GUI-->
 <script src="js/controllers/models.js"></script>
+<script src="js/controllers/hybridInstanceSchema.js"></script>
+<script src="js/controllers/hybridInstance.js"></script>
 <script src="js/controllers/dashboard.js"></script>
 
 <!-- endref -->
@@ -256,6 +260,24 @@
   </div>
 </script>
 
+<!-- static template for hybrid cube save/update result notification -->
+<script type="text/ng-template" id="hybridResultError.html">
+  <div class="callout callout-info">
+    <h4>Error Message</h4>
+    <p>{{text}}</p>
+  </div>
+  <div class="callout callout-danger">
+    <h4>Hybrid Instance Schema</h4>
+    <pre>{{schema}}</pre>
+  </div>
+</script>
+
+<script type="text/ng-template" id="hybridResultSuccess.html">
+  <div class="callout callout-info">
+    <p>{{text}}</p>
+  </div>
+</script>
+
 <!-- static template for cube save/update result notification -->
 <script type="text/ng-template" id="streamingResultError.html">
   <div class="callout">
diff --git a/webapp/app/js/controllers/hybridInstance.js b/webapp/app/js/controllers/hybridInstance.js
new file mode 100644
index 0000000..152feab
--- /dev/null
+++ b/webapp/app/js/controllers/hybridInstance.js
@@ -0,0 +1,110 @@
+/*
+ * 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('HybridInstanceCtrl', function (
+  $scope, $q, $location,
+  ProjectModel, hybridInstanceManager, SweetAlert, HybridInstanceService, loadingRequest
+) {
+  $scope.projectModel = ProjectModel;
+  $scope.hybridInstanceManager = hybridInstanceManager;
+
+  //trigger init with directive []
+  $scope.list = function () {
+    var defer = $q.defer();
+    var queryParam = {};
+    if (!$scope.projectModel.isSelectedProjectValid()) {
+      defer.resolve([]);
+      return defer.promise;
+    }
+
+    if (!$scope.projectModel.projects.length) {
+      defer.resolve([]);
+      return defer.promise;
+    }
+    queryParam.project = $scope.projectModel.selectedProject;
+    return hybridInstanceManager.list(queryParam).then(function (resp) {
+      defer.resolve(resp);
+      hybridInstanceManager.loading = false;
+      return defer.promise;
+    });
+  };
+
+  $scope.list();
+
+  $scope.$watch('projectModel.selectedProject', function() {
+    $scope.list();
+  });
+
+  $scope.editHybridInstance = function(hybridInstance){
+    // check for empty project of header, break the operation.
+    if (ProjectModel.selectedProject === null) {
+      SweetAlert.swal('Oops...', 'Please select your project first.', 'warning');
+      $location.path("/models");
+      return;
+    }
+    
+    $location.path("/hybrid/edit/" + hybridInstance.name);
+  };
+
+  $scope.dropHybridInstance = function (hybridInstance) {
+
+    // check for empty project of header, break the operation.
+    if (ProjectModel.selectedProject === null) {
+      SweetAlert.swal('Oops...', 'Please select your project first.', 'warning');
+      $location.path("/models");
+      return;
+    }
+
+    SweetAlert.swal({
+      title: '',
+      text: 'Are you sure to drop this hybrid?',
+      type: '',
+      showCancelButton: true,
+      confirmButtonColor: '#DD6B55',
+      confirmButtonText: "Yes",
+      closeOnConfirm: true
+    }, function (isConfirm) {
+      if (isConfirm) {
+        var schema = {
+          hybrid: hybridInstance.name,
+          model: hybridInstance.model,
+          project: hybridInstance.project,
+        };
+
+        loadingRequest.show();
+        HybridInstanceService.drop(schema, {}, function (result) {
+          loadingRequest.hide();
+          SweetAlert.swal('Success!', 'Hybrid drop is done successfully', 'success');
+          location.reload();
+        }, function (e) {
+          loadingRequest.hide();
+          if (e.data && e.data.exception) {
+            var message = e.data.exception;
+            var msg = !!(message) ? message : 'Failed to take action.';
+            SweetAlert.swal('Oops...', msg, 'error');
+          } else {
+            SweetAlert.swal('Oops...', "Failed to take action.", 'error');
+          }
+        });
+      }
+
+    });
+  };
+});
\ No newline at end of file
diff --git a/webapp/app/js/controllers/hybridInstanceSchema.js b/webapp/app/js/controllers/hybridInstanceSchema.js
new file mode 100644
index 0000000..033e3c1
--- /dev/null
+++ b/webapp/app/js/controllers/hybridInstanceSchema.js
@@ -0,0 +1,404 @@
+/*
+ * 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('HybridInstanceSchema', function (
+  $scope, $q, $location, $interpolate, $templateCache, $routeParams,
+  CubeList, HybridInstanceService, ProjectModel, modelsManager, SweetAlert, MessageService, loadingRequest, CubeService, CubeDescService
+) {
+
+  // check for empty project of header, break the operation.
+  if (!$scope.isEdit && ProjectModel.selectedProject === null) {
+    SweetAlert.swal('Oops...', 'Please select your project first.', 'warning');
+    $location.path("/models");
+    return;
+  }
+
+  $scope.LEFT = 'LEFT';
+  $scope.RIGHT = 'RIGHT';
+  $scope.isFormDisabled = false;
+
+  $scope.cubeList = CubeList;
+  $scope.projectModel = ProjectModel;
+  $scope.modelsManager = modelsManager;
+
+  $scope.route = { params: $routeParams.hybridName };
+  $scope.isEdit = !!$routeParams.hybridName;
+
+  $scope.isEditInitialized = false;
+  $scope.isLockEditModel = false;
+
+  $scope.form = {
+    name: '',
+    model: ''
+  };
+
+  resetPageData();
+
+  /**
+   * Computed: get the model's cubes
+   * 
+   * @param {'LEFT' | 'RIGHT'} dir 
+   */
+  $scope.getFiltedModelCube = function(dir) {
+    var dataRows = $scope.table[dir].dataRows;
+
+    return dataRows.filter(function(row) {
+      return row.model === $scope.form.model;
+    });
+  }
+
+  /**
+   * Computed: get the count of the model cubes
+   * 
+   * @param {'LEFT' | 'RIGHT'} dir 
+   */
+  $scope.getFiltedModelCubeCount = function(dir) {
+    return $scope.getFiltedModelCube(dir).length;
+  }
+
+  /**
+   * Computed: judge that current cube row is checked
+   * 
+   * @param {'LEFT' | 'RIGHT'} dir 
+   * @param {*} cube 
+   */
+  $scope.isCubeChecked = function(dir, cube) {
+    return $scope.table[dir].checkedCubeIds.indexOf(function(cubeId) {
+      return cubeId === cube.uuid;
+    }) !== -1;
+  };
+
+  /**
+   * Computed: judge that all rows of the table are checked
+   * 
+   * @param {'LEFT' | 'RIGHT'} dir 
+   */
+  $scope.isCheckAll = function(dir) {
+    var dataRows = $scope.getFiltedModelCube(dir);
+
+    return dataRows.length ? dataRows.every(function(row) {
+      return row.isChecked === true;
+    }) : false;
+  };
+
+  $scope.toggleCube = function(cube) {
+    cube.isChecked = !cube.isChecked;
+  }
+
+  /**
+   * Computed: judge that model select component can be chosen
+   */
+  $scope.isModelSelectDisabled = function() {
+    return !modelsManager.models.length
+      || $scope.table[$scope.RIGHT].dataRows.length;
+  }
+
+  /**
+   * Computed: judge is form valid
+   */
+  $scope.isFormValid = function() {
+    // get schema data
+    var schema = getSchema();
+
+    return Object.keys(schema).every(function(key) {
+      // Array.length for checking select cubes count >= 2
+      // otherwise checking empty value
+      return schema[key] instanceof Array ? schema[key].length > 1 : schema[key];
+    });
+  };
+
+  /**
+   * Action: toggle all rows' check status of the table
+   * 
+   * @param {'LEFT' | 'RIGHT'} dir 
+   * @param {undefined | boolean} toStatus: for change all the table's cubes hardly
+   */
+  $scope.toggleAll = function(dir, toStatus) {
+    var isCheckAll = $scope.isCheckAll(dir);
+    var dataRows = $scope.getFiltedModelCube(dir);
+
+    dataRows.forEach(function(row) {
+      if(toStatus !== undefined) {
+        row.isChecked = toStatus;
+      } else {
+        row.isChecked = !isCheckAll;
+      }
+    });
+  };
+
+  /**
+   * Action: transfer checked rows from destination table to source table
+   * 
+   * @param {'LEFT' | 'RIGHT'} dir 
+   */
+  $scope.transferTo = function(dir) {
+    var toDir = dir;
+    var fromDir = dir === $scope.RIGHT ? $scope.LEFT : $scope.RIGHT;
+    var srcTable = $scope.table[fromDir];
+    var disTable = $scope.table[toDir];
+
+    // get checked rows from source table to transfer rows
+    var transferRows = srcTable.dataRows.filter(function(row) {
+      return row.isChecked;
+    });
+
+    // filter unchecked row to source table rows
+    srcTable.dataRows = srcTable.dataRows.filter(function(row) {
+      return !row.isChecked;
+    });
+
+    // clean transfer rows check status
+    transferRows.forEach(function(row) {
+      row.isChecked = false;
+    });
+
+    // push transfer rows to destination table
+    disTable.dataRows = disTable.dataRows.concat(transferRows);
+  }
+
+  /**
+   * Action: page edit cancel handler
+   */
+  $scope.cancel = function() {
+    history.go(-1);
+  };
+
+  /**
+   * Action: page edit submit handler
+   */
+  $scope.submit = function() {
+    // get form data
+    var schema = getSchema();
+    // show save warning
+    saveWarning(function() {
+      // show loading
+      loadingRequest.show();
+      // save the hybrid cube
+      if(!$scope.isEdit) {
+        HybridInstanceService.save({}, schema, successHandler, failedHandler);
+      } else {
+        HybridInstanceService.update({}, schema, successHandler, failedHandler);
+      }
+    });
+
+    function successHandler(request) {
+      if(request.successful === false) {
+        var message = request.message;
+        var msg = !!message ? message : 'Failed to take action.';
+        var template = hybridInstanceResultTmpl({ text: msg, schema: schema });
+        MessageService.sendMsg(template, 'error', {}, true, 'top_center');
+      } else {
+        if($scope.isEdit) {
+          SweetAlert.swal('', 'Update hybrid cube successfully.', 'success');
+        } else {
+          SweetAlert.swal('', 'Create hybrid cube successfully.', 'success');
+        }
+        $location.path('/models');
+      }
+      // hide global loading
+      loadingRequest.hide();
+    }
+
+    function failedHandler(e) {
+      if (e.data && e.data.exception) {
+        var message = e.data.exception;
+        var msg = !!(message) ? message : 'Failed to take action.';
+        var template = hybridInstanceResultTmpl({ text: msg, schema: schema });
+        MessageService.sendMsg(template, 'error', {}, true, 'top_center');
+      } else {
+        var template = hybridInstanceResultTmpl({ text: 'Failed to take action.', schema: schema });
+        MessageService.sendMsg(template, 'error', {}, true, 'top_center');
+      }
+      // hide global loading
+      loadingRequest.hide();
+    }
+  }
+
+  doPerpare();
+
+  /**
+   * Init: initialize watcher
+   */
+  function doPerpare() {
+    $scope.$watch('projectModel.selectedProject', function (newValue, oldValue) {
+      if (newValue != oldValue || newValue == null) {
+        CubeList.removeAll();
+        resetPageData();
+        listModels();
+      }
+    });
+
+    $scope.$watch('modelsManager.models', function() {
+      $scope.form.model = modelsManager.models[0] && modelsManager.models[0].name || '';
+    });
+
+    $scope.$watch('form.model', function() {
+      if(!$scope.isLockEditModel) {
+        cleanCubeStatus();
+      }
+      $scope.isLockEditModel = false;
+    });
+
+    $scope.$watch('cubeList.cubes', function() {
+      loadTableData();
+
+      if ($scope.isEdit && !$scope.isEditInitialized && CubeList.cubes.length) {
+        getEditHybridInstance();
+        $scope.isEditInitialized = true;
+      }
+    });
+  }
+
+  /**
+   * Helper: get form data
+   */
+  function getSchema() {
+    const schema = {
+      hybrid: $scope.form.name,
+      project: $scope.projectModel.selectedProject,
+      model: $scope.form.model,
+      cubes: $scope.table[$scope.RIGHT].dataRows.map(function(row) {
+        return row.name;
+      })
+    };
+    return schema;
+  }
+
+  /**
+   * Helper: reset page data
+   */
+  function resetPageData() {
+    $scope.table = {};
+    $scope.form.model = '';
+    $scope.table[$scope.LEFT] = {
+      dataRows: []
+    };
+    $scope.table[$scope.RIGHT] = {
+      dataRows: []
+    };
+  }
+
+  /**
+   * Helper: ajax request models
+   */
+  function listModels () {
+    var defer = $q.defer();
+    var queryParam = {};
+    if (!$scope.projectModel.isSelectedProjectValid()) {
+      defer.resolve([]);
+      return defer.promise;
+    }
+
+    if (!$scope.projectModel.projects.length) {
+      defer.resolve([]);
+      return defer.promise;
+    }
+    queryParam.projectName = $scope.projectModel.selectedProject;
+    return modelsManager.list(queryParam).then(function (resp) {
+      defer.resolve(resp);
+      modelsManager.loading = false;
+      return defer.promise;
+    });
+  };
+
+  /**
+   * Helper: clean left table and reset status
+   */
+  function loadTableData() {
+    var cubesData = Object.create($scope.cubeList.cubes);
+    var unusedCubeTable = $scope.table[$scope.LEFT].dataRows = [];
+
+    cubesData.forEach(function(cubeData) {
+      cubeData.isChecked = false;
+      unusedCubeTable.push(cubeData);
+    });
+  }
+
+  function hybridInstanceResultTmpl(notification) {
+    // Get the static notification template.
+    var tmpl = notification.type == 'success' ? 'hybridResultSuccess.html' : 'hybridResultError.html';
+    return $interpolate($templateCache.get(tmpl))(notification);
+  };
+
+  function saveWarning(callback) {
+    SweetAlert.swal({
+      title: $scope.isEdit
+        ? 'Are you sure to update the Hybrid Cube?'
+        : 'Are you sure to save the Hybrid Cube?',
+      text: $scope.isEdit
+        ? ''
+        : '',
+      type: 'warning',
+      showCancelButton: true,
+      confirmButtonColor: '#DD6B55',
+      confirmButtonText: "Yes",
+      closeOnConfirm: true
+  }, function(isConfirm) {
+    if(isConfirm) {
+      callback();
+    }
+  })};
+
+  /**
+   * Helper: if $scope.form.model is changed, clean all the selected cube.
+   */
+  function cleanCubeStatus() {
+    // clean left table cubes
+    $scope.table[$scope.LEFT].dataRows.forEach(function(row) {
+      row.isChecked = false;
+    });
+    // check right table cubes
+    $scope.table[$scope.RIGHT].dataRows.forEach(function(row) {
+      row.isChecked = true;
+    });
+    // move right table cubes to left table
+    $scope.transferTo($scope.LEFT);
+  }
+
+  /**
+   * Helper: get edit hybrid cube
+   */
+  function getEditHybridInstance() {
+    loadingRequest.show();
+
+    HybridInstanceService.getByName({ hybrid_name: $routeParams.hybridName }, function (_hybridInstance) {
+      var hybridInstance = _hybridInstance.hybridInstance;
+
+      $scope.form.uuid = hybridInstance.uuid;
+      $scope.form.name = hybridInstance.name;
+
+      hybridInstance.realizations.forEach(function(realizationItem) {
+        var usedCubeName = realizationItem.realization;
+        var unusedCubeTable = $scope.table[$scope.LEFT];
+
+        unusedCubeTable.dataRows.forEach(function(row) {
+          if(row.name === usedCubeName)  {
+            row.isChecked = true;
+            $scope.form.model = row.model;
+          }
+        });
+      });
+
+      $scope.transferTo($scope.RIGHT);
+      loadingRequest.hide();
+      $scope.isLockEditModel = true;
+    });
+  }
+});
\ No newline at end of file
diff --git a/webapp/app/js/directives/directives.js b/webapp/app/js/directives/directives.js
index d6ed304..1371607 100644
--- a/webapp/app/js/directives/directives.js
+++ b/webapp/app/js/directives/directives.js
@@ -27,14 +27,14 @@ KylinApp.directive('kylinPagination', function ($parse, $q) {
     templateUrl: 'partials/directives/pagination.html',
     link: function (scope, element, attrs) {
       var _this = this;
-      scope.limit = 15;
       scope.hasMore = false;
       scope.data = $parse(attrs.data)(scope.$parent);
       scope.action = $parse(attrs.action)(scope.$parent);
       scope.loadFunc = $parse(attrs.loadFunc)(scope.$parent);
+      scope.isHideTotal = $parse(attrs.isHideTotal)();
+      scope.limit = $parse(attrs.limit)() || 15;
       scope.autoLoad = true;
 
-
       scope.$watch("action.reload", function (newValue, oldValue) {
         if (newValue != oldValue) {
           scope.reload();
diff --git a/webapp/app/js/model/hybridInstanceManager.js b/webapp/app/js/model/hybridInstanceManager.js
new file mode 100644
index 0000000..5a86ae3
--- /dev/null
+++ b/webapp/app/js/model/hybridInstanceManager.js
@@ -0,0 +1,60 @@
+/*
+ * 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.service('hybridInstanceManager', function($q, HybridInstanceService, ProjectModel) {
+  var _this = this;
+  this.hybridInstances = [];
+  this.hybridInstanceNameList = [];
+
+  //tracking models loading status
+  this.loading = false;
+
+  //list hybrid cubes
+  this.list = function(queryParam) {
+    _this.loading = true;
+
+    var defer = $q.defer();
+
+    HybridInstanceService.list(queryParam, function(_hybridInstances) {
+      _hybridInstances = _hybridInstances.map(function(_hybridInstance) {
+        var instance = _hybridInstance.hybridInstance;
+        instance.project = _hybridInstance.projectName;
+        instance.model = _hybridInstance.modelName;
+
+        return instance;
+      });
+
+      angular.forEach(_hybridInstances, function(hybridInstance) {
+        _this.hybridInstanceNameList.push(hybridInstance.name);
+        // hybridInstance.project = ProjectModel.getProjectByCubeModel(hybridInstance.name);
+      });
+
+      _hybridInstances = _.filter(_hybridInstances, function(hybridInstance) {
+        return hybridInstance.name !== undefined;
+      });
+
+      _this.hybridInstances = _hybridInstances;
+      _this.loading = false;
+    },
+    function() {
+      defer.reject('Failed to load models');
+    });
+
+    return defer.promise;
+  };
+})
diff --git a/webapp/app/js/services/hybridInstance.js b/webapp/app/js/services/hybridInstance.js
new file mode 100644
index 0000000..06aed20
--- /dev/null
+++ b/webapp/app/js/services/hybridInstance.js
@@ -0,0 +1,28 @@
+/*
+ * 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('HybridInstanceService', ['$resource', function ($resource, config) {
+    return $resource(Config.service.url + 'hybrids/:hybrid_name', {}, {
+        list: { method: 'GET', params: {}, isArray: true },
+        getByName: { method: 'GET', isArray: false },
+        drop: {method: 'DELETE', params: {}, isArray: false},
+        // clone: {method: 'PUT', params: {action: 'clone'}, isArray: false},
+        save: {method: 'POST', params: {}, isArray: false},
+        update: {method: 'PUT', params: {}, isArray: false}
+    });
+}]);
diff --git a/webapp/app/less/app.less b/webapp/app/less/app.less
index cf9748e..cb59fab 100644
--- a/webapp/app/less/app.less
+++ b/webapp/app/less/app.less
@@ -950,4 +950,110 @@ div[ng-controller="ModelConditionsSettingsCtrl"] {
     width: 49%;
     margin-top: 0;
   }
+}
+
+.form-title {
+  font-size: 24px;
+  color: #3A87AD;
+  line-height: 1;
+  margin: 0;
+}
+
+#main {
+  min-height: 100vh;
+}
+
+div[ng-controller="HybridInstanceSchema"] {
+  .form-title {
+    margin-bottom: 43px;
+  }
+  .control-label {
+    margin-top: 7px;
+  }
+  .split-line {
+    border-bottom: 1px solid #DDDDDD;
+    margin: 5px 0 20px 0;
+  }
+  .table {
+    margin-bottom: 0;
+  }
+  .table-header {
+    box-sizing: border-box;
+    border: 1px solid #DDDDDD;
+    border-bottom: none;
+  }
+  .transter-actions {
+    position: relative;
+    top: 50%;
+    transform: translateY(-50%);
+  }
+  .transter-action-item {
+    text-align: center;
+    margin-bottom: 10px;
+  }
+  .transter-action-item i {
+    width: 32px;
+    height: 32px;
+    display: inline-block;
+    cursor: pointer;
+    background: #FFFFFF;
+    border: 1px solid #DDDDDD;
+    border-radius: 2px;
+    padding-top: 7px;
+    color: #808080;
+    font-size: 16px;
+    &:hover {
+      border-color: #3A87AD;
+      color: #3A87AD;
+    }
+  }
+  .data-empty {
+    text-align: center;
+    font-size: 14px;
+    color: #808080;
+    line-height: 22px;
+    padding: 8px 0;
+    border: 1px solid #DDDDDD;
+  }
+  .col-xs-5 {
+    z-index: 1;
+  }
+  .fix-height-table {
+    max-height: 408px;
+    overflow: auto;
+  }
+  .edit-operator {
+    position: relative;
+    margin-bottom: 30px;
+  }
+  div[tooltip] + .tooltip {
+    white-space: nowrap;
+    .tooltip-inner {
+      max-width: 1000px;
+    }
+  }
+  .table-checkbox {
+    text-align: center;
+    width: 38px;
+    height: 38px;
+    img {
+      cursor: pointer;
+    }
+  }
+  .status-center {
+    text-align: center;
+    width: 150px;
+  }
+}
+
+.kylin-icon-hybrid {
+  margin-right: 10px;
+}
+.col-xs-offset-5 {
+  margin-left: 41.66666667%;
+}
+
+.text-info {
+  font-size: 18px;
+  color: #263238;
 }
\ No newline at end of file
diff --git a/webapp/app/less/build.less b/webapp/app/less/build.less
index 4271bac..241eee8 100644
--- a/webapp/app/less/build.less
+++ b/webapp/app/less/build.less
@@ -20,3 +20,4 @@
 @import 'app.less';
 @import 'component.less';
 @import 'animation.less';
+@import 'font.less';
\ No newline at end of file
diff --git a/webapp/app/less/font.less b/webapp/app/less/font.less
new file mode 100644
index 0000000..1f5484d
--- /dev/null
+++ b/webapp/app/less/font.less
@@ -0,0 +1,36 @@
+@font-face {
+  font-family: 'kylin';
+  src: url('../fonts/kylin.eot?pdayeh');
+  src: url('fonts/kylin.eot?pdayeh#iefix') format('embedded-opentype'),
+    url('../fonts/kylin.ttf?pdayeh') format('truetype'),
+    url('../fonts/kylin.woff?pdayeh') format('woff'),
+    url('../fonts/kylin.svg?pdayeh#kylin') format('svg');
+  font-weight: normal;
+  font-style: normal;
+}
+
+[class^='kylin-icon-'],
+[class*=' kylin-icon-'] {
+  /* use !important to prevent issues with browser extensions that change fonts */
+  font-family: 'kylin' !important;
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+
+  /* Better Font Rendering =========== */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.kylin-icon-hybrid:before {
+  content: '\e900';
+}
+.kylin-icon-arrows_right:before {
+  content: '\e901';
+}
+.kylin-icon-arrows_left:before {
+  content: '\e902';
+}
diff --git a/webapp/app/partials/cubes/hybrid_edit.html b/webapp/app/partials/cubes/hybrid_edit.html
new file mode 100644
index 0000000..21b130f
--- /dev/null
+++ b/webapp/app/partials/cubes/hybrid_edit.html
@@ -0,0 +1,176 @@
+<!--
+* 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 ng-controller="CubesCtrl">
+  <div class="page-header" style="height: 50px;">
+    <!--Project-->
+    <form class="navbar-form navbar-left" style="margin-top: 0px !important;" ng-if="userService.isAuthorized()">
+      <div class="form-group">
+        <a class="btn btn-xs btn-info" href="projects" tooltip="Manage Project">
+          <i class="fa fa-gears"></i>
+        </a>
+        <a class="btn btn-xs btn-primary" ng-if="userService.hasRole('ROLE_ADMIN')" style="width: 29px" tooltip="Add Project" ng-click="toCreateProj()">
+          <i class="fa fa-plus"></i>
+        </a>
+      </div>
+    </form>
+  </div>
+
+  <div class="row" ng-controller="HybridInstanceSchema">
+    <div class="col-xs-12">
+      <form role="form" name="hybrid_cube_form form-inline" novalidate>
+
+        <h1 class="form-title">Hybrid Designer</h1>
+
+        <div class="row">
+          <div class="col-xs-5 form-group">
+            <div class="row">
+              <label class="col-xs-3 control-label no-padding-right font-color-default" for="hybridName">Hybrid Name</label>
+              <div class="col-xs-9">
+                <input type="text" class="form-control" id="hybridName" placeholder="Hybrid Name" ng-disabled="isFormDisabled || isEdit" ng-model="form.name">
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="split-line"></div>
+
+        <div class="row edit-operator">
+          <div class="col-xs-5">
+            <div class="dataTables_wrapper no-footer">
+              <div class="row table-header">
+                <label class="col-xs-2 control-label no-padding-right font-color-default" for="modelName">Model</label>
+                <div class="col-xs-10">
+                  <div ng-show="!isModelSelectDisabled()">
+                      <select width="'100%'" chosen ng-model="form.model" ng-change="toggleAll(LEFT, false)" ng-options="model.name as model.name for model in modelsManager.models" ng-disabled="isModelSelectDisabled()"></select>
+                  </div>
+                  <div tooltip="If you want to switch model, remove the selected cubes." ng-show="isModelSelectDisabled()">
+                      <select width="'100%'" chosen ng-model="form.model" ng-change="toggleAll(LEFT, false)" ng-options="model.name as model.name for model in modelsManager.models" ng-disabled="isModelSelectDisabled()"></select>
+                  </div>
+                </div>
+              </div>
+              <div class="row fix-height-table">
+                <table class="table table-striped table-bordered table-hover dataTable no-footer ng-scope" ng-if="getFiltedModelCubeCount(LEFT)">
+                  <thead>
+                    <tr>
+                      <th class="table-checkbox">
+                        <img ng-if="!isCheckAll(LEFT)" src="image/checkbox-.svg" ng-click="toggleAll(LEFT)" />
+                        <img ng-if="isCheckAll(LEFT)" src="image/checkbox+.svg" ng-click="toggleAll(LEFT)" />
+                      </th>
+                      <th>Name</th>
+                      <th class="status-center">Status</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr ng-repeat="cube in table[LEFT].dataRows" ng-if="cube.model === form.model" ng-click="toggleCube(cube)" style="cursor: pointer;">
+                      <td class="table-checkbox">
+                        <img ng-if="!cube.isChecked" src="image/checkbox-.svg" />
+                        <img ng-if="cube.isChecked" src="image/checkbox+.svg" />
+                      </td>
+                      <td>{{ cube.name }}</td>
+                      <td class="status-center">
+                        <span class="label" ng-class="{'label-success': cube.status=='READY', 'label-default': cube.status=='DISABLED', 'label-warning': cube.status=='DESCBROKEN'}">
+                          {{ cube.status }}
+                        </span>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+
+              <div class="data-empty" ng-if="!getFiltedModelCubeCount(LEFT)">
+                Empty
+              </div>
+
+              <div class="row">
+                <div class="col-xs-12">
+                  <kylin-pagination data="cubeList.cubes" load-func="list" action="action" is-hide-total="true" limit="999999" />
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="col-xs-2 col-lg-1"></div>
+
+          <div style="position: absolute; height: 100%; width: 100%;">
+            <div class="col-xs-offset-5 col-xs-2 col-lg-1 transter-actions">
+              <div class="transter-action-item">
+                <i class="kylin-icon-arrows_right" ng-click="transferTo(RIGHT)"></i>
+              </div>
+              <div class="transter-action-item">
+                <i class="kylin-icon-arrows_left" ng-click="transferTo(LEFT)"></i>
+              </div>
+            </div>
+          </div>
+
+          <div class="col-xs-5">
+            <div class="dataTables_wrapper no-footer">
+              <div class="row table-header">
+                <label class="col-xs-12 control-label no-padding-right">Selected Cubes: {{table[RIGHT].dataRows.length}}</label>
+              </div>
+              <div class="row fix-height-table">
+                <table class="table table-striped table-bordered table-hover dataTable no-footer ng-scope" ng-if="table[RIGHT].dataRows.length">
+                  <thead>
+                    <tr>
+                      <th class="table-checkbox">
+                        <img ng-if="!isCheckAll(RIGHT)" src="image/checkbox-.svg" ng-click="toggleAll(RIGHT)" />
+                        <img ng-if="isCheckAll(RIGHT)" src="image/checkbox+.svg" ng-click="toggleAll(RIGHT)" />
+                      </th>
+                      <th>Name</th>
+                      <th class="status-center">Status</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <tr ng-repeat="cube in table[RIGHT].dataRows" ng-click="toggleCube(cube)" style="cursor: pointer;">
+                      <td class="table-checkbox">
+                        <img ng-if="!cube.isChecked" src="image/checkbox-.svg" />
+                        <img ng-if="cube.isChecked" src="image/checkbox+.svg" />
+                      </td>
+                      <td>{{ cube.name }}</td>
+                      <td class="status-center">
+                        <span class="label" ng-class="{'label-success': cube.status=='READY', 'label-default': cube.status=='DISABLED', 'label-warning': cube.status=='DESCBROKEN'}">
+                          {{ cube.status }}
+                        </span>
+                      </td>
+                    </tr>
+                  </tbody>
+                </table>
+              </div>
+
+              <div class="data-empty" ng-if="!table[RIGHT].dataRows.length">
+                Empty
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="split-line"></div>
+
+        <div class="row">
+          <div class="col-xs-12">
+            <div class="pull-right">
+                <button class="btn btn-sm btn-default" ng-click="cancel()">Cancel</button>
+                <button class="btn btn-sm btn-primary" ng-click="submit()" ng-disabled="!isFormValid()">Submit</button>
+            </div>
+          </div>
+        </div>
+
+      </form>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/webapp/app/partials/directives/pagination.html b/webapp/app/partials/directives/pagination.html
index dba9fae..d8fd242 100644
--- a/webapp/app/partials/directives/pagination.html
+++ b/webapp/app/partials/directives/pagination.html
@@ -21,7 +21,7 @@
         <i class="icon-plus icon-white"></i> <span>{{ loaded ? 'More' : 'Loading...' }}</span>
     </button>
     <div class="clearfix" style="margin: 5px"></div>
-    <div class="pull-left" style="padding-right: 3%">
+    <div class="pull-left" style="padding-right: 3%" ng-if="!isHideTotal">
         <span class="pull-left font-color-default" style="font-size: 15px"><strong>Total: {{getLength(data)}}</strong></span>
     </div>
 </div>
diff --git a/webapp/app/partials/models/models_tree.html b/webapp/app/partials/models/models_tree.html
index 4a91b00..d2064a2 100644
--- a/webapp/app/partials/models/models_tree.html
+++ b/webapp/app/partials/models/models_tree.html
@@ -17,57 +17,86 @@
 -->
 
 <div class="tree-border">
-    <div class="row">
-      <div class="col-xs-12" style="margin-top:10px;">
-        <!--<i class="fa fa-plus fa-2x" style="color:green;"> New</i>-->
-        <a ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)" class="dropdown-toggle" data-toggle="dropdown" href="#" aria-expanded="true">
-          <i class="fa fa-plus fa-2x" style="color:#2e8965;"> New<span class="caret"></span></i>
-          <!--<i> New </i> <span class="caret"></span>-->
-        </a>
-        <ul class="dropdown-menu">
-           <li  ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)">
-             <a href="models/add"><i class="fa fa-star"></i>New Model</a>
-          </li>
-          <li ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)">
-            <a href="cubes/add"><i class="fa fa-cube"></i>New Cube</a>
-          </li>
+  <div class="row">
+    <div class="col-xs-12" style="margin-top:10px;">
+      <!--<i class="fa fa-plus fa-2x" style="color:green;"> New</i>-->
+      <a ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)" class="dropdown-toggle" data-toggle="dropdown" href="#" aria-expanded="true">
+        <i class="fa fa-plus fa-2x" style="color:#2e8965;"> New<span class="caret"></span></i>
+        <!--<i> New </i> <span class="caret"></span>-->
+      </a>
+      <ul class="dropdown-menu">
+          <li  ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)">
+            <a href="models/add"><i class="fa fa-star"></i>New Model</a>
+        </li>
+        <li ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)">
+          <a href="cubes/add"><i class="fa fa-cube"></i>New Cube</a>
+        </li>
+        <li ng-if="userService.hasRole('ROLE_ADMIN') || hasPermission('project',projectModel, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)">
+          <a href="hybrid/add"><i class="kylin-icon-hybrid"></i>New Hybrid</a>
+        </li>
+      </ul>
+    </div>
 
-        </ul>
-      </div>
+  </div>
+  <div class="space-4 box-header with-border"></div>
+  <!--tree-->
 
-    </div>
-    <div class="space-4 box-header with-border"></div>
-    <!--tree-->
   <div>
-    <h3 class="text-info">Models</h3>
+    <h3 class="text-info">Models ({{modelsManager.models.length}})</h3>
   </div>
-<!--{{window}}px -->
-    <div id="cube_model_trees" style="width:100%; height:250px; overflow:auto;margin-top: 20px;" class="cube_model_trees">
+  <!--{{window}}px -->
+  <div id="cube_model_trees" style="width:100%; height:250px; overflow:auto;margin-top: 20px;" class="cube_model_trees">
 
-        <ul class="list-group models-tree" id="models-tree">
-          <li class="list-group-item" ng-repeat="model in modelsManager.models">
+      <ul class="list-group models-tree" id="models-tree">
+        <li class="list-group-item" ng-repeat="model in modelsManager.models">
 
-            <div class="pull-right" showonhoverparent style="display:none;" >
-              <div ng-click="$event.stopPropagation();" class="btn-group">
-                <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
-                  Action <span class="ace-icon fa fa-caret-down icon-on-right"></span>
-                </button>
-                <ul class="dropdown-menu" role="menu" style="right:0;left:auto;" ng-if="(userService.hasRole('ROLE_ADMIN') || hasPermission('model',model, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))">
-                  <li><a ng-click="editModel(model, false)"  title="Edit Model" style="cursor:pointer;margin-right: 8px;" >Edit</a></li>
-                  <li><a ng-click="cloneModel(model)" title="Clone Model"  style="cursor:pointer;margin-right: 8px;" >Clone </a></li>
-                  <li><a ng-click="dropModel(model)" title="Drop Model"  style="cursor:pointer;margin-right: 8px;">Drop</a></li>
-                  <li ng-if="userService.hasRole('ROLE_ADMIN')">
-                    <a ng-click="editModel(model, true)">Edit(JSON)</a></li>
-                </ul>
-              </div>
+          <div class="pull-right" showonhoverparent style="display:none;" >
+            <div ng-click="$event.stopPropagation();" class="btn-group">
+              <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+                Action <span class="ace-icon fa fa-caret-down icon-on-right"></span>
+              </button>
+              <ul class="dropdown-menu" role="menu" style="right:0;left:auto;" ng-if="(userService.hasRole('ROLE_ADMIN') || hasPermission('model',model, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))">
+                <li><a ng-click="editModel(model, false)"  title="Edit Model" style="cursor:pointer;margin-right: 8px;" >Edit</a></li>
+                <li><a ng-click="cloneModel(model)" title="Clone Model"  style="cursor:pointer;margin-right: 8px;" >Clone </a></li>
+                <li><a ng-click="dropModel(model)" title="Drop Model"  style="cursor:pointer;margin-right: 8px;">Drop</a></li>
+                <li ng-if="userService.hasRole('ROLE_ADMIN')">
+                  <a ng-click="editModel(model, true)">Edit(JSON)</a></li>
+              </ul>
             </div>
-            <span class="strong"><a style="cursor: pointer;word-break:break-all;" ng-click="openModal(model)">{{model.name}}</a></span>
+          </div>
+          <span class="strong"><a style="cursor: pointer;word-break:break-all;" ng-click="openModal(model)">{{model.name}}</a></span>
+
+        </li>
+      </ul>
+    <div ng-if="modelsManager.loading==true"><i class="fa fa-2x fa-spinner fa-spin"></i> Loading..</div>
+    <div no-result ng-if="modelsManager.loading!=true&&modelsManager.models.length==0"></div>
+  </div>
 
-          </li>
-        </ul>
-      <div ng-if="modelsManager.loading==true"><i class="fa fa-2x fa-spinner fa-spin"></i> Loading..</div>
-      <div no-result ng-if="modelsManager.loading!=true&&modelsManager.models.length==0"></div>
+  <div ng-controller="HybridInstanceCtrl">
+    <div>
+      <h3 class="text-info">Hybrids ({{hybridInstanceManager.hybridInstances.length}})</h3>
+    </div>
+    <div id="hybrid_cube_trees" style="width:100%; height:250px; overflow:auto;margin-top: 20px;" class="cube_model_trees">
+      <ul class="list-group models-tree" id="hybrid-tree">
+        <li class="list-group-item" ng-repeat="hybridInstance in hybridInstanceManager.hybridInstances">
+          <div class="pull-right" showonhoverparent style="display:none;" >
+            <div ng-click="$event.stopPropagation();" class="btn-group">
+              <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
+                Action <span class="ace-icon fa fa-caret-down icon-on-right"></span>
+              </button>
+              <ul class="dropdown-menu" role="menu" style="right:0;left:auto;" ng-if="(userService.hasRole('ROLE_ADMIN') || hasPermission('model',model, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))">
+                <li><a ng-click="editHybridInstance(hybridInstance)" title="Edit Hybrid" style="cursor:pointer;margin-right: 8px;" >Edit</a></li>
+                <li><a ng-click="dropHybridInstance(hybridInstance)" title="Drop Hybrid"  style="cursor:pointer;margin-right: 8px;">Drop</a></li>
+              </ul>
+            </div>
+          </div>
+          <span class="strong"><a style="cursor: pointer;word-break:break-all;" ng-click="openHybridInstance(hybridInstance)">{{hybridInstance.name}}</a></span>
+        </li>
+      </ul>
+      <div ng-if="hybridInstanceManager.loading === true"><i class="fa fa-2x fa-spinner fa-spin"></i> Loading..</div>
+      <div no-result ng-if="hybridInstanceManager.loading!==true && hybridInstanceManager.hybridInstances.length === 0"></div>
     </div>
+  </div>
 </div>
 
 <div ng-include="'partials/models/model_detail.html'"></div>
diff --git a/webapp/app/routes.json b/webapp/app/routes.json
index 65140a0..eefe35b 100644
--- a/webapp/app/routes.json
+++ b/webapp/app/routes.json
@@ -120,5 +120,19 @@
       "tab": "dashboard",
       "controller": "DashboardCtrl"
     }
+  },
+  {
+    "url": "/hybrid/add",
+    "params": {
+      "templateUrl": "partials/cubes/hybrid_edit.html",
+      "tab": "models"
+    }
+  },
+  {
+    "url": "/hybrid/edit/:hybridName",
+    "params": {
+      "templateUrl": "partials/cubes/hybrid_edit.html",
+      "tab": "models"
+    }
   }
 ]