This is an automated email from the ASF dual-hosted git repository. xxyu pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 569552d974105b55547bc9941ffbe004f4e0486d Author: Zhong, Yanghong <nju_y...@apache.org> AuthorDate: Mon May 18 13:35:57 2020 +0800 KYLIN-4488 Frontend support for auto-migration to allow user to do cube migration by self service --- .../kylin/rest/controller/MigrationController.java | 16 +++ webapp/app/js/controllers/cubes.js | 157 +++++++++++++++++++++ webapp/app/js/services/cubes.js | 17 ++- webapp/app/partials/cubes/cube_migrate.html | 91 ++++++++++++ webapp/app/partials/cubes/cubes.html | 5 + 5 files changed, 285 insertions(+), 1 deletion(-) diff --git a/cube-migration/src/main/java/org/apache/kylin/rest/controller/MigrationController.java b/cube-migration/src/main/java/org/apache/kylin/rest/controller/MigrationController.java index 9588e51..106d51f 100644 --- a/cube-migration/src/main/java/org/apache/kylin/rest/controller/MigrationController.java +++ b/cube-migration/src/main/java/org/apache/kylin/rest/controller/MigrationController.java @@ -45,6 +45,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.fasterxml.jackson.core.JsonParseException; @@ -82,6 +83,21 @@ public class MigrationController extends BasicController { return cube; } + @RequestMapping(value = "/{cubeName}/migrateRuleCheck", method = { RequestMethod.GET }) + @ResponseBody + public String migrationRuleCheck(@PathVariable String cubeName, @RequestParam String projectName, + @RequestParam(value = "targetHost", required = false) String targetHost) { + CubeInstance cube = getCubeInstance(cubeName); + try { + MigrationRuleSet.Context ctx = new MigrationRuleSet.Context(queryService, cube, getTargetHost(targetHost), + projectName); + return migrationService.checkRule(ctx); + } catch (Exception e) { + logger.error("Request migration failed.", e); + throw new BadRequestException(e.getMessage()); + } + } + @RequestMapping(value = "/{cubeName}/migrateRequest", method = { RequestMethod.PUT }) @ResponseBody public String requestMigration(@PathVariable String cubeName, @RequestBody MigrationRequest request) { diff --git a/webapp/app/js/controllers/cubes.js b/webapp/app/js/controllers/cubes.js index 2d9c032..ddc037e 100644 --- a/webapp/app/js/controllers/cubes.js +++ b/webapp/app/js/controllers/cubes.js @@ -581,6 +581,39 @@ KylinApp.controller('CubesCtrl', function ($scope, $q, $routeParams, $location, }); }; + $scope.startMigrateCube = function(cube, action) { + $scope.loadDetail(cube).then(function () { + switch (action) { + case 0: + var template = 'cubeMigrate.html'; + break; + case 1: + var template = 'migrateApprove.html'; + break; + case -1: + var template = 'migrateReject.html'; + break; + default: + var template = ''; + } + + if (!template) { + return; + } + + $modal.open({ + templateUrl: template, + controller: cubeMigrateCtrl, + windowClass:"clone-cube-window", + resolve: { + cube: function () { + return cube; + } + } + }); + }); + }; + $scope.listCubeAccess = function (cube) { //check project auth for user $scope.cubeProjectEntity = _.find($scope.projectModel.projects, function(project) {return project.name == $scope.projectModel.selectedProject;}); @@ -1080,3 +1113,127 @@ var lookupRefreshCtrl = function($scope, scope, CubeList, $modalInstance, CubeSe }; }; + +var cubeMigrateCtrl = function ($scope, $modalInstance, CubeService, cube, ProjectModel, loadingRequest, SweetAlert) { + $scope.migrate={ + targetProject: ProjectModel.selectedProject, + cubeValidate: true, + lockProjectName: false + } + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + + $scope.validate = function () { + $scope.MigrationRequest = { + projectName:$scope.migrate.targetProject + } + loadingRequest.show(); + CubeService.ruleCheck({cubeId: cube.name, projectName: $scope.migrate.targetProject}, function(result) { + loadingRequest.hide(); + $scope.migrate.cubeValidate = false; + $scope.migrate.lockProjectName = true; + if (result.length > 0) { + SweetAlert.swal('Attention', result, 'warning'); + } + }, 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'); + } + }); + } + + $scope.migrateCube = function(){ + if(!$scope.migrate.targetProject){ + SweetAlert.swal('Oops...', "Please input target project name.", 'info'); + return; + } + + $scope.MigrationRequest = { + projectName:$scope.migrate.targetProject + } + + SweetAlert.swal({ + title: '', + text: 'Are you sure to migrate the cube? ', + type: '', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: "Yes", + closeOnConfirm: true + }, function (isConfirm) { + if (isConfirm) { + loadingRequest.show(); + CubeService.migrate({cubeId: cube.name}, $scope.MigrationRequest, function (result) { + loadingRequest.hide(); + $modalInstance.dismiss('cancel'); + SweetAlert.swal('Success!', 'Your Migration Request has been well received. Please check your email to get timely update of this request.', 'success'); + }, function (e) { + loadingRequest.hide(); + if (e.data && e.data.exception) { + var msg = e.data.exception; + if(e.status == '400' && (e.data.exception.indexOf("QueryLatencyRule") > -1 || e.data.exception.indexOf("ExpansionRateRule") > -1)){ + msg += '. Please refer to the guidance to optimize your cube design'; + } + msg += '. For any question, please contact support team.'; + SweetAlert.swal('Oops...', msg, 'error'); + } else { + SweetAlert.swal('Oops...', 'Failed to take action.', 'error'); + } + }); + } + }); + }; + + $scope.migrateApprove = function(){ + $scope.MigrationRequest = { + projectName:$scope.migrate.targetProject + } + loadingRequest.show(); + CubeService.approve({cubeId: cube.name}, $scope.MigrationRequest, function (result) { + loadingRequest.hide(); + $modalInstance.dismiss('cancel'); + SweetAlert.swal('Success!', 'Approve cube migration successfully', 'success'); + }, 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'); + } + }); + }; + + $scope.migrateReject = function(){ + if(!$scope.migrate.reason) { + SweetAlert.swal('Oops...', 'Please enter reason for refusal.', 'info'); + return; + } + $scope.MigrationRequest = { + reason: $scope.migrate.reason + }; + loadingRequest.show(); + CubeService.reject({cubeId: cube.name}, $scope.MigrationRequest, function (result) { + loadingRequest.hide(); + $modalInstance.dismiss('cancel'); + SweetAlert.swal('Success!', 'Reject cube migration successfully', 'success'); + }, 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/services/cubes.js b/webapp/app/js/services/cubes.js index 65a4d5d..522d043 100644 --- a/webapp/app/js/services/cubes.js +++ b/webapp/app/js/services/cubes.js @@ -79,7 +79,22 @@ KylinApp.factory('CubeService', ['$resource', function ($resource, config) { } }, optimize: {method: 'PUT', params: {action: 'optimize'}, isArray: false}, + ruleCheck: { + method: 'GET', + params: { + action: 'migrateRuleCheck' + }, + isArray: false, + interceptor: { + response: function(response) { + return response.data; + } + } + }, lookupRefresh: {method: 'PUT', params: {action: 'refresh_lookup'}, isArray: false}, - checkDuplicateCubeName: {method: 'GET', params: {action: 'validate'}, isArray: false} + checkDuplicateCubeName: {method: 'GET', params: {action: 'validate'}, isArray: false}, + migrate: {method: 'PUT', params: {action: 'migrateRequest'}, isArray: false}, + approve: {method: 'PUT', params: {action: 'migrateApprove'}, isArray: false}, + reject: {method: 'PUT', params: {action: 'migrateReject'}, isArray: false} }); }]); diff --git a/webapp/app/partials/cubes/cube_migrate.html b/webapp/app/partials/cubes/cube_migrate.html new file mode 100644 index 0000000..51b0900 --- /dev/null +++ b/webapp/app/partials/cubes/cube_migrate.html @@ -0,0 +1,91 @@ + +<!-- +* 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="cubeMigrate.html"> + <div class="modal-header"> + <h4 tooltip="submit">MIGRATE CUBE</h4> + </div> + <div class="modal-body" style="background-color: white"> + <div class="row"> + <div class="col-md-8 col-md-offset-2"> + <div class="form-group"> + <b>Target Project Name:</b> + <br/> + <p style="color:red;font-size:12px;">You can change the below project name if it's different on Kylin Prod.</p> + <input type="text" class="form-control" ng-readonly="migrate.lockProjectName" ng-model="migrate.targetProject" /> + </div> + </div> + </div> + <div class="row"> + <div class="col-md-8 col-md-offset-2"> + <div class="form-group"> + <div ng-show="migrate.cubeValidate" > + <b>Cube Validation:</b> + <br/> + <button style="height:40px;width:150px;" class="btn btn-info" ng-click="validate()">Validate</button> + </div> + </div> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-success" ng-disabled="migrate.cubeValidate" ng-click="migrateCube()">Submit</button> + <button class="btn btn-primary" ng-click="cancel()">Close</button> + </div> +</script> + +<script type="text/ng-template" id="migrateApprove.html"> + <div class="modal-header"> + <h4 tooltip="submit">MIGRATE APPROVE</h4> + </div> + <div class="modal-body" style="background-color: white"> + <div class="row"> + <div class="col-md-8 col-md-offset-2"> + <div class="form-group"> + <b>Target Project Name:</b> + <br/> + <input type="text" class="form-control" ng-model="migrate.targetProject" value={{migrate.targetProject}}/> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-success" ng-click="migrateApprove()">Approve</button> + <button class="btn btn-primary" ng-click="cancel()">Close</button> + </div> +</script> + +<script type="text/ng-template" id="migrateReject.html"> + <div class="modal-header"> + <h4 tooltip="submit">MIGRATE REJECT</h4> + </div> + <div class="modal-body" style="background-color: white"> + <div class="row"> + <div class="col-md-8 col-md-offset-2"> + <div class="form-group"> + <b>Please enter reject reason:</b> + <br/> + <textarea ng-model="migrate.reason" placeholder="Reject Reason..." rows="5" cols="30" class="form-control" style="overflow:scroll"></textarea> + </div> + </div> + </div> + </div> + <div class="modal-footer"> + <button class="btn btn-success" ng-click="migrateReject()">Reject</button> + <button class="btn btn-primary" ng-click="cancel()">Close</button> + </div> +</script> diff --git a/webapp/app/partials/cubes/cubes.html b/webapp/app/partials/cubes/cubes.html index 65ebc87..dba3c8b 100644 --- a/webapp/app/partials/cubes/cubes.html +++ b/webapp/app/partials/cubes/cubes.html @@ -102,6 +102,7 @@ <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> <li ng-if="cube.status=='DISABLED' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="purge(cube)">Purge</a></li> <li ng-if="cube.status!='DESCBROKEN' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="cloneCube(cube)">Clone</a></li> + <li ng-if="cube.status=='READY' && (userService.hasRole('ROLE_ADMIN') || hasPermission(cube, permissions.ADMINISTRATION.mask) || hasPermission(cubeProjectEntity, permissions.ADMINISTRATION.mask))"><a ng-click="startMigrateCube(cube, 0);">Migrate</a></li> </ul> <ul ng-if="(userService.hasRole('ROLE_ADMIN') || hasPermission('cube', cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask, permissions.OPERATION.mask)) && cube.streamingV2 && actionLoaded" class="dropdown-menu" role="menu" style="right:0;left:auto;"> @@ -116,6 +117,7 @@ <li ng-if="cube.status=='DISABLED'"><a ng-click="enable(cube, $index)">Enable</a></li> <li ng-if="cube.status=='DISABLED'"><a ng-click="purge(cube, $index)">Purge</a></li> <li><a ng-click="cloneCube(cube)">Clone</a></li> + <li ng-if="cube.status=='READY' && (userService.hasRole('ROLE_ADMIN') || hasPermission(cube, permissions.ADMINISTRATION.mask))"><a ng-click="startMigrateCube(cube, 0);">Migrate</a></li> </ul> <span ng-if="!(userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask, permissions.OPERATION.mask))" class="dropdown-menu" role="menu"> N/A @@ -130,6 +132,8 @@ <ul class="dropdown-menu" role="menu" style="right:0;left:auto;"> <li ng-if="cube.status!='READY'"><a href="cubes/edit/{{cube.name}}/descriptionjson">Edit CubeDesc</a></li> <li><a href="cubes/view/{{cube.name}}/instancejson">View Cube</a></li> + <li ng-if="cube.status=='READY' && kylinConfig.getDeployEnv().indexOf('QA') > -1"><a ng-click="startMigrateCube(cube, 1)">Approve Migration</a></li> + <li ng-if="cube.status=='READY' && kylinConfig.getDeployEnv().indexOf('QA') > -1"><a ng-click="startMigrateCube(cube, -1)">Reject Migration</a></li> </ul> </div> </td> @@ -160,6 +164,7 @@ <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/cubes/cube_migrate.html'"></div> <div ng-include="'partials/jobs/lookup_refresh.html'"></div> <div ng-include="'partials/streaming/cubeAssignment.html'"></div> </div>