CHE-221 Add export workspace portability flow

It creates a remote workspace, start it and import projects that have a pre-defined remote location

Change-Id: I8b57c8656138ecb1832f10dd0401babf20a58e0c
Signed-off-by: Florent BENOIT <fbenoit@codenvy.com>
6.19.x
Florent BENOIT 2016-02-15 11:04:58 +01:00
parent ec9f7ea5b7
commit d598ecab2d
9 changed files with 409 additions and 48 deletions

View File

@ -0,0 +1,86 @@
<md-dialog flex="50">
<che-panel che-title="Export Workspace">
<md-dialog-content>
<md-tabs md-dynamic-height md-stretch-tabs="always" md-selected="selectedIndex">
<md-tab>
<md-tab-label>
<md-icon md-font-icon="fa fa-download" class="che-tab-label-icon"></md-icon>
<span class="che-tab-label-title">As a File</span>
</md-tab-label>
<md-tab-body>
<div layout="row" flex>
<ui-codemirror flex class="workspace-editor"
ui-codemirror="exportWorkspaceDialogController.editorOptions"
ng-model="exportWorkspaceDialogController.exportWorkspaceContent"></ui-codemirror>
</div>
<div layout="row" layout-align="end top">
<che-button-default che-button-title="download"
che-button-icon="fa fa-download"
ng-href="{{exportWorkspaceDialogController.downloadLink}}" ></che-button-default>
<che-button-default che-button-title="clipboard"
che-button-icon="fa fa-clipboard"
clip-copy="exportWorkspaceDialogController.exportWorkspaceContent"></che-button-default>
</div>
</md-tab-body>
</md-tab>
<md-tab>
<md-tab-label>
<md-icon md-font-icon="fa fa-cloud-upload" class="che-tab-label-icon"></md-icon>
<span class="che-tab-label-title">To Private Cloud</span>
</md-tab-label>
<md-tab-body>
<ng-form flex layout="column" name="privateCloudForm">
<che-input che-form="privateCloudForm"
che-name="url"
che-label-name="Host"
che-place-holder="URL of the remote cloud"
ng-model="exportWorkspaceDialogController.privateCloudUrl"
required>
<div ng-message="required">An URL is required.</div>
</che-input>
<che-input che-form="privateCloudForm"
che-name="username"
che-label-name="Login"
che-place-holder="Username used to login on the remote cloud"
ng-model="exportWorkspaceDialogController.privateCloudLogin"
required
ng-maxlength="128">
<div ng-message="required">A name is required.</div>
<div ng-message="maxlength">The name has to be less than 128 characters long.</div>
<div ng-message="md-maxlength">The name has to be less than 128 characters long.</div>
</che-input>
<che-input che-form="privateCloudForm"
che-name="password"
che-label-name="Password"
che-place-holder="Password used to login on the remote cloud"
ng-model="exportWorkspaceDialogController.privateCloudPassword"
required
type="password"
ng-maxlength="128">
<div ng-message="required">A password is required.</div>
<div ng-message="maxlength">The name has to be less than 128 characters long.</div>
<div ng-message="md-maxlength">The name has to be less than 128 characters long.</div>
</che-input>
</ng-form>
<div layout="row" layout-align="end top">
<che-button-default che-button-title="export"
che-button-icon="fa fa-cloud-upload"
ng-disabled="privateCloudForm.$invalid || exportWorkspaceDialogController.importInProgress"
ng-click="exportWorkspaceDialogController.exportToPrivateCloud()"></che-button-default>
</div>
<div layout="row" layout-align="start center" flex ng-if="exportWorkspaceDialogController.importInProgress">
<div><md-icon md-svg-src="assets/images/loader.svg" class="export-workspace-progress-icon" aria-label="loader"></md-icon></div>
<div ng-bind-html="exportWorkspaceDialogController.exportInCloudSteps"></div>
</div>
</md-tab-body>
</md-tab>
</md-tabs>
</md-dialog-content>
</che-panel>
<md-dialog-actions>
<md-button ng-hide="exportWorkspaceDialogController.importInProgress" ng-click="exportWorkspaceDialogController.hide()" tabindex="0" type="button">HIDE</md-button>
</md-dialog-actions>
</md-dialog>

View File

