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/05/21 00:27:33 UTC

[kylin] 01/05: KYLIN-3373 Some improvements for lookup table - UI part change

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

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

commit fd6dd9cf279df1928f636aa8114654d8c69fde7c
Author: liapan <li...@ebay.com>
AuthorDate: Thu May 10 11:54:27 2018 +0800

    KYLIN-3373 Some improvements for lookup table - UI part change
    
    Signed-off-by: shaofengshi <sh...@apache.org>
---
 webapp/app/js/controllers/cubeAdvanceSetting.js    |  56 +++++++++
 webapp/app/js/controllers/cubes.js                 | 127 +++++++++++++++++++++
 webapp/app/js/controllers/sourceMeta.js            |  28 +++++
 webapp/app/js/directives/select.js                 |   2 +-
 webapp/app/js/model/cubeConfig.js                  |   4 +
 webapp/app/js/model/tableConfig.js                 |  10 +-
 webapp/app/js/services/cubes.js                    |   3 +-
 webapp/app/js/services/tables.js                   |   3 +-
 webapp/app/less/app.less                           |   7 ++
 .../partials/cubeDesigner/advanced_settings.html   |  99 ++++++++++++++++
 webapp/app/partials/cubes/cubes.html               |   2 +
 webapp/app/partials/jobs/lookup_refresh.html       |  71 ++++++++++++
 webapp/app/partials/tables/table_detail.html       |  50 +++++++-
 13 files changed, 457 insertions(+), 5 deletions(-)

diff --git a/webapp/app/js/controllers/cubeAdvanceSetting.js b/webapp/app/js/controllers/cubeAdvanceSetting.js
index 5687e12..211b4c8 100755
--- a/webapp/app/js/controllers/cubeAdvanceSetting.js
+++ b/webapp/app/js/controllers/cubeAdvanceSetting.js
@@ -445,4 +445,60 @@ KylinApp.controller('CubeAdvanceSettingCtrl', function ($scope, $modal,cubeConfi
       $scope.$emit('AdvancedSettingEdited');
     });
   }
+
+  $scope.newSnapshot = {
+    select: {}
+  };
+
+  $scope.removeSnapshotTable = function(index) {
+    $scope.cubeMetaFrame.snapshot_table_desc_list.splice(index, 1);
+  };
+
+  $scope.addSnapshot = function(newSnapshot) {
+    if (!newSnapshot.table_name || !newSnapshot.storage_type) {
+      swal('Oops...', 'Snapshot table name or storage should not be empty', 'warning');
+      return;
+    } else if ($scope.cubeMetaFrame.snapshot_table_desc_list.length){
+      var existSnapshot = _.find($scope.cubeMetaFrame.snapshot_table_desc_list, function(snapshot){ return snapshot.table_name === newSnapshot.table_name;});
+      if (!!existSnapshot) {
+        swal('Oops...', 'Snapshot table already existed', 'warning');
+        return;
+      }
+    }
+    $scope.cubeMetaFrame.snapshot_table_desc_list.push(angular.copy(newSnapshot));
+    $scope.newSnapshot.select = {};
+  };
+
+  $scope.changeSnapshotStorage = function(snapshot) {
+    if (snapshot.storage_type == 'hbase') {
+      snapshot.global = true;
+    }
+  };
+
+  $scope.changeSnapshotTable = function(changeSnapshot, beforeTableName, snapshotTableDescList) {
+    var existSnapshot = _.find(snapshotTableDescList, function(snapshot) {
+      return snapshot.table_name === changeSnapshot.table_name;
+    });
+    if (!!existSnapshot) {
+      changeSnapshot.table_name = beforeTableName;
+      swal('Oops...', 'Snapshot table already existed', 'warning');
+    }
+  };
+
+  $scope.getCubeLookups = function() {
+    var modelDesc = modelsManager.getModel($scope.cubeMetaFrame.model_name);
+    var modelLookups = modelDesc ? modelDesc.lookups : [];
+    var cubeLookups = [];
+    angular.forEach(modelLookups, function(modelLookup, index) {
+      var dimensionLookup = _.find($scope.cubeMetaFrame.dimensions, function(dimension){ return dimension.table === modelLookup.alias;});
+      if (!!dimensionLookup) {
+        if (cubeLookups.indexOf(modelLookup.table) === -1) {
+          cubeLookups.push(modelLookup.table);
+        }
+      }
+    });
+    return cubeLookups;
+  };
+
+  $scope.cubeLookups = $scope.getCubeLookups();
 });
