blob: aa35dfd742723c875b926c7fc03224ab5e03c5dc [file] [log] [blame]
/*
* 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.
*/
import angular from 'angular';
import template from './catalog-uploader.html';
import catalogApi from '../providers/catalog-api.provider';
const MODULE_NAME = 'brooklyn.components.catalog-uploader';
/**
* @ngdoc module
* @name brooklyn.components.catalog-uploader
* @requires catalogApi
*
* @description
* Adds an overlay on top of the current DOM element to upload files to the catalog. Files can either by added via
* classic file selection or drag & drop. This support multiple files to be uploaded at once.
*/
angular.module(MODULE_NAME, [catalogApi])
.service('brooklynCatalogUploader', ['$q', 'catalogApi', catalogUploaderService])
.directive('customOnChange', customOnChangeDirective)
.directive('brooklynCatalogUploader', ['$compile', 'brooklynCatalogUploader', catalogUploaderDirective]);
export default MODULE_NAME;
/**
* @ngdoc directive
* @name brooklynCatalogUploader
* @module brooklyn.components.catalog-uploader
* @restrict A
*
* @description
* Attaches an overlay on the current DOM element to handle file upload to the catalog. Files can either by added via
* classic file selection or drag & drop. The overlay can be triggered by broadcasting an event: for this to work, the
* event name need to be passed as value for the `brooklynCatalogUploader` attribute.
*
* @param {string} brooklynCatalogUploader The value can be empty. Otherwise, the directive will listen for any event broadcasted
* with this name and will trigger the overlay upon reception.
*/
export function catalogUploaderDirective($compile, brooklynCatalogUploader) {
return {
restrict: 'A',
link: link
};
function link(scope, element, attrs) {
let div = document.createElement('div');
if ((('draggable' in div) || ('ondragstart' in div && 'ondrop' in div)) && 'FormData' in window && 'FileReader' in window) {
element.addClass('br-has-drag-upload');
}
element.append($compile(template)(scope));
let counter = 0;
element.bind('drag dragstart dragend dragover dragenter dragleave drop', (event)=> {
event.preventDefault();
event.stopPropagation();
}).bind('drag dragstart dragover dragenter', (event)=> {
event.dataTransfer.dropEffect = 'copy';
element.addClass('br-drag-active');
}).bind('dragenter', ()=> {
counter++;
}).bind('dragleave', (event)=> {
counter--;
if (counter === 0) {
element.removeClass('br-drag-active');
}
}).bind('drop', (event)=> {
scope.upload(event.dataTransfer.files);
});
let field = attrs.brooklynCatalogUploader;
if (angular.isDefined(field)) {
scope.$on(field, ()=> {
counter++;
element.addClass('br-drag-active');
});
}
scope.selectedFiles = [];
scope.close = ()=> {
counter--;
element.removeClass('br-drag-active');
};
scope.filesChanged = (event)=> {
scope.upload(event.target.files);
};
scope.upload = (files)=> {
for (let i = 0; i < files.length; i++) {
let file = files[i];
brooklynCatalogUploader.upload(file).then((data)=> {
file.result = data;
}).catch((error)=> {
file.error = error;
}).finally(()=> {
scope.$applyAsync();
});
scope.selectedFiles.unshift(file);
scope.$apply();
}
};
scope.getCatalogItemUrl = (item)=> {
let itemTraits = item.tags? item.tags.find(item => item.hasOwnProperty("traits")) : {"traits":[]};
return (item.supertypes ? item.supertypes : itemTraits.traits)
.includes('org.apache.brooklyn.api.location.Location')
? `/brooklyn-ui-location-manager/#!/location?symbolicName=${item.symbolicName}&version=${item.version}`
: `/brooklyn-ui-catalog/#!/bundles/${item.containingBundle.split(':')[0]}/${item.containingBundle.split(':')[1]}/types/${item.symbolicName}/${item.version}`;
};
}
}
/**
* @ngdoc service
* @name brooklynCatalogUploader
* @module brooklyn.components.catalog-uploader
*
* @description
* Encapsulate the logic to validate files to upload to the catalog.
*/
export function catalogUploaderService($q, catalogApi) {
let extensions = {
'bom': {
headers: {'Content-Type': 'application/yaml'}
},
'yml': {
headers: {'Content-Type': 'application/yaml'}
},
'yaml': {
headers: {'Content-Type': 'application/yaml'}
},
'zip' : {
headers: {'Content-Type': 'application/x-zip'},
transformRequest: angular.identity
},
'jar' : {
headers: {'Content-Type': 'application/x-jar'},
transformRequest: angular.identity
}
};
return {
/**
* @ngdoc method
* @name upload
* @methodOf brooklynCatalogUploader
*
* @description
* Upload a file to the catalog. This will validate the extensions and reject the promises if the current one is
* not supported.
*
* @param {object} file A file object, representing a file to upload to the catalog.
*
* @return A promise that gets resolve if the upload *and* process of the file server side is successful; the
* resolve data contains the catalog items that have been added a map of `{id: item}`. Otherwise, the promise
* is rejected with the error message as parameter.
*/
upload: upload
};
function upload(file) {
let defer = $q.defer();
if (new RegExp('^.*\.(' + Object.keys(extensions).join('|') + ')$').test(file.name)) {
Object.keys(extensions).forEach((extension)=> {
if (!new RegExp('^.*\.(' + extension + ')$').test(file.name)) {
return;
}
let options = extensions[extension];
let reader = new FileReader();
reader.addEventListener('load', ()=> {
try {
let rawData = new Uint8Array(reader.result);
let data = ['zip', 'jar'].indexOf(extension) > -1 ? rawData : String.fromCharCode.apply(null, rawData);
catalogApi.create(data, {}, options).then((response)=> {
defer.resolve(response);
}).catch((response)=> {
defer.reject('Cannot upload item to the catalog: ' + response.error.message);
});
} catch (error) {
defer.reject('Cannot read file: ' + error.message);
}
}, false);
reader.readAsArrayBuffer(file);
});
} else {
defer.reject('Unsupported file type. Please upload only files with the following extensions: ' + Object.keys(extensions).map((extension)=>('*.' + extension)).join(', '));
}
return defer.promise;
}
}
export function customOnChangeDirective() {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.on('change', x => {
var onChangeHandler = scope.$eval(attrs.customOnChange);
onChangeHandler(x);
});
element.on('$destroy', function() {
element.off();
});
}
};
}