You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@kylin.apache.org by GitBox <gi...@apache.org> on 2018/07/25 06:36:43 UTC

[GitHub] Emiya0306 closed pull request #173: KYLIN-3418 User interface for hybrid model - Frontend

Emiya0306 closed pull request #173: KYLIN-3418 User interface for hybrid model - Frontend
URL: https://github.com/apache/kylin/pull/173
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/webapp/app/fonts/kylin.eot b/webapp/app/fonts/kylin.eot
new file mode 100755
index 0000000000..4cb39d1272
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 0000000000..df507bb9a8
--- /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.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 170.036 91.525v156.281h303.139v115.95h-95.273v260.426h260.426v-260.426h-112.046v-115.95h8.222v-0.276zM646.655 300.737l169.346-91.327 169.346 91.327-169.346 91.327-169.346-91.327zM998.61 82.351v198.835l-171.951-93.147v-198.835l171.951 93.147zM38.995 300.737l169.346-91.327 169.346 91.327-169.346 91.327-169.346-91.327zM390.949 82.351v198.835l-171.951-93.147v-198.835l171.951 93.147z" />
+<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 0000000000..8294ac99f9
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 0000000000..4b6f197e4c
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 0000000000..b8630a3dd8
--- /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 0000000000..6de232a15b
--- /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 12daaa2486..18c4c9ce45 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 @@ <h4>Model Schema</h4>
   </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 Cube 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 0000000000..d7de8736b2
--- /dev/null
+++ b/webapp/app/js/controllers/hybridInstance.js
@@ -0,0 +1,106 @@
+/*
+ * 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){
+    if (ProjectModel.selectedProject === null) {
+      SweetAlert.swal('Oops...', 'Please select your project first.', 'warning');
+      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');
+      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 0000000000..d226ad3ff2
--- /dev/null
+++ b/webapp/app/js/controllers/hybridInstanceSchema.js
@@ -0,0 +1,399 @@
+/*
+ * 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.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() {
+      cleanCubeStatus();
+    });
+
+    $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 (_hybirdCube) {
+      var hybirdCube = _hybirdCube.hybridInstance;
+
+      $scope.form.uuid = hybirdCube.uuid;
+      $scope.form.name = hybirdCube.name;
+
+      hybirdCube.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();
+    });
+  }
+});
\ No newline at end of file
diff --git a/webapp/app/js/directives/directives.js b/webapp/app/js/directives/directives.js
index d6ed304fd9..13716079fb 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 0000000000..5a86ae30e2
--- /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 0000000000..06aed2000b
--- /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 cf9748e291..cb59fab4ff 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 4271bacf95..241eee8a90 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 0000000000..1f5484d992
--- /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 0000000000..21b130fc32
--- /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 dba9fae5fb..d8fd242bab 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 4a91b0066a..d2064a29de 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 65140a05c1..eefe35b04e 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"
+    }
   }
 ]


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services