diff --git a/webapp/app/js/controllers/cubes.js b/webapp/app/js/controllers/cubes.js
index cbd6fad..136d86e 100644
--- a/webapp/app/js/controllers/cubes.js
+++ b/webapp/app/js/controllers/cubes.js
@@ -542,6 +542,27 @@ KylinApp.controller('CubesCtrl', function ($scope, $q, $routeParams, $location,
        });
      };
 
+    $scope.startLookupRefresh = function(cube) {
+      $scope.loadDetail(cube).then(function () {
+        $scope.metaModel={
+          model:cube.model
+        };
+        $modal.open({
+          templateUrl: 'lookupRefresh.html',
+          controller: lookupRefreshCtrl,
+          resolve: {
+            cube: function () {
+              return cube;
+            },
+            scope:function(){
+              return $scope;
+            }
+          }
+        });
+        }
+      );
+    };
+
   });
 
 
@@ -780,3 +801,109 @@ var deleteSegmentCtrl = function($scope, $modalInstance, CubeService, SweetAlert
     });
   };
 };
+
+var lookupRefreshCtrl = function($scope, scope, CubeList, $modalInstance, CubeService, cube, SweetAlert, loadingRequest) {
+  $scope.cubeList = CubeList;
+  $scope.cube = cube;
+  $scope.dispalySegment = false;
+
+  $scope.getLookups = function() {
+    var modelLookups = cube.model ? cube.model.lookups : [];
+    var cubeLookups = [];
+    angular.forEach(modelLookups, function(modelLookup, index) {
+      var dimensionTables = _.find(cube.detail.dimensions, function(dimension){ return dimension.table === modelLookup.alias;});
+      if (!!dimensionTables) {
+        if (cubeLookups.indexOf(modelLookup.table) === -1) {
+          cubeLookups.push(modelLookup.table);
+        }
+      }
+    });
+    return cubeLookups;
+  };
+
+  $scope.cubeLookups = $scope.getLookups();
+
+  $scope.lookup = {
+    select: {}
+  };
+
+  $scope.getReadySegment = function(segment) {
+    return segment.status === 'READY';
+  };
+
+  $scope.cancel = function () {
+    $modalInstance.dismiss('cancel');
+  };
+
+  $scope.updateLookupTable = function(tableName) {
+    var lookupTable = _.find(cube.detail.snapshot_table_desc_list, function(table){ return table.table_name == tableName});
+    if (!!lookupTable && lookupTable.global) {
+      $scope.dispalySegment = false;
+      $scope.lookup.select.segments = [];
+    } else {
+      $scope.dispalySegment = true;
+    }
+  };
+
+  $scope.selectAllSegments = function(allSegments) {
+    if (allSegments) {
+      $scope.lookup.select.segments = $scope.cube.segments;
+    } else {
+      $scope.lookup.select.segments = [];
+    }
+  };
+
+  $scope.refresh = function() {
+    if (!$scope.lookup.select.table_name) {
+      SweetAlert.swal('Warning', 'Lookup table should not be empty', 'warning');
+      return;
+    }
+
+    // cube advance lookup table
+    var lookupTable = _.find(cube.detail.snapshot_table_desc_list, function(table){ return table.table_name == $scope.lookup.select.table_name});
+    if (!!lookupTable) {
+      if (!lookupTable.global && $scope.lookup.select.segments.length == 0) {
+        SweetAlert.swal('Warning', 'Segment should not be empty', 'warning');
+        return;
+      }
+    } else {
+      // cube lookup table
+      lookupTable = _.find($scope.cubeLookups, function(table){ return table == $scope.lookup.select.table_name});
+      if (!lookupTable) {
+        SweetAlert.swal('Warning', 'Lookup table not existed in cube', 'warning');
+        return;
+      } else {
+        if ($scope.lookup.select.segments.length == 0) {
+          SweetAlert.swal('Warning', 'Segment should not be empty', 'warning');
+          return;
+        }
+      }
+    }
+
+    var lookupSnapshotBuildRequest = {
+      lookupTableName: $scope.lookup.select.table_name,
+      segmentIDs: _.map($scope.lookup.select.segments, function(segment){ return segment.uuid})
+    };
+
+    loadingRequest.show();
+    CubeService.lookupRefresh({cubeId: cube.name}, lookupSnapshotBuildRequest, function (job) {
+      loadingRequest.hide();
+      $modalInstance.dismiss('cancel');
+      SweetAlert.swal('Success!', 'Lookup refresh job was submitted successfully', 'success');
+      scope.refreshCube(cube).then(function(_cube){
+          $scope.cubeList.cubes[$scope.cubeList.cubes.indexOf(cube)] = _cube;
+        });
+    }, 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');
+      }
+    });
+  };
+
+};
diff --git a/webapp/app/js/controllers/sourceMeta.js b/webapp/app/js/controllers/sourceMeta.js
index 8a795d5..49d8998 100755
--- a/webapp/app/js/controllers/sourceMeta.js
+++ b/webapp/app/js/controllers/sourceMeta.js
@@ -925,3 +925,31 @@ KylinApp
 
   });
 