@ -0,0 +1,208 @@
/*
* Copyright (c) 2015-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*/
'use strict';
/**
* @ngdoc controller
* @name workspace.export.controller:ExportWorkspaceDialogController
* @description This class is handling the controller for the dialog box about the export of workspace
* @author Florent Benoit
*/
export class ExportWorkspaceDialogController {
/**
* Default constructor that is using resource
* @ngInject for Dependency injection
*/
constructor($q, $filter, lodash, cheRemote, cheNotification, $http, cheRecipeTemplate, $mdDialog, $log) {
this.$q = $q;
this.$filter = $filter;
this.$http = $http;
this.lodash = lodash;
this.cheRemote = cheRemote;
this.cheNotification = cheNotification;
this.cheRecipeTemplate = cheRecipeTemplate;
this.$mdDialog = $mdDialog;
this.$log = $log;
this.editorOptions = {
lineWrapping : true,
lineNumbers: false,
matchBrackets: true,
readOnly: 'nocursor',
mode: 'application/json'
};
this.privateCloudUrl = '';
this.privateCloudLogin = '';
this.privateCloudPassword = '';
this.retrieveWorkspace();
this.importInProgress = false;
}
/**
* It will hide the dialog box.
*/
hide() {
this.$mdDialog.hide();
}
/**
* Gets the workspace details and links to get download the JSON file
*/
retrieveWorkspace() {
let copyOfWorkspace = angular.copy(this.workspaceDetails);
this.downloadLink = '/api/workspace/' + this.workspaceId + '?downloadAsFile=' + this.workspaceDetails.name + '.json';
//remove links
delete copyOfWorkspace.links;
this.exportWorkspaceContent = this.$filter('json')(angular.fromJson(copyOfWorkspace), 2);
}
/**
* Start the process to export to the private Cloud
*/
exportToPrivateCloud() {
this.exportInCloudSteps = '';
this.importInProgress = true;
let login = this.cheRemote.newAuth(this.privateCloudUrl, this.privateCloudLogin, this.privateCloudPassword);
login.then((authData) => {
let copyOfWorkspace = angular.copy(this.workspaceDetails);
copyOfWorkspace.name = 'import-' + copyOfWorkspace.name;
// get content of the recipe
let environments = copyOfWorkspace.environments;
let defaultEnvName = copyOfWorkspace.defaultEnv;
let defaultEnvironment = this.lodash.find(environments, (environment) => {
return environment.name === defaultEnvName;
});
let recipeLocation = defaultEnvironment.machineConfigs[0].source.location;
// get content of recipe
this.$http.get(recipeLocation).then((response) => {
let recipeScriptContent = response.data;
// now upload the recipe to the remote service
let remoteRecipeAPI = this.cheRemote.newRecipe(authData);
let recipeContent = this.cheRecipeTemplate.getDefaultRecipe();
recipeContent.name = 'recipe-' + copyOfWorkspace.name;
recipeContent.script = recipeScriptContent;
let createRecipePromise = remoteRecipeAPI.create(recipeContent);
createRecipePromise.then((recipe) => {
let findLink = this.lodash.find(recipe.links, (link) => {
return link.rel === 'get recipe script';
});
// update copy of workspace with new recipe link
let recipeLink = findLink.href;
defaultEnvironment.machineConfigs[0].source.location = recipeLink;
let remoteWorkspaceAPI = this.cheRemote.newWorkspace(authData);
let remoteProjectAPI = this.cheRemote.newProject(authData);
this.exportInCloudSteps += 'Creating remote workspace...';
let createWorkspacePromise = remoteWorkspaceAPI.createWorkspaceFromConfig(null, copyOfWorkspace);
createWorkspacePromise.then((remoteWorkspace) => {
this.exportInCloudSteps += 'ok !<br>';
// ok now we've to import each project with a location into the remote workspace
let importProjectsPromise = this.importProjectsIntoWorkspace(remoteWorkspaceAPI, remoteProjectAPI, remoteWorkspace, authData);
importProjectsPromise.then(() => {
this.exportInCloudSteps += 'Export of workspace ' + copyOfWorkspace.name + 'finished <br>';
this.cheNotification.showInfo('Successfully exported the workspace to ' + copyOfWorkspace.name + ' on ' + this.privateCloudUrl);
this.hide();
}, (error) => {
this.handleError(error);
})
}, (error) => {
this.handleError(error);
})
});
}, (error) => {
this.handleError(error);
});
}, (error) => {
this.handleError(error);
});
}
/**
* Import all projects with a given source location into the remote workspace
* @param workspace the remote workspace
*/
importProjectsIntoWorkspace(remoteWorkspaceAPI, remoteProjectAPI, workspace, authData) {
var projectPromises = [];
// ok so
workspace.projects.forEach((project) => {
if (project.source && project.source.location && project.source.location.length > 0) {
let deferred = this.$q.defer();
let deferredPromise = deferred.promise;
projectPromises.push(deferredPromise);
this.exportInCloudSteps += 'Starting remote workspace...';
// compute WS url
let remoteURL = authData.url;
let remoteWsURL = remoteURL.replace('http', 'ws') + '/api/ws/';
let startWorkspacePromise = remoteWorkspaceAPI.startWorkspace(remoteWsURL, workspace.id, workspace.defaultEnv);
startWorkspacePromise.then(() => {
this.exportInCloudSteps += 'ok !<br>';
let importProjectPromise = remoteProjectAPI.importProject(workspace.id, project.name, project.source);
importProjectPromise.then(() => {
this.exportInCloudSteps += 'Importing project ' + project.name + '...<br>';
let updateProjectPromise = remoteProjectAPI.updateProject(workspace.id, project.name, project);
updateProjectPromise.then(() => {
deferred.resolve(workspace);
}, (error) => {
deferred.reject(error);
});
}, (error) => {
deferred.reject(error);
});
}, (error) => {
this.handleError(error);
});
}
});
return this.$q.all(projectPromises);
}
/**
* Notify user about the error.
* @param error the error message to display
*/
handleError(error) {
this.importInProgress = false;
var message;
if (error.data) {
if (error.data.message) {
message = error.data.message;
} else {
message = error.data;
}
} else {
message = 'unable to connect to ' + error.config.url;
}
this.cheNotification.showError('Exporting workspace failed: ' + message);
this.$log.error('error', message, error);
}
}

