Merge "initial delivery of python-osmclient"
diff --git a/Makefile b/Makefile
index db2cda9..31c9233 100644
--- a/Makefile
+++ b/Makefile
@@ -36,7 +36,7 @@
 CONFD = XML_ONLY
 
 BUILD_TYPE = Debug
-NOT_DEVELOPER_TYPE = FALSE
+NOT_DEVELOPER_BUILD ?= FALSE
 COVERAGE_BUILD = FALSE
 RIFT_AGENT_BUILD = $(CONFD)
 PROJECT_TOP_DIR = $(TOP_ROOT_PATH)
@@ -47,7 +47,7 @@
 	mkdir -p $(RIFT_BUILD)
 	mkdir -p $(RIFT_ARTIFACTS)
 	mkdir -p $(RIFT_INSTALL)
-	cd $(RIFT_BUILD) && $(RIFT_SHELL_EXE) cmake $(TOP_SRC_PATH) -DCMAKE_INSTALL_PREFIX=$(TOP_ROOT_PATH) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DNOT_DEVELOPER_BUILD=$(NOT_DEVELOPER_TYPE) -DCOVERAGE_BUILD=$(COVERAGE_TYPE) -DRIFT_AGENT_BUILD=$(RIFT_AGENT_BUILD) -DPROJECT_TOP_DIR=$(PROJECT_TOP_DIR) -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} -DRIFT_SUBMODULE_NAME=$(PROJECT_TOP_DIR) -DRIFT_PACKAGE_GENERATOR=DEB -DRELEASE_NUMBER=$(RELEASE_NUMBER) -DBUILD_NUMBER=$(BUILD_NUMBER)
+	cd $(RIFT_BUILD) && $(RIFT_SHELL_EXE) cmake $(TOP_SRC_PATH) -DCMAKE_INSTALL_PREFIX=$(TOP_ROOT_PATH) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -DNOT_DEVELOPER_BUILD=$(NOT_DEVELOPER_BUILD) -DCOVERAGE_BUILD=$(COVERAGE_TYPE) -DRIFT_AGENT_BUILD=$(RIFT_AGENT_BUILD) -DPROJECT_TOP_DIR=$(PROJECT_TOP_DIR) -DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH} -DRIFT_SUBMODULE_NAME=$(PROJECT_TOP_DIR) -DRIFT_PACKAGE_GENERATOR=DEB -DRELEASE_NUMBER=$(RELEASE_NUMBER) -DBUILD_NUMBER=$(BUILD_NUMBER)
 
 rw: cmake
 	$(RIFT_SHELL_EXE) $(MAKE) -C $(RIFT_BUILD)
diff --git a/skyquake/framework/widgets/button/rw.button.js b/skyquake/framework/widgets/button/rw.button.js
index 41730eb..5d684ec 100644
--- a/skyquake/framework/widgets/button/rw.button.js
+++ b/skyquake/framework/widgets/button/rw.button.js
@@ -251,7 +251,8 @@
         onKeyPress:        this.onKeyPress,
         onKeyUp:           this.onKeyUp,
         onFocus:           this.onFocus,
-        onBlur:            this.onBlur
+        onBlur:            this.onBlur,
+        disabled:          this.isDisabled, 
       },
       button_icon,
       React.createElement("span", {className: "rw-button__label"}, display)
diff --git a/skyquake/plugins/about/src/about.jsx b/skyquake/plugins/about/src/about.jsx
index 6458764..1a43aa4 100644
--- a/skyquake/plugins/about/src/about.jsx
+++ b/skyquake/plugins/about/src/about.jsx
@@ -62,6 +62,23 @@
     // If in the mission control, create an uptime table;
     var uptime = this.state.createTime && this.state.createTime;
 
+    var fossInfoComponent = (
+      <div className="table-container">
+        <h2> FOSS Info </h2>
+        <table>
+          <thead>
+            <tr>
+              <th>
+                <a target="_blank" href='https://open.riftio.com/open-source-software-usage/'>
+                  Click here for FOSS Info (requires Internet connection)
+                </a>
+              </th>
+            </tr>
+          </thead>
+        </table>
+      </div>
+    );
+
     var uptimeComponent = (
       <div className="table-container">
                   <h2> Uptime Info </h2>
@@ -103,6 +120,7 @@
     if (this.state != null) {
       var html = (
               <div className="table-container-wrapper">
+                {fossInfoComponent}
                 {uptimeComponent}
                 <div className="table-container">
                   <h2> Version Info </h2>
diff --git a/skyquake/plugins/accounts/src/account/accountStore.js b/skyquake/plugins/accounts/src/account/accountStore.js
index aeb5f48..def033a 100644
--- a/skyquake/plugins/accounts/src/account/accountStore.js
+++ b/skyquake/plugins/accounts/src/account/accountStore.js
@@ -122,7 +122,7 @@
                 label: 'Management Network',
                 ref: 'mgmt-network'
             }, {
-                label: 'Floating IP Pool',
+                label: 'Floating IP Pool Network Name',
                 ref: 'floating-ip-pool',
                 optional: true
             }, {
diff --git a/skyquake/plugins/composer/api/composer.js b/skyquake/plugins/composer/api/composer.js
index 5ff92c8..e9e4be1 100644
--- a/skyquake/plugins/composer/api/composer.js
+++ b/skyquake/plugins/composer/api/composer.js
@@ -523,6 +523,9 @@
     })
 }
 
+function makeAssetTypeParamName (type) {
+    return type.toLowerCase() + '-file-type';
+}
 FileManager.addFile = function(req) {
     console.log(' Uploading file', req.file.originalname, 'as', req.file.filename);
     var api_server = req.query['api_server'];
@@ -530,15 +533,17 @@
     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) {
+     if (!download_host) {
         download_host = req.protocol + '://' + req.get('host');//api_server + ':' + utils.getPortForProtocol(req.protocol);
     }
     var 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
+        'package-path': package_path ? package_path + '/' + req.file.filename : req.file.filename
     }