+/*snapshot controller*/
+KylinApp
+  .controller('TableSnapshotCtrl', function ($scope, TableService, CubeService, uiGridConstants) {
+    $scope.initSnapshots = function() {
+      var tableFullName = $scope.tableModel.selectedSrcTable.database + '.' + $scope.tableModel.selectedSrcTable.name
+      TableService.getSnapshots({tableName: tableFullName, pro: $scope.projectModel.selectedProject}, {}, function (data) {
+        var orgData = JSON.parse(angular.toJson(data));
+        angular.forEach(orgData, function(snapshot) {
+          if(!!snapshot.cubesAndSegmentsUsage && snapshot.cubesAndSegmentsUsage.length > 0) {
+            snapshot.usageInfo = '';
+            angular.forEach(snapshot.cubesAndSegmentsUsage, function(info) {
+              snapshot.usageInfo += info;
+              snapshot.usageInfo += '</br>';
+            });
+          } else {
+            snapshot.usageInfo = 'No Usage Info';
+          }
+        });
+        $scope.tableSnapshots = orgData;
+      });
+    };
+    $scope.$watch('tableModel.selectedSrcTable', function (newValue, oldValue) {
+      if (!newValue || !newValue.name) {
+        return;
+      }
+      $scope.initSnapshots();
+    });
+  }); 
\ No newline at end of file
diff --git a/webapp/app/js/directives/select.js b/webapp/app/js/directives/select.js
index 7327af9..038cf57 100644
--- a/webapp/app/js/directives/select.js
+++ b/webapp/app/js/directives/select.js
@@ -1627,7 +1627,7 @@ uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelec
                   }
                 }
             }
