User Management: Create and delete. Initial pass.
authorLaurence Maultsby <laurence.maultsby@riftio.com>
Tue, 7 Mar 2017 18:59:18 +0000 (13:59 -0500)
committerLaurence Maultsby <laurence.maultsby@riftio.com>
Tue, 7 Mar 2017 18:59:18 +0000 (13:59 -0500)
Signed-off-by: Laurence Maultsby <laurence.maultsby@riftio.com>
32 files changed:
skyquake/.storybook/config.js
skyquake/framework/core/modules/api/userManagementAPI.js [new file with mode: 0644]
skyquake/framework/core/modules/navigation_manager.js
skyquake/framework/core/modules/routes/userManagement.js [new file with mode: 0644]
skyquake/framework/style/_colors.scss
skyquake/framework/widgets/button/button.scss
skyquake/framework/widgets/button/sq-button.jsx
skyquake/framework/widgets/form_controls/formControls.scss
skyquake/framework/widgets/form_controls/input.jsx [new file with mode: 0644]
skyquake/framework/widgets/form_controls/selectOption.jsx
skyquake/framework/widgets/form_controls/textInput.jsx
skyquake/framework/widgets/panel/panel.jsx
skyquake/framework/widgets/panel/panel.scss
skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx
skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
skyquake/plugins/launchpad/package.json
skyquake/plugins/user-management/CMakeLists.txt [new file with mode: 0644]
skyquake/plugins/user-management/api/ro.js [new file with mode: 0644]
skyquake/plugins/user-management/config.json [new file with mode: 0644]
skyquake/plugins/user-management/package.json [new file with mode: 0644]
skyquake/plugins/user-management/routes.js [new file with mode: 0644]
skyquake/plugins/user-management/scripts/build.sh [new file with mode: 0755]
skyquake/plugins/user-management/scripts/install.sh [new file with mode: 0755]
skyquake/plugins/user-management/server.js [new file with mode: 0644]
skyquake/plugins/user-management/src/dashboard/dashboard.jsx [new file with mode: 0644]
skyquake/plugins/user-management/src/dashboard/userMgmt.scss [new file with mode: 0644]
skyquake/plugins/user-management/src/dashboard/userMgmtActions.js [new file with mode: 0644]
skyquake/plugins/user-management/src/dashboard/userMgmtSource.js [new file with mode: 0644]
skyquake/plugins/user-management/src/dashboard/userMgmtStore.js [new file with mode: 0644]
skyquake/plugins/user-management/src/main.js [new file with mode: 0644]
skyquake/plugins/user-management/webpack.production.config.js [new file with mode: 0644]
skyquake/skyquake.js

index 42edf3c..04aee50 100644 (file)
@@ -6,8 +6,11 @@ function loadStories() {
   // require('../tests/stories/sshKeyCard');
   // require('../tests/stories/button');
   // require('../tests/stories/sq-input-slider');
-  require('../tests/stories/catalogCard');
+  // require('../tests/stories/catalogCard');
+  require('../tests/stories/inputs');
   // require as many stories as you need.
 }
 
 configure(loadStories, module);
