NOTICKET: Merging OSM/master to OSM/projects
authorKIRAN KASHALKAR <kiran.kashalkar@riftio.com>
Thu, 20 Apr 2017 13:18:13 +0000 (09:18 -0400)
committerKIRAN KASHALKAR <kiran.kashalkar@riftio.com>
Thu, 20 Apr 2017 13:18:13 +0000 (09:18 -0400)
Signed-off-by: KIRAN KASHALKAR <kiran.kashalkar@riftio.com>
18 files changed:
1  2 
skyquake/framework/utils/utils.js
skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx
skyquake/framework/widgets/skyquake_container/skyquakeContainerSource.js
skyquake/framework/widgets/skyquake_container/skyquakeContainerStore.js
skyquake/framework/widgets/skyquake_nav/skyquakeNav.jsx
skyquake/plugins/accounts/src/account/account.jsx
skyquake/plugins/composer/api/composer.js
skyquake/plugins/composer/src/src/components/CatalogPanel.js
skyquake/plugins/composer/src/src/components/CatalogPanelToolbar.js
skyquake/plugins/composer/src/src/components/DetailsPanel.js
skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
skyquake/plugins/composer/src/src/libraries/utils.js
skyquake/plugins/launchpad/api/launchpad.js
skyquake/plugins/launchpad/package.json
skyquake/plugins/launchpad/src/instantiate/instantiateDashboard.jsx
skyquake/plugins/launchpad/src/launchpad.jsx
skyquake/plugins/launchpad/src/recordViewer/recordCard.jsx
skyquake/skyquake.js

Simple merge
@@@ -115,16 -106,14 +114,17 @@@ export default class skyquakeContainer 
                              type={notificationType}
                              hidden={!(displayNotification && notificationMessage)}
                              onDismiss={SkyquakeContainerActions.hideNotification}
+                             timeout= {5000}
                          />
                          <ScreenLoader show={displayScreenLoader}/>
 -                        <SkyquakeNav nav={this.state.nav}
 -                            currentPlugin={this.state.currentPlugin}
 -                            store={SkyquakeContainerStore} />
 +                        <SkyquakeNav nav={nav}
 +                            currentPlugin={this.state.user.currentPlugin}
 +                            currentUser={this.state.user.userId}
 +                            currentProject={this.state.user.projectId}
 +                            store={SkyquakeContainerStore}
 +                            projects={this.state.projects} />
                          <div className="titleBar">
 -                            <h1>{this.state.currentPlugin + tag}</h1>
 +                            <h1>{(this.state.nav.name ? this.state.nav.name.replace('_', ' ').replace('-', ' ') : this.state.currentPlugin && this.state.currentPlugin.replace('_', ' ').replace('-', ' ')) + tag}</h1>
                          </div>
                          <div className={"application " + routeName}>
                              {this.props.children}
  import Alt from './skyquakeAltInstance.js';
  import SkyquakeContainerSource from './skyquakeContainerSource.js';
  import SkyquakeContainerActions from './skyquakeContainerActions';
- import _ from 'lodash';
 +let Utils = require('utils/utils.js');
+ import _indexOf from 'lodash/indexOf';
++import _isEqual from 'lodash/isEqual';
  //Temporary, until api server is on same port as webserver
- var rw = require('utils/rw.js');
+ import rw from 'utils/rw.js';
  var API_SERVER = rw.getSearchParams(window.location).api_server;
  var UPLOAD_SERVER = rw.getSearchParams(window.location).upload_server;
  
@@@ -164,43 -162,6 +166,43 @@@ class SkyquakeContainerStore 
          })
      }
  