View File

@ -0,0 +1,5 @@
.export-workspace-progress-icon
width 52px
height 52px
fill $primary-color
margin-right 15px

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2015-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*/
'use strict';
/**
* @ngdoc controller
* @name workspace.export.controller:ExportWorkspaceController
* @description This class is handling the controller for the export of workspace
* @author Florent Benoit
*/
export class ExportWorkspaceController {
/**
* Default constructor that is using resource
* @ngInject for Dependency injection
*/
constructor($mdDialog) {
this.$mdDialog = $mdDialog;
}
showExport($event) {
this.$mdDialog.show({
targetEvent: $event,
controller: 'ExportWorkspaceDialogController',
controllerAs: 'exportWorkspaceDialogController',
bindToController: true,
clickOutsideToClose: true,
locals: {workspaceId: this.workspaceId,
workspaceDetails: this.workspaceDetails, callbackController: this},
templateUrl: 'app/workspaces/workspace-details/export-workspace/dialog/export-tab-dialog.html'
});
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2015-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*/
'use strict';
/**
* @ngdoc directive
* @name workspaces.details.directive:workspaceDetailsProjects
* @restrict E
* @element
*
* @description
* <export-workspace workspace-id="workspaceID" workspace-details="workspaceDetails"></export-workspace>
*
* @usage
* <export-workspace workspace-id="workspaceID" workspace-details="workspaceDetails"></export-workspace>
*
* @author Florent Benoit
*/
export class ExportWorkspace {
/**
* Default constructor that is using resource
* @ngInject for Dependency injection
*/
constructor () {
this.restrict = 'E';
this.templateUrl = 'app/workspaces/workspace-details/export-workspace/export-workspace.html';
this.controller = 'ExportWorkspaceController';
this.controllerAs = 'exportWorkspaceCtrl';
this.bindToController = true;
// scope values
this.scope = {
workspaceId: '@workspaceId',
workspaceDetails: '=workspaceDetails'
};
}
}

View File

@ -0,0 +1,10 @@
<che-panel che-title="Export Workspace" class="workspace-details-content">
<div ng-if="!exportWorkspaceCtrl.exportWorkspaceContent" layout="row" flex layout-align="space-around start">
<label flex="15" class="workspace-details-description">
Export your workspace.
</label>
<div layout="row" flex="85">
<che-button-default che-button-title="export" ng-click="exportWorkspaceCtrl.showExport($event)"></che-button-default>
</div>
</div>
</che-panel>

View File

@ -20,24 +20,15 @@ export class WorkspaceDetailsCtrl {
* Default constructor that is using resource injection
* @ngInject for Dependency injection
*/
constructor($route, $location, cheWorkspace, cheAPI, $mdDialog, cheNotification, $filter) {
constructor($route, $location, cheWorkspace, cheAPI, $mdDialog, cheNotification) {
this.cheNotification = cheNotification;
this.cheAPI = cheAPI;
this.cheWorkspace = cheWorkspace;
this.$mdDialog = $mdDialog;
this.$location = $location;
this.$filter = $filter;
this.workspaceId = $route.current.params.workspaceId;
this.editorOptions = {
lineWrapping : true,
lineNumbers: false,
matchBrackets: true,
readOnly: 'nocursor',
mode: 'application/json'
};
this.loading = true;
if (!this.cheWorkspace.getWorkspacesById().get(this.workspaceId)) {
@ -120,15 +111,6 @@ export class WorkspaceDetailsCtrl {
});
}
exportWorkspace() {
let copyOfWorkspace = angular.copy(this.workspaceDetails);
this.downloadLink = '/api/workspace/' + this.workspaceId + '?downloadAsFile=' + this.workspaceDetails.name + '.json';
//remove links
delete copyOfWorkspace.links;
this.exportWorkspaceContent = this.$filter('json')(angular.fromJson(copyOfWorkspace), 2);
}
runWorkspace() {
let promise = this.cheAPI.getWorkspace().startWorkspace(this.workspaceId, this.workspaceDetails.defaultEnv);

View File

@ -55,35 +55,7 @@
</div>
</div>
</che-panel>
<che-panel che-title="Export Workspace" class="workspace-details-content">
<div ng-if="!workspaceDetailsCtrl.exportWorkspaceContent" layout="row" flex layout-align="space-around start">
<label flex="15" class="workspace-details-description">
Export your workspace to a JSON file.
</label>
<div layout="column" flex="85">
<che-button-default che-button-title="export" ng-click="workspaceDetailsCtrl.exportWorkspace($event)"/>
</div>
</div>
<div ng-if="workspaceDetailsCtrl.exportWorkspaceContent.length > 0">
<div layout="row" flex>
<div flex="90">
<ui-codemirror class="workspace-editor"
ui-codemirror="workspaceDetailsCtrl.editorOptions"
ng-model="workspaceDetailsCtrl.exportWorkspaceContent"></ui-codemirror>
</div>
<div layout="row" layout-align="center center"><che-clipboard class="copy-clipboard" che-value="workspaceDetailsCtrl.exportWorkspaceContent"></che-clipboard></div>
</div>
<div layout="column">
<che-button-default che-button-title="download"
che-button-icon="fa fa-download"
ng-href="{{workspaceDetailsCtrl.downloadLink}}"/>
</div>
</div>
</che-panel>
<export-workspace workspace-id="{{workspaceDetailsCtrl.workspaceId}}" workspace-details="workspaceDetailsCtrl.workspaceDetails"></export-workspace>
<che-panel che-title="Delete Workspace" class="workspace-details-content">
<div layout="row" flex layout-align="space-around start">
<label flex="15" class="workspace-details-description">

View File

@ -18,6 +18,9 @@ import {UsageChart} from './list-workspaces/workspace-item/usage-chart.directive
import {WorkspaceItemCtrl} from './list-workspaces/workspace-item/workspace-item.controller';
import {WorkspaceDetailsCtrl} from './workspace-details/workspace-details.controller';
import {WorkspaceDetailsProjectsCtrl} from './workspace-details/workspace-projects/workspace-details-projects.controller';
import {ExportWorkspaceController} from './workspace-details/export-workspace/export-workspace.controller';
import {ExportWorkspace} from './workspace-details/export-workspace/export-workspace.directive';
import {ExportWorkspaceDialogController} from './workspace-details/export-workspace/dialog/export-workspace-dialog.controller';
import {WorkspaceDetailsProjects} from './workspace-details/workspace-projects/workspace-details-projects.directive';
import {ReadyToGoStacksCtrl} from './create-workspace/select-stack/ready-to-go-stacks/ready-to-go-stacks.controller';
import {ReadyToGoStacks} from './create-workspace/select-stack/ready-to-go-stacks/ready-to-go-stacks.directive';
@ -62,6 +65,10 @@ export class WorkspacesConfig {
register.controller('WorkspaceDetailsProjectsCtrl', WorkspaceDetailsProjectsCtrl);
register.directive('workspaceDetailsProjects', WorkspaceDetailsProjects);
register.controller('ExportWorkspaceDialogController', ExportWorkspaceDialogController);
register.controller('ExportWorkspaceController', ExportWorkspaceController);
register.directive('exportWorkspace', ExportWorkspace);
register.controller('WorkspaceRecipeCtrl', WorkspaceRecipeCtrl);
register.directive('cheWorkspaceRecipe', WorkspaceRecipe);