+
+
diff --git a/skyquake/framework/core/modules/api/userManagementAPI.js b/skyquake/framework/core/modules/api/userManagementAPI.js
new file mode 100644 (file)
index 0000000..cf33313
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ *
+ *   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.
+ *
+ */
+// DescriptorModelMeta API (NSD + VNFD)
+
+
+var UserManagement = {};
+var Promise = require('bluebird');
+var rp = require('request-promise');
+var Promise = require('promise');
+var constants = require('../../api_utils/constants');
+var utils = require('../../api_utils/utils');
+var _ = require('lodash');
+
+UserManagement.get = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) + '/api/operational/user-config/users',
+                method: 'GET',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.get('Authorization')
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            })
+        ]).then(function(result) {
+            var response = {};
+            response['data'] = {};
+            if (result[0].body) {
+                response['data']['users'] = JSON.parse(result[0].body)['rw-user:users'];
+            }
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+
+            resolve(response);
+        }).catch(function(error) {
+            var response = {};
+            console.log('Problem with UserManagement.get', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to get UserManagement' + error
+            };
+            reject(response);
+        });
+    });
+};
+UserManagement.create = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var data = req.body;
+    data = {
+        "users":[data]
+    }
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) + '/api/config/user-config',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.get('Authorization')
+                }),
+                forever: constants.FOREVER_ON,
+                json: data,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            })
+        ]).then(function(result) {
+            var response = {};
+            response['data'] = {};
+            if (result[0].body) {
+                response['data'] = result[0].body;
+            }
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+
+            resolve(response);
+        }).catch(function(error) {
+            var response = {};
+            console.log('Problem with UserManagement.create', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to create user' + error
+            };
+            reject(response);
+        });
+    });
+};
+
+UserManagement.delete = function(req) {
+    var self = this;
+    var username = req.params.username;
+    var domain = req.params.domain;
+    var api_server = req.query["api_server"];
+    var requestHeaders = {};
+    var url = `${utils.confdPort(api_server)}/api/config/user-config/users/${username},${domain}`
+    return new Promise(function(resolve, reject) {
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.data,
+            constants.HTTP_HEADERS.content_type.data, {
+                'Authorization': req.get('Authorization')
+            });
+        rp({
+            url: url,
+            method: 'DELETE',
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+        }, function(error, response, body) {
+            if (utils.validateResponse('UserManagement.DELETE', error, response, body, resolve, reject)) {
+                return resolve({
+                    statusCode: response.statusCode,
+                    data: JSON.stringify(response.body)
+                });
+            };
+        });
+    })
+}
+module.exports = UserManagement;
index c85eba6..6ed1e49 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -42,7 +42,6 @@ function addNavigation(plugin_name, routes) {
        if (!NAVIGATION[plugin_name]) {
                NAVIGATION[plugin_name] = {};
        }
-
        if (!NAVIGATION[plugin_name].routes) {
                NAVIGATION[plugin_name].routes = routes;
        } else {
diff --git a/skyquake/framework/core/modules/routes/userManagement.js b/skyquake/framework/core/modules/routes/userManagement.js
new file mode 100644 (file)
index 0000000..359d985
--- /dev/null
@@ -0,0 +1,64 @@
+
+/*
+ *
+ *   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.
+ *
+ */
+
+/**
+ * inactivity routes module. Provides a RESTful API for this
+ * skyquake instance's inactivity state.
+ * @module framework/core/modules/routes/inactivity
+ * @author Laurence Maultsby <laurence.maultsby@riftio.com>
+ */
+
+var cors = require('cors');
+var bodyParser = require('body-parser');
+var Router = require('express').Router();
+var utils = require('../../api_utils/utils');
+var UserManagementAPI = require('../api/UserManagementAPI.js');
+
+Router.use(bodyParser.json());
+Router.use(cors());
+Router.use(bodyParser.urlencoded({
+    extended: true
+}));
+
+Router.get('/user', cors(), function(req, res) {
+    UserManagementAPI.get(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+Router.post('/user', cors(), function(req, res) {
+    UserManagementAPI.create(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+Router.delete('/user/:username/:domain', cors(), function(req, res) {
+    UserManagementAPI.delete(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+module.exports = Router;
+
+
+
index 9378984..353a103 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -31,7 +31,7 @@ $body-color:$lightest-gray;
 
 $error-red:#FF5F5F;
 
-//PC
+/*PC*/
 
 $black: #000;
 $gray-lightest: #f1f1f1;
@@ -42,9 +42,9 @@ $gray-dark: #999;
 $gray-darker: #666;
 $gray-darkest: #333;
 $white: #FFF;
-//
-// Brand Colors
-//
+/**/
+/* Brand Colors*/
+/**/
 $brand-blue-light: #30baef;
 $brand-blue: #00acee;
 $brand-blue-dark: #147ca3;
@@ -68,9 +68,9 @@ $neutral-light-5: hsl(360, 100%, 50%);
 
 $neutral-dark-1: hsl(360, 100%, 50%);
 $neutral-dark-2: hsl(360, 100%, 50%);
-$neutral-dark-3: hsl(360, 100%, 50%);
-$neutral-dark-4: hsl(360, 100%, 50%);
-$neutral-dark-5: hsl(360, 100%, 50%);
+$neutral-dark-3: hsl(0, 0%, 49.7%);
+$neutral-dark-4: hsl(0, 0%, 42.7%);
+$neutral-dark-5: hsl(0, 0%, 35.7%);
 $netral-black: hsl(0, 100%, 0%);
 
 
index c972e14..e4eb7a4 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -62,15 +62,18 @@ button{
 ############################################################################ */
 
 .SqButton {
-  align-items: center;
+  -ms-flex-align: center;
+      align-items: center;
   border-style: solid;
   border-radius: 3px;
   border-width: 0px;
   cursor: pointer;
+  display: -ms-inline-flexbox;
   display: inline-flex;
   font-size: 1rem;
   height: 50px;
-  justify-content: center;
+  -ms-flex-pack: center;
+      justify-content: center;
   margin: 0 10px;
   outline: none;
   padding: 0 15px;
@@ -107,7 +110,7 @@ button{
 
   /* Focus */
   &:focus {
-    // box-shadow: $focus-shadow;
+    /* box-shadow: $focus-shadow;*/
     border: 1px solid red;
   }
 
@@ -256,11 +259,14 @@ button{
       fill: $primaryForeground;
     }
   }
-
-
 }
 
-
+.sqButtonGroup {
+  display: -ms-flexbox;
+  display: flex;
+  -ms-flex-pack: center;
+      justify-content: center;
+}
 
 
 
index ae93128..8d0ec77 100644 (file)
@@ -36,17 +36,35 @@ export default class SqButton extends React.Component {
             Class += " is-disabled";
         }
         return (
-                <div style={{display: 'flex'}}>
+        <div style={{display: 'flex'}} onClick={this.props.onClick}>
             <div className={Class} tabIndex="0">
-            {svgHTML}
-              <div className="SqButton-content">{label}</div>
+                {svgHTML}
+                <div className="SqButton-content">{label}</div>
             </div>
+        </div>
+        )
+    }
+}
+
+export class ButtonGroup extends React.Component {
+    render() {
+        let className = "sqButtonGroup";
+        if (this.props.className) {
+            className = `${className} ${this.props.className}`
+        }
+        return (
+            <div className={className} style={this.props.style}>
+                {this.props.children}
             </div>
         )
     }
 }
 
+
 SqButton.defaultProps = {
+    onClick: function(e) {
+        console.log('Clicked')
+    },
     icon: false,
     primary: false,
     disabled: false,
index 4a88435..fcc08de 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
         border:0px;
         height: 100%;
     }
+    &.checkbox {
+        -ms-flex-direction:row;
+            flex-direction:row;
+            -ms-flex-align:center;
+                align-items:center;
+        margin-bottom:0;
+            >span {
+                -ms-flex-order: 1;
+                    order: 1;
+                    padding-left:1rem;
+            }
+            >input {
+                -ms-flex-order: 0;
+                    order: 0;
+
+                    box-shadow:none;
+                    height:25px;
+            }
+    }
 }
+
+
diff --git a/skyquake/framework/widgets/form_controls/input.jsx b/skyquake/framework/widgets/form_controls/input.jsx
new file mode 100644 (file)
index 0000000..b758242
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ *
+ *   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 './formControls.scss';
+import SelectOption from 'widgets/form_controls/selectOption.jsx';
+
+import React, {Component} from 'react';
+
+export default class Input extends Component {
+    render() {
+        let {label, value, defaultValue, ...props} = this.props;
+        let inputProperties = {
+            value: value
+        }
+        let isRequired;
+        let inputType;
+        let className = `sqTextInput ${props.className}`;
+
+        if(this.props.required) {
+           isRequired = <span className="required">*</span>
+        }
+        if (defaultValue) {
+            inputProperties.defaultValue = defaultValue;
+        }
+        if (props.pattern) {
+            inputProperties.pattern = props.pattern;
+        }
+        if(props.hasOwnProperty('type') && (props.type.toLowerCase() == 'checkbox')) {
+            inputProperties.checked = props.checked;
+            className = `${className} checkbox`;
+        }
+        if (value == undefined) {
+            value = defaultValue;
+        }
+        switch(props.type) {
+            case 'textarea':
+                inputType = <textarea key={props.key} {...inputProperties} value={value} onChange={props.onChange} />
+                break;
+            case 'select':
+                inputType = <SelectOption
+                                key={props.key}
+                                initial={props.initial}
+                                defaultValue={defaultValue}
+                                options={props.options}
+                                onChange={props.onChange}
+                            />
+                break;
+            default:
+                inputType = <input key={props.key} type={props.type} {...inputProperties} onChange={props.onChange} placeholder={props.placeholder}/>;
+        }
+        let html = (
+            <label className={className} style={props.style}>
+              <span> { label } {isRequired}</span>
+              {
+                !props.readonly ? inputType : <div className="readonly">{value}</div>
+              }
+
+            </label>
+        );
+        return html;
+    }
+}
+
+
+function buildDropDown() {
+
+}
+
+Input.defaultProps = {
+    onChange: function(e) {
+        console.log(e.target.value, e);
+        console.dir(e.target);
+    },
+    label: '',
+    defaultValue: null,
+    type: 'text',
+    readonly: false,
+    style:{},
+    className: ''
+
+}
+
index 41a8b13..81a3860 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,14 +29,23 @@ export default class SelectOption extends React.Component {
     let html;
     let defaultValue = this.props.defaultValue;
     let options =  this.props.options.map(function(op, i) {
-      let value = JSON.stringify(op.value);
-      return <option key={i} value={JSON.stringify(op.value)}>{op.label}</option>
+    let value;
+    let label;
+    if(typeof(op) == 'object') {
+      value = JSON.stringify(op.value);
+      label = op.label;
+    } else {
+      value = op;
+      label = op;
+    }
+
+      return <option key={i} value={JSON.stringify(value)}>{label}</option>
     });
     if (this.props.initial) {
       options.unshift(<option key='blank' value={JSON.stringify(this.props.defaultValue)}></option>);
     }
     html = (
-        <label>
+        <label key={this.props.key}>
             {this.props.label}
             <select className={this.props.className} onChange={this.handleOnChange} defaultValue={JSON.stringify(defaultValue)} >
                 {
@@ -49,11 +58,25 @@ export default class SelectOption extends React.Component {
   }
 }
 SelectOption.defaultProps = {
+  /**
+   * [options description]
+   * @type {Array} - Expects items to contain objects with the properties 'label' and 'value' which are both string types. Hint: JSON.stringify()
+   */
   options: [],
   onChange: function(e) {
+    console.log(e.target.value)
     console.dir(e)
   },
-  defaultValue: false,
+  /**
+   *  Selected or default value
+​
+   * @type {[type]}
+   */
+  defaultValue: null,
+  /**
+   * True if first entry in dropdown should be blank
+   * @type {Boolean}
+   */
   initial: false,
   label: null
 }
index 03dfa9c..000dcf7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
 import './formControls.scss';
 
 import React, {Component} from 'react';
-
-export default class TextInput extends Component {
-    render() {
-        let {label, onChange, value, defaultValue, ...props} = this.props;
-        let inputProperties = {
-            value: value,
-            onChange: onChange
-        }
-        let isRequired;
-        let inputType;
-        if(this.props.required) {
-           isRequired = <span className="required">*</span>
-        }
-        if (defaultValue) {
-            inputProperties.defaultValue = defaultValue;
-        }
-        if (props.pattern) {
-            inputProperties.pattern = props.pattern;
-        }
-        if (value == undefined) {
-            value = defaultValue;
-        }
-        switch(props.type) {
-            case 'textarea':
-                inputType = <textarea {...inputProperties} value={value}/>
-
-                break;
-            default:
-                inputType = <input type={props.type} {...inputProperties} placeholder={props.placeholder}/>;
-        }
-        let html = (
-            <label className={"sqTextInput " + props.className} style={props.style}>
-              <span> { label } {isRequired}</span>
-              {
-                !props.readonly ? inputType : <div className="readonly">{value}</div>
-              }
-
-            </label>
-        );
-        return html;
+import Input from './input.jsx';
+class TextInput extends Input {
+    constructor(props) {
+        super(props);
+        console.warn('TextInput is deprecated. Use Input component instead')
     }
 }
-
-TextInput.defaultProps = {
-    onChange: function(e) {
-        console.log(e.target.value);
-    },
-    label: '',
-    defaultValue: undefined,
-    type: 'text',
-    readonly: false,
-    style:{}
-
-}
+export default TextInput;
 
index 4ef7c89..b26b1b1 100644 (file)
@@ -18,6 +18,7 @@
 import React, {Component} from 'react';
 import 'style/core.css';
 import './panel.scss';
+import circleXImage from '../../../node_modules/open-iconic/svg/circle-x.svg';
 export class Panel extends Component {
     constructor(props) {
         super(props)
@@ -28,8 +29,15 @@ export class Panel extends Component {
         let classRoot = className ? ' ' + className : ' ';
         let hasCorners = this.props['no-corners'];
         let titleTag = title ? <header className="skyquakePanel-title">{title}</header> : '';
+        let closeButton = (
+            <a onClick={self.props.hasCloseButton}
+              className={"close-btn"}>
+              <img src={circleXImage} title="Close card" />
+              </a>
+        );
         return (
             <section className={'skyquakePanel' + classRoot} style={props.style}>
+                {  self.props.hasCloseButton ? closeButton : null}
                 { !hasCorners ? <i className="corner-accent top left"></i> : null }
                 { !hasCorners ? <i className="corner-accent top right"></i> : null }
                 {titleTag}
index 2a31b1c..75dc8ac 100644 (file)
             width:100%;
             height:100%;
         }
+    .close-btn {
+        cursor:pointer;
+        position: absolute;
+        right: -0.5rem;
+        top: -0.5rem;
+        img {
+            width: 1rem;
+        }
+    }
 }
 
 .skyquakePanelWrapper.column {
+    -ms-flex-direction:column;
+            flex-direction:column;
+        display:-ms-flexbox;
+        display:flex;
     .skyquakePanel-wrapper {
         height:auto;
     }
index bf036e2..7a0418e 100644 (file)
@@ -113,7 +113,7 @@ export default class skyquakeContainer extends React.Component {
                             currentPlugin={this.state.currentPlugin}
                             store={SkyquakeContainerStore} />
                         <div className="titleBar">
-                            <h1>{this.state.currentPlugin + tag}</h1>
+                            <h1>{(this.state.nav.name ? this.state.nav.name : this.state.currentPlugin) + tag}</h1>
                         </div>
                         <div className={"application " + routeName}>
                             {this.props.children}
index a69f3bb..589eef9 100644 (file)
@@ -331,7 +331,7 @@ export default function EditDescriptorModelProperties(props) {
                                // write the current choice value into the state
                                let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
                                let isTopCase = false;
-                               if (!choiceObject) {
+                               if (choiceObject) {
                                        isTopCase = true;
                                        choiceObject = utils.resolvePath(this.model, [selected].join('.'));
                                }
index 7c531b4..f3a99d0 100644 (file)
     "normalizr": "^2.1.0",
     "open-iconic": "^1.1.1",
     "prismjs": "^1.4.1",
-    "react": "^0.14.8",
     "react-awesome-modal": "^0.3.3",
     "react-breadcrumbs": "^1.3.9",
     "react-crouton": "^0.2.7",
-    "react-dom": "^0.14.6",
     "react-router": "^2.0.1",
     "react-slick": "^0.11.1",
     "react-tabs": "^0.8.0",
     "react-treeview": "^0.4.2",
+    "react": ">=15.0.1",
+    "react-dom": ">=15.0.1"
     "request-promise": "^3.0.0",
     "underscore": "^1.8.3"
   },
diff --git a/skyquake/plugins/user-management/CMakeLists.txt b/skyquake/plugins/user-management/CMakeLists.txt
new file mode 100644 (file)
index 0000000..e12d4fd
--- /dev/null
@@ -0,0 +1,38 @@
+# RIFT_IO_STANDARD_CMAKE_COPYRIGHT_HEADER(BEGIN)
+# Author(s): Kiran Kashalkar
+# Creation Date: 08/18/2015
+# RIFT_IO_STANDARD_CMAKE_COPYRIGHT_HEADER(END)
+
+##
+# DEPENDENCY ALERT
+# The submodule dependencies must be specified in the
+# .gitmodules.dep file at the top level (supermodule) directory
+# If this submodule depends other submodules remember to update
+# the .gitmodules.dep
+##
+
+cmake_minimum_required(VERSION 2.8)
+
+##
+# Submodule specific includes will go here,
+# These are specified here, since these variables are accessed
+# from multiple sub directories. If the variable is subdirectory
+# specific it must be declared in the subdirectory.
+##
+
+rift_externalproject_add(
+  config
+  DEPENDS skyquake
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
+  CONFIGURE_COMMAND echo
+  BUILD_COMMAND
+    ${CMAKE_CURRENT_BINARY_DIR}/config/config-build/scripts/build.sh
+  INSTALL_COMMAND
+    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/install.sh
+    ${CMAKE_CURRENT_BINARY_DIR}/config/config-build
+    ${CMAKE_INSTALL_PREFIX}/usr/share/rw.ui/skyquake
+    ${RIFT_SUBMODULE_INSTALL_PREFIX}/skyquake/${CMAKE_INSTALL_PREFIX}/usr/share/rw.ui/skyquake
+
+  BCACHE_COMMAND echo
+)
+
diff --git a/skyquake/plugins/user-management/api/ro.js b/skyquake/plugins/user-management/api/ro.js
new file mode 100644 (file)
index 0000000..99fb1d7
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+var request = require('request');
+var Promise = require('bluebird');
+var rp = require('request-promise');
+var utils = require('../../../framework/core/api_utils/utils.js');
+var constants = require('../../../framework/core/api_utils/constants.js');
+var _ = require('underscore');
+var RO = {}
+RO.get = function(req) {
+return new Promise(function(resolve, reject) {
+        var self = this;
+        var api_server = req.query["api_server"];
+        var requestHeaders = {};
+        var url = utils.confdPort(api_server) + '/api/running/resource-orchestrator';
+        _.extend(
+            requestHeaders,
+            id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
+                'Authorization': req.get('Authorization')
+            }
+        );
+
+        request({
+                url: url + '?deep',
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+            },
+            function(error, response, body) {
+                var data;
+                if (utils.validateResponse('RO.get', error, response, body, resolve, reject)) {
+                    try {
+                        data = JSON.parse(response.body);
+                        if (!id) {
+                            data = data.collection;
+                        }
+
+                        data = data[objKey]
+                    } catch (e) {
+                        console.log('Problem with "' + type.toUpperCase() + '.get"', e);
+                        var err = {};
+                        err.statusCode = 500;
+                        err.errorMessage = {
+                            error: 'Problem with "' + type.toUpperCase() + '.get": ' + e
+                        }
+                        return reject(err);
+                    }
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: data
+                    });
+                };
+            });
+    });
+}
+
+RO.update = updateAccount;
+
+function updateAccount(req) {
+    var self = this;
+    var api_server = req.query["api_server"];
+    var data = req.body;
+    var requestHeaders = {};
+    var url = utils.confdPort(api_server) + '/api/config/resource-orchestrator';
+    var method = 'PUT'
+
+    return new Promise(function(resolve, reject) {
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.data,
+            constants.HTTP_HEADERS.content_type.data, {
+                'Authorization': req.get('Authorization')
+            });
+        request({
+            url: url,
+            method: method,
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+            json: data,
+        }, function(error, response, body) {
+            if (utils.validateResponse('RO.update', error, response, body, resolve, reject)) {
+                return resolve({
+                    statusCode: response.statusCode,
+                    data: JSON.stringify(response.body)
+                });
+            };
+        });
+    })
+}
+
+module.exports = RO;
diff --git a/skyquake/plugins/user-management/config.json b/skyquake/plugins/user-management/config.json
new file mode 100644 (file)
index 0000000..930b9cf
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "root": "public",
+    "name": "User Management",
+    "dashboard": "./dashboard/dashboard.jsx",
+    "order": 1,
+    "priority":1,
+    "routes": [
+    {
+        "label": "User Management Dashboard",
+        "route": "user-management",
+        "component": "./dashboard/dashboard.jsx",
+        "type": "internal"
+    }]
+}
diff --git a/skyquake/plugins/user-management/package.json b/skyquake/plugins/user-management/package.json
new file mode 100644 (file)
index 0000000..1d57bf3
--- /dev/null
@@ -0,0 +1,54 @@
+{
+  "name": "config",
+  "version": "1.0.0",
+  "description": "",
+  "main": "routes.js",
+  "scripts": {
+    "start": "rm -rf public/ && mkdir public && cd public && mkdir build && cp ../src/index.html ./ && node ../server.js"
+  },
+  "author": "RIFT.io",
+  "license": "Apache-2.0",
+  "dependencies": {
+    "alt": "^0.18.3",
+    "bluebird": "^3.4.1",
+    "express": "^4.13.3",
+    "history": "^1.17.0",
+    "jquery": "^2.2.1",
+    "json-loader": "^0.5.4",
+    "lodash": "^4.10.0",
+    "normalizr": "^2.1.0",
+    "open-iconic": "^1.1.1",
+    "prismjs": "^1.4.1",
+    "react": "^0.14.8",
+    "react-breadcrumbs": "^1.3.9",
+    "react-crouton": "^0.2.7",
+    "react-dom": "^0.14.6",
+    "react-router": "^2.0.1",
+    "react-slick": "^0.11.1",
+    "react-tabs": "^0.5.3",
+    "react-treeview": "^0.4.2",
+    "request-promise": "^3.0.0",
+    "underscore": "^1.8.3"
+  },
+  "devDependencies": {
+    "babel-core": "^6.4.5",
+    "babel-loader": "^6.2.1",
+    "babel-polyfill": "^6.9.1",
+    "babel-preset-es2015": "^6.6.0",
+    "babel-preset-react": "^6.5.0",
+    "babel-preset-stage-0": "^6.3.13",
+    "babel-runtime": "^6.3.19",
+    "cors": "^2.7.1",
+    "css-loader": "^0.23.1",
+    "file-loader": "^0.8.5",
+    "html-webpack-plugin": "^2.9.0",
+    "http-proxy": "^1.12.0",
+    "loaders.css": "^0.1.2",
+    "node-sass": "^3.4.2",
+    "react-addons-css-transition-group": "^0.14.7",
+    "sass-loader": "^3.1.2",
+    "style-loader": "^0.13.0",
+    "webpack": "^1.3.0",
+    "webpack-dev-server": "^1.16.3"
+  }
+}
diff --git a/skyquake/plugins/user-management/routes.js b/skyquake/plugins/user-management/routes.js
new file mode 100644 (file)
index 0000000..bf5915d
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+var app = require('express').Router();
+var cors = require('cors');
+var utils = require('../../framework/core/api_utils/utils.js')
+var ro = require('./api/ro.js')
+ // Begin Accounts API
+     app.get('/resource-orchestrator', cors(), function(req, res) {
+        ro.get(req).then(function(data) {
+            utils.sendSuccessResponse(data, res);
+        }, function(error) {
+            utils.sendErrorResponse(error, res);
+        });
+    });
+    app.put('/resource-orchestrator', cors(), function(req, res) {
+        ro.update(req).then(function(data) {
+            utils.sendSuccessResponse(data, res);
+        }, function(error) {
+            utils.sendErrorResponse(error, res);
+        });
+    })
+
+    utils.passThroughConstructor(app);
+
+module.exports = app;
diff --git a/skyquake/plugins/user-management/scripts/build.sh b/skyquake/plugins/user-management/scripts/build.sh
new file mode 100755 (executable)
index 0000000..04c5221
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+PLUGIN_NAME=user-management
+# change to the directory of this script
+THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+cd $THIS_DIR
+cd ..
+
+echo 'Building plugin '$PLUGIN_NAME
+echo 'Fetching third-party node_modules for '$PLUGIN_NAME
+npm install
+echo 'Fetching third-party node_modules for '$PLUGIN_NAME'...done'
+echo 'Packaging '$PLUGIN_NAME' using webpack'
+ui_plugin_cmake_build=true ./node_modules/.bin/webpack --progress --config webpack.production.config.js
+echo 'Packaging '$PLUGIN_NAME' using webpack... done'
+echo 'Building plugin '$PLUGIN_NAME'... done'
diff --git a/skyquake/plugins/user-management/scripts/install.sh b/skyquake/plugins/user-management/scripts/install.sh
new file mode 100755 (executable)
index 0000000..b883243
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+plugin=user-management
+source_dir=$1
+dest_dir=$2
+bcache_dir=$3
+
+# Create destination and build cache directories
+mkdir -p $dest_dir
+mkdir -p $bcache_dir
+
+# Create necessary directories under dest and cache dirs
+mkdir -p $dest_dir/framework
+mkdir -p $dest_dir/plugins
+mkdir -p $bcache_dir/framework
+mkdir -p $bcache_dir/plugins
+
+# Copy over built plugin's public folder, config.json, routes.js and api folder as per installed_plugins.txt
+mkdir -p $dest_dir/plugins/$plugin
+cp -Lrf $source_dir/public $dest_dir/plugins/$plugin/.
+cp -Lrf $source_dir/config.json $dest_dir/plugins/$plugin/.
+cp -Lrf $source_dir/routes.js $dest_dir/plugins/$plugin/.
+cp -Lrp $source_dir/api $dest_dir/plugins/$plugin/.
+tar -cf $dest_dir/plugins/$plugin/node_modules.tar node_modules package.json -C $source_dir
+#cp -Lrp $source_dir/node_modules $dest_dir/plugins/$plugin/.
+mkdir -p $bcache_dir/plugins/$plugin
+cp -Lrf $source_dir/public $bcache_dir/plugins/$plugin/.
+cp -Lrf $source_dir/config.json $bcache_dir/plugins/$plugin/.
+cp -Lrf $source_dir/routes.js $bcache_dir/plugins/$plugin/.
+cp -Lrp $source_dir/api $bcache_dir/plugins/$plugin/.
+tar -cf $bcache_dir/plugins/$plugin/node_modules.tar $source_dir/node_modules $source_dir/package.json
+#cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
diff --git a/skyquake/plugins/user-management/server.js b/skyquake/plugins/user-management/server.js
new file mode 100644 (file)
index 0000000..eb140b7
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+var express = require('express');
+var path = require('path');
+var httpProxy = require('http-proxy');
+var bodyParser = require('body-parser');
+var cors = require('cors');
+var session = require('express-session');
+var proxy = httpProxy.createProxyServer();
+var app = express();
+
+var isProduction = process.env.NODE_ENV === 'production';
+var port = isProduction ? 8080 : 8888;
+var publicPath = path.resolve(__dirname, 'public');
+
+if (!isProduction) {
+
+  //Routes for local development
+  var lpRoutes = require('./routes.js');
+
+  app.use(express.static(publicPath));
+  app.use(session({
+    secret: 'ritio rocks',
+  }));
+  app.use(bodyParser.urlencoded({
+      extended: true
+  }));
+  app.use(bodyParser.json());
+  app.use(cors());
+  app.use('/', lpRoutes);
+  var bundle = require('./server/bundle.js');
+  bundle();
+
+  app.all('/build/*', function (req, res) {
+    proxy.web(req, res, {
+        target: 'http://localhost:8080'
+    });
+  });
+
+}
+proxy.on('error', function(e) {
+  console.log('Could not connect to proxy, please try again...');
+});
+
+app.listen(port, function () {
+  console.log('Server running on port ' + port);
+});
+
+app.get('/*')
diff --git a/skyquake/plugins/user-management/src/dashboard/dashboard.jsx b/skyquake/plugins/user-management/src/dashboard/dashboard.jsx
new file mode 100644 (file)
index 0000000..b5924ae
--- /dev/null
@@ -0,0 +1,337 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import UserManagementStore from './userMgmtStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import 'style/layout.scss';
+import './userMgmt.scss';
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+
+
+import TextInput from 'widgets/form_controls/textInput.jsx';
+import Input from 'widgets/form_controls/input.jsx';
+import Button, {ButtonGroup} from 'widgets/button/sq-button.jsx';
+import SelectOption from 'widgets/form_controls/selectOption.jsx';
+import 'widgets/form_controls/formControls.scss';
+import imgAdd from '../../node_modules/open-iconic/svg/plus.svg'
+import imgRemove from '../../node_modules/open-iconic/svg/trash.svg'
+
+class UserManagementDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('UserManagementStore') ? this.props.flux.stores.UserManagementStore : this.props.flux.createStore(UserManagementStore);
+        this.Store.getUsers();
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+    }
+    componentDidUpdate() {
+
+      ReactDOM.findDOMNode(this.UserList).addEventListener('transitionend', this.onTransitionEnd, false);
+    }
+    componentWillMount() {
+        this.Store.listen(this.updateState);
+    }
+    componentWillUnmount() {
+        this.Store.unlisten(this.updateState);
+    }
+    updateState = (state) => {
+        this.setState(state);
+    }
+    updateInput = (key, e) => {
+        let property = key;
+        this.actions.handleUpdateInput({
+            [property]:e.target.value
+        })
+    }
+    disabledChange = (e) => {
+        this.actions.handleDisabledChange(e.target.checked);
+    }
+    platformChange = (platformRole, e) => {
+        this.actions.handlePlatformRoleUpdate(platformRole, e.currentTarget.checked);
+    }
+    addProjectRole = (e) => {
+        this.actions.handleAddProjectItem();
+    }
+    removeProjectRole = (i, e) => {
+        this.actions.handleRemoveProjectItem(i);
+    }
+    updateProjectRole = (i, e) => {
+        this.actions.handleUpdateProjectRole(i, e)
+    }
+    addUser = () => {
+        this.actions.handleAddUser();
+    }
+    viewUser = (un, index) => {
+        console.log(un)
+        this.actions.viewUser(un, index);
+    }
+    closePanel = () => {
+        this.actions.handleCloseUserPanel();
+    }
+    updateUser = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+
+        this.Store.updateUser();
+    }
+    deleteUser = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        this.Store.deleteUser({
+                'user-name': this.state['user-name'],
+                'user-domain': this.state['user-domain']
+            });
+    }
+    createUser = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if(this.state['new-password'] != this.state['confirm-password']) {
+            this.props.actions.showNotification('Passwords do not match')
+        } else {
+            this.Store.createUser({
+                'user-name': this.state['user-name'],
+                'user-domain': this.state['user-domain'],
+                'password': this.state['new-password']
+                // 'confirm-password': this.state['confirm-password']
+            });
+        }
+    }
+
+    onTransitionEnd = (e) => {
+        this.actions.handleHideColumns(e);
+        console.log('transition end')
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        html = (
+            <PanelWrapper className={`row userManagement ${!this.state.userOpen ? 'userList-open' : ''}`} style={{'alignContent': 'center', 'flexDirection': 'row'}} >
+                <PanelWrapper ref={(div) => { this.UserList = div}} className={`column userList expanded ${this.state.userOpen ? 'collapsed ' : ' '} ${this.state.hideColumns ? 'hideColumns ' : ' '}`}>
+                    <Panel title="User List" style={{marginBottom: 0}} no-corners>
+                        <div className="tableRow tableRow--header">
+                            <div className="userName">
+                                Username
+                            </div>
+                            <div>
+                                Domain
+                            </div>
+                        </div>
+                        {state.users.map((u, k) => {
+                            let platformRoles = [];
+                            for(let role in u.platformRoles) {
+                                platformRoles.push(<div>{`${role}: ${u.platformRoles[role]}`}</div>)
+                            }
+                            return (
+                                <div className={`tableRow tableRow--data ${((self.state.activeIndex == k) && !self.state.userOpen) ? 'tableRow--data-active' : ''}`} key={k}>
+                                    <div
+                                        className={`userName userName-header ${((self.state.activeIndex == k) && self.state.userOpen) ? 'activeUser' : ''}`}
+                                        onClick={self.viewUser.bind(null, u, k)}>
+                                        {u['user-name']}
+                                    </div>
+                                    <div>
+                                        {u['user-domain']}
+                                    </div>
+
+
+                                </div>
+                            )
+                        })}
+                    </Panel>
+                    <ButtonGroup  className="buttonGroup" style={{margin: '0 0.5rem 0.5rem', background: '#ddd', paddingBottom: '0.5rem'}}>
+                        <Button label="Add User" onClick={this.addUser} />
+                    </ButtonGroup>
+                </PanelWrapper>
+                <PanelWrapper
+                    className={`userAdmin column`}>
+                    <Panel
+                        title={state.isEdit ? state['user-name'] : 'Create User'}
+                        style={{marginBottom: 0}}
+                        hasCloseButton={this.closePanel}
+                        no-corners>
+                        <FormSection title="USER INFO">
+                        {
+                            this.state.isEdit ?
+                                null
+                                : <Input label="Username" value={state['user-name']} onChange={this.updateInput.bind(null, 'user-name')} />
+                        }
+                            <Input label="Domain" value={state['user-domain']}  onChange={this.updateInput.bind(null, 'user-domain')}></Input>
+                            <Input label="Disabled" onChange={this.disabledChange} checked={state.disabled} type="checkbox" />
+                        </FormSection>
+                        <FormSection title="PLATFORM ROLES">
+                            <Input label="Super Admin" onChange={this.platformChange.bind(null, 'super_admin')} checked={state.platformRoles.super_admin} type="checkbox" />
+                            <Input label="Platform Admin" onChange={this.platformChange.bind(null, 'platform_admin')}  checked={state.platformRoles.platform_admin} type="checkbox" />
+                            <Input label="Platform Oper" onChange={this.platformChange.bind(null, 'platform_oper')}  checked={state.platformRoles.platform_oper} type="checkbox" />
+                        </FormSection>
+                        <FormSection title="PROJECT ROLES">
+                            <InputCollection
+                                inital={true}
+                                type='select'
+                                options={state.projectRolesOptions}
+                                collection={state.projectRoles}
+                                onChange={this.updateProjectRole}
+                                AddItemFn={this.addProjectRole}
+                                RemoveItemFn={this.removeProjectRole}
+                                />
+                        </FormSection>
+                        { this.state.isEdit ?
+                            (
+                                <FormSection title="PASSWORD CHANGE">
+                                    <Input label="OLD PASSWORD" type="password" value={state['old-password']} onChange={this.updateInput.bind(null, 'old-password')} />
+                                    <Input label="NEW PASSWORD" type="password" value={state['new-password']}  onChange={this.updateInput.bind(null, 'new-password')}/>
+                                    <Input label="REPEAT NEW PASSWORD" type="password"  value={state['confirm-password']}  onChange={this.updateInput.bind(null, 'confirm-password')}/>
+                                </FormSection>
+                            ) :
+                            (
+                                <FormSection title="CREATE PASSWORD">
+                                    <Input label="CREATE PASSWORD" type="password" value={state.newPassword}  onChange={this.updateInput.bind(null, 'new-password')}/>
+                                    <Input label="REPEAT PASSWORD" type="password"  value={state.repeatNewPassword}  onChange={this.updateInput.bind(null, 'confirm-password')}/>
+                                </FormSection>
+                            )
+                        }
+
+                    </Panel>
+
+                        {
+                            state.isEdit ?
+                            (
+                                <ButtonGroup className="buttonGroup">
+                                    <Button label="Update" onClick={this.updateUser} />
+                                    <Button label="Delete" onClick={this.deleteUser} />
+                                </ButtonGroup>
+                            )
+                            : (
+                                <ButtonGroup className="buttonGroup">
+                                    <Button label="Create" onClick={this.createUser}  />
+                                </ButtonGroup>
+                            )
+                        }
+
+                </PanelWrapper>
+
+            </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+UserManagementDashboard.contextTypes = {
+    router: React.PropTypes.object
+};
+
+UserManagementDashboard.defaultProps = {
+    userList: [],
+    selectedUser: {}
+}
+
+export default SkyquakeComponent(UserManagementDashboard);
+
+
+
+
+/**
+ * AddItemFn:
+ */
+class InputCollection extends React.Component {
+    constructor(props) {
+        super(props);
+        this.collection = props.collection;
+    }
+    buildTextInput(onChange, v, i) {
+        return (
+            <Input
+                style={{flex: '1 1'}}
+                key={i}
+                value={v}
+                onChange= {onChange.bind(null, i)}
+            />
+        )
+    }
+    buildSelectOption(initial, options, onChange, v, i) {
+        return (
+            <SelectOption
+                key={`${i}-${v.replace(' ', '_')}`}
+                intial={initial}
+                defaultValue={v}
+                options={options}
+                onChange={onChange.bind(null, i)}
+            />
+        );
+    }
+    showInput() {
+
+    }
+    render() {
+        const props = this.props;
+        let inputType;
+        let className = "InputCollection";
+        if (props.className) {
+            className = `${className} ${props.className}`;
+        }
+        if (props.type == 'select') {
+            inputType = this.buildSelectOption.bind(this, props.initial, props.options, props.onChange);
+        } else {
+            inputType = this.buildTextInput.bind(this, props.onChange)
+        }
+        let html = (
+            <div className="InputCollection-wrapper">
+                {props.collection.map((v,i) => {
+                    return (
+                        <div key={i} className={className} >
+                            {inputType(v, i)}
+                            <span onClick={props.RemoveItemFn.bind(null, i)} className="removeInput"><img src={imgRemove} />Remove</span>
+                        </div>
+                    )
+                })}
+                <span onClick={props.AddItemFn} className="addInput"><img src={imgAdd} />Add</span>
+            </div>
+        );
+        return html;
+    }
+}
+
+InputCollection.defaultProps = {
+    input: Input,
+    collection: [],
+    onChange: function(i, e) {
+        console.log(`
+                        Updating with: ${e.target.value}
+                        At index of: ${i}
+                    `)
+    },
+    AddItemFn: function(e) {
+        console.log(`Adding a new item to collection`)
+    },
+    RemoveItemFn: function(i, e) {
+        console.log(`Removing item from collection at index of: ${i}`)
+    }
+}
+
+class FormSection extends React.Component {
+    render() {
+        let className = 'FormSection ' + this.props.className;
+        let html = (
+            <div
+                style={this.props.style}
+                className={className}
+            >
+                <div className="FormSection-title">
+                    {this.props.title}
+                </div>
+                <div className="FormSection-body">
+                    {this.props.children}
+                </div>
+            </div>
+        );
+        return html;
+    }
+}
+
+FormSection.defaultProps = {
+    className: ''
+}
diff --git a/skyquake/plugins/user-management/src/dashboard/userMgmt.scss b/skyquake/plugins/user-management/src/dashboard/userMgmt.scss
new file mode 100644 (file)
index 0000000..1271729
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+/* If there's time this really needs to be rewritten. Ideally with smooth animations.*/
+@import "style/_colors.scss";
+
+.userManagement {
+    .skyquakePanel-wrapper {
+        overflow-x: hidden;
+    }
+    .userList {
+
+        -ms-flex: 0 1 200px;
+        flex: 0 1 200px;
+
+        .activeUser {
+            font-weight:bold;
+        }
+
+        /* transition: all 2s;*/
+        &.expanded {
+            -ms-flex: 1 1 100%;
+            flex: 1 1 100%;
+            /* transition: all 300ms;*/
+            .tableRow>div:not(.userName) {
+                opacity: 1;
+                /* width:auto;*/
+                /* transition: width 600ms;*/
+                /* transition: opacity 300ms;*/
+            }
+            &.collapsed {
+                -ms-flex: 0 1 200px;
+                flex: 0 1 200px;
+                /* transition: all 2s;*/
+                .tableRow>div:not(.userName) {
+                    /* opacity: 0;*/
+                    /* width:0px;*/
+                    display:none;
+                    overflow:hidden;
+                    /* transition: all 600ms;*/
+                }
+            }
+        }
+        &.hideColumns {
+            overflow:hidden;
+            >div {
+                overflow:hidden;
+            }
+            .tableRow>div:not(.userName) {
+                width: 0px;
+                /* transition: all 600ms;*/
+            }
+            .userName {
+                &--header {
+                    /* display:none;*/
+             }
+            }
+        }
+        .userName {
+            cursor:pointer;
+        }
+
+    }
+
+    .userAdmin {
+            -ms-flex: 0 1 450px;
+            flex: 0 1 450px;
+            width:auto;
+            opacity:1;
+    }
+    &.userList-open {
+        .userAdmin {
+            -ms-flex: 0 1 0px;
+                flex: 0 1 0px;
+            opacity:0;
+            /* width: 0px;*/
+            display:none;
+            /* transition: opacity 300ms;*/
+            /* transition: width 600ms;*/
+
+        }
+    }
+    .buttonGroup {
+        margin: 0 0.5rem 0.5rem;
+        background: #ddd;
+        padding-bottom: 0.5rem;
+        padding: 0.5rem 0;
+        border-top: #d3d3d3 1px solid;
+    }
+}
+
+
+
+.FormSection {
+    &-title {
+        color: #000;
+        background: lightgray;
+        padding: 0.5rem;
+        border-top: 1px solid #f1f1f1;
+        border-bottom: 1px solid #f1f1f1;
+    }
+    &-body {
+        padding: 0.5rem 0.75rem;
+    }
+    label {
+        -ms-flex: 1 0;
+            flex: 1 0;
+    }
+    /* label {*/
+    /*     display: -ms-flexbox;*/
+    /*     display: flex;*/
+    /*     -ms-flex-direction: column;*/
+    /*     flex-direction: column;*/
+    /*     width: 100%;*/
+    /*     margin: 0.5rem 0;*/
+    /*     -ms-flex-align: start;*/
+    /*     align-items: flex-start;*/
+    /*     -ms-flex-pack: start;*/
+    /*     justify-content: flex-start;*/
+    /* }*/
+    select {
+        font-size: 1rem;
+        min-width: 75%;
+        height: 35px;
+    }
+}
+
+
+.InputCollection {
+    display:-ms-flexbox;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+    -ms-flex-align: center;
+        align-items: center;
+    button {
+        padding: 0.25rem;
+        height: 1.5rem;
+        font-size: 0.75rem;
+    }
+    select {
+        min-width: 100%;
+    }
+    margin-bottom:0.5rem;
+    &-wrapper {
+
+    }
+}
+.tableRow {
+    display:-ms-flexbox;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+    >div {
+        padding:0.25rem 1rem 0.25rem 0;
+        -ms-flex: 1 1 33%;
+            flex: 1 1 33%;
+    }
+    &--header {
+        font-weight:bold;
+    }
+    &--data {
+        &:hover, .activeUser{
+            cursor:pointer;
+            background:$neutral-dark-3;
+            color:white;
+        }
+    }
+}
+
+.addInput, .removeInput {
+    display:-ms-flexbox;
+    display:flex;
+    -ms-flex-align:center;
+    align-items:center;
+    margin-left: 1rem;
+
+    font-size:0.75rem;
+    text-transform:uppercase;
+    font-weight:bold;
+
+    cursor:pointer;
+    img {
+        height:0.75rem;
+        margin-right:0.5rem;
+        width:auto;
+    }
+    span {
+        color: #5b5b5b;
+        text-transform: uppercase;
+    }
+}
diff --git a/skyquake/plugins/user-management/src/dashboard/userMgmtActions.js b/skyquake/plugins/user-management/src/dashboard/userMgmtActions.js
new file mode 100644 (file)
index 0000000..8430f35
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+module.exports = function(Alt) {
+   return Alt.generateActions(
+                                       'handleUpdateInput',
+                                       'handleAddProjectItem',
+                                       'handleRemoveProjectItem',
+                                       'handleUpdateProjectRole',
+                                       'viewUser',
+                                       'handleCloseUserPanel',
+                                       'handleHideColumns',
+                                       'getUsersSuccess',
+                                       'getUsersNotification',
+                                       'handleDisabledChange',
+                                       'handlePlatformRoleUpdate',
+                                       'handleAddUser',
+                                       'handleCreateUser',
+                                       'handleUpdateUser',
+                                       'updateUserSuccess',
+                                       'createUserSuccess',
+                                       'deleteUserSuccess'
+                                       );
+}
diff --git a/skyquake/plugins/user-management/src/dashboard/userMgmtSource.js b/skyquake/plugins/user-management/src/dashboard/userMgmtSource.js
new file mode 100644 (file)
index 0000000..18cb83d
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import $ from 'jquery';
+var Utils = require('utils/utils.js');
+let API_SERVER = require('utils/rw.js').getSearchParams(window.location).api_server;
+let HOST = API_SERVER;
+let NODE_PORT = require('utils/rw.js').getSearchParams(window.location).api_port || ((window.location.protocol == 'https:') ? 8443 : 8000);
+let DEV_MODE = require('utils/rw.js').getSearchParams(window.location).dev_mode || false;
+
+if (DEV_MODE) {
+    HOST = window.location.protocol + '//' + window.location.hostname;
+}
+
+
+let Users = mockUsers();
+
+
+module.exports = function(Alt) {
+    return {
+        getUsers: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                // setTimeout(function() {
+                //   resolve(Users);
+                // }, 1000)
+                $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.users);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                });
+              });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error retrieving the resource orchestrator information.'
+          }),
+          success: Alt.actions.global.getUsersSuccess,
+                    loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.showNotification
+        },
+        updateUser: {
+          remote: function(state, user) {
+            return new Promise(function(resolve, reject) {
+              setTimeout(function() {
+                  resolve(true);
+              }, 1000)
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the user.'
+          }),
+          success: Alt.actions.global.updateUserSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.showNotification
+        },
+        deleteUser: {
+          remote: function(state, user) {
+            return new Promise(function(resolve, reject) {
+                         // setTimeout(function() {
+              //     resolve(true);
+              // }, 1000)
+              $.ajax({
+                url: `/user/${user['user-name']}/${user['user-domain']}?api_server=${API_SERVER}`,
+                type: 'DELETE',
+                data: user,
+                beforeSend: Utils.addAuthorizationStub,
+                success: function(data, textStatus, jqXHR) {
+                  resolve(data);
+                }
+              }).fail(function(xhr){
+                //Authentication and the handling of fail states should be wrapped up into a connection class.
+                Utils.checkAuthentication(xhr.status);
+              });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error deleting the user.'
+          }),
+          success: Alt.actions.global.deleteUserSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.showNotification
+        },
+        createUser: {
+            remote: function(state, user) {
+
+              return new Promise(function(resolve, reject) {
+                // setTimeout(function() {
+                //     resolve(true);
+                // }, 1000)
+                $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'POST',
+                  data: user,
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                });
+              });
+            },
+            interceptResponse: interceptResponse({
+              'error': 'There was an error updating the account.'
+            }),
+            success: Alt.actions.global.createUserSuccess,
+            loading: Alt.actions.global.showScreenLoader,
+            error: Alt.actions.global.showNotification
+        }
+      }
+}
+
+function interceptResponse (responses) {
+  return function(data, action, args) {
+    if(responses.hasOwnProperty(data)) {
+      return {
+        type: data,
+        msg: responses[data]
+      }
+    } else {
+      return data;
+    }
+  }
+}
+
+function mockUsers() {
+  let data = [];
+  let count = 10;
+  for(let i = 0; i < 10; i++) {
+    data.push({
+      username: `Tester ${i}`,
+      domain: 'Some Domain',
+      platformRoles: {
+        super_admin: true,
+        platform_admin: false,
+        platform_oper: false
+      },
+      disabled: false,
+      projectRoles: [
+        'Project:Role'
+      ]
+    })
+  }
+  return data;
+}
diff --git a/skyquake/plugins/user-management/src/dashboard/userMgmtStore.js b/skyquake/plugins/user-management/src/dashboard/userMgmtStore.js
new file mode 100644 (file)
index 0000000..2293aca
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import UserManagementActions from './userMgmtActions.js';
+import UserManagementSource from './userMgmtSource.js';
+import _ from 'lodash';
+export default class UserManagementStore {
+    constructor() {
+        this.actions = UserManagementActions(this.alt);
+        this.bindActions(this.actions);
+        this.registerAsync(UserManagementSource);
+        this.users = [];
+        this['user-name'] = '';
+        this['user-domain'] = '';
+        this.disabled = false;
+        this.platformRoles = {
+            super_admin: false,
+            platform_admin: false,
+            platform_oper: false
+        };
+        this.projectRoles = ['Project:Role'];
+        this.projectRolesOptions = ['Choose your adventure', 'Project:Role', 'Project:Another Role'];
+        this.currentPassword = '';
+        this['old-password'] = '';
+        this['new-password'] = '';
+        this['confirm-password'] = '';
+
+        this.activeIndex = null;
+        this.userOpen = false;
+        this.hideColumns = false;
+        this.isEdit = false;
+        // this.exportPublicMethods({})
+    }
+    /**
+     * [handleFieldUpdate description]
+     * @param  {Object} data {
+     *                       [store_property] : [value]
+     * }
+     * @return {[type]}      [description]
+     */
+    handleUpdateInput(data) {
+        this.setState(data);
+    }
+    handleAddProjectItem(item) {
+        let projectRoles = this.projectRoles;
+        projectRoles.push('');
+        this.setState({projectRoles});
+    }
+    handleRemoveProjectItem(i) {
+        let projectRoles = this.projectRoles;
+        projectRoles.splice(i, 1);
+        console.log('Removing', projectRoles)
+        this.setState({projectRoles});
+    }
+    handleUpdateProjectRole(data) {
+        let i = data[0];
+        let e = data[1];
+        let projectRoles = this.projectRoles
+        projectRoles[i] = JSON.parse(e.currentTarget.value);
+        this.setState({
+            projectRoles
+        });
+    }
+    viewUser(data) {
+        let user = data[0];
+        let userIndex = data[1];
+
+        let ActiveUser = {
+            'user-name': user['user-name'],
+            'user-domain': user['user-domain'],
+            platformRoles: user.platformRoles || this.platformRoles,
+            disabled: user.disabled || this.disabled,
+            projectRoles: user.projectRoles || this.projectRoles
+        }
+        let state = _.merge({
+            activeIndex: userIndex,
+            userOpen: true,
+            isEdit: true
+        }, ActiveUser);
+        this.setState(state)
+    }
+    handleCloseUserPanel() {
+        this.setState({
+            userOpen: false,
+            isEdit: false
+        })
+    }
+    handleHideColumns(e) {
+        if(this.userOpen && e.currentTarget.classList.contains('hideColumns')) {
+            this.setState({
+                hideColumns: true
+            })
+        } else {
+            this.setState({
+                hideColumns: false
+            })
+        }
+    }
+    handleDisabledChange(isDisabled){
+        this.setState({
+            disabled: isDisabled
+        })
+    }
+    handlePlatformRoleUpdate(data){
+        let platform_role = data[0];
+        let checked = data[1];
+        let platformRoles = this.platformRoles;
+        platformRoles[platform_role] = checked;
+        this.setState({
+            platformRoles
+        })
+    }
+    resetUser() {
+        let username = '';
+        let domain = '';
+        let disabled = false;
+        let platformRoles = {
+            super_admin: false,
+            platform_admin: false,
+            platform_oper: false
+        };
+        let projectRoles = [];
+        let currentPassword = '';
+        let oldPassword = '';
+        let newPassword = '';
+        let confirmPassword = '';
+        return {
+            'user-name' : username,
+            'user-domain' : domain,
+            disabled,
+            platformRoles,
+            projectRoles,
+            currentPassword,
+            'old-password': oldPassword,
+            'new-password': newPassword,
+            'confirm-password': confirmPassword
+        }
+    }
+    resetPassword() {
+        let currentPassword = '';
+        let oldPassword = '';
+        let newPassword = '';
+        let confirmPassword = '';
+        return {
+            currentPassword,
+            'old-password': oldPassword,
+            'new-password': newPassword,
+            'confirm-password': confirmPassword
+        }
+    }
+    handleAddUser() {
+        this.setState(_.merge( this.resetUser() ,{ isEdit: false, userOpen: true }))
+    }
+    handleCreateUser() {
+
+    }
+    handleUpdateUser() {
+
+    }
+
+    getUsersSuccess(users) {
+        this.alt.actions.global.hideScreenLoader.defer();
+        this.setState({users});
+    }
+    updateUserSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let users = this.users;
+        users[this.activeIndex] = {
+            'user-name': this['user-name'],
+            'user-domain': this['user-domain'],
+            platformRoles: this.platformRoles,
+            disabled: this.disabled,
+            projectRoles: this.projectRoles
+        }
+        this.setState({users})
+    }
+    deleteUserSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let users = this.users;
+        users.splice(this.activeIndex, 1);
+        this.setState({users, userOpen: false})
+    }
+    createUserSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let users = this.users;
+        users.push({
+            'user-name': this['user-name'],
+            'user-domain': this['user-domain'],
+            platformRoles: this.platformRoles,
+            disabled: this.disabled,
+            projectRoles: this.projectRoles
+         });
+        let newState = {users, isEdit: true};
+        _.merge(newState, this.resetPassword())
+        this.setState(newState);
+    }
+}
diff --git a/skyquake/plugins/user-management/src/main.js b/skyquake/plugins/user-management/src/main.js
new file mode 100644 (file)
index 0000000..5dc626f
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import "babel-polyfill";
+import { render } from 'react-dom';
+import SkyquakeRouter from 'widgets/skyquake_container/skyquakeRouter.jsx';
+const config = require('json!../config.json');
+
+let context = require.context('./', true, /^\.\/.*\.jsx$/);
+let router = SkyquakeRouter(config, context);
+let element = document.querySelector('#app');
+
+render(router, element);
+
+
diff --git a/skyquake/plugins/user-management/webpack.production.config.js b/skyquake/plugins/user-management/webpack.production.config.js
new file mode 100644 (file)
index 0000000..49ad631
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+var Webpack = require('webpack');
+var path = require('path');
+var nodeModulesPath = path.resolve(__dirname, 'node_modules');
+var buildPath = path.resolve(__dirname, 'public', 'build');
+var mainPath = path.resolve(__dirname, 'src', 'main.js');
+var uiPluginCmakeBuild = process.env.ui_plugin_cmake_build || false;
+var frameworkPath = uiPluginCmakeBuild?'../../../../skyquake/skyquake-build/framework':'../../framework';
+var HtmlWebpackPlugin = require('html-webpack-plugin');
+var CommonsPlugin = new require("webpack/lib/optimize/CommonsChunkPlugin")
+// Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
+process.env.UV_THREADPOOL_SIZE=64;
+var config = {
+    devtool: 'source-map',
+    entry: mainPath,
+    output: {
+        path: buildPath,
+        filename: 'bundle.js',
+        publicPath: "build/"
+    },
+    resolve: {
+        extensions: ['', '.js', '.jsx', '.css', '.scss'],
+        root: path.resolve(frameworkPath),
+        alias: {
+            'widgets': path.resolve(frameworkPath) + '/widgets',
+            'style':  path.resolve(frameworkPath) + '/style',
+            'utils':  path.resolve(frameworkPath) + '/utils'
+        }
+    },
+    module: {
+        loaders: [{
+                test: /\.(jpe?g|png|gif|svg|ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/i,
+                loader: "file-loader"
+            },
+            {
+                test: /\.(js|jsx)$/,
+                exclude: /react-treeview/,
+                loader: 'babel-loader',
+                query: {
+                    presets: ["es2015", "stage-0", "react"]
+                }
+            }, {
+                test: /\.css$/,
+                loader: 'style!css'
+            }, {
+                test: /\.scss/,
+                loader: 'style!css!sass?includePaths[]='+ path.resolve(frameworkPath)
+            }
+        ]
+    },
+    plugins: [
+        new HtmlWebpackPlugin({
+            filename: '../index.html'
+            , templateContent: '<div id="app"></div>'
+        }),
+        new Webpack.optimize.CommonsChunkPlugin("vendor", "vendor.js", Infinity)
+    ]
+};
+module.exports = config;
index 726757f..41ac03b 100644 (file)
@@ -135,6 +135,7 @@ if (cluster.isMaster && clusteredLaunch) {
        var descriptor_routes = require('./framework/core/modules/routes/descriptorModel');
        var configuration_routes = require('./framework/core/modules/routes/configuration');
        var configurationAPI = require('./framework/core/modules/api/configuration');
+       var userManagement_routes = require('./framework/core/modules/routes/userManagement');
        /**
         * Processing when a plugin is added or modified
         * @param {string} plugin_name - Name of the plugin
@@ -214,6 +215,9 @@ if (cluster.isMaster && clusteredLaunch) {
                //Configure descriptor route(s)
                app.use(descriptor_routes);
 
+               //Configure user management route(s)
+               app.use(userManagement_routes);
+
                // app.get('/testme', function(req, res) {
                //      res.sendFile(__dirname + '/index.html');
                // });