-                 if (!_.isEqual(data.project, self.projects)) {
 +    openProjectSocketSuccess = (connection) => {
 +        var self = this;
 +        var ws = window.multiplexer.channel(connection);
 +        if (!connection) return;
 +        self.setState({
 +            socket: ws.ws,
 +            channelId: connection
 +        });
 +        ws.onmessage = function(socket) {
 +            try {
 +                var data = JSON.parse(socket.data);
 +                Utils.checkAuthentication(data.statusCode, function() {
 +                    self.closeSocket();
 +                });
++                if (!_isEqual(data.project, self.projects)) {
 +                    let user = self.user;
 +                    user.projects = data.project;
 +                    self.setState({
 +                        user: user,
 +                        projects: data.project
 +                    });
 +                }
 +            } catch(e) {
 +                console.log('HIT an exception in openProjectSocketSuccess', e);
 +            }
 +        };
 +    }
 +    getUserProfileSuccess = (user) => {
 +        this.alt.actions.global.hideScreenLoader.defer();
 +        this.setState({user})
 +    }
 +    selectActiveProjectSuccess = (projectId) => {
 +        let user = this.user;
 +        user.projectId = projectId;
 +        this.setState({user});
 +        window.location.reload(true);
 +    }
      //Notifications
      showNotification = (data) => {
          let state = {
index f9161cc,0000000..d9dff0b
mode 100644,000000..100644
--- /dev/null
@@@ -1,356 -1,0 +1,357 @@@
- var rw = require('utils/rw.js');
 +/*
 + *
 + *   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 React from 'react';
 +import { Link } from 'react-router';
 +import Utils from 'utils/utils.js';
 +import Crouton from 'react-crouton';
 +import 'style/common.scss';
 +
 +import './skyquakeNav.scss';
 +import SelectOption from '../form_controls/selectOption.jsx';
 +import {FormSection} from '../form_controls/formControls.jsx';
 +import {isRBACValid, SkyquakeRBAC} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
 +
 +//Temporary, until api server is on same port as webserver
++import rw from 'utils/rw.js';
++
 +var API_SERVER = rw.getSearchParams(window.location).api_server;
 +var UPLOAD_SERVER = rw.getSearchParams(window.location).upload_server;
 +
 +//
 +// Internal classes/functions
 +//
 +
 +class LogoutAppMenuItem extends React.Component {
 +    handleLogout() {
 +        Utils.clearAuthentication();
 +    }
 +    render() {
 +        return (
 +            <div className="app">
 +                <h2>
 +                    <a onClick={this.handleLogout}>
 +                        Logout
 +                    </a>
 +                </h2>
 +            </div>
 +        );
 +    }
 +}
 +
 +class SelectProject extends React.Component {
 +    constructor(props) {
 +        super(props);
 +    }
 +    selectProject(e) {
 +        let value = JSON.parse(e.currentTarget.value);
 +        console.log('selected project', value)
 +    }
 +    render() {
 +        let props = this.props;
 +        let currentValue = JSON.stringify(props.currentProject);
 +        let projects = this.props.projects && this.props.projects.map((p,i) => {
 +            return {
 +                label: p.name,
 +                value: p.name
 +            }
 +        });
 +        let hasProjects = (this.props.projects && (this.props.projects.length > 0))
 +        return (
 +            <div className="userSection app">
 +                {
 +                    hasProjects ?  'Project:' : 'No Projects Assigned'
 +                }
 +                {
 +                    hasProjects ?
 +                    <SelectOption
 +                        options={projects}
 +                        value={currentValue}
 +                        defaultValue={currentValue}
 +                        onChange={props.onSelectProject}
 +                        className="projectSelect" />
 +                    : null
 +                }
 +            </div>
 +        )
 +    }
 +}
 +
 +class UserNav extends React.Component {
 +    constructor(props) {
 +        super(props);
 +    }
 +    handleLogout() {
 +        Utils.clearAuthentication();
 +    }
 +    selectProject(e) {
 +        let value = JSON.parse(e.currentTarget.value)
 +        console.log('selected project', value)
 +    }
 +    render() {
 +        let props = this.props;
 +        let userProfileLink = '';
 +        this.props.nav['user_management'] && this.props.nav['user_management'].routes.map((r) => {
 +            if(r.unique) {
 +                userProfileLink = returnLinkItem(r, props.currentUser)
 +            }
 +        })
 +        return (
 +            <div className="app">
 +                <h2>
 +                    USER: {userProfileLink}
 +                    <span className="oi" data-glyph="caret-bottom"></span>
 +                </h2>
 +                <ul className="menu">
 +                    <li>
 +                        <a onClick={this.handleLogout}>
 +                            Logout
 +                        </a>
 +                    </li>
 +                </ul>
 +            </div>
 +        )
 +    }
 +}
 +
 +UserNav.defaultProps = {
 +    projects: [
 +
 +    ]
 +}
 +
 +//
 +// Exported classes and functions
 +//
 +
 +//
 +/**
 + * Skyquake Nav Component. Provides navigation functionality between all plugins
 + */
 +export default class skyquakeNav extends React.Component {
 +    constructor(props) {
 +        super(props);
 +        this.state = {};
 +        this.state.validateErrorEvent = 0;
 +        this.state.validateErrorMsg = '';
 +    }
 +    componentDidMount() {
 +        this.props.store.openProjectSocket();
 +        this.props.store.getUserProfile();
 +    }
 +    validateError = (msg) => {
 +        this.setState({
 +            validateErrorEvent: true,
 +            validateErrorMsg: msg
 +        });
 +    }
 +    validateReset = () => {
 +        this.setState({
 +            validateErrorEvent: false
 +        });
 +    }
 +    returnCrouton = () => {
 +        return <Crouton
 +            id={Date.now()}
 +            message={this.state.validateErrorMsg}
 +            type={"error"}
 +            hidden={!(this.state.validateErrorEvent && this.state.validateErrorMsg)}
 +            onDismiss={this.validateReset}
 +        />;
 +    }
 +    render() {
 +        let html;
 +        html = (
 +                <div>
 +                {this.returnCrouton()}
 +            <nav className="skyquakeNav">
 +                {buildNav.call(this, this.props.nav, this.props.currentPlugin, this.props)}
 +            </nav>
 +
 +            </div>
 +        )
 +        return html;
 +    }
 +}
 +skyquakeNav.defaultProps = {
 +    nav: {}
 +}
 +skyquakeNav.contextTypes = {
 +  userProfile: React.PropTypes.object
 +};
 +/**
 + * Returns a React Component
 + * @param  {object} link  Information about the nav link
 + * @param  {string} link.route Hash route that the SPA should resolve
 + * @param  {string} link.name Link name to be displayed
 + * @param  {number} index index of current array item
 + * @return {object} component A React LI Component
 + */
 +//This should be extended to also make use of internal/external links and determine if the link should refer to an outside plugin or itself.
 +export function buildNavListItem (k, link, index) {
 +    let html = false;
 +    if (link.type == 'external') {
 +        this.hasSubNav[k] = true;
 +        html = (
 +            <li key={index}>
 +                {returnLinkItem(link)}
 +            </li>
 +        );
 +    }
 +    return html;
 +}
 +
 +/**
 + * Builds a link to a React Router route or a new plugin route.
 + * @param  {object} link Routing information from nav object.
 + * @return {object}  component   returns a react component that links to a new route.
 + */
 +export function returnLinkItem(link, label) {
 +    let ref;
 +    let route = link.route;
 +    if(link.isExternal) {
 +        ref = (
 +            <a href={route}>{label || link.label}</a>
 +        )
 +    } else {
 +        if(link.path && link.path.replace(' ', '') != '') {
 +            route = link.path;
 +        }
 +        if(link.query) {
 +            let query = {};
 +            query[link.query] = '';
 +            route = {
 +                pathname: route,
 +                query: query
 +            }
 +        }
 +        ref = (
 +            <Link to={route}>
 +                {label || link.label}
 +            </Link>
 +        )
 +    }
 +    return ref;
 +}
 +
 +
 +
 +
 +/**
 + * Constructs nav for each plugin, along with available subnavs
 + * @param  {array} nav List returned from /nav endpoint.
 + * @return {array}     List of constructed nav element for each plugin
 + */
 +export function buildNav(nav, currentPlugin, props) {
 +    let navList = [];
 +    let navListHTML = [];
 +    let secondaryNav = [];
 +    let adminNav = [];
 +    let self = this;
 +    const User = this.context.userProfile;
 +    self.hasSubNav = {};
 +    let secondaryNavHTML = (
 +        <div className="secondaryNav" key="secondaryNav">
 +            {secondaryNav}
 +            <div className="app admin">
 +                <h2>
 +                    <a>
 +                        ADMIN <span className="oi" data-glyph="caret-bottom"></span>
 +                    </a>
 +                </h2>
 +                <ul className="menu">
 +                    {
 +                        adminNav
 +                    }
 +                </ul>
 +            </div>
 +            <SelectProject
 +                onSelectProject={props.store.selectActiveProject}
 +                projects={props.projects}
 +                currentProject={props.currentProject} />
 +            <UserNav
 +                currentUser={props.currentUser}
 +                nav={nav}  />
 +        </div>
 +    )
 +    for (let k in nav) {
 +        if (nav.hasOwnProperty(k)) {
 +            self.hasSubNav[k] = false;
 +            let header = null;
 +            let navClass = "app";
 +            let routes = nav[k].routes;
 +            let navItem = {};
 +            //Primary plugin title and link to dashboard.
 +            let route;
 +            let NavList;
 +            if (API_SERVER) {
 +                route = routes[0].isExternal ? '/' + k + '/index.html?api_server=' + API_SERVER + '' + '&upload_server=' + UPLOAD_SERVER + '' : '';
 +            } else {
 +                route = routes[0].isExternal ? '/' + k + '/' : '';
 +            }
 +            let dashboardLink = returnLinkItem({
 +                isExternal: routes[0].isExternal,
 +                pluginName: nav[k].pluginName,
 +                label: nav[k].label || k,
 +                route: route
 +            });
 +            let shouldAllow = nav[k].allow || ['*'];
 +            if (nav[k].pluginName == currentPlugin) {
 +                navClass += " active";
 +            }
 +            NavList = nav[k].routes.map(buildNavListItem.bind(self, k));
 +            navItem.priority = nav[k].priority;
 +            navItem.order = nav[k].order;
 +            if(nav[k].admin_link) {
 +
 +                if (isRBACValid(User, shouldAllow) ){
 +                    adminNav.push((
 +                        <li key={nav[k].name}>
 +                            {dashboardLink}
 +                        </li>
 +                    ))
 +                }
 +            } else {
 +                if (isRBACValid(User, shouldAllow) ){
 +                    navItem.html = (
 +                        <div  key={k} className={navClass}>
 +                            <h2>{dashboardLink} {self.hasSubNav[k] ? <span className="oi" data-glyph="caret-bottom"></span> : ''}</h2>
 +                            <ul className="menu">
 +                                {
 +                                    NavList
 +                                }
 +                            </ul>
 +                        </div>
 +                    );
 +                }
 +            navList.push(navItem)
 +            }
 +
 +        }
 +    }
 +    //Sorts nav items by order and returns only the markup
 +    navListHTML = navList.sort((a,b) => a.order - b.order).map(function(n) {
 +        if((n.priority  < 2)){
 +            return n.html;
 +        } else {
 +            secondaryNav.push(n.html);
 +        }
 +    });
 +    navListHTML.push(secondaryNavHTML);
 +    return navListHTML;
 +}
@@@ -313,18 -313,9 +314,19 @@@ PackageManager.upload = function(req) 
      var download_host = req.query['dev_download_server'];
  
      if (!download_host) {
-         download_host = req.protocol + '://' + req.get('host');//api_server + ':' + utils.getPortForProtocol(req.protocol);
+         download_host = req.protocol + '://' + req.get('host');//req.api_server + ':' + utils.getPortForProtocol(req.protocol);
      }
-         'external-url': download_host + '/composer/update/' + req.file.filename,
 +    var input = {
-     var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-update');
++        'external-url': download_host + '/composer/upload/' + req.file.filename,
 +        'package-type': 'VNFD',
 +        'package-id': uuid()
 +    }
 +
++    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-create');
 +
 +    input = utils.addProjectContextToRPCPayload(req, uri, input);
 +
      return new Promise(function(resolve, reject) {
          Promise.all([
              rp({
@@@ -374,19 -369,13 +376,18 @@@ PackageManager.update = function(req) 
      var download_host = req.query['dev_download_server'];
  
      if (!download_host) {
-         download_host = req.protocol + '://' + req.get('host');//req.api_server + ':' + utils.getPortForProtocol(req.protocol);
+         download_host = req.protocol + '://' + req.get('host');//api_server + ':' + utils.getPortForProtocol(req.protocol);
      }
      var input = {
-         'external-url': download_host + '/composer/upload/' + req.file.filename,
+         'external-url': download_host + '/composer/update/' + req.file.filename,
          'package-type': 'VNFD',
          'package-id': uuid()
 -    }
 +    };
 +
-     var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-create');
++    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-update');
 +
 +    input = utils.addProjectContextToRPCPayload(req, uri, input);
 +
      return new Promise(function(resolve, reject) {
          Promise.all([
              rp({
      });
  };
  
- Composer.addFile = function(req) {
-     console.log(' Uploading file', req.file.originalname, 'as', req.file.filename);
+ PackageManager.export = function(req) {
+     // /api/operations/package-export
      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.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
-     }
-     var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-file-add');
++    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-export');
++    var input = req.body;
 +    input = utils.addProjectContextToRPCPayload(req, uri, input);
      return new Promise(function(resolve, reject) {
          Promise.all([
              rp({
-                 uri: uri,
+                 uri: utils.confdPort(api_server) + '/api/operations/package-export',
                  method: 'POST',
                  headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
 -                    'Authorization': req.get('Authorization')
 +                    'Authorization': req.session && req.session.authorization
                  }),
                  forever: constants.FOREVER_ON,
                  rejectUnauthorized: false,
                  resolveWithFullResponse: true,
                  json: true,
-                 body: {
-                     input: input
-                 }
 -                body: { "input": req.body}
++                body: { "input": input }
              })
          ]).then(function(result) {
              var data = {};
      });
  }
  
- Composer.exportPackage = function(req) {
+ PackageManager.copy = function(req) {
+     // /api/operations/package-copy
      var api_server = req.query['api_server'];
-     var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-export');
++    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-copy');
 +    var input = req.body;
 +    input = utils.addProjectContextToRPCPayload(req, uri, input);
++
      return new Promise(function(resolve, reject) {
          Promise.all([
              rp({
 -                uri: utils.confdPort(api_server) + '/api/operations/package-copy',
 +                uri: uri,
                  method: 'POST',
                  headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                     'Authorization': req.session && req.session.authorization
+                     'Authorization': req.get('Authorization')
                  }),
                  forever: constants.FOREVER_ON,
                  rejectUnauthorized: false,
                  resolveWithFullResponse: true,
                  json: true,
-                 body: { "input": input }
 -                body: { "input": req.body}
++                body: { "input": input}
              })
          ]).then(function(result) {
              var data = {};
      });
  }
  
 -    var url = uri + '/api/operational/copy-jobs' + (id ? '/job/' + id : '');
+ /**
+  * This methods retrieves the status of package operations. It takes an optional 
+  * transaction id (id) this if present will return only that status otherwise
+  * an array of status' will be response.
+  */
+ PackageManager.getJobStatus = function(req) {
+     var api_server = req.query["api_server"];
+     var uri = utils.confdPort(api_server);
+     var id = req.params['id'];
 -    }
++    var url = utils.projectContextUrl(req, uri + '/api/operational/copy-jobs' + (id ? '/job/' + id : ''));
+     return new Promise(function(resolve, reject) {
+         request({
+             url: url,
+             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 returnData;
+                 if (id) {
+                     returnData = JSON.parse(response.body)['rw-pkg-mgmt:job'];
+                 } else {
+                     var data = JSON.parse(response.body)['rw-pkg-mgmt:copy-jobs'];
+                     returnData = (data && data.job) || [];
+                 }
+                 resolve({
+                     statusCode: response.statusCode,
+                     data: returnData
+                 })
+             };
+         })
+     })
+ }
+ FileManager.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.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
 -                uri: utils.confdPort(api_server) + '/api/operations/package-file-add',
++    };
++
++    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-file-add');
++
++    input = utils.addProjectContextToRPCPayload(req, uri, input);
++
++
+     return new Promise(function(resolve, reject) {
+         Promise.all([
+             rp({
++                uri: uri,
+                 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: input
+                 }
+             })
+         ]).then(function(result) {
+             var data = {};
+             data['transaction_id'] = result[0].body['output']['task-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);
+         });
+     });
+ }
  FileManager.get = function(req) {
      var api_server = req.query['api_server'];
      var type = req.query['package_type'] && req.query['package_type'].toUpperCase();
@@@ -207,9 -219,24 +227,24 @@@ export default function EditDescriptorM
                        const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
                        if (!isValueSet || property.cardinality === '0..1') {
                                const noValueDisplayText = changeCase.title(property.name);
-                               options.unshift(<option key={'(value-not-in-enum)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
+                               options.unshift(<option key={'(value-not-in-enum)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
                        }
-                       return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={value} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} disabled={!isEditable}>{options}</select>;
+                       return (
+                               <select 
+                                       key={fieldKey} 
+                                       id={fieldKey}
+                                       className={ClassNames({'-value-not-set': !isValueSet})} 
+                                       defaultValue={value} 
+                                       title={pathToProperty} 
+                                       onChange={onSelectChange} 
+                                       onFocus={onFocus} 
+                                       onBlur={endEditing} 
+                                       onMouseDown={startEditing} 
+                                       onMouseOver={startEditing} 
 -                                      readOnly={!isEditable}>
++                                      disabled={!isEditable}>
+                                               {options}
+                               </select>
+                       );
                }
  
                if (isLeafRef) {
                        const isValueSet = leafRefPathValues.filter(d => d.isSelected).length > 0;
                        if (!isValueSet || property.cardinality === '0..1') {
                                const noValueDisplayText = changeCase.title(property.name);
-                               options.unshift(<option key={'(value-not-in-leafref)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
+                               options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
                        }
-                       return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={value} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} disabled={!isEditable}>{options}</select>;
+                       return (
+                               <select 
+                                       key={fieldKey} 
+                                       id={fieldKey} 
+                                       className={ClassNames({'-value-not-set': !isValueSet})} 
+                                       defaultValue={value} 
+                                       title={pathToProperty} 
+                                       onChange={onSelectChange} 
+                                       onFocus={onFocus} 
+                                       onBlur={endEditing} 
+                                       onMouseDown={startEditing} 
+                                       onMouseOver={startEditing} 
 -                                      readOnly={!isEditable}>
++                                      disabled={!isEditable}>
+                                               {options}
+                               </select>
+                       );
                }
  
                if (isBoolean) {
                                val = value ? "TRUE" : "FALSE"
                        }
                        const isValueSet = (val != '' && val)
-                       return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={val && val.toUpperCase()} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} disabled={!isEditable}>{options}</select>;
+                       return (
+                               <select 
+                                       key={fieldKey} 
+                                       id={fieldKey} 
+                                       className={ClassNames({'-value-not-set': !isValueSet})} 
+                                       defaultValue={val && val.toUpperCase()} 
+                                       title={pathToProperty} 
+                                       onChange={onSelectChange} 
+                                       onFocus={onFocus} 
+                                       onBlur={endEditing} 
+                                       onMouseDown={startEditing} 
+                                       onMouseOver={startEditing} 
 -                                      readOnly={!isEditable}>
++                                      disabled={!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} 
+                                       onMouseDown={startEditing} 
+                                       onMouseOver={startEditing} 
 -                                      readOnly={!isEditable}>
++                                      disabled={!isEditable}>
+                                               {options}
+                               </select>
+                       );
                }
  
                if (property['preserve-line-breaks']) {
-                       return <textarea key={fieldKey.toString()} cols="5" id={fieldKey.toString()} name={name} value={value} placeholder={placeholder} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing} readOnly={!!isEditable} />;
+                       return (
+                               <textarea 
+                                       key={fieldKey} 
+                                       cols="5" 
+                                       id={fieldKey} 
+                                       defaultValue={value} 
+                                       placeholder={placeholder} 
+                                       onChange={onTextChange} 
+                                       onFocus={onFocus} 
+                                       onBlur={endEditing} 
+                                       onMouseDown={startEditing} 
+                                       onMouseOver={startEditing} 
+                                       onMouseOut={endEditing} 
+                                       onMouseLeave={endEditing} 
 -                                      readOnly={!isEditable} />
++                                      disabled={!isEditable} />
+                       );
                }
  
-               return <input
-                                       key={fieldKey.toString()}
-                                       id={fieldKey.toString()}
-                                       type="text"
-                                       name={name}
-                                       value={fieldValue}
-                                       className={className}
-                                       placeholder={placeholder}
-                                       onChange={onChange}
-                                       onFocus={onFocus}
-                                       onBlur={endEditing}
-                                       onMouseDown={startEditing}
-                                       onMouseOver={startEditing}
-                                       onMouseOut={endEditing}
-                                       onMouseLeave={endEditing}
-                                       readOnly={!isEditable}
-               />;
+               return (
+                       <input 
+                               key={fieldKey}
+                               id={fieldKey}
+                               type="text"
+                               defaultValue={fieldValue}
+                               className={className}
+                               placeholder={placeholder}
+                               onChange={onTextChange}
+                               onFocus={onFocus}
+                               onBlur={endEditing}
+                               onMouseDown={startEditing}
+                               onMouseOver={startEditing}
+                               onMouseOut={endEditing}
+                               onMouseLeave={endEditing}
 -                              readOnly={!isEditable}
++                              disabled={!isEditable}
+                       />
+               );
  
        }
  
  
                return (
                        <div key={key} className="choice">
-                               <select key={Date.now()} className={ClassNames({'-value-not-set': !selectedOptionValue})} name={selectName} value={selectedOptionValue} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing} readOnly={!isEditable}>
+                               <select 
+                                       key={Date.now()} 
+                                       className={ClassNames({'-value-not-set': !selectedOptionValue})} 
+                                       defaultValue={selectedOptionValue} 
+                                       onChange={onChange} 
+                                       onFocus={onFocus} 
+                                       onBlur={endEditing} 
+                                       onMouseDown={startEditing} 
+                                       onMouseOver={startEditing} 
+                                       onMouseOut={endEditing} 
+                                       onMouseLeave={endEditing}
++                                      disabled={!isEditable}
+                               >
                                        {options}
                                </select>
                                {valueResponse}
@@@ -239,10 -243,11 +246,14 @@@ export default 
                                                }
                                        } else {
                                                // contains no predicate
 +                                              if (!objectCopy) {
 +                                                      break;
 +                                              }
                                                objectCopy = objectCopy[fragment];
+                                               if (!objectCopy) {
+                                                       // contains no value
+                                                       break;
+                                               }
                                        }
                                }
                        }
@@@ -25,14 -25,7 +25,13 @@@ import NsCardPanel from './nsCardPanel/
  import NsListPanel from './nsListPanel/nsListPanel.jsx';
  import Crouton from 'react-crouton'
  import AppHeader from 'widgets/header/header.jsx';
- import Utils from 'utils/utils.js';
  import './launchpad.scss';
 +
 +import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
 +import ROLES from 'utils/roleConstants.js';
 +
 +const PROJECT_ROLES = ROLES.PROJECT;
 +
  let ReactCSSTransitionGroup = require('react-addons-css-transition-group');
  var LaunchpadFleetActions = require('./launchpadFleetActions.js');
  var LaunchpadFleetStore = require('./launchpadFleetStore.js');
Simple merge