+    var assetType = req.query['asset_type'].toUpperCase();
+    input[makeAssetTypeParamName(package_type)] = assetType;
     return new Promise(function(resolve, reject) {
         Promise.all([
             rp({
@@ -580,16 +585,17 @@
     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
-        }
+    var assetType = req.query['asset_type'];
+    var input = {
+        "package-type": type,
+        "package-id": id
     }
+    var payload = {input: input};
     if(req.method == 'GET') {
         if(downloadUrl && path) {
             payload.input['external-url'] = downloadUrl;
             payload.input['package-path'] = path;
+            payload.input[makeAssetTypeParamName(type)] = assetType;
             return download(payload);
         } else {
             return list(payload);
@@ -597,6 +603,7 @@
     }
     if(req.method == 'DELETE') {
         payload.input['package-path'] = path;
+        payload.input[makeAssetTypeParamName(type)] = assetType;
         return deleteFile(payload)
     }
 
diff --git a/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js b/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
index 41e87b3..ad2bf3d 100644
--- a/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
+++ b/skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
@@ -25,6 +25,10 @@
 import _isArray from 'lodash/isArray'
 import _cloneDeep from 'lodash/cloneDeep'
 import _debounce from 'lodash/debounce';
+import _uniqueId from 'lodash/uniqueId';
+import _set from 'lodash/set';
+import _get from 'lodash/get';
+import _has from 'lodash/has';
 import utils from '../libraries/utils'
 import React from 'react'
 import ClassNames from 'classnames'
@@ -48,6 +52,16 @@
 
 import '../styles/EditDescriptorModelProperties.scss'
 
+const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
+
+function resolveReactKey(value) {
+	const keyPath =  ['uiState', 'fieldKey'];
+	if (!_has(value, keyPath)) {
+		_set(value, keyPath, _uniqueId());
+	}
+	return _get(value, keyPath);
+}
+
 function getDescriptorMetaBasicForType(type) {
 	const basicPropertiesFilter = d => _includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
 	return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
@@ -292,7 +306,37 @@
 					key={fieldKey} 
 					id={fieldKey} 
 					className={ClassNames({'-value-not-set': !isValueSet})} 
-					defaultValue={val && val.toUpperCase()} title={pathToProperty} 
+					defaultValue={val && val.toUpperCase()} 
+					title={pathToProperty} 
+					onChange={onSelectChange} 
+					onFocus={onFocus} 
+					onBlur={endEditing} 
+					onMouseDown={startEditing} 
+					onMouseOver={startEditing} 
+					readOnly={!isEditable}>
+						{options}
+				</select>
+			);
+		}
+		
+		if (Property.isLeafEmpty(property)) {
+			// A null value indicates the leaf exists (as opposed to undefined).
+			// We stick in a string when the user actually sets it to simplify things
+			// but the correct thing happens when we serialize to user data
+			let isEmptyLeafPresent = (value === EMPTY_LEAF_PRESENT || value === null); 
+			let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
+			const options = [
+				<option key={'true'} value={EMPTY_LEAF_PRESENT}>Enabled</option>,
+				<option key={'false'} value="">Not Enabled</option>
+			]
+
+			return (
+				<select 
+					key={fieldKey} 
+					id={fieldKey} 
+					className={ClassNames({'-value-not-set': !isEmptyLeafPresent})} 
+					defaultValue={present} 
+					title={pathToProperty} 
 					onChange={onSelectChange} 
 					onFocus={onFocus} 
 					onBlur={endEditing} 
@@ -344,21 +388,35 @@
 
 	}
 
-	function buildElement(container, property, valuePath, value) {
-		return property.properties.map((property, index) => {
-			let childValue;
-			const childPath = valuePath.slice();
-			if (typeof value === 'object') {
-				childValue = value[property.name];
+	/**
+	 * buiid and return an array of components representing an editor for each property.
+	 * 
+	 * @param {any} container the master document being edited
+	 * @param {[property]} properties 
+	 * @param {string} pathToProperties path within the container to the properties
+	 * @param {Object} data source for each property
+	 * @param {any} props object containing main data panel information, e.g. panel width {width: 375}
+	 * which may be useful/necessary to a components rendering.
+	 * @returns an array of react components
+	 */
+	function buildComponentsForProperties(container, properties, pathToProperties, data, props) {
+		return properties.map((property) => {
+			let value;
+			let propertyPath = pathToProperties.slice();
+			if (data && typeof data === 'object') {
+				value = data[property.name];
 			}
 			if(property.type != 'choice'){
-						childPath.push(property.name);
+				propertyPath.push(property.name);
 			}
-			return build(container, property, childPath, childValue);
-
+			return build(container, property, propertyPath, value, props);
 		});
 	}
 
+	function buildElement(container, property, valuePath, value) {
+		return buildComponentsForProperties(container, property.properties, valuePath, value);
+	}
+
 	function buildChoice(container, property, path, value, key) {
 
 		function processChoiceChange(name, value) {
@@ -473,14 +531,14 @@
 			if(fieldProperties) {
 				//Check each case statement in model and see if it is present in container model.
 				cases.map(function(c){
-					if(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
+					if(c.optionValue && fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
 						utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), c.optionValue);
 					}
 				});
 				selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
 			} else {
 				property.properties.map(function(p) {
-					let pname = p.properties[0].name;
+					let pname = p.properties[0] && p.properties[0].name;
 					if(container.model.hasOwnProperty(pname)) {
 						utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
 					}
@@ -494,8 +552,15 @@
 		const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
 		const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
 		//Some magic that prevents errors for arising
-		const valueResponse = valueProperty.properties && valueProperty.properties.length ? valueProperty.properties.map(valuePropertyFn) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) :
-		valueProperty.map && valueProperty.map(valuePropertyFn);
+		let valueResponse = null;
+		if (valueProperty.properties && valueProperty.properties.length) { 
+			valueResponse = valueProperty.properties.map(valuePropertyFn);
+		} else if (!isMissingDescriptorMeta) {
+			let value = utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name];
+			valueResponse = build(container, valueProperty, path.concat(valueProperty.name), value)
+		} else {
+			valueResponse = valueProperty.map && valueProperty.map(valuePropertyFn);
+		}
 		function valuePropertyFn(d, i) {
 			const childPath = path.concat(valueProperty.name, d.name);
 			const childValue = utils.resolvePath(container.model, childPath.join('.'));
@@ -624,7 +689,7 @@
 
 			if (isArray) {
 				valuePath.push(index);
-				fieldId += index;
+				fieldId = isLeafList ? fieldId + index + value : resolveReactKey(value);
 			}
 
 			if (isMetaField) {
@@ -682,7 +747,7 @@
 			value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
 			if(!value) {
 				property.properties.map(function(p) {
-					let pname = p.properties[0].name;
+					let pname = p.properties[0] && p.properties[0].name;
 					if(container.model.hasOwnProperty(pname)) {
 						value = container.model[pname];
 					}
@@ -728,11 +793,7 @@
 			<div className="basic-properties-group">
 				<h2>Basic</h2>
 				<div>
-					{basicProperties.map(property => {
-						const path = [property.name];
-						const value = container.model[property.name];
-						return build(container, property, path, value);
-					})}
+					{buildComponentsForProperties(container, basicProperties, [], container.model)}
 				</div>
 			</div>
 		);
@@ -752,11 +813,7 @@
 					<a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
 				</h1>
 				<div className="toggleable">
-					{properties.map(property => {
-						const path = [property.name];
-						const value = container.model[property.name];
-						return build(container, property, path, value, {toggle: true, width: props.width});
-					})}
+					{buildComponentsForProperties(container, properties, [], container.model, {toggle: true, width: props.width})}
 				</div>
 				<div className="toggle-bottom-spacer" style={{visibility: 'hidden', 'height': '50%', position: 'absolute'}}>We need this so when the user closes the panel it won't shift away and scare the bj out of them!</div>
 			</div>
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx b/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx
index ed9ea93..8702957 100644
--- a/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx
@@ -20,6 +20,7 @@
 //https://raw.githubusercontent.com/RIFTIO/RIFT.ware/master/rift-shell
 import _cloneDeep from 'lodash/cloneDeep'
 import _findIndex from 'lodash/findIndex'
+import _uniqueId from 'lodash/uniqueId'
 import React from 'react';
 import ReactDOM from 'react-dom';
 import TreeView from 'react-treeview';
@@ -28,7 +29,7 @@
 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 { Panel, PanelWrapper } from 'widgets/panel/panel';
 import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
 import LoadingIndicator from 'widgets/loading-indicator/loadingIndicator.jsx';
 
@@ -37,17 +38,61 @@
 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);
+const ASSET_TYPE = {
+    'nsd': [
+        { id: 'ICONS', folder: 'icons', title: "Icons", allowFolders: false },
+        { id: 'SCRIPTS', folder: 'scripts', title: "scripts", allowFolders: true },
+        { id: 'NS_CONFIG', folder: 'ns_config', title: "NS Config", allowFolders: false },
+        { id: 'VNF_CONFIG', folder: 'vnf_config', title: "VNF Config", allowFolders: false }
+    ],
+    'vnfd': [
+        { id: 'ICONS', folder: 'icons', title: "Icons", allowFolders: false },
+        { id: 'CHARMS', folder: 'charms', title: "charms", allowFolders: true },
+        { id: 'SCRIPTS', folder: 'scripts', title: "scripts", allowFolders: true },
+        { id: 'IMAGES', folder: 'images', title: "images", allowFolders: false },
+        { id: 'CLOUD_INIT', folder: 'cloud_init', title: "cloud_init", allowFolders: false },
+        { id: 'README', folder: '.', title: ".", allowFolders: false }
+    ]
+}
+
+const createDropZone = function (action, clickable, getUploadPropsCallback, dropTarget) {
+    const dropZone = new FileManagerUploadDropZone(ReactDOM.findDOMNode(dropTarget), [clickable], action, getUploadPropsCallback);
     // dropZone.on('dragover', this.onDragOver);
     // dropZone.on('dragend', this.onDragEnd);
     // dropZone.on('addedfile', this.onFileAdded);
     return dropZone;
 };
-//updateFileLocationInput
+
+function normalizeAssets(packageType, assetInfo, filesStatus) {
+    let assets = {};
+    let assetTypes = ASSET_TYPE[packageType];
+    assetTypes.forEach(assetGroup => {
+        const typeFolder = assetGroup.folder;
+        let folders = assetInfo.id.filter(name => name.startsWith(typeFolder));
+        if (folders.length) {
+            folders.reverse();
+            assets[assetGroup.id] = folders.map(fullName => {
+                let path = fullName.slice(typeFolder.length + 1);
+                let files = assetInfo.data[fullName].map(info => ({
+                    name: info.name.startsWith(fullName) ? info.name.slice(fullName.length + 1) : info.name,
+                    status: filesStatus[info.name]
+                }));
+                return { path, files };
+            });
+        }
+    });
+    return assets;
+}
+
+function sendDeleteFileRequest(assetType, path, name) {
+    path = path ? path + '/' + name : name;
+    FileManagerActions.deletePackageFile({ assetType, path });
+}
+
 class FileManager extends React.Component {
     constructor(props) {
         super(props)
+        let assests = props.files;
     }
     componentWillMount() {
         // FileManagerActions.openFileManagerSockets()
@@ -57,62 +102,41 @@
     }
     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 { files, filesState, type, item, actions } = this.props;
+        let assets = normalizeAssets(type, files, filesState);
+        let children = [];
+        let assetTypes = ASSET_TYPE[type];
+        assetTypes.forEach(assetGroup => {
+            const typeFolder = assetGroup.folder;
+            let folders = assets[assetGroup.id];
+            let rootAssets = { path: '', files: [] };
+            let subFolders = null;
+            if (folders && folders.length) {
+                rootAssets = folders[0];
+                subFolders = folders.slice(1);
+            }
+            children.push(
+                <AssetGroup
+                    key={typeFolder}
+                    packageId={item.id}
+                    packageType={type}
+                    title={assetGroup.title}
+                    assetGroup={assetGroup}
+                    path={rootAssets.path}
+                    files={rootAssets.files}
+                    allowsFolders={assetGroup.allowFolders}
+                    folders={subFolders}
+                    showNotification={actions.showNotification}
+                />
+            )
+        }, this);
+
         let html = (
             <div className="FileManager">
-                <PanelWrapper style={{flexDirection: 'column'}}>
-                <Panel className="addFileSection" style={{backgroundColor: 'transparent'}} no-corners>
-                    <div className="inputSection">
-                        <TextInput placeholder="some/path" value={this.props.newPathName} label="create a new directory" onChange={FileManagerActions.newPathNameUpdated} />
-                        <Button label="Create" onClick={FileManagerActions.createDirectory} />
-                    </div>
-                </Panel>
-                {self.props.files && self.props.files.id && buildList(self, self.props.files) }
+                <PanelWrapper style={{ flexDirection: 'column' }}>
+                    {children}
                 </PanelWrapper>
             </div>
         )
@@ -121,89 +145,159 @@
 
 }
 
-function buildList(self, data) {
-    let toReturn = [];
-    data.id.map(function(k,i) {
-        toReturn.push (contentFolder(self, data.data[k], 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;
-    let classId = `DZ-${path.replace(/\/|\s+/g, '-')}`;
-    const onboardDropZone = createDropZone.bind(this, FileManagerUploadDropZone.ACTIONS.onboard, '.ComposerAppAddFile.' + classId, type, id, path);
+function NewHierachy(props) {
     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>
-
+        <Panel className="addFileSection" style={{ backgroundColor: 'transparent' }} no-corners>
+            <div className="inputSection">
+                <TextInput placeholder="some/path" label="create a folder hierarchy" onChange={FileManagerActions.newPathNameUpdated} />
+                <Button label="Create" onClick={e => FileManagerActions.createDirectory(props.assetGroup.id)} />
             </div>
         </Panel>
     );
 }
+
+class AssetGroup extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = { downloadPath: "" };
+    }
+
+    render() {
+        let { title, packageType, packageId, assetGroup, files, allowsFolders, folders, path, inputState, showNotification } = this.props;
+        let children = [];
+        if (folders && folders.length) {
+            folders.map(function (folder, i) {
+                children.push(
+                    <AssetGroup
+                        key={folder.path}
+                        packageId={packageId}
+                        title={folder.path}
+                        packageType={packageType}
+                        files={folder.files}
+                        assetGroup={assetGroup}
+                        path={folder.path}
+                        showNotification={showNotification}
+                    />
+                )
+            });
+        }
+        let folderCreateComponent = allowsFolders ? <NewHierachy assetGroup={assetGroup} /> : null;
+        let entries = null
+
+        function uploadFileFromUrl(url) {
+            if (!url || url == "") {
+                return;
+            }
+            let splitUrl = url.split('/');
+            let fileName = splitUrl[splitUrl.length - 1];
+            if (files.findIndex(f => f.name === fileName) > -1) {
+                showNotification('It seems you\'re attempting to upload a file with a duplicate file name');
+            } else {
+                FileManagerActions.sendDownloadFileRequest({ url, assetType: assetGroup.id, path: path });
+            }
+        }
+
+        return (
+            <Panel title={title} itemClassName="nested" no-corners>
+                {folderCreateComponent}
+                <div className="folder">
+                    <FileAssetList files={files} path={path} packageId={packageId} packageType={packageType} assetGroup={assetGroup} />
+                    <Panel className="addFileSection" no-corners>
+                        <ItemUpload packageType={packageType} packageId={packageId} path={path} assetGroup={assetGroup} />
+                        <div style={{ marginLeft: '0.5rem' }}>
+                            OR
+                    </div>
+                        <div className="inputSection">
+                            <TextInput placeholder="URL" className="" label="External URL" value={this.state.downloadPath} onChange={e => this.setState({ downloadPath: e.target.value })} />
+                            <Button className='ComposerAppSave' label="DOWNLOAD" onClick={e => uploadFileFromUrl(this.state.downloadPath)} />
+                        </div>
+                    </Panel>
+                    <div>
+                        {children}
+                    </div>
+                </div>
+            </Panel>
+        );
+    }
+}
+
+function FileAssetList(props) {
+    let { packageType, packageId, assetGroup, files, path } = props;
+    let children = null;
+    if (files) {
+        children = files.map(function (file, i) {
+            if (!file.hasOwnProperty('contents')) {
+                return <FileAsset key={file.name} file={file} path={path} id={packageId} type={packageType} assetGroup={assetGroup} />
+            }
+        })
+    }
+    return (
+        <div className='file-list'>
+            {children}
+        </div>
+    );
+
+}
+
+function FileAsset(props) {
+    let { file, path, type, assetGroup, id } = props;
+    const name = file.name;
+    const downloadHost = API_SERVER.match('localhost') || API_SERVER.match('127.0.0.1') ? `${window.location.protocol}//${window.location.hostname}` : API_SERVER;
+    //{`${window.location.protocol}//${API_SERVER}:4567/api/package${type}/${id}/${path}/${name}`}
+    return (
+        <div className="file">
+            <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={`${downloadHost}:4567/api/package/${type}/${id}/${assetGroup.folder}${path}/${name}`}>{name}</a>
+                    </div>
+                </div>
+                <div className="file-action"
+                    style={{ display: (!file.status || (file && file.status.toLowerCase() != 'loading...')) ? 'inherit' : 'none', cursor: 'pointer' }}
+                    onClick={e => sendDeleteFileRequest(assetGroup.id, path, name)}>
+                    X
+                </div>
+            </div>
+        </div>
+    )
+}
+
 class ItemUpload extends React.Component {
     constructor(props) {
         super(props);
+        this.state = { dropzoneIdClass: 'DZ-' + _uniqueId() };
     }
     componentDidMount() {
-        if (this.props.dropZone) {
-            const dropTarget = this;
-            const dropZone = this.props.dropZone(dropTarget);
-        }
+        createDropZone(
+            FileManagerUploadDropZone.ACTIONS.onboard,
+            '.ComposerAppAddFile.' + this.state.dropzoneIdClass,
+            () => {
+            let theCode = 'crap';
+            return ({
+                packageType: this.props.packageType,
+                packageId: this.props.packageId,
+                assetGroup: this.props.assetGroup,
+                path: this.props.path
+            })},
+            this);
     }
+
     render() {
-        let {type, id, path, key, ...props} = this.props;
-        let classId = `DZ-${path.replace(/\/|\s+/g, '-')}`;
+        let { dropzoneIdClass } = this.props;
         return (
             <div className="inputSection">
-                <label className="sqTextInput" style={{flexDirection: 'row', alignItems:'center'}}>
+                <label className="sqTextInput" style={{ flexDirection: 'row', alignItems: 'center' }}>
                     <span>Upload File</span>
-                    <Button className={'ComposerAppAddFile ' + classId} label="BROWSE"/>
+                    <Button className={'ComposerAppAddFile ' + this.state.dropzoneIdClass} 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;
-    const downloadHost = API_SERVER.match('localhost') || API_SERVER.match('127.0.0.1') ? `${window.location.protocol}//${window.location.hostname}` : API_SERVER;
-    //{`${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={`${downloadHost}: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) ? '/' : '';
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js
index 93e1e5e..e5791a4 100644
--- a/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerActions.js
@@ -21,11 +21,26 @@
 class FileManagerActions {
 
     constructor() {
-        this.generateActions('getFilelistSuccess', 'getFilelistError', 'updateFileLocationInput','sendDownloadFileRequst', 'addFileSuccess', 'addFileError','deletePackageFile','deleteFileSuccess','deleteFileError','openDownloadMonitoringSocketSuccess', 'openDownloadMonitoringSocketError',
-                             'getFilelistSocketSuccess',
-                             'openFileManagerSockets', 'closeFileManagerSockets','newPathNameUpdated', 'createDirectory');
+        this.generateActions(
+            'getFilelistSuccess',
+            'getFilelistError',
+            'updateFileLocationInput',
+            'sendDownloadFileRequest',
+            'addFileSuccess',
+            'addFileError',
+            'deletePackageFile',
+            'deleteFileSuccess',
+            'deleteFileError',
+            'openDownloadMonitoringSocketSuccess',
+            'openDownloadMonitoringSocketError',
+            'getFilelistSocketSuccess',
+            'openFileManagerSockets',
+            'closeFileManagerSockets',
+            'newPathNameUpdated',
+            'createDirectory'
+        );
     }
 
 }
 
-export default alt.createActions(FileManagerActions);
+export default alt.createActions(FileManagerActions);
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js
index 1f0fd80..616eaad 100644
--- a/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js
+++ b/skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js
@@ -57,20 +57,29 @@
     },
     addFile: function() {
         return {
-            remote: function(state, id, type, path, url, refresh) {
+            remote: function(state, id, type, assetType, path, url, refresh) {
                 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];
-                    let packagePath = refresh ? path + ((path[path.length - 1] == '/') ? '' : '/') : path + '/' + fileName;
+                    let packagePath = refresh ? path + ((path[path.length - 1] == '/') ? '' : '/') : (path ? path + '/' + fileName : fileName);
+                    let assetFolder = assetType.toLowerCase();
                     $.ajax({
-                        beforeSend: Utils.addAuthorizationStub,
-                        url: 'api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id + '&package_path=' + packagePath + '&url=' + url,
+                        beforeSend: (xhr) => {
+                                Utils.addAuthorizationStub(xhr);
+                                // lets get the buzy graphic rolling
+                                FileManagerActions.addFileSuccess.defer({
+                                                path: assetFolder + (path ? '/' + path: ''),
+                                                fileName: fileName,
+                                                refresh: refresh
+                                            });                            
+                            },
+                        url: 'api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id + '&package_path=' + packagePath + '&asset_type=' + assetType + '&url=' + url,
                         success: function(data) {
                             resolve({
-                                data:data,
-                                path: path,
+                                data: data,
+                                path: assetFolder + (path ? '/' + path: ''),
                                 fileName: fileName,
                                 refresh: refresh
                             });
@@ -93,33 +102,25 @@
     },
     deleteFile: function() {
         return {
-            remote: function(state, id, type, path) {
+            remote: function(state, id, type, assetType, path) {
+                let assetFolder = assetType.toLowerCase();
                 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 ,
+                        url: 'api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id + '&asset_type=' + assetType + '&package_path=' + path ,
                         success: function(data) {
                             if (data.output.status == 'True') {
-                                resolve({
-                                    data: data,
-                                    path: path
-                                });
+                                resolve({data, assetFolder, path});
                             } else {
-                                reject({
-                                    data: data,
-                                    path: path
-                                })
+                                reject({data, assetFolder, path})
                             }
                         },
                         error: function(error) {
                             if (typeof error == 'string') {
                                 error = JSON.parse(error);
                             }
-                            reject({
-                                path: path,
-                                data: error
-                            });
+                            reject({data, assetFolder, path});
                         }
                     }).fail(function(xhr){
                         //Authentication and the handling of fail states should be wrapped up into a connection class.
diff --git a/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js b/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js
index 963e57e..680a02f 100644
--- a/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js
+++ b/skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js
@@ -40,17 +40,26 @@
 	return window.location.origin;
 }
 
-function initializeDropZone(element = '#dropzone', button = false, action = ACTIONS.onboard, type, id, path) {
+function initializeDropZone(element = '#dropzone', button = false, action = ACTIONS.onboard, getUploadProps) {
 	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() {
+			let {packageType, packageId, assetGroup, path} = getUploadProps();
 			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 : '');
+			let url = getCatalogPackageManagerServerOrigin() 
+				+ '/composer/api/file-manager?api_server=' 
+				+ Utils.getSearchParams(window.location).api_server 
+				+ '&package_type=' + packageType 
+				+ '&package_id=' + packageId 
+				+ '&package_path=' + path 
+				+ '&asset_type=' + assetGroup.id
+				+ ( dev_download_server ? '&dev_download_server=' + dev_download_server : '');
+			return url;
 		},
 		headers: {
 			'Authorization': Auth
@@ -61,11 +70,12 @@
 		previewTemplate: '',
 		sending(file, xhr, formData) {
 			// NOTE ie11 does not get this form data
+			let {packageType, packageId, assetGroup, path} = getUploadProps();
 			formData.append('id', file.id);
 			FileManagerActions.addFileSuccess({
-				fileName: file.name,
-				path: path
-			})
+				path: assetGroup.folder + (path ? '/' + path: ''),
+				fileName: file.name
+			});
 		},
 		error(file, errorMessage) {
 			const response = {
@@ -119,10 +129,10 @@
 	});
 }
 
-export default class CatalogPackageManagerUploadDropZone {
+export default class FileManagerUploadDropZone {
 
-	constructor(element, button, action, type, id, path) {
-		this.dropZone = initializeDropZone(element, button, action, type, id, path);
+	constructor(element, button, action, getUploadProps) {
+		this.dropZone = initializeDropZone(element, button, action, getUploadProps);
 	}
 
 	static get ACTIONS() {
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
index 5488e77..1ba8912 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
@@ -4,10 +4,11 @@
  * This class provides methods to get the metadata about descriptor models.
  */
 
-'use strict';
-
 import _cloneDeep from 'lodash/cloneDeep'
-import utils from './../utils'
+import _isEmpty from 'lodash/isEmpty'
+import _pick from 'lodash/pick'
+import _get from 'lodash/get'
+import _set from 'lodash/set'
 import DescriptorModelMetaProperty from './DescriptorModelMetaProperty'
 import CommonUtils from 'utils/utils';
 const assign = Object.assign;
@@ -24,6 +25,152 @@
 	return type;
 }
 
+const uiStateToSave = ['containerPositionMap'];
+
+//////
+// Data serialization will be done on a meta model basis. That is,
+// given a schema and data, retrieve from the data only that which is 
+// defined by the schema.
+//
+
+// serialize data for a list of properties
+function serializeAll(properties, data) {
+	if (data) {
+		return properties.reduce((obj, p) => {
+			return Object.assign(obj, p.serialize(data));
+		}, {});
+	}
+	return null;
+}
+
+function serialize_container(data) {
+	data = data[this.name];
+	if (_isEmpty(data)) {
+		return null;
+	}
+	let obj = {};
+	obj[this.name] = serializeAll(this.properties, data);
+	return obj;
+}
+
+function serialize_list(data) {
+	data = data[this.name];
+	if (data) {
+		if (!Array.isArray(data)) {
+			return serializeAll(this.properties, data);
+		} else if (data.length) {
+			let list = data.reduce((c, d) => {
+				let obj = serializeAll(this.properties, d);
+				if (!_isEmpty(obj)) {
+					c.push(obj);
+				}
+				return c;
+			}, []);
+			if (!_isEmpty(list)){
+				let obj = {};
+				obj[this.name] = list;
+				return obj;
+			}
+		}
+	}
+	return null;
+}
+
+function serialize_leaf(data) {
+	let value = data[this.name];
+	if (value === null || typeof value === 'undefined' || value === '') {
+		return null;
+	}
+	let obj = {};
+	if (this['data-type'] === 'empty') {
+		value = ''; // empty string does get sent as value
+	}
+	obj[this.name] = value;
+	return obj;
+}
+
+function serialize_leaf_empty(data) {
+	let value = data[this.name];
+	if (value) {
+		let obj = {};
+		obj[this.name] = "";
+		return obj;
+	}
+	return null;
+}
+
+function serialize_leaf_list(data) {
+	data = data[this.name];
+	if (data) {
+		data = data.reduce((result, value) => {
+			if (value !== '' && value !== null && value !== undefined && typeof value !== 'object') {
+				result.push(value);
+			}
+			return result;
+		}, []);
+		if (data.length){
+			let obj = {};
+			obj[this.name] = data;
+			return obj;
+		}
+	}
+	return null;
+}
+
+function serialize_choice(data) {
+	let keys = Object.keys(data);
+	if (keys) {
+		const chosen = this.properties.find(
+			c => c.type === 'case' && c.properties && c.properties.some(p => keys.indexOf(p.name) > -1));
+		return chosen && serializeAll(chosen.properties, data);
+	}
+	return null;
+}
+
+function serialize_case(data) {
+	return Serializer.container.call(this, data);
+}
+
+// special ui data handler for leaf of type string named 'meta' 
+function serialize_meta(data) {
+	let uiState = data['uiState'];
+	let meta = uiState && _pick(uiState, uiStateToSave);
+	// if there is no uiState to save perhaps this was not a ui state property
+	return _isEmpty(meta) ? null : {meta: JSON.stringify(meta)};
+}
+
+function serialize_unsupported(data) {
+	console.error('unsupported property', property);
+	return null;
+}
+
+function getSerializer(property) {
+	switch (property.name) {
+		case 'rw-nsd:meta':
+		case 'rw-vnfd:meta':
+			return serialize_meta.bind(property);
+	}
+	switch (property.type) {
+		case 'list':
+		return serialize_list.bind(property);
+		case 'container':
+		return serialize_container.bind(property);
+		case 'choice':
+		return serialize_choice.bind(property);
+		case 'case':
+		return serialize_case.bind(property);
+		case 'leaf_list':
+		return serialize_leaf_list.bind(property);
+		case 'leaf':
+		switch (property['data-type']){
+			case 'empty':
+			return serialize_leaf_empty.bind(property);
+		}
+		return serialize_leaf.bind(property);
+	}
+	return serialize_unsupported.bind(property);
+}
+
 let modelMetaByPropertyNameMap = [];
 
 let cachedDescriptorModelMetaRequest = null;
@@ -31,32 +178,38 @@
 export default {
 	init() {
 		if (!cachedDescriptorModelMetaRequest) {
-			cachedDescriptorModelMetaRequest = new Promise(function(resolve, reject) {
-				CommonUtils.getDescriptorModelMeta().then(function(data) {
+			cachedDescriptorModelMetaRequest = new Promise(function (resolve, reject) {
+				CommonUtils.getDescriptorModelMeta().then(function (data) {
 					let DescriptorModelMetaJSON = data;
 					modelMetaByPropertyNameMap = Object.keys(DescriptorModelMetaJSON).reduce((map, key) => {
 						function mapProperties(parentMap, parentObj) {
+							// let's beef up the meta info with a helper (more to come?)
+							parentObj.serialize = getSerializer(parentObj);
 							parentMap[':meta'] = parentObj;
 							const properties = parentObj && parentObj.properties ? parentObj.properties : [];
 							properties.forEach(p => {
-								parentMap[p.name] = mapProperties({}, assign(p, {':qualified-type': parentObj[':qualified-type'] + '.' + p.name}));
+								parentMap[p.name] = mapProperties({}, assign(p, {
+									':qualified-type': parentObj[':qualified-type'] + '.' + p.name
+								}));
 								return map;
 							}, parentMap);
 							return parentMap;
 						}
-						map[key] = mapProperties({}, assign(DescriptorModelMetaJSON[key], {':qualified-type': key}));
+						map[key] = mapProperties({}, assign(DescriptorModelMetaJSON[key], {
+							':qualified-type': key
+						}));
 						return map;
 					}, {});
 
 					(() => {
 						// initialize the UI centric properties that CONFD could care less about
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'nsd.meta.:meta.preserve-line-breaks', true);
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'vnfd.meta.:meta.preserve-line-breaks', true);
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'vnfd.vdu.cloud-init.:meta.preserve-line-breaks', true);
-						utils.assignPathValue(modelMetaByPropertyNameMap, 'nsd.constituent-vnfd.vnf-configuration.config-template.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'nsd.meta.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'vnfd.meta.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'vnfd.vdu.cloud-init.:meta.preserve-line-breaks', true);
+						_set(modelMetaByPropertyNameMap, 'nsd.constituent-vnfd.vnf-configuration.config-template.:meta.preserve-line-breaks', true);
 					})();
 					resolve();
-				}, function(error) {
+				}, function (error) {
 					cachedDescriptorModelMetaRequest = null;
 				})
 			})
@@ -78,7 +231,7 @@
 	},
 	getModelMetaForType(typeOrPath, filterProperties = () => true) {
 		// resolve paths like 'nsd' or 'vnfd.vdu' or 'nsd.constituent-vnfd'
-		const found = utils.resolvePath(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
+		const found = _get(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
 		if (found) {
 			const uiState = _cloneDeep(found[':meta']);
 			uiState.properties = uiState.properties.filter(filterProperties);
@@ -88,18 +241,18 @@
 	},
 	getModelFieldNamesForType(typeOrPath) {
 		// resolve paths like 'nsd' or 'vnfd.vdu' or 'nsd.constituent-vnfd'
-		const found = utils.resolvePath(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
+		const found = _get(modelMetaByPropertyNameMap, getPathForType(typeOrPath));
 		if (found) {
 			let result = [];
 			found[':meta'].properties.map((p) => {
 				// if(false) {
-				if(p.type == 'choice') {
+				if (p.type == 'choice') {
 					result.push(p.name)
-					return p.properties.map(function(q){
+					return p.properties.map(function (q) {
 						result.push(q.properties[0].name);
 					})
 
-				} else  {
+				} else {
 					return result.push(p.name);
 				}
 			})
@@ -120,10 +273,10 @@
 	 *  will be used. 
 	 * @returns {string}
 	 */
-	generateItemUniqueName (list, property, prefix) {
-		if (   property.type !== 'list' 
-			|| property.key.length !== 1
-			|| property.properties.find(prop => prop.name === property.key[0])['data-type'] !== 'string') {
+	generateItemUniqueName(list, property, prefix) {
+		if (property.type !== 'list' ||
+			property.key.length !== 1 ||
+			property.properties.find(prop => prop.name === property.key[0])['data-type'] !== 'string') {
 			// only support list with a single key of type string
 			return null;
 		}
@@ -133,11 +286,12 @@
 		let key = property.key[0];
 		let suffix = list ? list.length + 1 : 1
 		let keyValue = prefix + '-' + suffix;
+
 		function makeUniqueName() {
 			if (list) {
 				for (let i = 0; i < list.length; i = ++i) {
 					if (list[i][key] === keyValue) {
-						keyValue = keyValue + '-' + (i+1);
+						keyValue = keyValue + '-' + (i + 1);
 						makeUniqueName(); // not worried about recursing too deep (chances ??)
 						break;
 					}
@@ -148,4 +302,4 @@
 		return keyValue;
 	}
 
-}
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
index 2955e55..e064457 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
@@ -21,8 +21,6 @@
  * This class provides utility methods for interrogating an instance of model uiState object.
  */
 
-'use strict';
-
 import _includes from 'lodash/includes'
 import _isArray from 'lodash/isArray'
 import guid from './../guid'
@@ -36,6 +34,9 @@
 	isBoolean(property = {}) {
 		return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'boolean')
 	},
+	isLeafEmpty(property = {}) {
+		return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'empty')
+	},
 	isLeaf(property = {}) {
 		return /leaf|choice/.test(property.type);
 	},
diff --git a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
index 737078f..b496041 100644
--- a/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
+++ b/skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
@@ -1,4 +1,3 @@
-
 /*
  *
  *   Copyright 2016 RIFT.IO Inc
@@ -20,227 +19,28 @@
  * Created by onvelocity on 10/20/15.
  */
 
-import _isNumber from 'lodash/isNumber'
-import _cloneDeep from 'lodash/cloneDeep'
-import _isEmpty from 'lodash/isEmpty'
-import _omit from 'lodash/omit'
-import _pick from 'lodash/pick'
-import utils from './../utils'
-import DescriptorModelFields from './DescriptorModelFields'
 import DescriptorModelMetaFactory from './DescriptorModelMetaFactory'
 
-let nsdFields = null;
-let vldFields = null;
-let vnfdFields = null;
-let cvnfdFields = null;
-
-
-
-
-/**
- * Serialize DescriptorModel JSON into CONFD JSON. Also, cleans up the data as needed.
- *
- * @type {{serialize: (function(*=)), ':clean': (function(*=)), nsd: {serialize: (function(*=))}, vld: {serialize: (function(*=))}, vnfd-connection-point-ref: {serialize: (function(*=))}, constituent-vnfd: {serialize: (function(*=))}, vnfd: {serialize: (function(*=))}, vdu: {serialize: (function(*=))}}}
- */
 const DescriptorModelSerializer = {
+	/**
+	 * Create a json object that can be sent to the backend. I.e. CONFD JSON compliant to the schema definition.
+	 * 
+	 * @param {any} model - the data blob from the editor. This is not modified.
+	 * @returns cleansed data model
+	 */
 	serialize(model) {
-		const type = model.uiState && model.uiState.type;
-		const serializer = this[type];
-		if (serializer) {
-			model = serializer.serialize(model);
-			this[':clean'](model);
-			return model;
+		if (!model.uiState) {
+			console.error('model uiState null', model);
+			return {};
 		}
-		return false;
-	},
-	':clean'(model) {
-		// remove uiState from all elements accept nsd and vnfd
-		// remove empty / blank value fields
-		function clean(m) {
-			Object.keys(m).forEach(k => {
-				const isEmptyObject = typeof m[k] === 'object' && _isEmpty(m[k]);
-				if (typeof m[k] === 'undefined' || isEmptyObject || m[k] === '') {
-					delete m[k];
-				}
-				const isMetaAllowed = /^nsd|vnfd$/.test(m.uiState && m.uiState.type);
-				if (k === 'uiState') {
-					if (isMetaAllowed) {
-						// remove any transient ui state properties
-						const uiState = _pick(m.uiState, DescriptorModelFields.meta);
-						if (!_isEmpty(uiState)) {
-							// uiState field must be a string
-							m['meta'] = JSON.stringify(uiState);
-						}
-					}
-					delete m[k];
-				}
-				if (typeof m[k] === 'object') {
-					clean(m[k]);
-				}
-			});
-		}
-		clean(model);
-		return model;
-	},
-	nsd: {
-		serialize(nsdModel) {
-			if(!nsdFields) nsdFields = DescriptorModelMetaFactory.getModelFieldNamesForType('nsd').concat('uiState');
-			const confd = _pick(nsdModel, nsdFields);
-
-			// vnfd is defined in the ETSI etsi_gs reference manual but RIFT does not use it
-			delete confd.vnfd;
-
-			// map the vnfd instances into the CONFD constituent-vnfd ref instances
-			confd['constituent-vnfd'] = confd['constituent-vnfd'].map((d, index) => {
-
-				const constituentVNFD = {
-					'member-vnf-index': d['member-vnf-index'],
-					'vnfd-id-ref': d['vnfd-id-ref']
-				};
-
-				if (d['vnf-configuration']) {
-					const vnfConfig = _cloneDeep(d['vnf-configuration']);
-					const configType = vnfConfig['config-type'] || 'none';
-					// make sure we send the correct values based on config type
-					if (configType === 'none') {
-						constituentVNFD['vnf-configuration'] = {'config-type': 'none'};
-						const configPriority = utils.resolvePath(vnfConfig, 'input-params.config-priority');
-						const configPriorityValue = _isNumber(configPriority) ? configPriority : d.uiState['member-vnf-index'];
-						utils.assignPathValue(constituentVNFD['vnf-configuration'], 'input-params.config-priority', configPriorityValue);
-					} else {
-						// remove any unused configuration options
-						['netconf', 'rest', 'script', 'juju'].forEach(type => {
-							if (configType !== type) {
-								delete vnfConfig[type];
-							}
-						});
-						constituentVNFD['vnf-configuration'] = vnfConfig;
-					}
-				}
-
-				if (d.hasOwnProperty('start-by-default')) {
-					constituentVNFD['start-by-default'] = d['start-by-default'];
-				}
-
-				return constituentVNFD;
-
-			});
-			for (var key in confd) {
-				checkForChoiceAndRemove(key, confd, nsdModel);
-			}
-			// serialize the VLD instances
-			confd.vld = confd.vld.map(d => {
-				return DescriptorModelSerializer.serialize(d);
-			});
-
-			return cleanEmptyTopKeys(confd);
-
-		}
-	},
-	vld: {
-		serialize(vldModel) {
-			if(!vldFields) vldFields = DescriptorModelMetaFactory.getModelFieldNamesForType('nsd.vld');
-			const confd = _pick(vldModel, vldFields);
-			const property = 'vnfd-connection-point-ref';
-
-			// TODO: There is a bug in RIFT-REST that is not accepting empty
-			// strings for string properties.
-			// once that is fixed, remove this piece of code.
-			// fix-start
-			for (var key in confd) {
-			  	if (confd.hasOwnProperty(key) && confd[key] === '') {
-                	delete confd[key];
-                } else {
-                	//removes choice properties from top level object and copies immediate children onto it.
-					checkForChoiceAndRemove(key, confd, vldModel);
-                }
-			}
-
-
-			const deepProperty = 'provider-network';
-			for (var key in confd[deepProperty]) {
-				if (confd[deepProperty].hasOwnProperty(key) && confd[deepProperty][key] === '') {
-					delete confd[deepProperty][key];
-				}
-			}
-			// fix-end
-			confd[property] = confd[property].map(d => DescriptorModelSerializer[property].serialize(d));
-			return cleanEmptyTopKeys(confd);
-		}
-	},
-	'vnfd-connection-point-ref': {
-		serialize(ref) {
-			return _pick(ref, ['member-vnf-index-ref', 'vnfd-id-ref', 'vnfd-connection-point-ref']);
-		}
-	},
-	'internal-connection-point': {
-		serialize(ref) {
-			return _pick(ref, ['id-ref']);
-		}
-	},
-	'constituent-vnfd': {
-		serialize(cvnfdModel) {
-			if(!cvnfdFields) cvnfdFields = DescriptorModelMetaFactory.getModelFieldNamesForType('nsd.constituent-vnfd');
-			return _pick(cvnfdModel, cvnfdFields);
-		}
-	},
-	vnfd: {
-		serialize(vnfdModel) {
-			if(!vnfdFields) vnfdFields = DescriptorModelMetaFactory.getModelFieldNamesForType('vnfd').concat('uiState');
-			const confd = _pick(vnfdModel, vnfdFields);
-			confd.vdu = confd.vdu.map(d => DescriptorModelSerializer.serialize(d));
-			return cleanEmptyTopKeys(confd);
-		}
-	},
-	vdu: {
-		serialize(vduModel) {
-			const copy = _cloneDeep(vduModel);
-			for (let k in copy) {
-				checkForChoiceAndRemove(k, copy, vduModel)
-			}
-			const confd = _omit(copy, ['uiState']);
-			return cleanEmptyTopKeys(confd);
-		}
+		const path = model.uiState['qualified-type'] || model.uiState['type'];
+		const metaModel = DescriptorModelMetaFactory.getModelMetaForType(path);
+		const data = {};
+		const name = model.uiState['type'];
+		data[name] = model; // lets get the meta hierachy from the top
+		const result = metaModel.serialize(data);
+		console.debug(result);
+		return result;
 	}
-};
-
-
-function checkForChoiceAndRemove(k, confd, model) {
-    let state = model.uiState;
-    if (state.choice) {
-        let choice = state.choice[k]
-        if(choice) {
-            if (choice.constructor.name == "Array") {
-                for(let i = 0; i < choice.length; i++) {
-                    for (let key in confd[k][i]) {
-                        if(choice[i] && (choice[i].selected.indexOf(key) > -1)) {
-                            confd[k][i][key] = confd[k][i][key]
-                        }
-                        confd[key];
-                    };
-                }
-            } else {
-                for (let key in confd[k]) {
-                    if(choice && (choice.selected.indexOf(key) > -1)) {
-                        confd[key] = confd[k][key]
-                    }
-                };
-                delete confd[k];
-            }
-
-        }
-    }
-    return confd;
 }
-
-function cleanEmptyTopKeys(m){
-    Object.keys(m).forEach(k => {
-        const isEmptyObject = typeof m[k] === 'object' && _isEmpty(m[k]);
-        if (typeof m[k] === 'undefined' || isEmptyObject || m[k] === '') {
-            delete m[k];
-        }
-    });
-    return m;
-}
-
-export default DescriptorModelSerializer;
+export default DescriptorModelSerializer;
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js b/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
index c677a44..c9675b7 100644
--- a/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
+++ b/skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
@@ -168,7 +168,7 @@
 			showDescriptor: ComposerAppActions.showDescriptor,
 			getFilelistSuccess: FileManagerActions.getFilelistSuccess,
 			updateFileLocationInput: FileManagerActions.updateFileLocationInput,
-			sendDownloadFileRequst: FileManagerActions.sendDownloadFileRequst,
+			sendDownloadFileRequest: FileManagerActions.sendDownloadFileRequest,
 			addFileSuccess: FileManagerActions.addFileSuccess,
 			deletePackageFile: FileManagerActions.deletePackageFile,
 			deleteFileSuccess: FileManagerActions.deleteFileSuccess,
@@ -485,7 +485,7 @@
         if (self.fileMonitoringSocketID) {
         	let newState = {};
         	if(data.hasOwnProperty('contents')) {
-        		filesState = addInputState( _cloneDeep(this.filesState),data);
+        		filesState = updateFileState( _cloneDeep(this.filesState),data);
 				let normalizedData = normalizeTree(data);
 				newState = {
 					files: {
@@ -511,7 +511,6 @@
 				id:[],
 				data:{}
 			};
-			data.contents.map(getContents);
 			function getContents(d) {
 				if(d.hasOwnProperty('contents')) {
 					let contents = [];
@@ -526,25 +525,26 @@
 					f.data[d.name] = contents;
 				}
 			}
+			getContents(data);
  			return f;
 		}
-		function addInputState(obj, d) {
+		function updateFileState(obj, d) {
 			d.newFile = '';
 			if(d.hasOwnProperty('contents')) {
-				d.contents.map(addInputState.bind(null, obj))
+				d.contents.map(updateFileState.bind(null, obj))
 			}
-			if(!obj[d.name]) {
-				obj[d.name] = '';
-			}
+			// override any "pending" state we may have initialized
+			obj[d.name] = '';
 			return obj;
 		}
 	}
-	sendDownloadFileRequst(data) {
+	sendDownloadFileRequest(data) {
 		let id = data.id || this.item.id;
 		let type = data.type || this.item.uiState.type;
+		let assetType = data.assetType;
 		let path = data.path;
 		let url = data.url;
-		this.getInstance().addFile(id, type, path, url, data.refresh);
+		this.getInstance().addFile(id, type, assetType, path, url, data.refresh);
 	}
 	updateFileLocationInput = (data) => {
 		let name = data.name;
@@ -558,13 +558,27 @@
 	addFileSuccess = (data) => {
 		if(!data.refresh) {
 			let path = data.path;
+			if (path.startsWith('readme')) {
+				// this asset type stuff should be in a more common location
+				// this is a wee bit of a hack till it is
+				path = '.' + path.slice(6);
+			}
 			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});
+			let assetGroup = files.data[path] || [];
+			if (fileName) {
+				let name = path + '/' + fileName;
+				if (assetGroup.findIndex(f => f.name === name) == -1){
+					assetGroup.push({name});
+				}
+			}
+			files.data[path] = assetGroup;
+			if (files.id.indexOf(path) == -1){
+				files.id.push(path);
+			}
+			let filesState = _cloneDeep(this.filesState);
+			filesState[name] = "DOWNLOADING";
+			this.setState({files, filesState});
 		}
 
 	}
@@ -634,6 +648,11 @@
 		}
 
 		this.setState({
+			filesState: [],
+			files: {
+				id:[],
+				data:{}
+			},
 			fileMonitoringSocketID: id,
 			fileMonitoringSocket: ws
 		})
@@ -663,19 +682,28 @@
 	endWatchingJob(id) {
 
 	}
-	deletePackageFile(name) {
+	deletePackageFile(asset) {
+		let {assetType, path} = asset;
 		let id = this.item.id;
 		let type = this.item.uiState.type;
-		this.getInstance().deleteFile(id, type, name);
+		this.getInstance().deleteFile(id, type, assetType, path);
 	}
 	deleteFileSuccess = (data) => {
-		let path = data.path.split('/')
+		let name = null;
+		let path = null;
+		if (data.assetFolder === 'readme'){
+			// freak'n root folder is special
+			name = data.path;
+			path = ['.'];
+		} else {
+			name = data.assetFolder + '/' + data.path;
+			path = name.split('/');
+			path.pop();
+		}
 		let files = _cloneDeep(this.files);
-		path.pop();
-		path = path.join('/');
-		let pathFiles = files.data[path]
-		_remove(pathFiles, function(c) {
-			return c.name == data.path;
+		let filesForPath = files.data[path.join('/')]
+		_remove(filesForPath, function(c) {
+			return c.name == name;
 		});
 
 		this.setState({
@@ -697,12 +725,13 @@
 			newPathName: value
 		})
 	}
-	createDirectory = () => {
+	createDirectory = (assetType) => {
 		console.log(this.newPathName);
-		this.sendDownloadFileRequst({
+		this.sendDownloadFileRequest({
 			id: this.item.id,
 			type: this.item.uiState.type,
-			path: this.item.name + '/' + this.newPathName,
+			assetType: assetType,
+			path: this.newPathName,
 			url: utils.getSearchParams(window.location).dev_download_server || window.location.protocol + '//' + window.location.host,
 			refresh: true
 		});
diff --git a/skyquake/plugins/launchpad/src/instantiate/instantiateInputParams.jsx b/skyquake/plugins/launchpad/src/instantiate/instantiateInputParams.jsx
index d1e295c..6273d8c 100644
--- a/skyquake/plugins/launchpad/src/instantiate/instantiateInputParams.jsx
+++ b/skyquake/plugins/launchpad/src/instantiate/instantiateInputParams.jsx
@@ -105,12 +105,13 @@
   }
   inputParametersHTML = (props) => {
     let inputParameters = props.inputParameters;
+    const handleChange = (i, event) => props.updateInputParam(i, event.target.value);
     return inputParameters && inputParameters.map(function(input, i) {
         return (
                 <div className="configure-nsd_section" key={i}>
                   <h3 className="launchpadCard_title">Input Parameters</h3>
                   <div className="inputControls">
-                      <TextInput label={ input.label || input.xpath } type="text" onChange={props.updateInputParam.bind(self, i)} />
+                      <TextInput label={ input.label || input.xpath } type="text" onChange={handleChange.bind(this, i)} />
                   </div>
                 </div>
         )
diff --git a/skyquake/plugins/launchpad/src/instantiate/instantiateStore.js b/skyquake/plugins/launchpad/src/instantiate/instantiateStore.js
index 3ec2a80..0e91ee3 100644
--- a/skyquake/plugins/launchpad/src/instantiate/instantiateStore.js
+++ b/skyquake/plugins/launchpad/src/instantiate/instantiateStore.js
@@ -189,10 +189,10 @@
         });
         return window.location.hash = 'launchpad/' + tokenizedHash[2];
     }
-    launchNSRError(error) {
+    launchNSRError(data) {
         var msg = 'Something went wrong while trying to instantiate. Check the error logs for more information';
-        if(error) {
-            msg = error;
+        if (data.error) {
+            msg = data.error;
         }
         Alt.actions.global.showNotification.defer(msg);
         Alt.actions.global.hideScreenLoader.defer();
diff --git a/skyquake/plugins/launchpad/src/launchpad_card/launchpadCardCloudAccount.jsx b/skyquake/plugins/launchpad/src/launchpad_card/launchpadCardCloudAccount.jsx
index 1856dda..39397ff 100644
--- a/skyquake/plugins/launchpad/src/launchpad_card/launchpadCardCloudAccount.jsx
+++ b/skyquake/plugins/launchpad/src/launchpad_card/launchpadCardCloudAccount.jsx
@@ -36,7 +36,7 @@
         (<li key="nsr"><h3>NSR: {this.props.nsr['cloud-account']}</h3></li>)
       )
     }
-    this.props.nsr['vnfrs'].map(function(v,i) {
+    this.props.nsr['vnfrs'] && this.props.nsr['vnfrs'].map(function(v,i) {
       if(v.hasOwnProperty('cloud-account')) {
         status.push(
           (<li key={i}><h3>VNFR {v['short-name']}: {v['cloud-account']}</h3></li>)
diff --git a/skyquake/plugins/launchpad/src/launchpad_card/nsrScalingGroups.jsx b/skyquake/plugins/launchpad/src/launchpad_card/nsrScalingGroups.jsx
index be8d4eb..db94ded 100644
--- a/skyquake/plugins/launchpad/src/launchpad_card/nsrScalingGroups.jsx
+++ b/skyquake/plugins/launchpad/src/launchpad_card/nsrScalingGroups.jsx
@@ -17,18 +17,24 @@
  */
 import React from 'react';
 import RecordViewStore from '../recordViewer/recordViewStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
 import Button from 'widgets/button/rw.button.js';
 import Utils from 'utils/utils.js';
 import UpTime from 'widgets/uptime/uptime.jsx';
 import './nsrScalingGroups.scss';
 
-export default class NsrScalingGroups extends React.Component {
+class NsrScalingGroups extends React.Component {
 	constructor(props) {
 		super(props);
 		this.state = {};
 	}
 
-	handleExecuteClick = (nsr_id, scaling_group_id, event) => {
+	handleExecuteClick = (nsr_id, scaling_group_id, max_instance_count, event) => {
+		let self = this;
+		if (this.getInstancesForScalingGroup(scaling_group_id) == max_instance_count) {
+			self.props.flux.actions.global.showNotification("Maximum allowed scaling instances created for this group. Cannot create any more");
+			return;
+		}
 		RecordViewStore.createScalingGroupInstance({
 			nsr_id: nsr_id,
 			scaling_group_id: scaling_group_id
@@ -68,7 +74,7 @@
 					);
 				}
 			}) : trows.push(
-				<tr key={sgrInstanceIndex}>
+				<tr key={sgri}>
 					<td colSpan="5" style={{textAlign: 'center'}}>No network services scaled in this group</td>
 				</tr>
 			);
@@ -132,7 +138,7 @@
 
 			let sgInstanceTable = this.createScalingGroupTable(sgd.name);
 
-			let sgCreateInstanceButton = <Button label='Create Scaling Group Instance' className="dark" isDisabled={this.getInstancesForScalingGroup(sgd.name) == sgd["max-instance-count"]} isLoading={false} onClick={this.handleExecuteClick.bind(this, this.props.data.id, sgd.name)} />
+			let sgCreateInstanceButton = <Button label='Create Scaling Group Instance' className="dark" isDisabled={this.getInstancesForScalingGroup(sgd.name) == sgd["max-instance-count"]} isLoading={false} onClick={this.handleExecuteClick.bind(this, this.props.data.id, sgd.name, sgd['max-instance-count'])} />
 
 			let scalingGroup =
 				<div>
@@ -157,3 +163,5 @@
 	}
 
 }
+
+export default SkyquakeComponent(NsrScalingGroups);
diff --git a/skyquake/plugins/launchpad/src/recordViewer/recordCard.jsx b/skyquake/plugins/launchpad/src/recordViewer/recordCard.jsx
index ea3eb2f..f9a400b 100644
--- a/skyquake/plugins/launchpad/src/recordViewer/recordCard.jsx
+++ b/skyquake/plugins/launchpad/src/recordViewer/recordCard.jsx
@@ -369,11 +369,11 @@
           consoleUrlsTabTitle = 'VDU Console Links';
 
           tabList.push(
-            <Tab key={cardData.id + '-cp'}>{consoleUrlsTabTitle}</Tab>
+            <Tab key={cardData.id + '-vducl'}>{consoleUrlsTabTitle}</Tab>
           );
 
           tabPanels.push(
-            <TabPanel key={cardData.id + '-cp-panel'}>
+            <TabPanel key={cardData.id + '-vducl-panel'}>
               <div className="consoleUrls">
                 {consoleUrlsComponent}
               </div>