-            if (angular.equals(result.toUpperCase(),value.toUpperCase())){
+            if (angular.equals((typeof result =='string') ? result.toUpperCase() : result, (typeof value == 'string') ? value.toUpperCase() : value)) {
               resultMultiple.unshift(list[p]);
               return true;
             }
diff --git a/webapp/app/js/model/cubeConfig.js b/webapp/app/js/model/cubeConfig.js
index e163d75..eacb915 100644
--- a/webapp/app/js/model/cubeConfig.js
+++ b/webapp/app/js/model/cubeConfig.js
@@ -114,6 +114,10 @@ KylinApp.constant('cubeConfig', {
     {name:"Segment Dictionary", value:"org.apache.kylin.dict.global.SegmentAppendTrieDictBuilder"}
   ],
   needSetLengthEncodingList:['fixed_length','fixed_length_hex','int','integer'],
+  snapshotStorageTypes: [
+    {name: 'Meta Store', value: 'metaStore'},
+    {name: 'HBase', value: 'hbase'}
+  ],
   baseChartOptions: {
     chart: {
       type: 'sunburstChart',
diff --git a/webapp/app/js/model/tableConfig.js b/webapp/app/js/model/tableConfig.js
index 3989531..0dc4a52 100644
--- a/webapp/app/js/model/tableConfig.js
+++ b/webapp/app/js/model/tableConfig.js
@@ -121,7 +121,15 @@ KylinApp.constant('tableConfig', {
       "fixed_length_hex",
       "integer"
     ]
-  }
+  },
+  snapshotTheaditems: [
+    {attr: 'snapshotID', name: 'ID'},
+    {attr: 'storageType', name: 'Storage Type'},
+    {attr: 'lastBuildTime', name: 'Last Build Time'},
+    {attr: 'sourceTableLastModifyTime', name: 'Source Table Last Modify Time'},
+    {attr: 'sourceTableSize', name: 'Size'},
+    {attr: 'usageInfo', name: 'Useage Info'}
+  ]
 
 
 });
diff --git a/webapp/app/js/services/cubes.js b/webapp/app/js/services/cubes.js
index 6140521..537e5c1 100644
--- a/webapp/app/js/services/cubes.js
+++ b/webapp/app/js/services/cubes.js
@@ -79,6 +79,7 @@ KylinApp.factory('CubeService', ['$resource', function ($resource, config) {
       }
     },
     optimize: {method: 'PUT', params: {action: 'optimize'}, isArray: false},
-    autoMigrate: {method: 'POST', params: {action: 'migrate'}, isArray: false}
+    autoMigrate: {method: 'POST', params: {action: 'migrate'}, isArray: false},
+    lookupRefresh: {method: 'PUT', params: {action: 'refresh_lookup'}, isArray: false}
   });
 }]);
diff --git a/webapp/app/js/services/tables.js b/webapp/app/js/services/tables.js
index 7d8bc1a..9a62cf8 100755
--- a/webapp/app/js/services/tables.js
+++ b/webapp/app/js/services/tables.js
@@ -24,6 +24,7 @@ KylinApp.factory('TableService', ['$resource', function ($resource, config) {
     unLoadHiveTable: {method: 'DELETE', params: {}, isArray: false},
     genCardinality: {method: 'PUT', params: {action: 'cardinality'}, isArray: false},
     showHiveDatabases: {method: 'GET', params: {action:'hive'}, cache: true, isArray: true},
-    showHiveTables: {method: 'GET', params: {action:'hive'}, cache: true, isArray: true}
+    showHiveTables: {method: 'GET', params: {action:'hive'}, cache: true, isArray: true},
+    getSnapshots: {method: 'GET', params: {action: 'snapshots'}, isArray: true}
   });
 }]);
