Merge branch 'pkg_mgmt'

Signed-off-by: Laurence Maultsby <laurence.maultsby@riftio.com>
diff --git a/skyquake/plugins/composer/api/composer.js b/skyquake/plugins/composer/api/composer.js
index d9dc5e2..ce12860 100644
--- a/skyquake/plugins/composer/api/composer.js
+++ b/skyquake/plugins/composer/api/composer.js
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -22,9 +22,13 @@
 var utils = require('../../../framework/core/api_utils/utils.js');
 var constants = require('../../../framework/core/api_utils/constants.js');
 var _ = require('underscore');
+var URL = require('url');
+var uuid = require('uuid');
 var APIVersion = '/v1';
-var Composer = {};
+var PackageFileHandler = require('./packageFileHandler.js');
 
+var Composer = {};
+var FileManager = {};
 var DataCenters = {};
 // Catalog module methods
 Composer.get = function(req) {
@@ -298,4 +302,295 @@
         });
     });
 };
-module.exports = Composer;
+
+Composer.upload = function(req) {
+    console.log(' Uploading file', req.file.originalname, 'as', req.file.filename);
+    var api_server = req.query['api_server'];
+    // dev_download_server is for testing purposes.
+    // It is the direct IP address of the Node server where the
+    // package will be hosted.
+    var download_host = req.query['dev_download_server'];
+
+    if (!download_host) {
+        download_host = req.protocol + '://' + req.headers.host;
+    }
+
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operations/package-create',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                    'Authorization': req.get('Authorization')
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true,
+                json: true,
+                body: {
+                    input: {
+                        'external-url': download_host + '/composer/upload/' + req.file.filename,
+                        'package-type': 'VNFD',
+                        'package-id': uuid()
+                    }
+                }
+            })
+        ]).then(function(result) {
+            var data = {};
+            data['transaction_id'] = result[0].body['output']['transaction-id'];
+
+            // Add a status checker on the transaction and then to delete the file later
+            PackageFileHandler.checkCreatePackageStatusAndHandleFile(req, data['transaction_id']);
+            
+            // Return status to composer UI to update the status.
+            resolve({
+                statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                data: data
+            });
+        }).catch(function(error) {
+            var res = {};
+            console.log('Problem with Composer.upload', error);
+            res.statusCode = error.statusCode || 500;
+            res.errorMessage = {
+                error: 'Failed to upload package ' + req.file.originalname + '. Error: ' + error
+            };
+            reject(res);
+        });
+    });
+};
+Composer.addFile = function(req) {
+    console.log(' Uploading file', req.file.originalname, 'as', req.file.filename);
+    var api_server = req.query['api_server'];
+    var download_host = req.query['dev_download_server'];
+    var package_id = req.query['package_id'];
+    var package_type = req.query['package_type'].toUpperCase();
+    var package_path = req.query['package_path'];
+    if (!download_host) {
+        download_host = req.protocol + '://' + req.headers.host;
+    }
+
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operations/package-file-add',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                    'Authorization': req.get('Authorization')
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true,
+                json: true,
+                body: {
+                    input: {
+                        'external-url': download_host + '/composer/upload/' + req.query['package_id'] + '/' + req.file.filename,
+                        'package-type': package_type,
+                        'package-id': package_id,
+                        'package-path': package_path + '/' + req.file.filename
+                    }
+                }
+            })
+        ]).then(function(result) {
+            var data = {};
+            data['transaction_id'] = result[0].body['output']['transaction-id'];
+            resolve({
+                statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                data: data
+            });
+        }).catch(function(error) {
+            var res = {};
+            console.log('Problem with Composer.upload', error);
+            res.statusCode = error.statusCode || 500;
+            res.errorMessage = {
+                error: 'Failed to upload package ' + req.file.originalname + '. Error: ' + error
+            };
+            reject(res);
+        });
+    });
+}
+
+Composer.exportPackage = function(req) {
+    // /api/operations/package-export
+    var api_server = req.query['api_server'];
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operations/package-export',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                    'Authorization': req.get('Authorization')
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true,
+                json: true,
+                body: { "input": req.body}
+            })
+        ]).then(function(result) {
+            var data = {};
+            resolve({
+                statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                data: result[0].body
+            });
+        }).catch(function(error) {
+            var res = {};
+            console.log('Problem with Composer.exportPackage', error);
+            res.statusCode = error.statusCode || 500;
+            res.errorMessage = {
+                error: error
+            };
+            reject(res);
+        });
+    });
+}
+
+FileManager.get = function(req) {
+    var api_server = req.query['api_server'];
+    var type = req.query['package_type'] && req.query['package_type'].toUpperCase();
+    var id = req.query['package_id'];
+    var downloadUrl = req.query['url'];
+    var path = req.query['package_path'];
+    var payload = {
+        "input": {
+            "package-type": type,
+            "package-id": id
+        }
+    }
+    if(req.method == 'GET') {
+        if(downloadUrl && path) {
+            payload.input['external-url'] = downloadUrl;
+            payload.input['package-path'] = path;
+            return download(payload);
+        } else {
+            return list(payload);
+        }
+    }
+    if(req.method == 'DELETE') {
+        payload.input['package-path'] = path;
+        return deleteFile(payload)
+    }
+
+    function deleteFile(payload) {
+        return new Promise(function(resolve, reject) {
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operations/rw-pkg-mgmt:package-file-delete',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                    'Authorization': req.get('Authorization')
+                }),
+                json: payload,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            }).then(function(data) {
+                        if (utils.validateResponse('FileManager.delete', data.error, data, data.body, resolve, reject)) {
+                            resolve({
+                                statusCode: data.statusCode,
+                                data: data.body
+                            });
+                        }
+                    })
+        })
+    }
+    function download(payload) {
+        return new Promise(function(resolve, reject) {
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operations/rw-pkg-mgmt:package-file-add',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                    'Authorization': req.get('Authorization')
+                }),
+                json: payload,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            }).then(function(data) {
+                        if (utils.validateResponse('FileManager.get', data.error, data, data.body, resolve, reject)) {
+                            resolve({
+                                statusCode: data.statusCode,
+                                data: data.body
+                            });
+                        }
+                    })
+        })
+    }
+    function list(payload) {
+        return new Promise(function(resolve, reject) {
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operations/get-package-endpoint',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                    'Authorization': req.get('Authorization')
+                }),
+                json: payload,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            }).then(function(data) {
+                if (utils.validateResponse('FileManager.get', data.error, data, data.body, resolve, reject)) {
+                    var endpoint = null;
+                    var parsedEndpoint = null;
+                    try {
+                        endpoint = data.body.output.endpoint
+                    } catch(e) {
+                        console.log('Something went wrong with the FileManager.get data that was returned');
+                        reject({});
+                    }
+                    parsedEndpoint = URL.parse(endpoint);
+                    rp({
+                        uri: api_server + ':' + parsedEndpoint.port + parsedEndpoint.path,
+                        method: 'GET',
+                        headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                            'Authorization': req.get('Authorization')
+                        }),
+                        forever: constants.FOREVER_ON,
+                        rejectUnauthorized: false,
+                        resolveWithFullResponse: true
+                    }).then(function(data) {
+                        if (utils.validateResponse('FileManager.get', data.error, data, data.body, resolve, reject)) {
+                            resolve({
+                                statusCode: data.statusCode,
+                                data: data.body
+                            });
+                        }
+                    })
+                }
+            })
+        })
+    }
+}
+FileManager.job = function(req) {
+    var api_server = req.query["api_server"];
+    var uri = utils.confdPort(api_server);
+    var url = '/api/operational/download-jobs';
+    var id = req.params['id'];
+    return new Promise(function(resolve, reject) {
+        request({
+            url: uri + url + '?deep',
+            method: 'GET',
+            headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                'Authorization': req.get('Authorization')
+            }),
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+        }, function(error, response, body) {
+            if (utils.validateResponse('restconfAPI.streams', error, response, body, resolve, reject)) {
+                var data = JSON.parse(response.body)['rw-pkg-mgmt:download-jobs'];
+                var returnData = [];
+                data && data.job.map(function(d) {
+                    if(d['package-id'] == id) {
+                        returnData.push(d)
+                    }
+                })
+                resolve({
+                    statusCode: response.statusCode,
+                    data: returnData
+                })
+            };
+        })
+    })
+}
+module.exports = {
+    Composer:Composer,
+    FileManager: FileManager
+};
diff --git a/skyquake/plugins/composer/api/packageFileHandler.js b/skyquake/plugins/composer/api/packageFileHandler.js
new file mode 100644
index 0000000..f199610
--- /dev/null
+++ b/skyquake/plugins/composer/api/packageFileHandler.js
@@ -0,0 +1,59 @@
+var request = require('request');
+var Promise = require('bluebird');
+var rp = require('request-promise');
+var utils = require('../../../framework/core/api_utils/utils.js');
+var constants = require('../../../framework/core/api_utils/constants.js');
+var fs = require('fs');
+var _ = require('lodash');
+
+PackageFileHandler = {};
+
+function deleteFile(filename) {
+	setTimeout(function() {
+		fs.unlinkSync(constants.BASE_PACKAGE_UPLOAD_DESTINATION + filename);
+	}, constants.PACKAGE_FILE_DELETE_DELAY_MILLISECONDS);
+};
+
+function checkStatus(req, transactionId) {
+	var upload_server = req.query['upload_server'];
+	var headers = _.extend({},
+        {
+            'Authorization': req.get('Authorization')
+        }
+    );
+	request({
+		url: upload_server + ':' + constants.PACKAGE_MANAGER_SERVER_PORT + '/api/upload/' + transactionId + '/state',
+		type: 'GET',
+		headers: headers,
+		forever: constants.FOREVER_ON,
+       	rejectUnauthorized: false
+	}, function(error, response, body) {
+		if (error) {
+			console.log('Error checking status for transaction', transactionId, '. Will delete file', req.file.filename, ' in ', constants.PACKAGE_FILE_DELETE_DELAY_MILLISECONDS ,' milliseconds');
+			deleteFile(req.file.filename);
+		} else {
+			var jsonStatus = null;
+			if (typeof body == 'string' || body instanceof String) {
+				jsonStatus = JSON.parse(body);
+			} else {
+				jsonStatus = body;
+			}
+
+			if (jsonStatus.status && (jsonStatus.status == 'success' || jsonStatus.status == 'failure')) {
+				console.log('Transaction ', transactionId, ' completed with status ', jsonStatus.status ,'. Will delete file', req.file.filename, ' in ', constants.PACKAGE_FILE_DELETE_DELAY_MILLISECONDS ,' milliseconds');
+				deleteFile(req.file.filename);
+			} else {
+				setTimeout(function() {
+					checkStatus(req, transactionId);
+				}, constants.PACKAGE_FILE_ONBOARD_TRANSACTION_STATUS_CHECK_DELAY_MILLISECONDS);
+			}
+		}
+	});
+};
+
+PackageFileHandler.checkCreatePackageStatusAndHandleFile = function(req, transactionId) {
+	checkStatus(req, transactionId);
+};
+
+
+module.exports = PackageFileHandler;
\ No newline at end of file
diff --git a/skyquake/plugins/composer/config.json b/skyquake/plugins/composer/config.json
index 2a8a516..80a2a0c 100644
--- a/skyquake/plugins/composer/config.json
+++ b/skyquake/plugins/composer/config.json
@@ -1,4 +1,6 @@
 {
+    "copyright": "2016 RIFT.IO Inc",
+    "license": "Apache 2.0",
     "root": "public",
     "name": "Catalog",
     "dashboard" : "./src/components/ComposerApp.js",
diff --git a/skyquake/plugins/composer/package.json b/skyquake/plugins/composer/package.json
index 1cf415a..6a01af1 100644
--- a/skyquake/plugins/composer/package.json
+++ b/skyquake/plugins/composer/package.json
@@ -26,8 +26,10 @@
     "grunt-cli": "^0.1.13",
     "jquery": "^2.1.4",
     "loaders.css": "^0.1.1",
-    "lodash": "^3.10.1",
+    "lodash": "^4.0.0",
+    "mkdirp": "^0.5.1",
     "moment": "^2.10.6",
+    "multer": "^1.2.0",
     "normalize.css": "^3.0.3",
     "numeral": "^1.5.3",
     "object-assign": "^4.0.1",
@@ -39,8 +41,10 @@
     "react-crouton": "^0.2.7",
     "react-dom": "^0.14.3",
     "react-popout": "^0.4.0",
+    "react-treeview": "^0.4.2",
     "request-promise": "^3.0.0",
-    "require-json": "0.0.1"
+    "require-json": "0.0.1",
+    "uuid": "^3.0.0"
   },
   "devDependencies": {
     "babel-core": "^6.4.5",
diff --git a/skyquake/plugins/composer/routes.js b/skyquake/plugins/composer/routes.js
index af67bfa..9fcf104 100644
--- a/skyquake/plugins/composer/routes.js
+++ b/skyquake/plugins/composer/routes.js
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,10 +15,62 @@
  *   limitations under the License.
  *
  */
-var router = require('express').Router();
+var express = require('express');
+var router = express.Router();
 var cors = require('cors');
 var utils = require('../../framework/core/api_utils/utils.js')
-var Composer = require('./api/composer.js');
+var constants = require('../../framework/core/api_utils/constants.js');
+var C = require('./api/composer.js');
+var Composer = C.Composer;
+var FileManager = C.FileManager;
+var multer = require('multer');
+var fs = require('fs');
+var path = require('path');
+var mkdirp = require('mkdirp');
+
+var storage = multer.diskStorage({
+    // destination: 'upload/packages/',
+    destination: function(req, file, cb) {
+        var dir = constants.BASE_PACKAGE_UPLOAD_DESTINATION;
+        if (req.query['package_id']) {
+            dir += req.query['package_id'] + '/';
+        }
+        if (!fs.existsSync(dir)){
+            mkdirp(dir, function(err) {
+                if (err) {
+                    console.log('Error creating folder for uploads. All systems FAIL!');
+                    throw err;
+                }
+                cb(null, dir);
+            });
+        } else {
+            cb(null, dir);
+        }
+    },
+    filename: function (req, file, cb) {
+        if (req.query['package_id']) {
+            cb(null, file.originalname);
+        } else  {
+            cb(null, Date.now() + '_' + file.fieldname + '_' + file.originalname);
+        }
+    },
+    // limits: {
+    //     fieldNameSize: 100,
+    //     fieldSize: 10000,
+    //     fields: Infinity,
+    //     fileSize: 10000,
+    //     files: Infinity
+    //     parts: Infinity
+    //     headerPairs: 2000
+    // }
+});
+
+
+var upload = multer({
+    storage: storage
+});
+
+
 router.get('/api/catalog', cors(), function(req, res) {
     Composer.get(req).then(function(data) {
         utils.sendSuccessResponse(data, res);
@@ -51,5 +103,53 @@
         res.send(error.errorMessage);
     });
 });
+router.post('/upload', cors(), upload.single('package'), function (req, res, next) {
+    Composer.upload(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+router.use('/upload', cors(), express.static('upload/packages'));
+
+
+
+router.post('/api/file-manager', cors(), upload.single('package'), function (req, res, next) {
+    Composer.addFile(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+router.get('/api/file-manager', cors(), function(req, res) {
+    FileManager.get(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+})
+router.get('/api/file-manager/jobs/:id', cors(), function(req, res) {
+    FileManager.job(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+router.delete('/api/file-manager', cors(), function(req, res) {
+    FileManager.get(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+router.post('/api/package-export', cors(), function (req, res, next) {
+    Composer.exportPackage(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
 
 module.exports = router;
diff --git a/skyquake/plugins/composer/scripts/install.sh b/skyquake/plugins/composer/scripts/install.sh
index 76e5e75..9d27f5e 100755
--- a/skyquake/plugins/composer/scripts/install.sh
+++ b/skyquake/plugins/composer/scripts/install.sh
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/skyquake/plugins/composer/src/src/actions/ComposerAppActions.js b/skyquake/plugins/composer/src/src/actions/ComposerAppActions.js
index 03e784a..80ec497 100644
--- a/skyquake/plugins/composer/src/src/actions/ComposerAppActions.js
+++ b/skyquake/plugins/composer/src/src/actions/ComposerAppActions.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,9 +24,10 @@
 class ComposerAppActions {
 
 	constructor() {
-		this.generateActions('showError', 'clearError', 'setDragState', 'propertySelected', 'showJsonViewer', 'closeJsonViewer', 'selectModel', 'outlineModel', 'clearSelection', 'enterFullScreenMode', 'exitFullScreenMode');
+		this.generateActions('showError', 'clearError', 'setDragState', 'propertySelected', 'showJsonViewer', 'closeJsonViewer', 'selectModel', 'outlineModel', 'clearSelection', 'enterFullScreenMode', 'exitFullScreenMode',
+                             'showAssets', 'showDescriptor');
 	}
 
 }
 
-export default alt.createActions(ComposerAppActions);
\ No newline at end of file
+export default alt.createActions(ComposerAppActions);
diff --git a/skyquake/plugins/composer/src/src/components/Button.js b/skyquake/plugins/composer/src/src/components/Button.js
index 7a5a7f5..76a569f 100644
--- a/skyquake/plugins/composer/src/src/components/Button.js
+++ b/skyquake/plugins/composer/src/src/components/Button.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -59,8 +59,8 @@
 		const className = ClassNames(this.props.className, 'Button');
 		return (
 			<div className={className} onClick={this.props.onClick} title={title} draggable={draggable} onDragStart={this.props.onDragStart}>
-				<img src={src} />
-				<span>{label}</span>
+				{ src ? <img src={src} /> : null }
+				{label}
 			</div>
 		);
 	}
diff --git a/skyquake/plugins/composer/src/src/components/CanvasPanel.js b/skyquake/plugins/composer/src/src/components/CanvasPanel.js
index 6b04bd9..96904ea 100644
--- a/skyquake/plugins/composer/src/src/components/CanvasPanel.js
+++ b/skyquake/plugins/composer/src/src/components/CanvasPanel.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,6 +35,8 @@
 import SelectionManager from '../libraries/SelectionManager'
 import DescriptorModelIconFactory from '../libraries/model/IconFactory'
 
+import FileManager from './filemanager/FileManager.jsx';
+
 import '../styles/CanvasPanel.scss'
 
 const CanvasPanel = React.createClass({
@@ -69,8 +71,22 @@
 		var req = require.context("../", true, /^\.\/.*\.svg$/);
 		const hasItem = this.props.containers.length !== 0;
 		const isEditingNSD = DescriptorModelFactory.isNetworkService(this.props.containers[0]);
+		const isDescriptorView = (this.props.panelTabShown == 'descriptor');
 		const hasNoCatalogs = this.props.hasNoCatalogs;
 		const bodyComponent = hasItem ? <CatalogItemCanvasEditor zoom={this.props.zoom} isShowingMoreInfo={this.props.showMore} containers={this.props.containers}/> : messages.canvasWelcome();
+		const viewFiles = this.props.panelTabShown == 'assets';
+		const viewButtonTabs =  !hasItem ? null : (
+	           <div className="CanvasPanelTabs">
+	               <div className="CatalogFilter">
+						<button className={isDescriptorView ? '-selected' : ''} onClick={ComposerAppActions.showDescriptor}>
+							Descriptor
+						</button>
+						<button className={!isDescriptorView ? '-selected' : ''}  onClick={ComposerAppActions.showAssets}>
+							Assets
+						</button>
+					</div>
+				</div>
+       		)
 		return (
 			<div id="canvasPanelDiv" className="CanvasPanel" style={style} onDragOver={this.onDragOver} onDrop={this.onDrop}>
 				<div onDoubleClick={this.onDblClickOpenFullScreen}  className="CanvasPanelHeader panel-header" data-resizable="limit_bottom">
@@ -79,18 +95,23 @@
 						<span className="model-name">{this.props.title}</span>
 					</h1>
 				</div>
+				{viewButtonTabs}
 				<div className="CanvasPanelBody panel-body" style={{marginRight: this.props.layout.right, bottom: this.props.layout.bottom}} >
-					{hasNoCatalogs ? null : bodyComponent}
+					{hasNoCatalogs ? null : viewFiles ? <FileManager files={this.props.files} type={this.props.type} item={this.props.item} filesState={this.props.filesState} /> : bodyComponent}
 				</div>
-				<CanvasZoom zoom={this.props.zoom} style={{bottom: this.props.layout.bottom + 20}}/>
-				<CanvasPanelTray layout={this.props.layout} show={isEditingNSD}>
+				{
+					isDescriptorView ?
+						<CanvasZoom zoom={this.props.zoom} style={{bottom: this.props.layout.bottom + 20}}/>
+						: null
+				}
+				<CanvasPanelTray layout={this.props.layout} show={isEditingNSD && isDescriptorView}>
 					<EditForwardingGraphPaths containers={this.props.containers} />
 				</CanvasPanelTray>
 			</div>
 		);
 	},
 	onDragOver(event) {
-		const isDraggingFiles = _.contains(event.dataTransfer.types, 'Files');
+		const isDraggingFiles = _.includes(event.dataTransfer.types, 'Files');
 		if (!isDraggingFiles) {
 			event.preventDefault();
 			event.dataTransfer.dropEffect = 'copy';
diff --git a/skyquake/plugins/composer/src/src/components/CatalogPanel.js b/skyquake/plugins/composer/src/src/components/CatalogPanel.js
index 535a928..d7ebf0f 100644
--- a/skyquake/plugins/composer/src/src/components/CatalogPanel.js
+++ b/skyquake/plugins/composer/src/src/components/CatalogPanel.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -167,7 +167,7 @@
 			uiTransientState.isDrop = false;
 			uiTransientState.isDragging = true;
 			uiTransientState.wasTrayOpen = this.state.isTrayOpen;
-			uiTransientState.isDraggingFiles = _.contains(e.dataTransfer.types, 'Files');
+			uiTransientState.isDraggingFiles = _.includes(e.dataTransfer.types, 'Files');
 			const dragState = ComposerAppStore.getState().drag || {};
 			if (uiTransientState.isDraggingFiles || (dragState.type === 'catalog-item')) {
 				CatalogPanelTrayActions.open();
diff --git a/skyquake/plugins/composer/src/src/components/ComposerApp.js b/skyquake/plugins/composer/src/src/components/ComposerApp.js
index 9e6daa4..b5cfa75 100644
--- a/skyquake/plugins/composer/src/src/components/ComposerApp.js
+++ b/skyquake/plugins/composer/src/src/components/ComposerApp.js
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -47,11 +47,12 @@
 import TooltipManager from '../libraries/TooltipManager'
 import CatalogItemsActions from '../actions/CatalogItemsActions'
 import CommonUtils from 'utils/utils.js'
-
+import FileManagerActions from './filemanager/FileManagerActions';
 import 'normalize.css'
 import '../styles/AppRoot.scss'
 import 'style/layout.scss'
 
+
 const resizeManager = new ResizableManager(window);
 
 const clearLocalStorage = utils.getSearchParams(window.location).hasOwnProperty('clearLocalStorage');
@@ -59,6 +60,7 @@
 const preventDefault = e => e.preventDefault();
 const clearDragState = () => ComposerAppActions.setDragState(null);
 
+
 const ComposerApp = React.createClass({
 	mixins: [PureRenderMixin],
 	getInitialState() {
@@ -71,6 +73,9 @@
 		if (clearLocalStorage) {
 			window.localStorage.clear();
 		}
+        if(this.item) {
+            FileManagerActions.openFileManagerSockets();
+        }
 		this.state.isLoading = CatalogDataStore.getState().isLoading;
 		ComposerAppStore.listen(this.onChange);
 		CatalogDataStore.listen(this.onCatalogDataChanged);
@@ -88,6 +93,7 @@
 		window.removeEventListener('dragover', preventDefault);
 		window.removeEventListener('drop', preventDefault);
 		window.removeEventListener('drop', clearDragState);
+        FileManagerActions.closeFileManagerSockets();
 		// resizeManager automatically registered its event handlers
 		resizeManager.removeAllEventListeners();
 		ComposerAppStore.unlisten(this.onChange);
@@ -178,14 +184,17 @@
 			const hasNoCatalogs = CatalogDataStore.getState().catalogs.length === 0;
 			const isLoading = self.state.isLoading;
 
+            //Bridge element for Crouton fix. Should eventually put Composer on same flux context
+            const Bridge = this.state.ComponentBridgeElement;
+
 			html = (
 				<div ref="appRoot" id="RIFT_wareLaunchpadComposerAppRoot" className="AppRoot" onClick={onClickUpdateSelection}>
+                    <Bridge />
 					<i className="corner-accent top left" />
 					<i className="corner-accent top right" />
 					<i className="corner-accent bottom left" />
 					<i className="corner-accent bottom right" />
 					{AppHeader}
-					<Crouton id={Date.now()} type={self.state.messageType} message={self.state.message} onDismiss={ComposerAppActions.clearError} />
 					<div className="AppBody">
 						<div className={classNames}>
 							<CatalogPanel layout={self.state.layout}
@@ -197,12 +206,23 @@
 										 showMore={self.state.showMore}
 										 containers={containers}
 										 title={canvasTitle}
-										 zoom={self.state.zoom} />
-							<DetailsPanel layout={self.state.layout}
+										 zoom={self.state.zoom}
+										 panelTabShown={self.state.panelTabShown}
+										 files={self.state.files}
+										 filesState={self.state.filesState}
+										 item={self.state.item}
+										 type={self.state.filterCatalogByTypeValue}
+										  />
+						 	{
+ 						 		(self.state.panelTabShown == 'descriptor') ?
+ 						 		<DetailsPanel layout={self.state.layout}
 										  hasNoCatalogs={hasNoCatalogs}
 										  showMore={self.state.showMore}
 										  containers={containers}
 										  showJSONViewer={self.state.showJSONViewer} />
+							  : null
+ 						 	}
+
 							<ComposerAppToolbar layout={self.state.layout}
 												showMore={self.state.showMore}
 												isEditingNSD={isEditingNSD}
@@ -210,7 +230,8 @@
 												isModified={isModified}
 												isNew={isNew}
 												disabled={!hasItem}
-												onClick={event => event.stopPropagation()}/>
+												onClick={event => event.stopPropagation()}
+												panelTabShown={self.state.panelTabShown}/>
 						</div>
 					</div>
 					<ModalOverlay />
diff --git a/skyquake/plugins/composer/src/src/components/ComposerAppToolbar.js b/skyquake/plugins/composer/src/src/components/ComposerAppToolbar.js
index 8515182..458e774 100644
--- a/skyquake/plugins/composer/src/src/components/ComposerAppToolbar.js
+++ b/skyquake/plugins/composer/src/src/components/ComposerAppToolbar.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -142,8 +142,16 @@
 			);
 		}
 		const hasSelection = SelectionManager.getSelections().length > 0;
+		if(this.props.panelTabShown != 'descriptor') {
+			style.pointerEvents = 'none';
+		}
 		return (
 			<div className="ComposerAppToolbar" style={style}>
+			{
+				(this.props.panelTabShown != 'descriptor') ?
+					<div className="disableOverlay"></div>
+				: null
+			}
 				{(()=>{
 					if (this.props.isEditingNSD || this.props.isEditingVNFD) {
 						return (
diff --git a/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js b/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
index b510715..4ee4345 100644
--- a/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
+++ b/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
@@ -47,12 +47,12 @@
 import '../styles/EditDescriptorModelProperties.scss'
 
 function getDescriptorMetaBasicForType(type) {
-	const basicPropertiesFilter = d => _.contains(DESCRIPTOR_MODEL_FIELDS[type], d.name);
+	const basicPropertiesFilter = d => _.includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
 	return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
 }
 
 function getDescriptorMetaAdvancedForType(type) {
-	const advPropertiesFilter = d => !_.contains(DESCRIPTOR_MODEL_FIELDS[type], d.name);
+	const advPropertiesFilter = d => !_.includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
 	return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
 }
 
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx b/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx
new file mode 100644
index 0000000..ce490a0
--- /dev/null
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx
@@ -0,0 +1,258 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed 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.
+ *
+ */
+
+
+//https://raw.githubusercontent.com/RIFTIO/RIFT.ware/master/rift-shell
+import _ from 'lodash'
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TreeView from 'react-treeview';
+import TextInput from 'widgets/form_controls/textInput.jsx';
+import Button from '../Button';
+import './FileMananger.scss';
+import FileManagerActions from './FileManagerActions.js';
+import imgSave from '../../../../node_modules/open-iconic/svg/data-transfer-upload.svg'
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import LoadingIndicator from 'widgets/loading-indicator/loadingIndicator.jsx';
+
+import DropZone from 'dropzone'
+import Utils from '../../libraries/utils'
+import FileManagerUploadDropZone from '../../libraries/FileManagerUploadDropZone';
+let API_SERVER = require('utils/rw.js').getSearchParams(window.location).api_server;
+
+const createDropZone = function (action, clickable, type, id, path, dropTarget) {
+    const dropZone = new FileManagerUploadDropZone(ReactDOM.findDOMNode(dropTarget), [clickable], action, type, id, path);
+    // dropZone.on('dragover', this.onDragOver);
+    // dropZone.on('dragend', this.onDragEnd);
+    // dropZone.on('addedfile', this.onFileAdded);
+    return dropZone;
+};
+//updateFileLocationInput
+class FileManager extends React.Component {
+    constructor(props) {
+        super(props)
+    }
+    componentWillMount() {
+        // FileManagerActions.openFileManagerSockets()
+    }
+    componentWillUnmount() {
+        // FileManagerActions.closeFileManagerSockets();
+    }
+    generateFolder(data, nesting) {
+        let nestingLevel = nesting || 1;
+
+    }
+    deleteFile(name) {
+        return function(e) {
+            FileManagerActions.deletePackageFile(name);
+        }
+
+    }
+    updateFileLocationInput(name) {
+        return function(e) {
+            FileManagerActions.updateFileLocationInput({
+                name: name,
+                value: e.target.value
+            });
+        }
+    }
+    sendDownloadFileRequst = (url, path) => {
+        let self = this;
+        return function(e) {
+            if(!url || url == "") {
+                return self.props.actions.showNotification.defer({type: 'error', msg: 'Value missing in download request'});;
+            }
+            let files = self.props.files.data;
+            let folder = path.split('/');
+            let splitUrl = url.split('/');
+            let fileName = splitUrl[splitUrl.length - 1];
+            folder.pop;
+            let fullPath = _.cloneDeep(folder);
+            fullPath.push(fileName);
+            fullPath = fullPath.join('/');
+            folder = folder.join('/');
+            let fileIndex = _.findIndex(files[folder], function(f) {
+                return f.name == fullPath;
+            })
+            if (fileIndex == -1) {
+                FileManagerActions.sendDownloadFileRequst({
+                    url: url,
+                    path: path
+                });
+            } else {
+                self.props.actions.showNotification('It seems you\'re attempting to upload a file with a duplicate file name');
+            }
+        }
+    }
+    render() {
+        let self = this;
+        let html = (
+            <div className="FileManager">
+                <PanelWrapper style={{flexDirection: 'column'}}>
+                    {self.props.files && self.props.files.id && buildList(self, self.props.files) }
+                </PanelWrapper>
+            </div>
+        )
+        return html;
+    }
+
+}
+
+function buildList(self, data) {
+    let toReturn = [];
+    data.id.map(function(k,i) {
+        toReturn.push (contentFolder(self, data.data[k], k, i, self.props.filesState, self.updateFileLocationInput, self.sendDownloadFileRequst, self.deleteFile));
+    });
+    return toReturn.reverse();
+}
+
+function contentFolder(context, folder, path, key, inputState, updateFn, sendDownloadFileRequst, deleteFn) {
+    let type = context.props.type;
+    let id = context.props.item.id;
+    const onboardDropZone = createDropZone.bind(this, FileManagerUploadDropZone.ACTIONS.onboard, '.ComposerAppAddFile.' + path.replace('/', '-'), type, id, path);
+    return (
+        <Panel title={path} key={key} itemClassName="nested" no-corners>
+        <div className="folder">
+            {
+                folder.map(function(f, i) {
+                    if( !f.hasOwnProperty('contents') ){
+                        return contentFile(context, f, path, i, deleteFn);
+                    }
+                })
+            }
+            <Panel className="addFileSection" no-corners>
+                <ItemUpload type={type} id={id} path={path} key={key} dropZone={onboardDropZone} />
+                <div style={{marginLeft: '0.5rem'}}>
+                    OR
+                </div>
+                <div className="inputSection">
+                    <TextInput placeholder="URL" className="" label="External URL" value={inputState[path]} onChange={updateFn(path)} />
+                    <Button className='ComposerAppSave' label="DOWNLOAD" onClick={sendDownloadFileRequst(inputState[path], path)}/>
+                </div>
+            </Panel>
+
+            </div>
+        </Panel>
+    );
+}
+class ItemUpload extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    componentDidMount() {
+        if (this.props.dropZone) {
+            const dropTarget = this;
+            const dropZone = this.props.dropZone(dropTarget);
+        }
+    }
+    render() {
+        let {type, id, path, key, ...props} = this.props;
+        return (
+            <div className="inputSection">
+                <label className="sqTextInput" style={{flexDirection: 'row', alignItems:'center'}}>
+                    <span>Upload File</span>
+                    <Button className={'ComposerAppAddFile ' + path.replace('/', '-')} label="BROWSE"/>
+                </label>
+            </div>
+        )
+    }
+}
+function contentFile(context, file, path, key, deleteFn) {
+    const name = stripPath(file.name, path);
+    const id = context.props.item.id;
+    const type = context.props.type;
+    //{`${window.location.protocol}//${API_SERVER}:4567/api/package${type}/${id}/${path}/${name}`}
+    return (
+        <div className="file" key={key}>
+            <div className="file-section">
+                <div className="file-info">
+                    <div className="file-status" style={{display: (file.status && file.status.toLowerCase() != 'completed') ? 'inherit' : 'none', color: (file.status == 'FAILED' ? 'red' : 'inherit')}}>
+                        {file.status && (file.status == 'IN_PROGRESS' || file.status == 'DOWNLOADING'  )  ? <LoadingIndicator size={2} /> : file.status }
+                    </div>
+                    <div className="file-name">
+                        <a target="_blank" href={`${API_SERVER}:4567/api/package/${type}/${id}/${path}/${name}`}>{name}</a>
+                    </div>
+                </div>
+                <div className="file-action" style={{display: (!file.status || (file && file.status.toLowerCase() != 'loading...')) ? 'inherit' : 'none', cursor: 'pointer'}} onClick={deleteFn(file.name)}>X</div>
+            </div>
+        </div>
+    )
+}
+
+function stripPath(name, path, returnPath) {
+    let stripSlash = (name.indexOf('/') > -1) ? '/' : '';
+    // return name.split(path + stripSlash)[1].replace('/', '');
+    let split = name.split(path + stripSlash)[returnPath ? 0 : 1];
+    return split ? split.replace('/', '') : name;
+}
+
+
+
+export default SkyquakeComponent(FileManager)
+/**
+ * Sample Data
+ */
+// let files = {
+//   "name": ".",
+//   "contents": [
+//     {
+//       "name": "pong_vnfd",
+//       "contents": [
+//         {
+//           "name": "pong_vnfd/checksums.txt",
+//           "last_modified_time": 1474458399.6218443,
+//           "byte_size": 168
+//         },
+//         {
+//           "name": "pong_vnfd/pong_vnfd.yaml",
+//           "last_modified_time": 1474458399.6258445,
+//           "byte_size": 3514
+//         },
+//         {
+//           "name": "pong_vnfd/icons",
+//           "contents": [
+//             {
+//               "name": "pong_vnfd/icons/rift_logo.png",
+//               "last_modified_time": 1474458399.6218443,
+//               "byte_size": 1658
+//             }
+//           ],
+//           "last_modified_time": 1474458399.6218443,
+//           "byte_size": 3
+//         },
+//         {
+//           "name": "pong_vnfd/cloud_init",
+//           "contents": [
+//             {
+//               "name": "pong_vnfd/cloud_init/pong_cloud_init.cfg",
+//               "last_modified_time": 1474458399.6258445,
+//               "byte_size": 227
+//             }
+//           ],
+//           "last_modified_time": 1474458399.6258445,
+//           "byte_size": 3
+//         }
+//       ],
+//       "last_modified_time": 1474458399.6258445,
+//       "byte_size": 6
+//     }
+//   ],
+//   "last_modified_time": 1474458399.6218443,
+//   "byte_size": 3
+// };
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js
new file mode 100644
index 0000000..f39c2a9
--- /dev/null
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js
@@ -0,0 +1,31 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed 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 alt from '../../alt';
+
+class FileManagerActions {
+
+    constructor() {
+        this.generateActions('getFilelistSuccess', 'getFilelistError', 'updateFileLocationInput','sendDownloadFileRequst', 'addFileSuccess', 'addFileError','deletePackageFile','deleteFileSuccess','deleteFileError','openDownloadMonitoringSocketSuccess', 'openDownloadMonitoringSocketError',
+                             'getFilelistSocketSuccess',
+                             'openFileManagerSockets', 'closeFileManagerSockets');
+    }
+
+}
+
+export default alt.createActions(FileManagerActions);
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js
new file mode 100644
index 0000000..d2d9765
--- /dev/null
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js
@@ -0,0 +1,193 @@
+
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed 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.
+ *
+ */
+'use strict';
+
+import _ from 'lodash'
+import $ from 'jquery'
+import alt from '../../alt'
+import utils from '../../libraries/utils'
+import FileManagerActions from './FileManagerActions'
+let Utils = require('utils/utils.js');
+let API_SERVER = require('utils/rw.js').getSearchParams(window.location).api_server;
+let HOST = API_SERVER;
+let NODE_PORT = require('utils/rw.js').getSearchParams(window.location).api_port || ((window.location.protocol == 'https:') ? 8443 : 8000);
+const FileManagerSource = {
+
+    getFilelist: function() {
+        return {
+            remote: function(state, id, type) {
+                return new Promise(function(resolve, reject) {
+                    console.log('Getting File Manager');
+                    $.ajax({
+                        beforeSend: Utils.addAuthorizationStub,
+                        url: 'api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id,
+                        success: function(data) {
+                            resolve(JSON.parse(data));
+                        },
+                        error: function(error) {
+                            if (typeof error == 'string') {
+                                error = JSON.parse(error);
+                            }
+                            reject(error);
+                        }
+                    }).fail(function(xhr){
+                        //Authentication and the handling of fail states should be wrapped up into a connection class.
+                        Utils.checkAuthentication(xhr.status);
+                    });
+                });
+            },
+            success: FileManagerActions.getFilelistSuccess,
+            error: FileManagerActions.getFilelistError
+        }
+    },
+    addFile: function() {
+        return {
+            remote: function(state, id, type, path, url) {
+                return new Promise(function(resolve, reject) {
+                    console.log('Adding file');
+                    console.log(id, type, path, url);
+                    let splitUrl = url.split('/');
+                    let fileName = splitUrl[splitUrl.length -1];
+                    $.ajax({
+                        beforeSend: Utils.addAuthorizationStub,
+                        url: 'api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id + '&package_path=' + path + '/' + fileName + '&url=' + url,
+                        success: function(data) {
+                            resolve({
+                                data:data,
+                                path: path,
+                                fileName: fileName
+                            });
+                        },
+                        error: function(error) {
+                            if (typeof error == 'string') {
+                                error = JSON.parse(error);
+                            }
+                            reject(error);
+                        }
+                    }).fail(function(xhr){
+                        //Authentication and the handling of fail states should be wrapped up into a connection class.
+                        Utils.checkAuthentication(xhr.status);
+                    });
+                });
+            },
+            success: FileManagerActions.addFileSuccess,
+            error: FileManagerActions.addFileError
+        }
+    },
+    deleteFile: function() {
+        return {
+            remote: function(state, id, type, path) {
+                return new Promise(function(resolve, reject) {
+                    $.ajax({
+                        method: 'DELETE',
+                        beforeSend: Utils.addAuthorizationStub,
+                        url: 'api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id + '&package_path=' + path ,
+                        success: function(data) {
+                            resolve({
+                                data: data,
+                                path: path
+                            });
+                        },
+                        error: function(error) {
+                            if (typeof error == 'string') {
+                                error = JSON.parse(error);
+                            }
+                            reject(error);
+                        }
+                    }).fail(function(xhr){
+                        //Authentication and the handling of fail states should be wrapped up into a connection class.
+                        Utils.checkAuthentication(xhr.status);
+                    });
+                });
+            },
+            success: FileManagerActions.deleteFileSuccess,
+            error: FileManagerActions.deleteFileError
+        }
+    },
+    updateFile: function() {
+        return {
+            remote: function(state, file) {
+                return new Promise(function(resolve, reject) {
+                    console.log('Getting File Manager');
+                    if(file) {
+                        console.log('Updating single file');
+                    }
+                    if(!file) {
+                        console.log('Update all files')
+                    }
+                    resolve({});
+                });
+            },
+            success: FileManagerActions.getFilelistSuccess,
+            error: FileManagerActions.getFilelistError
+        }
+    },
+    openDownloadMonitoringSocket: function() {
+        return {
+            remote: function(state, packageID) {
+                return new Promise(function(resolve, reject) {
+                    //api/operational/download-jobs/job/
+                   $.ajax({
+                    url: '/socket-polling?api_server=' + API_SERVER ,
+                    type: 'POST',
+                    beforeSend: Utils.addAuthorizationStub,
+                    data: {
+                      url: 'http://localhost:8000/composer/api/file-manager/jobs/' + packageID + '?api_server=' + API_SERVER,
+                    },
+                    success: function(data, textStatus, jqXHR) {
+                        Utils.checkAndResolveSocketRequest(data, resolve, reject);
+                    }
+                  }).fail(function(xhr){
+                    //Authentication and the handling of fail states should be wrapped up into a connection class.
+                    Utils.checkAuthentication(xhr.status);
+                  });
+                });
+            },
+            success: FileManagerActions.openDownloadMonitoringSocketSuccess,
+            error: FileManagerActions.openDownloadMonitoringSocketError
+        }
+    },
+    openFileMonitoringSocket: function() {
+        return {
+            remote: function(state, id, type) {
+                return new Promise(function(resolve, reject) {
+                    //api/operational/download-jobs/job/
+                   $.ajax({
+                    url: '/socket-polling?api_server=' + API_SERVER ,
+                    type: 'POST',
+                    beforeSend: Utils.addAuthorizationStub,
+                    data: {
+                      url: 'http://localhost:8000/composer/api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id
+                    },
+                    success: function(data, textStatus, jqXHR) {
+                        Utils.checkAndResolveSocketRequest(data, resolve, reject);
+                    }
+                  }).fail(function(xhr){
+                    //Authentication and the handling of fail states should be wrapped up into a connection class.
+                    Utils.checkAuthentication(xhr.status);
+                  });
+                });
+            },
+            success: FileManagerActions.getFilelistSocketSuccess,
+            error: FileManagerActions.getFilelistError
+        }
+    }
+};
+
+export default FileManagerSource;
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileMananger.scss b/skyquake/plugins/composer/src/src/components/filemanager/FileMananger.scss
new file mode 100644
index 0000000..136c7f5
--- /dev/null
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileMananger.scss
@@ -0,0 +1,171 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed 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 'style/_colors.scss';
+
+.FileManager {
+    width:600px;
+    .skyquakePanel {
+        display:block;
+        -ms-flex:0;
+            flex:0;
+        &-wrapper {
+            height:auto;
+        }
+        &-body {
+            padding: 0.5rem;
+        }
+    }
+    .addFileSection {
+        background:$gray;
+        margin:1rem 0;
+        &-body {
+            padding-bottom: 1rem;
+        }
+        .inputSection {
+            display:-ms-flexbox;
+            display:flex;
+            -ms-flex-align: end;
+            align-items: flex-end;
+            padding:0 0 0 0.5rem;
+            margin:0.5rem 0;
+            .sqTextInput {
+                margin-bottom:0rem;
+                input {
+                    font-size:10px;
+                    height:auto;
+                }
+            }
+            &:last-child {
+            }
+        }
+        .ComposerAppAddFile {
+            span {
+                color: black;
+            }
+        }
+    }
+    .tree-view {
+        margin:0 0.5rem;
+        margin-bottom: 0.5rem;
+         padding:0 0.5rem;
+         /* border-top:1px solid $gray-lightest;*/
+         /* border-left:0.5px solid $gray-lightest;*
+        /* border-bottom:1px solid white;*/
+        /* border: 1px solid rgba(143, 143, 143, 0.5);*/
+        /* background-color: rgba(147, 161, 161, 0.5);*/
+        border-radius: 3px;
+        font-size:10px;
+        &_item {
+            padding:0.5rem;
+            display:-ms-flexbox;
+            display:flex;
+            -ms-flex-align: center;
+            align-items: center;
+            color: #586e75;
+            text-transform: uppercase;
+            font-size:10px;
+            background:white;
+        }
+        &_children {
+            margin-left:0;
+            /* background: $gray;*/
+            padding: 0.125rem;
+            margin-bottom: 0.5rem;
+            /* background: $gray-light;*/
+            &-collapsed {
+                height: 0px;
+                overflow: hidden;
+            }
+        }
+        &_arrow{
+            height: 2rem;
+            width: 2rem;
+            display: -ms-flexbox;
+            display: flex;
+            -ms-flex-pack: center;
+            justify-content: center;
+            -ms-flex-align: center;
+            align-items: center;
+
+            margin-right: 6px;
+
+            font-size: 1.25rem;
+            cursor: pointer;
+            -webkit-user-select: none;
+            -moz-user-select: none;
+            -ms-user-select: none;
+            user-select: none;
+
+            display:none;
+
+            &:after {
+                content: '\25BE';
+            }
+            &-collapsed {
+                transform: rotate(-90deg);
+            }
+        }
+    }
+    .folder {
+        /* margin:0.5rem 0.5rem;*/
+        /* padding: 0.5rem;*/
+        /* background-color: rgba(238, 232, 213, 0.33);*/
+        /* border-radius:3px;*/
+    }
+    .nested {
+        & + div {
+            & > div:not(.nested-alt):not(.folder) {
+                &:nth-child(even) {
+                    background: $gray-light;
+                }
+                &:nth-child(odd) {
+                    background: $gray;
+                }
+            }
+        }
+    }
+    .file {
+        margin: 0.5rem 0;
+        -ms-flex-pack:justify;
+        justify-content:space-between;
+        &-section {
+            display:-ms-flexbox;
+            display:flex;
+            -ms-flex-pack:justify;
+                justify-content:space-between;
+            &>div {
+                margin:0 0.5rem;
+            }
+        }
+        &-info{
+            display: -ms-flexbox;
+            display: flex;
+            -ms-flex-pack: center;
+                justify-content: center;
+            -ms-flex-align: center;
+                align-items: center;
+        }
+        &-status {
+            padding-right:0.5rem;
+        }
+        &-name {
+            color:#586e75;
+        }
+    }
+}
diff --git a/skyquake/plugins/composer/src/src/libraries/CatalogPackageManagerUploadDropZone.js b/skyquake/plugins/composer/src/src/libraries/CatalogPackageManagerUploadDropZone.js
index 5f1ee24..87afb6a 100644
--- a/skyquake/plugins/composer/src/src/libraries/CatalogPackageManagerUploadDropZone.js
+++ b/skyquake/plugins/composer/src/src/libraries/CatalogPackageManagerUploadDropZone.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -35,18 +35,24 @@
 };
 
 function getCatalogPackageManagerServerOrigin() {
-	return Utils.getSearchParams(window.location).upload_server + ':4567';
+	// return Utils.getSearchParams(window.location).upload_server + ':4567';
+	return window.location.origin;
 }
 
 function initializeDropZone(element = '#dropzone', button = false, action = ACTIONS.onboard) {
+	let Auth = 'Basic ' + window.sessionStorage.getItem("auth");
+    let dev_download_server = Utils.getSearchParams(window.location).dev_download_server;
 	DropZone.autoDiscover = false;
 	return new DropZone(element, {
-		paramName: 'descriptor',
+		paramName: 'package',
 		url() {
 			if (action === ACTIONS.update) {
 				return getCatalogPackageManagerServerOrigin() + '/api/update';
 			}
-			return getCatalogPackageManagerServerOrigin() + '/api/upload';
+			return getCatalogPackageManagerServerOrigin() + '/composer/upload?api_server=' + Utils.getSearchParams(window.location).api_server + '&upload_server=' + Utils.getSearchParams(window.location).upload_server + ( dev_download_server ? '&dev_download_server=' + dev_download_server : '');
+		},
+		headers: {
+			'Authorization': Auth
 		},
 		maxFilesize: 10000000000,
 		clickable: button,
diff --git a/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js b/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js
new file mode 100644
index 0000000..963e57e
--- /dev/null
+++ b/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js
@@ -0,0 +1,136 @@
+
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed 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.
+ *
+ */
+/**
+ * Created by onvelocity on 10/27/15.
+ */
+
+import guid from '../libraries/guid'
+import DropZone from 'dropzone'
+import Utils from '../libraries/utils'
+import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions';
+import FileManagerActions from '../components/filemanager/FileManagerActions.js';
+
+/**
+ * This class is responsible for wiring the DropZone.js to our React actions.
+ */
+
+const ACTIONS = {
+	onboard: 'onboard',
+	update: 'update'
+};
+
+function getCatalogPackageManagerServerOrigin() {
+	// return Utils.getSearchParams(window.location).upload_server + ':4567';
+	return window.location.origin;
+}
+
+function initializeDropZone(element = '#dropzone', button = false, action = ACTIONS.onboard, type, id, path) {
+	let Auth = 'Basic ' + window.sessionStorage.getItem("auth");
+	let dev_download_server = Utils.getSearchParams(window.location).dev_download_server;
+	DropZone.autoDiscover = false;
+	return new DropZone(element, {
+		paramName: 'package',
+		url() {
+			if (action === ACTIONS.update) {
+				return getCatalogPackageManagerServerOrigin() + '/api/update';
+			}
+			return getCatalogPackageManagerServerOrigin() + '/composer/api/file-manager?api_server=' + Utils.getSearchParams(window.location).api_server + '&package_type=' + type + '&package_id=' + id + '&package_path=' + path + ( dev_download_server ? '&dev_download_server=' + dev_download_server : '');
+		},
+		headers: {
+			'Authorization': Auth
+		},
+		maxFilesize: 10000000000,
+		clickable: button,
+		autoProcessQueue: true,
+		previewTemplate: '',
+		sending(file, xhr, formData) {
+			// NOTE ie11 does not get this form data
+			formData.append('id', file.id);
+			FileManagerActions.addFileSuccess({
+				fileName: file.name,
+				path: path
+			})
+		},
+		error(file, errorMessage) {
+			const response = {
+				state: file,
+				data: {
+					status: 'upload-error',
+					message: errorMessage
+				}
+			};
+			CatalogPackageManagerActions.uploadCatalogPackageError(response);
+		},
+		success(file) {
+			const data = JSON.parse(file.xhr.responseText);
+			data.status = 'upload-success';
+			const response = {
+				state: file,
+				data: data
+			};
+			//CatalogPackageManagerActions.uploadCatalogPackageStatusUpdated(response);
+		},
+		addedfile(file) {
+			file.id = file.id || guid();
+			file.riftAction = action;
+			//CatalogPackageManagerActions.uploadCatalogPackage(file);
+		},
+		thumbnail(file, dataUrl) {
+			const response = {
+				state: file,
+				data: {
+					status: 'upload-thumbnail',
+					dataUrl: dataUrl
+				}
+			};
+			//CatalogPackageManagerActions.uploadCatalogPackageStatusUpdated(response);
+		},
+		uploadprogress(file, progress, bytesSent) {
+			// FileManagerActions.addFileSuccess({
+			// 	path: path,
+			// 	fileName: file.name
+			// });
+			const response = {
+				state: file,
+				data: {
+					status: 'upload-progress',
+					progress: progress,
+					bytesSent: bytesSent
+				}
+			};
+			//CatalogPackageManagerActions.uploadCatalogPackageStatusUpdated(response);
+		}
+	});
+}
+
+export default class CatalogPackageManagerUploadDropZone {
+
+	constructor(element, button, action, type, id, path) {
+		this.dropZone = initializeDropZone(element, button, action, type, id, path);
+	}
+
+	static get ACTIONS() {
+		return ACTIONS;
+	}
+
+	on(eventName, eventCallback) {
+		this.dropZone.on(eventName, eventCallback);
+	}
+
+}
diff --git a/skyquake/plugins/composer/src/src/libraries/PackageManagerApi.js b/skyquake/plugins/composer/src/src/libraries/PackageManagerApi.js
new file mode 100644
index 0000000..6b8862c
--- /dev/null
+++ b/skyquake/plugins/composer/src/src/libraries/PackageManagerApi.js
@@ -0,0 +1,113 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed 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 guid from '../libraries/guid'
+import DropZone from 'dropzone'
+import Utils from '../libraries/utils'
+import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions'
+import ReactDOM from 'react-dom'
+import $ from 'jquery'
+
+const API_SERVER = Utils.getSearchParams(window.location).api_server;
+
+
+
+
+export default class PackageManager {
+    constructor(element, button, action) {
+        this.stagingArea = {
+            packages: {
+                ids: []
+            }
+        }
+        this.stagingAreaMonitor = null;
+    }
+    createStagingArea(type, name) {
+        return $.ajax({
+            url: Utils.getSearchParams(window.location).api_server + ':8008/api/operations/create-staging-area',
+            type: 'POST',
+            data: {
+                "input" : {
+                    // Package type not important for package upload.
+                    "package-type": type || "NSD",
+                    "name": name || "Package Staging Area"
+                }
+            },
+            error: function() {
+                console.log('Something went wrong creating the staging area: ', arguments)
+            }
+        }).then(function(data) {
+            /*
+            {
+              "output": {
+                "endpoint": "api/upload/85f8e2dc-638b-46e7-89cb-ee8de322066f",
+                "port": "4568"
+              }
+            }
+             */
+            const id = data.output.endpoint.split('/')[2];
+            const port = data.output.port;
+            this.stagingArea.packages.ids.push(id);
+            this.stagingArea.packages[id] = {
+                port: port
+            };
+            return data
+        })
+    }
+    monitoringStagingAreaSocket() {
+        let self = this;
+        if(self.stagingAreaMonitor) {
+            return self.stagingAreaMonitor;
+        }
+        new Promise(function(resolve, reject) {
+            $.ajax({
+                url: '/socket-polling?api_server=' + API_SERVER ,
+                type: 'POST',
+                beforeSend: Utils.addAuthorizationStub,
+                data: {
+                  url: 'launchpad/api/nsr?api_server=' + API_SERVER
+                },
+                success: function(data, textStatus, jqXHR) {
+                  Utils.checkAndResolveSocketRequest(data, resolve, reject, self.monitoringStagingAreaSocketHandler);
+                }
+            })
+        })
+
+        return undefined;
+    }
+    monitoringStagingAreaSocketHandler(connection) {
+        let self = this;
+        let ws = window.multiplexer.channel(connection);
+        if (!connection) return;
+        self.stagingAreaMonitor = connection;
+        ws.onmessage = function(socket) {
+            try {
+                Utils.checkAuthentication(data.statusCode, function() {
+                    ws.close();
+                });
+
+            } catch(e) {
+                console.log('An exception occurred in monitoringStagingAreaSocketHandler', e)
+            }
+        }
+    }
+
+}
+
+
+
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
index 8ba295a..89d1fe9 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
@@ -66,7 +66,7 @@
         return !/^(leaf|leaf_list)$/.test(property.type);
 	},
 	isSimpleList(property = {}) {
-		return _.contains(DescriptorModelFields.simpleList, property.name);
+		return _.includes(DescriptorModelFields.simpleList, property.name);
 	},
 	isPrimativeDataType(property = {}) {
 		const Property = this;
@@ -152,7 +152,7 @@
 				if (uiState.name === 'name') {
 					return changeCase.param(parentMeta.name) + '-' + InstanceCounter.count(parentMeta[':qualified-type']);
 				}
-				if (_.isArray(parentMeta.key) && _.contains(parentMeta.key, uiState.name)) {
+				if (_.isArray(parentMeta.key) && _.includes(parentMeta.key, uiState.name)) {
 					if (/uuid/.test(uiState['data-type'])) {
 						return guid();
 					}
diff --git a/skyquake/plugins/composer/src/src/sources/CatalogPackageManagerSource.js b/skyquake/plugins/composer/src/src/sources/CatalogPackageManagerSource.js
index be11dfc..48d697b 100644
--- a/skyquake/plugins/composer/src/src/sources/CatalogPackageManagerSource.js
+++ b/skyquake/plugins/composer/src/src/sources/CatalogPackageManagerSource.js
@@ -27,8 +27,8 @@
 	return utils.getSearchParams(window.location).upload_server + ':4567';
 }
 
-function ajaxRequest(path, catalogPackage, resolve, reject, method = 'GET') {
-	$.ajax({
+function ajaxRequest(path, catalogPackage, resolve, reject, method = 'GET', input, urlOverride) {
+	let options = {
 		url: getApiServerOrigin() + path,
 		type: method,
 		beforeSend: Utils.addAuthorizationStub,
@@ -51,17 +51,26 @@
 				state: catalogPackage
 			});
 		}
-	}).fail(function(xhr){
+	};
+	if(input) {
+		options.data = input;
+	}
+	if (urlOverride) {
+		options.url = window.location.origin + path;
+	}
+	$.ajax(options).fail(function(xhr){
 			            //Authentication and the handling of fail states should be wrapped up into a connection class.
 			            Utils.checkAuthentication(xhr.status);
 		          	});
 }
 
+
+
 const CatalogPackageManagerSource = {
 
-	requestCatalogPackageDownload: function () {
+		requestCatalogPackageDownload: function () {
 		return {
-			remote: function (state, download, format, grammar) {
+			remote: function (state, download, format, grammar, schema) {
 				return new Promise((resolve, reject) => {
 					// the server does not add a status in the payload
 					// so we add one so that the success handler will
@@ -74,9 +83,20 @@
 					// Backend no longer supports mixed multi-package download.
 					// Probably does not even support multi-package download of same type.
 					// Hence, pick the type from the first element.
-					const path = '/api/export/' + download['catalogItems'][0]['uiState']['type'] + '?schema=' + format + '&grammar=' + grammar + '&format=yaml&ids=' + download.ids;
-					ajaxRequest(path, download, setStatusBeforeResolve, reject);
-				});
+					const data = {
+						"package-type": download['catalogItems'][0]['uiState']['type'].toUpperCase(),
+						"package-id": download.ids,
+						"export-format": format && format.toUpperCase() || 'YAML',
+						"export-grammar": grammar && grammar.toUpperCase() || 'OSM',
+						"export-schema": schema && schema.toUpperCase() || "RIFT"
+					}
+					const path = "/composer/api/package-export?api_server=" + utils.getSearchParams(window.location).api_server;
+					ajaxRequest(path, download, setStatusBeforeResolve, reject, 'POST', data, true);
+				})
+				//.then(function(data) {
+				//	let filename = data.data.output.filename;
+				//	window.open(getApiServerOrigin() + "/api/export/" + filename, "_blank")
+				//});
 			},
 			success: CatalogPackageManagerActions.downloadCatalogPackageStatusUpdated,
 			error: CatalogPackageManagerActions.downloadCatalogPackageError
diff --git a/skyquake/plugins/composer/src/src/stores/CatalogPackageManagerStore.js b/skyquake/plugins/composer/src/src/stores/CatalogPackageManagerStore.js
index 733e938..22d6359 100644
--- a/skyquake/plugins/composer/src/src/stores/CatalogPackageManagerStore.js
+++ b/skyquake/plugins/composer/src/src/stores/CatalogPackageManagerStore.js
@@ -136,8 +136,9 @@
 
 	downloadCatalogPackage(data) {
 		let catalogItems = data['selectedItems'] || [];
-		let format = data['selectedFormat'] || 'mano';
+		let schema = data['selectedFormat'] || 'mano';
 		let grammar = data['selectedGrammar'] || 'osm';
+		let format = "YAML";
 		if (catalogItems.length) {
 			const catalogPackage = Object.assign({}, defaults.downloadPackage, {id: guid()});
 			catalogPackage.name = catalogItems[0].name;
@@ -148,7 +149,7 @@
 			catalogPackage.ids = catalogItems.map(d => d.id).sort().toString();
 			catalogPackage.catalogItems = catalogItems;
 			this.addPackage(catalogPackage);
-			this.getInstance().requestCatalogPackageDownload(catalogPackage, format, grammar).catch(exception);
+			this.getInstance().requestCatalogPackageDownload(catalogPackage, format, grammar, schema).catch(exception);
 		}
 	}
 
@@ -189,9 +190,9 @@
 		success: false,
 		error: false
 	};
-	const responseData = response.data;
+	const responseData = (response.data.output) ? response.data.output :  response.data;
 	const catalogPackage = response.state;
-	switch(responseData.status) {
+	switch(response.data.status) {
 	case 'upload-progress':
 		statusInfo.pending = true;
 		statusInfo.progress = parseFloat(responseData.progress) || 0;
@@ -201,7 +202,7 @@
 		statusInfo.pending = true;
 		statusInfo.progress = 100;
 		statusInfo.message = 'Upload completed.';
-		statusInfo.transactionId = responseData.transaction_id;
+		statusInfo.transactionId = responseData['transaction-id'] || catalogPackage.transactionId;
 		break;
 	case 'upload-error':
 		statusInfo.error = true;
@@ -210,7 +211,7 @@
 	case 'download-requested':
 		statusInfo.pending = true;
 		statusInfo.progress = 25;
-		statusInfo.transactionId = responseData.transaction_id;
+		statusInfo.transactionId = responseData['transaction-id']  || catalogPackage.transactionId;
 		break;
 	case 'pending':
 		statusInfo.pending = true;
diff --git a/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js b/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
index 67e8e8d..4f27868 100644
--- a/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
+++ b/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,6 +33,23 @@
 import CatalogDataStore from '../stores/CatalogDataStore'
 import isFullScreen from '../libraries/isFullScreen'
 
+import FileManagerSource from '../components/filemanager/FileManagerSource';
+import FileManagerActions from '../components/filemanager/FileManagerActions';
+
+import React from 'react';
+
+//Hack for crouton fix. Should eventually put composer in skyquake alt context
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+let NotificationError = null;
+class ComponentBridge extends React.Component {
+    constructor(props) {
+        super(props);
+        NotificationError = this.props.flux.actions.global.showNotification;
+    }
+    render(){
+        return <i></i>
+    }
+}
 const getDefault = (name, defaultValue) => {
 	const val = window.localStorage.getItem('defaults-' + name);
 	if (val) {
@@ -79,6 +96,10 @@
 class ComposerAppStore {
 
 	constructor() {
+        //Bridge for crouton fix
+        this.ComponentBridgeElement = SkyquakeComponent(ComponentBridge);
+
+		this.exportAsync(FileManagerSource)
 		// the catalog item currently being edited in the composer
 		this.item = null;
 		// the left and right sides of the canvas area
@@ -99,6 +120,12 @@
 		this.showClassifiers = {};
 		this.editPathsMode = false;
 		this.fullScreenMode = false;
+		this.panelTabShown = 'descriptor';
+		//File manager values
+		this.files = [];
+		this.filesState = {};
+		this.downloadJobs = {};
+		//End File  manager values
 		this.bindListeners({
 			onResize: PanelResizeAction.RESIZE,
 			editCatalogItem: CatalogItemsActions.EDIT_CATALOG_ITEM,
@@ -124,8 +151,23 @@
 			openCanvasPanelTray: CanvasPanelTrayActions.OPEN,
 			closeCanvasPanelTray: CanvasPanelTrayActions.CLOSE,
 			enterFullScreenMode: ComposerAppActions.ENTER_FULL_SCREEN_MODE,
-			exitFullScreenMode: ComposerAppActions.EXIT_FULL_SCREEN_MODE
+			exitFullScreenMode: ComposerAppActions.EXIT_FULL_SCREEN_MODE,
+			showAssets: ComposerAppActions.showAssets,
+			showDescriptor: ComposerAppActions.showDescriptor,
+			getFilelistSuccess: FileManagerActions.getFilelistSuccess,
+			updateFileLocationInput: FileManagerActions.updateFileLocationInput,
+			sendDownloadFileRequst: FileManagerActions.sendDownloadFileRequst,
+			addFileSuccess: FileManagerActions.addFileSuccess,
+			deletePackageFile: FileManagerActions.deletePackageFile,
+			deleteFileSuccess: FileManagerActions.deleteFileSuccess,
+			closeFileManagerSockets: FileManagerActions.closeFileManagerSockets,
+			openFileManagerSockets: FileManagerActions.openFileManagerSockets,
+			openDownloadMonitoringSocketSuccess: FileManagerActions.openDownloadMonitoringSocketSuccess,
+			getFilelistSocketSuccess: FileManagerActions.getFilelistSocketSuccess
 		});
+        this.exportPublicMethods({
+            closeFileManagerSockets: this.closeFileManagerSockets.bind(this)
+        })
 	}
 
 	onResize(e) {
@@ -166,6 +208,7 @@
 	}
 
 	editCatalogItem(item) {
+		let self = this;
 		if (item && item.uiState) {
 			item.uiState.isOpenForEdit = true;
 			if (item.uiState.type !== 'nsd') {
@@ -174,8 +217,8 @@
 		}
 		SelectionManager.select(item);
 		this.updateItem(item);
+		this.openFileManagerSockets(item)
 	}
-
 	catalogItemMetaDataChanged(item) {
 		this.updateItem(item);
 	}
@@ -193,7 +236,8 @@
 	}
 
 	showError(data) {
-		this.setState({message: data.errorMessage, messageType: 'error'});
+        NotificationError.defer({msg: data.errorMessage, type: 'error'})
+        // this.setState({message: data.errorMessage, messageType: 'error'});
 	}
 
 	clearError() {
@@ -396,7 +440,210 @@
 		this.setState({fullScreenMode: false});
 
 	}
+	showAssets() {
+		this.setState({
+			panelTabShown: 'assets'
+		});
+	}
+	showDescriptor() {
+		this.setState({
+			panelTabShown: 'descriptor'
+		});
+	}
 
+	//File Manager methods
+	getFilelistSuccess(data) {
+		let self = this;
+		let filesState = null;
+        if (self.fileMonitoringSocketID) {
+			filesState = addInputState( _.cloneDeep(this.filesState),data);
+			// filesState = _.merge(self.filesState, addInputState({},data));
+			let normalizedData = normalizeTree(data);
+			this.setState({
+				files: {
+					data: _.mergeWith(normalizedData.data, self.files.data, function(obj, src) {
+						return _.uniqBy(obj? obj.concat(src) : src, 'name');
+					}),
+					id: self.files.id || normalizedData.id
+				},
+				filesState: filesState
+			});
+        }
+		function normalizeTree(data) {
+			let f = {
+				id:[],
+				data:{}
+			};
+			data.contents.map(getContents);
+			function getContents(d) {
+				if(d.hasOwnProperty('contents')) {
+					let contents = [];
+					d.contents.map(function(c,i) {
+						if (!c.hasOwnProperty('contents')) {
+							contents.push(c);
+						} else {
+							getContents(c);
+						}
+					})
+					f.id.push(d.name);
+					f.data[d.name] = contents;
+				}
+			}
+ 			return f;
+		}
+		function addInputState(obj, d) {
+			d.newFile = '';
+			if(d.hasOwnProperty('contents')) {
+				d.contents.map(addInputState.bind(null, obj))
+			}
+			if(!obj[d.name]) {
+				obj[d.name] = '';
+			}
+			return obj;
+		}
+	}
+	sendDownloadFileRequst(data) {
+		let id = data.id || this.item.id;
+		let type = data.type || this.item.uiState.type;
+		let path = data.path;
+		let url = data.url;
+		this.getInstance().addFile(id, type, path, url);
+	}
+	updateFileLocationInput = (data) => {
+		let name = data.name;
+		let value = data.value;
+		var filesState = _.cloneDeep(this.filesState);
+		filesState[name] = value;
+		this.setState({
+			filesState: filesState
+		});
+	}
+	addFileSuccess = (data) => {
+		let path = data.path;
+		let fileName = data.fileName;
+		let files = _.cloneDeep(this.files);
+		let loadingIndex = files.data[path].push({
+			status: 'DOWNLOADING',
+			name: path + '/' + fileName
+		}) - 1;
+		this.setState({files: files});
+
+	}
+	startWatchingJob = () => {
+		let ws = window.multiplexer.channel(this.jobSocketId);
+		this.setState({
+			jobSocket:null
+		})
+	}
+	openDownloadMonitoringSocketSuccess = (id) => {
+		let self = this;
+		let ws = window.multiplexer.channel(id);
+		let downloadJobs = _.cloneDeep(self.downloadJobs);
+		let newFiles = {};
+		ws.onmessage = (socket) => {
+            if (self.files && self.files.length > 0) {
+                let jobs = [];
+                try {
+                    jobs = JSON.parse(socket.data);
+                } catch(e) {}
+                newFiles = _.cloneDeep(self.files);
+                jobs.map(function(j) {
+                    //check if not in completed state
+                    let fullPath = j['package-path'];
+                    let path = fullPath.split('/');
+                    let fileName = path.pop();
+                    path = path.join('/');
+                    let index = _.findIndex(self.files.data[path], function(o){
+                        return fullPath == o.name
+                    });
+                    if((index > -1) && newFiles.data[path][index]) {
+                        newFiles.data[path][index].status = j.status
+					} else {
+                        if(j.status.toUpperCase() == 'LOADING...' || j.status.toUpperCase() == 'IN_PROGRESS') {
+                            newFiles.data[path].push({
+                                status: j.status,
+                                name: fullPath
+                            })
+                        } else {
+                            // if ()
+                        }
+					}
+                })
+                self.setState({
+                    files: newFiles
+                })
+			// console.log(JSON.parse(socket.data));
+            }
+		}
+		this.setState({
+			jobSocketId: id,
+			jobSocket: ws
+		})
+
+	}
+	getFilelistSocketSuccess = (id) => {
+		let self = this;
+		let ws = window.multiplexer.channel(id);
+		ws.onmessage = (socket) => {
+            if (self.fileMonitoringSocketID) {
+                let data = [];
+                try {
+                    data = JSON.parse(socket.data);
+                } catch(e) {}
+                self.getFilelistSuccess(data)
+            }
+		}
+
+		this.setState({
+			fileMonitoringSocketID: id,
+			fileMonitoringSocket: ws
+		})
+
+	}
+	closeFileManagerSockets() {
+        this.fileMonitoringSocketID = null;
+        this.setState({
+                jobSocketId : null,
+                fileMonitoringSocketID : null
+                // jobSocket : null,
+                // fileMonitoringSocket : null,
+        });
+		this.jobSocket && this.jobSocket.close();
+		this.fileMonitoringSocket && this.fileMonitoringSocket.close();
+        console.log('closing');
+	}
+	openFileManagerSockets(i) {
+		let self = this;
+		let item = i || self.item;
+		this.files = {data:[]};
+        // this.closeFileManagerSockets();
+		this.getInstance().openFileMonitoringSocket(item.id, item.uiState.type).then(function() {
+        // 	// self.getInstance().openDownloadMonitoringSocket(item.id);
+		});
+        this.getInstance().openDownloadMonitoringSocket(item.id);
+	}
+	endWatchingJob(id) {
+
+	}
+	deletePackageFile(name) {
+		let id = this.item.id;
+		let type = this.item.uiState.type;
+		this.getInstance().deleteFile(id, type, name);
+	}
+	deleteFileSuccess = (data) => {
+		let path = data.path.split('/')
+		let files = _.cloneDeep(this.files);
+		path.pop();
+		path = path.join('/');
+		let pathFiles = files.data[path]
+		_.remove(pathFiles, function(c) {
+			return c.name == data.path;
+		});
+
+		this.setState({
+			files: files
+		})
+	}
 }
 
 export default alt.createStore(ComposerAppStore, 'ComposerAppStore');
diff --git a/skyquake/plugins/composer/src/src/styles/CanvasPanel.scss b/skyquake/plugins/composer/src/src/styles/CanvasPanel.scss
index 4b9f153..9fa1149 100644
--- a/skyquake/plugins/composer/src/src/styles/CanvasPanel.scss
+++ b/skyquake/plugins/composer/src/src/styles/CanvasPanel.scss
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -45,5 +45,23 @@
 	.CanvasPanelBody {
 		position: absolute;
 		bottom: 0;
+		top: 142px;
+	}
+	.CanvasPanelTabs {
+	    height: 32px;
+        margin-top: 49px;
+	    background-color: #cbd1d1;
+	    .CatalogFilter {
+			max-width:25%;
+		    margin-left: 1rem;
+	        margin-top: 3px;
+			button {
+			    -ms-flex-pack: center;
+		        justify-content: center;
+			    &.-selected {
+			    	background:#e5e5e5;
+			    }
+			}
+	    }
 	}
 }
diff --git a/skyquake/plugins/composer/src/src/styles/ComposerAppToolbar.scss b/skyquake/plugins/composer/src/src/styles/ComposerAppToolbar.scss
index 7b2ef16..423e09e 100644
--- a/skyquake/plugins/composer/src/src/styles/ComposerAppToolbar.scss
+++ b/skyquake/plugins/composer/src/src/styles/ComposerAppToolbar.scss
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,17 +20,25 @@
 @import 'main';
 
 .ComposerAppToolbar {
-	@extend .panel-header;
-	top: 56px;
-	left: 300px;
+    @extend .panel-header;
+    top: 56px;
+    left: 300px;
     right: 0;
-	height: 55px;
-	z-index: 4;
+    height: 55px;
+    z-index: 4;
     position: absolute;
     padding: 10px 20px;
     background-color: $panel-bg-color-contrast;
-	white-space: nowrap;
-	div {
-		display: inline-block;
-	}
+    white-space: nowrap;
+    div {
+        display: inline-block;
+    }
+    .disableOverlay {
+        background-color: #cbd1d1;
+        width: 100%;
+        height: 40px;
+        opacity: 0.8;
+        position: absolute;
+        pointer-events: none;
+    }
 }
diff --git a/skyquake/plugins/composer/src/src/styles/_main.scss b/skyquake/plugins/composer/src/src/styles/_main.scss
index 15ff765..e8cee07 100644
--- a/skyquake/plugins/composer/src/src/styles/_main.scss
+++ b/skyquake/plugins/composer/src/src/styles/_main.scss
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,9 +19,9 @@
 @import 'variables';
 
 path[data-outline-indicator-svg-outline-path] {
-	stroke-dasharray: 0;
-	stroke: #b58900;
-	stroke-width: 5px;
+    stroke-dasharray: 0;
+    stroke: #b58900;
+    stroke-width: 5px;
 }
 
 $font-path: '../assets';
@@ -161,49 +161,45 @@
 $font-family-default: $font-family-sans-serif;
 
 html, body {
-	background: $primary-bg-color;
-	cursor: auto;
-	font-family: $font-family-sans-serif;
-	overflow: hidden;
+    background: $primary-bg-color;
+    cursor: auto;
+    font-family: $font-family-sans-serif;
+    overflow: hidden;
 }
 
 ::-webkit-scrollbar {
-	//display: none;
+    /*display: none;*/
 }
 
 html {
-	-webkit-box-sizing: border-box;
-	-moz-box-sizing: border-box;
-	box-sizing: border-box;
+    box-sizing: border-box;
 }
 
 * {
-	margin: 0;
-	border: 0;
-	padding: 0;
-	border-collapse: collapse;
+    margin: 0;
+    border: 0;
+    padding: 0;
+    border-collapse: collapse;
 }
 
 *, *:before, *:after {
-	-webkit-box-sizing: inherit;
-	-moz-box-sizing: inherit;
-	box-sizing: inherit;
+    box-sizing: inherit;
 }
 
 h1 {
-	margin: 0;
-	padding: 3px 6px;
-	color: #323232;
-	font-size: 0.75em;
-	font-weight: normal;
-	text-align: center;
-	font-variant-caps: all-small-caps;
-	font-variant: small-caps;
-	text-transform: uppercase;
+    margin: 0;
+    padding: 3px 6px;
+    color: #323232;
+    font-size: 0.75em;
+    font-weight: normal;
+    text-align: center;
+    font-variant-caps: all-small-caps;
+    font-variant: small-caps;
+    text-transform: uppercase;
 }
 
 img {
-	vertical-align: middle;
+    vertical-align: middle;
 }
 
 button,
@@ -217,239 +213,246 @@
     text-decoration: none;
     text-transform: uppercase;
 
-	&:hover {
-		background-color: #7E9BC1;
-		color:#ffffff;
-	}
-	&:active, &:visited{
-		background-color:#4C5E74;
-		color:#ffffff;
-	}
+    &:hover {
+        background-color: #7E9BC1;
+        color:#ffffff;
+    }
+    &:active, &:visited{
+        background-color:#4C5E74;
+        color:#ffffff;
+    }
 
-	&.-selected {
-		background-color: rgb(159, 196, 244);
-	}
+    &.-selected {
+        background-color: rgb(159, 196, 244);
+    }
 
 }
 
 input[type="image"] {
-	width: 28px;
-	height: 28px;
-	margin-right: 12px;
-	border: 0;
-	border-radius: 4px;
-	padding: 4px;
+    width: 28px;
+    height: 28px;
+    margin-right: 12px;
+    border: 0;
+    border-radius: 4px;
+    padding: 4px;
 }
 
 input,
 select,
 textarea {
-	height: 25px;
-	line-height: 25px;
-	max-width: 100%;
-	min-width: 100%;
-	margin: 0;
-	padding: 0 8px;
-	border: 1px solid $field-background-color !important;;
-	border-radius: $field-border-radius;
-	color: #002b36;
-	background-color: $field-background-color !important;
-	vertical-align: top;
-	&:focus {
-		color: #002b36;
-		background-color: white !important;
-	}
-	&::-webkit-input-placeholder {
-		color: #eee8d5 !important;
-	}
+    height: 25px;
+    line-height: 25px;
+    max-width: 100%;
+    min-width: 100%;
+    margin: 0;
+    padding: 0 8px;
+    border: 1px solid $field-background-color !important;;
+    border-radius: $field-border-radius;
+    color: #002b36;
+    background-color: $field-background-color !important;
+    vertical-align: top;
+    &:focus {
+        color: #002b36;
+        background-color: white !important;
+    }
+    &::-webkit-input-placeholder {
+        color: #eee8d5 !important;
+    }
 
-	&:-moz-placeholder { /* Firefox 18- */
-		color: #eee8d5 !important;
-	}
+    &:-moz-placeholder { /* Firefox 18- */
+        color: #eee8d5 !important;
+    }
 
-	&::-moz-placeholder {  /* Firefox 19+ */
-		color: #eee8d5 !important;
-	}
+    &::-moz-placeholder {  /* Firefox 19+ */
+        color: #eee8d5 !important;
+    }
 
-	&:-ms-input-placeholder {
-		color: #eee8d5 !important;
-	}
+    &:-ms-input-placeholder {
+        color: #eee8d5 !important;
+    }
 }
 
 select {
-	padding-right: 0;
-	margin-right: 0;
-	-moz-appearance: none;
-	text-indent: 0.01px;
-	text-overflow: '';
-	-webkit-appearance: none;
-	-webkit-border-radius: $field-border-radius;
-	&.-value-not-set {
-		color: #eee8d5;
-	}
+    padding-right: 0;
+    margin-right: 0;
+    -moz-appearance: none;
+    text-indent: 0.01px;
+    text-overflow: '';
+    -webkit-appearance: none;
+    -webkit-border-radius: $field-border-radius;
+    &.-value-not-set {
+        color: #eee8d5;
+    }
 }
 
 select {
-	appearance: none; // using -prefix-free http://leaverou.github.io/prefixfree/
+    -webkit-appearance: none;
+       -moz-appearance: none;
+            appearance: none; /* using -prefix-free http://leaverou.github.io/prefixfree/*/
     background: $field-background-color url(../../../node_modules/open-iconic/svg/caret-bottom.svg) no-repeat right 6px center;
-	background-size: 10px;
-	border: {
-		color: $field-background-color;
-		radius: $field-border-radius;
-		style: solid;
-		width: 1px;
-	}
+    background-size: 10px;
+    border: {
+        color: $field-background-color;
+        radius: $field-border-radius;
+        style: solid;
+        width: 1px;
+    }
 }
 
-// Removes default arrow for IE10+
-// IE 8/9 get dafault arrow which covers caret image
-// as long as caret image is small than and positioned
-// behind default arrow
+/* Removes default arrow for IE10+*/
+/* IE 8/9 get dafault arrow which covers caret image*/
+/* as long as caret image is small than and positioned*/
+/* behind default arrow*/
 select::-ms-expand {
-	display: none;
+    display: none;
 }
 
 textarea {
-	height: 50px;
+    height: 50px;
 }
 
 input[name$="id"],
 input.-is-guid {
-	font-size: 10px;
-	font-family: monospace;
+    font-size: 10px;
+    font-family: monospace;
 }
 
 .ContentEditableDiv {
-	margin: 0;
-	border: 0;
-	padding: 0;
-	overflow: hidden;
-	border-radius: $field-border-radius;
-	height: 25px;
-	line-height: 25px;
+    margin: 0;
+    border: 0;
+    padding: 0;
+    overflow: hidden;
+    border-radius: $field-border-radius;
+    height: 25px;
+    line-height: 25px;
 }
 
 .btn-group {
-	white-space: nowrap;
-	button {
-		border-right: 1px solid $button-border-color;
-		&:first-of-type {
-			border-top-left-radius: 2px;
-			border-bottom-left-radius: 2px;
-		}
-		&:last-of-type {
-			border-right: none;
-			border-top-right-radius: 2px;
-			border-bottom-right-radius: 2px;
-		}
-	}
+    white-space: nowrap;
+    button {
+        border-right: 1px solid $button-border-color;
+        &:first-of-type {
+            border-top-left-radius: 2px;
+            border-bottom-left-radius: 2px;
+        }
+        &:last-of-type {
+            border-right: none;
+            border-top-right-radius: 2px;
+            border-bottom-right-radius: 2px;
+        }
+    }
 }
 
 .panel {
-	overflow: hidden;
-	-ms-overflow-style: -ms-autohiding-scrollbar;
-	background-color: $panel-bg-color;
-	> div {
-		min-width: 200px;
-	}
+    overflow: hidden;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    background-color: $panel-bg-color;
+    > div {
+        min-width: 200px;
+    }
 }
 .panel-header {
-	h1 {
-		padding: 16px;
-		white-space: nowrap;
-		text-align: center;
-	}
+    h1 {
+        padding: 16px;
+        white-space: nowrap;
+        text-align: center;
+    }
 }
 .panel-body {
-	position: absolute;
-	overflow: hidden;
-	&:hover {
-		overflow: auto;
-	}
-	-ms-overflow-style: -ms-autohiding-scrollbar;
-	top: 111px;
-	bottom: 0px;
-	left: 0;
-	right: 0;
+    position: absolute;
+    overflow: hidden;
+    &:hover {
+        overflow: auto;
+    }
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    top: 111px;
+    bottom: 0px;
+    left: 0;
+    right: 0;
 
 
 }
 .panel-footer {
-	position: absolute;
-	bottom: 0;
-	left: 0;
-	right: 0;
-	height: 40px;
-	padding: 0 12px;
-	background-color: $panel-bg-color-contrast;
-	border-top: 1px solid $panel-border-color-light;
-	white-space: nowrap;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: 40px;
+    padding: 0 12px;
+    background-color: $panel-bg-color-contrast;
+    border-top: 1px solid $panel-border-color-light;
+    white-space: nowrap;
 }
 
 .welcome-message {
-	margin: 20px;
-	color: #06173c;
-	font-size: x-large;
-	font-weight: 200;
-	.Button {
-		display: inline-block;
-	}
+    margin: 20px;
+    color: #06173c;
+    font-size: x-large;
+    font-weight: 200;
+    .Button {
+        display: inline-block;
+    }
 }
 
 /* react-tooltip overrides */
 .__react_component_tooltip {
-	transition: opacity 300ms cubic-bezier(0.230, 1.000, 0.320, 1.000) !important;
+    transition: opacity 300ms cubic-bezier(0.230, 1.000, 0.320, 1.000) !important;
 }
 
 /* simple css-only drop-down menu */
 .menu {
-	display: inline-flex;
-	position: relative;
-	z-index: 9999;
-	> .Button:after {
-		content: "ˇ";
-		position: absolute;
-		right: 4px;
-		bottom: -13px;
-		font-size: 20px;
-	}
-	.sub-menu {
-		display: none;
-		position: absolute;
-		top: 36px;
-		left: -4px;
-		padding: 4px;
-		background-color: #f1f1f1;
-		box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15);
-		border-radius: 3px;
-		.Button {
-			display: block;
-			text-align: left;
-		}
-		&:hover {
-			display: block;
-		}
-	}
-	> .Button:hover .sub-menu {
-		display: block;
-	}
-	&:hover .sub-menu {
-		display: block;
-	}
+    display: -ms-inline-flexbox;
+    display: inline-flex;
+    position: relative;
+    z-index: 9999;
+    > .Button:after {
+        content: "ˇ";
+        position: absolute;
+        right: 4px;
+        bottom: -13px;
+        font-size: 20px;
+    }
+    .sub-menu {
+        display: none;
+        position: absolute;
+        top: 36px;
+        left: -4px;
+        padding: 4px;
+        background-color: #f1f1f1;
+        box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.15);
+        border-radius: 3px;
+        .Button {
+            display: block;
+            text-align: left;
+        }
+        &:hover {
+            display: block;
+        }
+    }
+    > .Button:hover .sub-menu {
+        display: block;
+    }
+    &:hover .sub-menu {
+        display: block;
+    }
 }
 
 .descriptor {
-	text {
-		&.badge {
-			font-weight: 100;
-			font-size: 12px;
-			text-anchor: middle;
-			fill: white;/* font color */
-			stroke: white !important;/* font color */
-		}
-	}
+    text {
+        &.badge {
+            font-weight: 100;
+            font-size: 12px;
+            text-anchor: middle;
+            fill: white;/* font color */
+            stroke: white !important;/* font color */
+        }
+    }
 }
 
 .hint {
-	margin: 5px;
+    margin: 5px;
+}
+
+.FileManager {
+    margin-top:1rem;
 }