ui-modules/utils/catalog-uploader/catalog-uploader.js (158 lines of code) (raw):

/* * 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', '$rootScope', 'brooklynCatalogUploader', catalogUploaderDirective]); export default MODULE_NAME; export const CATALOG_UPLOAD_COMPLETED = "brooklyn-catalog-upload-completed"; /** * @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 needs 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, $rootScope, 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; let requireManualClose = false; 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'); element.addClass('br-drag-active-2'); }).bind('dragenter', ()=> { counter++; }).bind('dragleave', (event)=> { counter--; element.removeClass('br-drag-active-2'); if (!requireManualClose && counter === 0) element.removeClass('br-drag-active'); // close if we were triggered by a drag }).bind('drop', (event)=> { scope.upload(event.dataTransfer.files); counter--; element.removeClass('br-drag-active-2'); requireManualClose = true; if (!requireManualClose && counter === 0) element.removeClass('br-drag-active'); // close if we were triggered by a drag }); let field = attrs.brooklynCatalogUploader; if (angular.isDefined(field)) { scope.$on(field, ()=> { requireManualClose = true; element.addClass('br-drag-active'); }); } scope.selectedFiles = []; scope.close = ()=> { requireManualClose = false; counter = 0; scope.selectedFiles = []; // clean up the imported file list on returning to catalog, still needs a manual refresh to show the imported bundle 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; $rootScope.$broadcast(CATALOG_UPLOAD_COMPLETED); }).catch((error)=> { console.warn("ERROR uploading "+file, 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) { function getFileTypeProperties(fn) { if (!fn) return null; const fnl = fn.toLowerCase(); if (["bom","yml","yaml"].find(ext => fnl.endsWith("."+ext))) { return { http: { headers: {'Content-Type': 'application/yaml'} } }; } if (fnl.endsWith(".jar")) { return { binary: true, http: { headers: {'Content-Type': 'application/x-jar'}, transformRequest: angular.identity } }; } if (fnl.endsWith(".zip") || fnl.endsWith("ar")) { // support other archive types, tar, csar, etc return { binary: true, http: { headers: {'Content-Type': 'application/x-zip'}, transformRequest: angular.identity } }; } return null; } 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(); const options = getFileTypeProperties(file.name); if (options!=null) { let reader = new FileReader(); reader.addEventListener('load', ()=> { try { let rawData = new Uint8Array(reader.result); let data = options.binary ? rawData : String.fromCharCode.apply(null, rawData); catalogApi.create(data, {}, options.http).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. Supported types include BOM, YAML, and ZIP. The extension is significant."); } 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(); }); } }; }