diff --git a/webapp/app/less/app.less b/webapp/app/less/app.less
index eca76ff..7aa7283 100644
--- a/webapp/app/less/app.less
+++ b/webapp/app/less/app.less
@@ -929,4 +929,11 @@ pre {
 div[title="Cube Info Detail"].popover {
   max-width: 1024px;
   with: min-content;
+}
+/*snapshot usage info tooltip*/
+td.snapshot-usage .tooltip {
+  font-size: 16px;
+}
+td.snapshot-usage .tooltip-inner {
+  max-width: 1024px;
 }
\ No newline at end of file
diff --git a/webapp/app/partials/cubeDesigner/advanced_settings.html b/webapp/app/partials/cubeDesigner/advanced_settings.html
index 7b9193b..fb7a1b1 100755
--- a/webapp/app/partials/cubeDesigner/advanced_settings.html
+++ b/webapp/app/partials/cubeDesigner/advanced_settings.html
@@ -503,6 +503,99 @@
              <button class="btn btn-link" ng-click="clearNewDictionaries()">Cancel</button>
            </div>
          </div>
+         <!-- Advanced Lookup Table-->
+         <div class="form-group large-popover" style="margin-bottom:30px;">
+           <h3 style="margin-left:42px;margin-bottom:30px;">Advanced Snapshot Table  <i kylinpopover placement="right" title="Cube Engine" template="AdvanceSnapshotTableTip.html" class="fa fa-info-circle"></i></h3>
+           <div style="margin-left:42px">
+             <!-- edit mode-->
+             <div ng-if="state.mode=='edit'" class="box-body">
+               <table class="table table-hover table-bordered list" style="table-layout: fixed;margin-left:42px;width:92%;">
+                 <thead>
+                   <tr>
+                     <th style="width:60%">Snapshot Table</th>
+                     <th style="width:25%">Type</th>
+                     <th style="width:10%">Global</th>
+                     <th style="width:5%"></th>
+                   </tr>
+                 </thead>
+                 <tbody>
+                   <tr ng-repeat="snapshot in cubeMetaFrame.snapshot_table_desc_list track by $index">
+                     <td>
+                       <select style="width:95%" chosen ng-model="snapshot.table_name"
+                               ng-change="changeSnapshotTable(snapshot, '{{snapshot.table_name}}', {{cubeMetaFrame.snapshot_table_desc_list}})"
+                               ng-options="tableName as tableName for tableName in cubeLookups">
+                         <option value=""></option>
+                       </select>
+                     </td>
+                     <td>
+                       <select style="width:95%" chosen ng-model="snapshot.storage_type"
+                               ng-change="changeSnapshotStorage(snapshot)"
+                               ng-options="storageType.value as storageType.name for storageType in cubeConfig.snapshotStorageTypes">
+                         <option value=""></option>
+                       </select>
+                     </td>
+                     <td>
+                       <input type="checkbox" ng-model="snapshot.global" ng-disabled="(snapshot.storage_type == 'hbase')">
+                     </td>
+                     <td>
+                       <button class="btn btn-xs btn-info" ng-click="removeSnapshotTable($index)">
+                         <i class="fa fa-minus"></i>
+                       </button>
+                     </td>
+                   </tr>
+                   <tr>
+                     <td>
+                       <select style="width:95%" chosen ng-model="newSnapshot.select.table_name"
+                               ng-options="tableName as tableName for tableName in cubeLookups">
+                         <option value=""></option>
+                       </select>
+                     </td>
+                     <td>
+                       <select style="width:95%" chosen ng-model="newSnapshot.select.storage_type"
+                               ng-change="changeSnapshotStorage(newSnapshot.select)"
+                               ng-options="storageType.value as storageType.name for storageType in cubeConfig.snapshotStorageTypes">
+                         <option value=""></option>
+                       </select>
+                     </td>
+                     <td>
+                       <input type="checkbox" ng-model="newSnapshot.select.global" ng-disabled="(newSnapshot.select.storage_type == 'hbase')">
+                     </td>
+                     <td>
+                       <button class="btn btn-xs btn-info" ng-click="addSnapshot(newSnapshot.select)">
+                         <i class="fa fa-plus"></i>
+                       </button>
+                     </td>
+                   </tr>
+                 </tbody>
+               </table>
+             </div>
+             <!-- view-->
+             <div ng-if="state.mode=='view'" class="box-body">
+               <table class="table table-hover table-bordered list" style="table-layout: fixed;margin-left:42px;width:92%;">
+                  <thead>
+                   <tr>
+                     <th style="width:60%">Snapshot Table</th>
+                     <th style="width:25%">Type</th>
+                     <th style="width:10%">Global</th>
+                   </tr>
+                 </thead>
+                 <tbody>
+                   <tr ng-repeat="snapshot in cubeMetaFrame.snapshot_table_desc_list track by $index">
+                     <td>
+                         <p>{{snapshot.table_name}}</p>
+                     </td>
+                     <td>
+                         <p>{{snapshot.storage_type}}</p>
+                     </td>
+                     <td>
+                         <input type="checkbox" ng-model="snapshot.global" disabled="true">
+                     </td>
+                   </tr>
+                 </tbody>
+               </table>
+             </div>
+           </div>
+         </div>
          <!--Edit ColumnFamily-->
          <div class="form-group large-popover" >
            <h3 style="margin-left:42px">Advanced ColumnFamily  <i kylinpopover placement="right" title="Advanced ColumnFamily" template="AdvancedColumnFamilyTip.html" class="fa fa-info-circle"></i></h3>
@@ -647,3 +740,9 @@
     </h4>
   </div>
 </script>
+
+<script type="text/ng-template" id="AdvanceSnapshotTableTip.html">
+  <div>
+    <h4>Advance snapshot design for global lookup table and provide different storage type.</h4>
+  </div>
+</script>
diff --git a/webapp/app/partials/cubes/cubes.html b/webapp/app/partials/cubes/cubes.html
index 3fd5e61..21b7a34 100644
--- a/webapp/app/partials/cubes/cubes.html
+++ b/webapp/app/partials/cubes/cubes.html
@@ -95,6 +95,7 @@
                         <li ng-if="cube.status!='DESCBROKEN'"><a ng-click="startJobSubmit(cube);">Build</a></li>
                         <li ng-if="cube.status!='DESCBROKEN'"><a ng-click="startRefresh(cube)">Refresh</a></li>
                         <li ng-if="cube.status!='DESCBROKEN'"><a ng-click="startMerge(cube)">Merge</a></li>
+                        <li ng-if="cube.status!='DESCBROKEN'"><a ng-click="startLookupRefresh(cube);">Lookup Refresh</a></li>
                         <li ng-if="cube.status=='READY' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="disable(cube)">Disable</a></li>
                         <li ng-if="cube.status=='DISABLED' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="enable(cube)">Enable</a></li>
                         <li ng-if="cube.status=='DISABLED' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="startDeleteSegment(cube)">Delete Segment</a></li>
@@ -146,4 +147,5 @@
 <div ng-include="'partials/models/model_detail.html'"></div>
 <div ng-include="'partials/cubes/cube_clone.html'"></div>
 <div ng-include="'partials/cubes/cube_delete_segment.html'"></div>
+<div ng-include="'partials/jobs/lookup_refresh.html'"></div>
 </div>
diff --git a/webapp/app/partials/jobs/lookup_refresh.html b/webapp/app/partials/jobs/lookup_refresh.html
new file mode 100644
index 0000000..ce8bbf9
--- /dev/null
+++ b/webapp/app/partials/jobs/lookup_refresh.html
@@ -0,0 +1,71 @@
+<!--
+* 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.
+-->
+
+<script type="text/ng-template" id="lookupRefresh.html">
+  <div class="modal-header">
+    <h4 tooltip="refresh">LOOKUP REFRESH CONFIRM</h4>
+  </div>
+  <div class="modal-body" style="background-color: white">
+    <div ng-if="!!cube.detail.snapshot_table_desc_list" class="row">
+      <div class="col-md-2"></div>
+      <div class="col-md-8">
+        <table class="table table-striped list">
+          <tbody>
+            <tr>
+              <td style="width:30%">Lookup Table</td>
+              <td style="width:70%" colspan="2">
+                <select style="width:95%" chosen ng-model="lookup.select.table_name"
+                  ng-change="updateLookupTable(lookup.select.table_name)"
+                  ng-options="lookup as lookup for lookup in cubeLookups">
+                  <option value=""></option>
+                </select>
+              </td>
+            </tr>
+            <tr ng-if="dispalySegment">
+              <td style="width:30%">Segments</td>
+              <td style="width:60%">
+                <ui-select multiple ng-model="lookup.select.segments" theme="bootstrap" sortable="true" close-on-select="false" class="form-control">
+                  <ui-select-match placeholder="Select Segments...">{{$item.name}}</ui-select-match>
+                  <ui-select-choices repeat="segment in cube.segments | filter:getReadySegment">
+                    <div ng-bind-html="segment.name | highlight: $select.search"></div>
+                  </ui-select-choices>
+                </ui-select>
+              </td>
+              <td style="width:10%">
+                <input type="checkbox" ng-change="selectAllSegments(allSegments)" ng-model="allSegments">&nbsp;All
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      <div class="col-md-2"></div>
+    </div>
+    <div ng-if="!cube.detail.snapshot_table_desc_list" class="row">
+      <div class="col-md-2"></div>
+      <div class="col-md-8">
+        <span>No lookup table defined. Please configurate lookup in model</span>
+      </div>
+      <div class="col-md-2"></div>
+    </div>
+  </div>
+
+  <div class="modal-footer">
+    <button class="btn btn-primary" ng-click="cancel()">Close</button>
+    <button ng-if="!!cube.detail.snapshot_table_desc_list" class="btn btn-success" ng-click="refresh()">Submit</button>
+  </div>
+</script>
\ No newline at end of file
diff --git a/webapp/app/partials/tables/table_detail.html b/webapp/app/partials/tables/table_detail.html
index d8209a8..441e093 100644
--- a/webapp/app/partials/tables/table_detail.html
+++ b/webapp/app/partials/tables/table_detail.html
@@ -35,6 +35,9 @@
         <li>
           <a data-toggle="tab"  href="#access">Access</a>
         </li>
+        <li>
+          <a data-toggle="tab" href="#snapshot">Snapshot</a>
+        </li>
       </ul>
       <div class="tab-content">
         <!--Schema-->
@@ -184,8 +187,53 @@
               </div>
             </div>
           </div>
+        </div>
 
-
+        <!--snapshot-->
+        <div id="snapshot" class="tab-pane" ng-controller="TableSnapshotCtrl">
+          <div ng-if="tableSnapshots.length > 0">
+            <table class="table table-hover table-striped list">
+              <thead>
+                <tr style="cursor: pointer">
+                  <th ng-repeat="theaditem in tableConfig.snapshotTheaditems"
+                    ng-click="state.filterAttr= theaditem.attr;state.reverseColumn=theaditem.attr;state.filterReverse=!state.filterReverse;">
+                  {{theaditem.name}}
+                    <i ng-if="state.reverseColumn!= theaditem.attr"
+                       class="fa fa-unsorted"></i>
+                    <i ng-if="state.reverseColumn== theaditem.attr && !state.filterReverse"
+                       class="fa fa-sort-asc"></i>
+                    <i ng-if="state.reverseColumn== theaditem.attr && state.filterReverse"
+                       class="fa fa-sort-desc"></i>
+                  </th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr ng-repeat="snapshot in tableSnapshots | filter: columnName | orderObjectBy:state.filterAttr:state.filterReverse">
+                  <td style="{{(snapshot.snapshotID == snapshot.snapshotID)? 'background-color:#EBF9FE':''}}">
+                    {{snapshot.snapshotID}}
+                  </td>
+                  <td style="{{(snapshot.snapshotID == snapshot.snapshotID)? 'background-color:#EBF9FE':''}}">
+                    {{snapshot.storageType}}
+                  </td>
+                  <td style="{{(snapshot.snapshotID == snapshot.snapshotID)? 'background-color:#EBF9FE':''}}">
+                    {{snapshot.lastBuildTime | utcToConfigTimeZone}}
+                  </td>
+                  <td style="{{(snapshot.snapshotID == snapshot.snapshotID)? 'background-color:#EBF9FE':''}}">
+                    {{snapshot.sourceTableLastModifyTime | utcToConfigTimeZone}}
+                  </td>
+                  <td style="{{(snapshot.snapshotID == snapshot.snapshotID)? 'background-color:#EBF9FE':''}}">
+                    {{snapshot.sourceTableSize | bytes}}
+                  </td>
+                  <td style="{{(snapshot.snapshotID == snapshot.snapshotID)? 'background-color:#EBF9FE':''}}" class="snapshot-usage">
+                    <i class="fa fa-list text-aqua" style="cursor: pointer;"  aria-hidden="true" tooltip-placement="left" tooltip-html-unsafe="<div style='text-align:left'>{{snapshot.usageInfo}}</div>"></i>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+          <div ng-if="tableSnapshots.length == 0">
+            <div no-result text="No Snapshot Info."></div>
+          </div>
         </div>
       </div>
 

-- 
To stop receiving notification emails like this one, please contact
shaofengshi@apache.org.