update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try 79/5479/2 v3.0.0rc2
authorJeremy Mordkoff <Jeremy.Mordkoff@riftio.com>
Sun, 1 Oct 2017 01:42:44 +0000 (21:42 -0400)
committerJeremy Mordkoff <Jeremy.Mordkoff@riftio.com>
Mon, 2 Oct 2017 21:43:41 +0000 (17:43 -0400)
Signed-off-by: Jeremy Mordkoff <Jeremy.Mordkoff@riftio.com>
Change-Id: Ib11aa03a2eff5a53c808342508a5d7bee7b202b8

380 files changed:
.gitignore
BUILD.sh [new file with mode: 0755]
CMakeLists.txt
Dockerfile
Jenkinsfile
manifest/LICENSE
postinst [new file with mode: 0755]
python-osmclient/.gitignore [deleted file]
python-osmclient/Makefile [deleted file]
python-osmclient/README.md [deleted file]
python-osmclient/osmclient/__init__.py [deleted file]
python-osmclient/osmclient/common/OsmAPI.py [deleted file]
python-osmclient/osmclient/common/__init__.py [deleted file]
python-osmclient/osmclient/scripts/__init__.py [deleted file]
python-osmclient/osmclient/scripts/osm.py [deleted file]
python-osmclient/setup.py [deleted file]
python-osmclient/stdeb.cfg [deleted file]
skyquake/.storybook/config.js
skyquake/framework/core/api_utils/auth.js [new file with mode: 0644]
skyquake/framework/core/api_utils/constants.js
skyquake/framework/core/api_utils/csrf.js [new file with mode: 0644]
skyquake/framework/core/api_utils/openidconnect_config.json [new file with mode: 0644]
skyquake/framework/core/api_utils/sockets.js
skyquake/framework/core/api_utils/utils.js
skyquake/framework/core/modules/api/appConfigAPI.js [new file with mode: 0644]
skyquake/framework/core/modules/api/configuration.js
skyquake/framework/core/modules/api/descriptorModelMetaAPI.js
skyquake/framework/core/modules/api/modelAPI.js [new file with mode: 0644]
skyquake/framework/core/modules/api/projectManagementAPI.js [new file with mode: 0644]
skyquake/framework/core/modules/api/restconf.js
skyquake/framework/core/modules/api/schemaAPI.js [new file with mode: 0644]
skyquake/framework/core/modules/api/sessions.js [new file with mode: 0644]
skyquake/framework/core/modules/api/userManagementAPI.js [new file with mode: 0644]
skyquake/framework/core/modules/navigation_manager.js
skyquake/framework/core/modules/routes/auth.js [new file with mode: 0644]
skyquake/framework/core/modules/routes/configuration.js
skyquake/framework/core/modules/routes/navigation.js
skyquake/framework/core/modules/routes/projectManagement.js [new file with mode: 0644]
skyquake/framework/core/modules/routes/sessions.js [new file with mode: 0644]
skyquake/framework/core/modules/routes/userManagement.js [new file with mode: 0644]
skyquake/framework/core/views/home.ejs [new file with mode: 0644]
skyquake/framework/core/views/idpconnectfail.ejs [new file with mode: 0644]
skyquake/framework/plugin-index.html [new file with mode: 0644]
skyquake/framework/source/SourceCache.js [new file with mode: 0644]
skyquake/framework/source/model/index.js [new file with mode: 0644]
skyquake/framework/source/model/modelActions.js [new file with mode: 0644]
skyquake/framework/source/model/modelSource.js [new file with mode: 0644]
skyquake/framework/source/schema/index.js [new file with mode: 0644]
skyquake/framework/source/schema/schemaActions.js [new file with mode: 0644]
skyquake/framework/source/schema/schemaSource.js [new file with mode: 0644]
skyquake/framework/style/_colors.scss
skyquake/framework/style/core.css
skyquake/framework/style/img/osm_header.png
skyquake/framework/style/img/osm_header_253x50.png
skyquake/framework/style/img/osm_header_506x100.png
skyquake/framework/utils/appConfiguration.js [new file with mode: 0644]
skyquake/framework/utils/roleConstants.js [new file with mode: 0644]
skyquake/framework/utils/utils.js
skyquake/framework/widgets/button/button.scss
skyquake/framework/widgets/button/sq-button.jsx
skyquake/framework/widgets/components.js
skyquake/framework/widgets/form_controls/formControls.jsx [new file with mode: 0644]
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/header/header.scss
skyquake/framework/widgets/operational-status/launchpadOperationalStatus.jsx
skyquake/framework/widgets/panel/panel.jsx
skyquake/framework/widgets/panel/panel.scss
skyquake/framework/widgets/skyquake_container/eventCenter.jsx
skyquake/framework/widgets/skyquake_container/skyquakeApp.scss
skyquake/framework/widgets/skyquake_container/skyquakeComponent.jsx
skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx
skyquake/framework/widgets/skyquake_container/skyquakeContainerActions.js
skyquake/framework/widgets/skyquake_container/skyquakeContainerSource.js
skyquake/framework/widgets/skyquake_container/skyquakeContainerStore.js
skyquake/framework/widgets/skyquake_container/skyquakeNav.jsx [deleted file]
skyquake/framework/widgets/skyquake_container/skyquakeRouter.jsx
skyquake/framework/widgets/skyquake_nav/skyquakeNav.jsx [new file with mode: 0644]
skyquake/framework/widgets/skyquake_nav/skyquakeNav.scss [new file with mode: 0644]
skyquake/framework/widgets/skyquake_notification/netConfErrors.js [new file with mode: 0644]
skyquake/framework/widgets/skyquake_notification/skyquakeNotification.jsx [new file with mode: 0644]
skyquake/framework/widgets/skyquake_rbac/skyquakeRBAC.jsx [new file with mode: 0644]
skyquake/package.json
skyquake/plugins/CMakeLists.txt
skyquake/plugins/about/api/about.js
skyquake/plugins/about/config.json
skyquake/plugins/about/scripts/build.sh
skyquake/plugins/about/scripts/install.sh
skyquake/plugins/about/src/about.jsx
skyquake/plugins/about/webpack.production.config.js
skyquake/plugins/accounts/api/accounts.js
skyquake/plugins/accounts/api/cloud_account/cloudAccount.js
skyquake/plugins/accounts/api/config_agent/configAgent.js
skyquake/plugins/accounts/api/sdn_account/sdnAccount.js
skyquake/plugins/accounts/config.json
skyquake/plugins/accounts/config_routes.js [new file with mode: 0644]
skyquake/plugins/accounts/images/brocade.png [new file with mode: 0644]
skyquake/plugins/accounts/routes.js
skyquake/plugins/accounts/scripts/build.sh
skyquake/plugins/accounts/scripts/install.sh
skyquake/plugins/accounts/src/account/account.jsx
skyquake/plugins/accounts/src/account/accountActions.js
skyquake/plugins/accounts/src/account/accountSource.js
skyquake/plugins/accounts/src/account/accountStore.js
skyquake/plugins/accounts/src/account/accountsDashboard.jsx
skyquake/plugins/accounts/src/account_sidebar/accountSidebar.jsx
skyquake/plugins/accounts/src/account_sidebar/accountSidebar.scss
skyquake/plugins/accounts/webpack.production.config.js
skyquake/plugins/admin/CMakeLists.txt [new file with mode: 0644]
skyquake/plugins/admin/api/admin.js [new file with mode: 0644]
skyquake/plugins/admin/config.json [new file with mode: 0644]
skyquake/plugins/admin/package.json [new file with mode: 0644]
skyquake/plugins/admin/routes.js [new file with mode: 0644]
skyquake/plugins/admin/scripts/build.sh [new file with mode: 0755]
skyquake/plugins/admin/scripts/install.sh [new file with mode: 0755]
skyquake/plugins/admin/server.js [new file with mode: 0644]
skyquake/plugins/admin/src/AdminStore.js [new file with mode: 0644]
skyquake/plugins/admin/src/admin.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/admin.scss [new file with mode: 0644]
skyquake/plugins/admin/src/adminActions.js [new file with mode: 0644]
skyquake/plugins/admin/src/adminSource.js [new file with mode: 0644]
skyquake/plugins/admin/src/components/ActionBar.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ChoiceCard.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ChoiceColumn.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ColumnCard.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ContainerCard.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ContainerColumn.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/EditorDialog.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ExplorerColumn.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/LeafGroup.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ListCard.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ListColumn.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ListEntryCard.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ListEntryColumn.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ListStack.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/LoadingCard.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/LoadingColumn.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/ModelExplorer.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/editor/LeafEditor.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/editor/LeafField.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/editor/Select.jsx [new file with mode: 0644]
skyquake/plugins/admin/src/components/editor/resolveLeafRef.js [new file with mode: 0644]
skyquake/plugins/admin/src/main.js [new file with mode: 0644]
skyquake/plugins/admin/src/store/ModelStore.js [new file with mode: 0644]
skyquake/plugins/admin/src/yang/leaf-utils.js [new file with mode: 0644]
skyquake/plugins/admin/src/yang/property-utils.js [new file with mode: 0644]
skyquake/plugins/admin/webpack.production.config.js [new file with mode: 0644]
skyquake/plugins/composer/api/composer.js
skyquake/plugins/composer/api/packageFileHandler.js
skyquake/plugins/composer/config.json
skyquake/plugins/composer/package.json
skyquake/plugins/composer/routes.js
skyquake/plugins/composer/scripts/build.sh
skyquake/plugins/composer/src/schemas/yang/confd2model.js
skyquake/plugins/composer/src/schemas/yang/mano-types.yang.src
skyquake/plugins/composer/src/src/actions/CatalogDataSourceActions.js
skyquake/plugins/composer/src/src/actions/ComposerAppActions.js
skyquake/plugins/composer/src/src/actions/DescriptorEditorActions.js [new file with mode: 0644]
skyquake/plugins/composer/src/src/actions/PanelResizeAction.js
skyquake/plugins/composer/src/src/alt.js
skyquake/plugins/composer/src/src/components/Button.js
skyquake/plugins/composer/src/src/components/CanvasPanel.js
skyquake/plugins/composer/src/src/components/CanvasPanelTray.js
skyquake/plugins/composer/src/src/components/CatalogItemCanvasEditor.js
skyquake/plugins/composer/src/src/components/CatalogItemDetailsEditor.js
skyquake/plugins/composer/src/src/components/CatalogItems.js
skyquake/plugins/composer/src/src/components/CatalogPanel.js
skyquake/plugins/composer/src/src/components/CatalogPanelToolbar.js
skyquake/plugins/composer/src/src/components/ComposerApp.js
skyquake/plugins/composer/src/src/components/ComposerAppToolbar.js
skyquake/plugins/composer/src/src/components/ConfigPrimitiveParameters/ConfigPrimitiveParameters.js [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/DetailsPanel.js
skyquake/plugins/composer/src/src/components/DetailsPanelToolbar.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/EditDescriptorModelProperties.js
skyquake/plugins/composer/src/src/components/EditorForwardingGraph/EditForwardingGraphPaths.js
skyquake/plugins/composer/src/src/components/NavigateDescriptorErrors.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/NavigateDescriptorModel.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/RiftHeader.js
skyquake/plugins/composer/src/src/components/filemanager/FileManager.jsx
skyquake/plugins/composer/src/src/components/filemanager/FileManagerSource.js
skyquake/plugins/composer/src/src/components/messages.js
skyquake/plugins/composer/src/src/components/model/Choice.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/Container.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/EditDescriptorUtils.js [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/LeafEditor.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/LeafField.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/List.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/ListItemAsLink.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/ModelBreadcrumb.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/PanelHeader.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/PropertyCrumb.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/PropertyNavigate.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/PropertyPanel.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/RemoveItemButton.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/components/model/Select.jsx [new file with mode: 0644]
skyquake/plugins/composer/src/src/libraries/FileManagerUploadDropZone.js
skyquake/plugins/composer/src/src/libraries/PackageManagerApi.js [deleted file]
skyquake/plugins/composer/src/src/libraries/TooltipManager.js
skyquake/plugins/composer/src/src/libraries/UniqueId.js
skyquake/plugins/composer/src/src/libraries/graph/DescriptorGraph.js
skyquake/plugins/composer/src/src/libraries/graph/layouts/RelationsAndNetworksLayout.js
skyquake/plugins/composer/src/src/libraries/model/DescriptorModel.js
skyquake/plugins/composer/src/src/libraries/model/DescriptorModelFactory.js
skyquake/plugins/composer/src/src/libraries/model/DescriptorModelFields.js
skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaFactory.js
skyquake/plugins/composer/src/src/libraries/model/DescriptorModelMetaProperty.js
skyquake/plugins/composer/src/src/libraries/model/DescriptorModelSerializer.js
skyquake/plugins/composer/src/src/libraries/model/descriptors/ConnectionPoint.js
skyquake/plugins/composer/src/src/libraries/model/descriptors/ConstituentVnfdConnectionPoint.js
skyquake/plugins/composer/src/src/libraries/model/descriptors/InternalVirtualLink.js
skyquake/plugins/composer/src/src/libraries/model/descriptors/NetworkService.js
skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunction.js
skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionAccessPoint.js [new file with mode: 0644]
skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionAccessPointMap.js [new file with mode: 0644]
skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionReadOnlyWrapper.js
skyquake/plugins/composer/src/src/libraries/utils.js
skyquake/plugins/composer/src/src/sources/CatalogDataSource.js
skyquake/plugins/composer/src/src/sources/CatalogPackageManagerSource.js
skyquake/plugins/composer/src/src/stores/CatalogDataStore.js
skyquake/plugins/composer/src/src/stores/CatalogPackageManagerStore.js
skyquake/plugins/composer/src/src/stores/ComposerAppStore.js
skyquake/plugins/composer/src/src/styles/Button.scss
skyquake/plugins/composer/src/src/styles/CanvasPanel.scss
skyquake/plugins/composer/src/src/styles/CanvasPanelTray.scss
skyquake/plugins/composer/src/src/styles/CatalogItemDetailsEditor.scss [new file with mode: 0644]
skyquake/plugins/composer/src/src/styles/CatalogItems.scss
skyquake/plugins/composer/src/src/styles/DetailsPanel.scss
skyquake/plugins/composer/src/src/styles/DetailsPanelErrors.scss [new file with mode: 0644]
skyquake/plugins/composer/src/src/styles/DetailsPanelToolbar.scss [new file with mode: 0644]
skyquake/plugins/composer/src/src/styles/EditConfigParameterMap.scss [new file with mode: 0644]
skyquake/plugins/composer/src/src/styles/EditDescriptorModelProperties.scss
skyquake/plugins/composer/src/src/styles/_main.scss
skyquake/plugins/composer/webpack.production.config.js
skyquake/plugins/config/CMakeLists.txt [deleted file]
skyquake/plugins/config/api/ro.js [deleted file]
skyquake/plugins/config/config.json [deleted file]
skyquake/plugins/config/images/OpenDaylight_logo.png [deleted file]
skyquake/plugins/config/images/aws.png [deleted file]
skyquake/plugins/config/images/juju.svg [deleted file]
skyquake/plugins/config/images/openmano.png [deleted file]
skyquake/plugins/config/images/openstack-horizontal.png [deleted file]
skyquake/plugins/config/images/openstack.png [deleted file]
skyquake/plugins/config/images/riftio.png [deleted file]
skyquake/plugins/config/package.json [deleted file]
skyquake/plugins/config/routes.js [deleted file]
skyquake/plugins/config/scripts/build.sh [deleted file]
skyquake/plugins/config/scripts/install.sh [deleted file]
skyquake/plugins/config/server.js [deleted file]
skyquake/plugins/config/src/dashboard/config.scss [deleted file]
skyquake/plugins/config/src/dashboard/configActions.js [deleted file]
skyquake/plugins/config/src/dashboard/configSource.js [deleted file]
skyquake/plugins/config/src/dashboard/configStore.js [deleted file]
skyquake/plugins/config/src/dashboard/dashboard.jsx [deleted file]
skyquake/plugins/config/src/dashboard/inputs.jsx [deleted file]
skyquake/plugins/config/src/main.js [deleted file]
skyquake/plugins/config/webpack.production.config.js [deleted file]
skyquake/plugins/debug/api/debug.js
skyquake/plugins/debug/config.json
skyquake/plugins/debug/scripts/build.sh
skyquake/plugins/debug/scripts/install.sh
skyquake/plugins/debug/webpack.production.config.js
skyquake/plugins/goodbyeworld/scripts/build.sh
skyquake/plugins/goodbyeworld/webpack.production.config.js
skyquake/plugins/helloworld/scripts/build.sh
skyquake/plugins/helloworld/webpack.production.config.js
skyquake/plugins/launchpad/api/launchpad.js
skyquake/plugins/launchpad/config.json
skyquake/plugins/launchpad/package.json
skyquake/plugins/launchpad/routes.js
skyquake/plugins/launchpad/scripts/build.sh
skyquake/plugins/launchpad/scripts/install.sh
skyquake/plugins/launchpad/src/instantiate/instantiateDashboard.jsx
skyquake/plugins/launchpad/src/instantiate/instantiateDashboard.scss
skyquake/plugins/launchpad/src/instantiate/instantiateInputParams.jsx
skyquake/plugins/launchpad/src/instantiate/instantiateParameters.jsx
skyquake/plugins/launchpad/src/instantiate/instantiateSelectDescriptorPanel.jsx
skyquake/plugins/launchpad/src/instantiate/instantiateSelectDescriptorPanel.scss
skyquake/plugins/launchpad/src/instantiate/instantiateStore.js
skyquake/plugins/launchpad/src/instantiate/launchNetworkServiceActions.js
skyquake/plugins/launchpad/src/instantiate/launchNetworkServiceSource.js
skyquake/plugins/launchpad/src/launchpad-create.js [deleted file]
skyquake/plugins/launchpad/src/launchpad.jsx
skyquake/plugins/launchpad/src/launchpadFleetSource.js
skyquake/plugins/launchpad/src/launchpad_card/launchpad-card.js [deleted file]
skyquake/plugins/launchpad/src/launchpad_card/launchpadCard.jsx
skyquake/plugins/launchpad/src/launchpad_card/launchpadCardCloudAccount.jsx
skyquake/plugins/launchpad/src/launchpad_card/launchpadHeader.jsx
skyquake/plugins/launchpad/src/launchpad_card/launchpad_card.scss
skyquake/plugins/launchpad/src/launchpad_card/nsrConfigPrimitives.jsx
skyquake/plugins/launchpad/src/launchpad_card/nsrScalingGroups.jsx
skyquake/plugins/launchpad/src/launchpad_card/vnfrConfigPrimitives.jsx
skyquake/plugins/launchpad/src/monitoring_params/monitoringParamComponents.js
skyquake/plugins/launchpad/src/monitoring_params/monitoring_params.scss
skyquake/plugins/launchpad/src/nsCardPanel/nsCardPanel.jsx
skyquake/plugins/launchpad/src/nsListPanel/nsListPanel.jsx
skyquake/plugins/launchpad/src/recordViewer/recordCard.jsx
skyquake/plugins/launchpad/src/recordViewer/recordView.jsx
skyquake/plugins/launchpad/src/recordViewer/recordViewSource.js
skyquake/plugins/launchpad/src/recordViewer/recordViewStore.js
skyquake/plugins/launchpad/src/topologyL2View/topologyL2Source.js
skyquake/plugins/launchpad/src/topologyView/topologySource.js
skyquake/plugins/launchpad/src/virtual_links/nsVirtualLinkCreateSource.js
skyquake/plugins/launchpad/src/virtual_links/nsVirtualLinks.jsx
skyquake/plugins/launchpad/src/vnfr/vnfrSource.js
skyquake/plugins/launchpad/webpack.production.config.js
skyquake/plugins/logging/api/logging.js
skyquake/plugins/logging/api/transforms.js
skyquake/plugins/logging/config.json
skyquake/plugins/logging/package.json
skyquake/plugins/logging/scripts/build.sh
skyquake/plugins/logging/scripts/install.sh
skyquake/plugins/logging/src/loggingSource.js
skyquake/plugins/logging/src/loggingStore.js
skyquake/plugins/logging/webpack.production.config.js
skyquake/plugins/logging/yarn.lock
skyquake/plugins/project_management/CMakeLists.txt [new file with mode: 0644]
skyquake/plugins/project_management/config.json [new file with mode: 0644]
skyquake/plugins/project_management/package.json [new file with mode: 0644]
skyquake/plugins/project_management/routes.js [new file with mode: 0644]
skyquake/plugins/project_management/scripts/build.sh [new file with mode: 0755]
skyquake/plugins/project_management/scripts/install.sh [new file with mode: 0755]
skyquake/plugins/project_management/server.js [new file with mode: 0644]
skyquake/plugins/project_management/src/dashboard/dashboard.jsx [new file with mode: 0644]
skyquake/plugins/project_management/src/dashboard/projectMgmt.scss [new file with mode: 0644]
skyquake/plugins/project_management/src/dashboard/projectMgmtActions.js [new file with mode: 0644]
skyquake/plugins/project_management/src/dashboard/projectMgmtSource.js [new file with mode: 0644]
skyquake/plugins/project_management/src/dashboard/projectMgmtStore.js [new file with mode: 0644]
skyquake/plugins/project_management/src/main.js [new file with mode: 0644]
skyquake/plugins/project_management/webpack.production.config.js [new file with mode: 0644]
skyquake/plugins/redundancy/CMakeLists.txt [new file with mode: 0644]
skyquake/plugins/redundancy/api/redundancy.js [new file with mode: 0644]
skyquake/plugins/redundancy/config.json [new file with mode: 0644]
skyquake/plugins/redundancy/package.json [new file with mode: 0644]
skyquake/plugins/redundancy/routes.js [new file with mode: 0644]
skyquake/plugins/redundancy/scripts/build.sh [new file with mode: 0755]
skyquake/plugins/redundancy/scripts/install.sh [new file with mode: 0755]
skyquake/plugins/redundancy/server.js [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/config.jsx [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/dashboard.jsx [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/redundancy.scss [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/redundancyActions.js [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/redundancySource.js [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/redundancyStore.js [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/sites.jsx [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/status.jsx [new file with mode: 0644]
skyquake/plugins/redundancy/src/dashboard/utils/utils.js [new file with mode: 0644]
skyquake/plugins/redundancy/src/main.js [new file with mode: 0644]
skyquake/plugins/redundancy/webpack.production.config.js [new file with mode: 0644]
skyquake/plugins/user_management/CMakeLists.txt [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/src/platformRoleManagement/platformRoleManagement.jsx [new file with mode: 0644]
skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagement.scss [new file with mode: 0644]
skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementActions.js [new file with mode: 0644]
skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementSource.js [new file with mode: 0644]
skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementStore.js [new file with mode: 0644]
skyquake/plugins/user_management/src/userProfile/userProfile.jsx [new file with mode: 0644]
skyquake/plugins/user_management/src/userProfile/userProfileActions.js [new file with mode: 0644]
skyquake/plugins/user_management/src/userProfile/userProfileSource.js [new file with mode: 0644]
skyquake/plugins/user_management/src/userProfile/userProfileStore.js [new file with mode: 0644]
skyquake/plugins/user_management/webpack.production.config.js [new file with mode: 0644]
skyquake/scripts/build-dev.sh [new file with mode: 0755]
skyquake/scripts/build-plugin.sh [new file with mode: 0644]
skyquake/scripts/build.sh
skyquake/scripts/handle_plugin_node_modules [new file with mode: 0755]
skyquake/scripts/launch_ui.sh
skyquake/skyquake.js
skyquake/tests/stories/FileManager.js [new file with mode: 0644]

index 40934df..6747eae 100644 (file)
@@ -1,8 +1,10 @@
 .DS_Store/
 .DS_Store
+._.DS_Store
 err.log
 out.log
 node_modules/
 npm-debug.log
 fixtures/
 .build
+.fuse_*
diff --git a/BUILD.sh b/BUILD.sh
new file mode 100755 (executable)
index 0000000..e244b7c
--- /dev/null
+++ b/BUILD.sh
@@ -0,0 +1,121 @@
+#!/usr/bin/env bash
+# 
+#   Copyright 2016,2017 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.
+#
+# Author(s): Jeremy Mordkoff, Lezz Giles
+# Creation Date: 08/29/2016
+# 
+#
+
+# BUILD.sh
+#
+# This is a top-level build script for OSM SO or UI
+#
+# Arguments and options: use -h or --help
+#
+# dependencies -- requires sudo rights
+
+MODULE=UI
+
+# Defensive bash programming flags
+set -o errexit    # Exit on any error
+trap 'echo ERROR: Command failed: \"$BASH_COMMAND\"' ERR
+set -o nounset    # Expanding an unset variable is an error.  Variables must be
+                  # set before they can be used.
+
+###############################################################################
+# Options and arguments
+
+# There 
+params="$(getopt -o h -l install,help --name "$0" -- "$@")"
+if [ $? != 0 ] ; then echo "Failed parsing options." >&2 ; exit 1 ; fi
+
+eval set -- $params
+
+installFromPackages=false
+
+while true; do
+    case "$1" in
+       --install) installFromPackages=true; shift;;
+       -h|--help)
+           echo
+           echo "NAME:"
+           echo "  $0"
+           echo
+           echo "SYNOPSIS:"
+           echo "  $0 -h|--help"
+           echo "  $0 [--install] [PLATFORM_REPOSITORY] [PLATFORM_VERSION]"
+           echo
+           echo "DESCRIPTION:"
+           echo "  Prepare current system to run $MODULE.  By default, the system"
+           echo "  is set up to support building $MODULE; optionally, "
+           echo "  $MODULE can be installed from a Debian package repository."
+           echo
+           echo "  --install:  install $MODULE from package"
+           echo "  PLATFORM_REPOSITORY (optional): name of the RIFT.ware repository."
+           echo "  PLATFORM_VERSION (optional): version of the platform packages to be installed."
+           echo
+           exit 0;;
+       --) shift; break;;
+       *) echo "Not implemented: $1" >&2; exit 1;;
+    esac
+done
+
+# Turn this on after handling options, so the output doesn't get cluttered.
+set -x             # Print commands before executing them
+
+###############################################################################
+# Set up repo and version
+
+PLATFORM_REPOSITORY=${1:-osm-rbac}
+PLATFORM_VERSION=${2:-5.1.3.9999.70283}
+
+###############################################################################
+# Main block
+
+# must be run from the top of a workspace
+cd $(dirname $0)
+
+# enable the right repos
+curl http://repos.riftio.com/public/xenial-riftware-public-key | sudo apt-key add -
+
+# always use the same file name so that updates will overwrite rather than enable a second repo
+sudo curl -o /etc/apt/sources.list.d/rift.list http://buildtracker.riftio.com/repo_file/ub16/${PLATFORM_REPOSITORY}/ 
+sudo apt-get update
+
+sudo apt install -y --allow-downgrades rw.tools-container-tools=${PLATFORM_VERSION} rw.tools-scripts=${PLATFORM_VERSION}
+
+if $installFromPackages; then
+    
+    # Install module and platform from packages
+    sudo -H /usr/rift/container_tools/mkcontainer --modes $MODULE --repo ${PLATFORM_REPOSITORY} --rw-version ${PLATFORM_VERSION}
+    
+else
+
+    # Install environment to build module
+    sudo -H /usr/rift/container_tools/mkcontainer --modes $MODULE-dev --repo ${PLATFORM_REPOSITORY} --rw-version ${PLATFORM_VERSION}
+
+    # Build  and install module
+    make -j16 
+    sudo make install
+
+fi
+
+if [[ $MODULE == SO ]]; then
+    echo "Creating Service ...."
+    sudo /usr/rift/bin/create_launchpad_service
+fi
+
+
index 1c9b576..8a21f2f 100644 (file)
@@ -31,7 +31,7 @@ cmake_minimum_required(VERSION 2.8)
 # DO NOT add any code before this and DO NOT
 # include this file anywhere else
 ##
-include(rift_submodule)
+include(rift_submodule NO_POLICY_SCOPE)
 
 ##
 # Submodule specific includes will go here,
@@ -53,6 +53,15 @@ rift_add_subdirs(
     ${subdirs}
   )
 
+##
+# Only skyquake package contains anything
+##
+rift_set_component_package_fields(
+  skyquake
+  DESCRIPTION "RIFT.ware UI"
+  POST_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/postinst"
+  )
+
 ##
 # This macro adds targets for documentaion, unittests, code coverage and packaging
 ##
index 40dc922..d576e2f 100644 (file)
@@ -1,24 +1,13 @@
 FROM ubuntu:16.04
 
-RUN apt-get update && apt-get -y install python3 curl build-essential
+RUN apt-get update && apt-get -y install python3 curl build-essential apt-transport-https sudo
 RUN curl http://repos.riftio.com/public/xenial-riftware-public-key | apt-key add - && \
-       curl -o /etc/apt/sources.list.d/OSM.list http://buildtracker.riftio.com/repo_file/ub16/OSM/ && \
+       curl -o /etc/apt/sources.list.d/rift.list http://buildtracker.riftio.com/repo_file/ub16/OSM/ && \
        apt-get update && \
-       apt-get -y install rw.toolchain-rwbase \
-               rw.toolchain-rwtoolchain \
-               rw.core.mgmt-mgmt \
-               rw.core.util-util \
-               rw.core.rwvx-rwvx \
-               rw.core.rwvx-rwdts \
-               rw.automation.core-RWAUTO \
-               rw.tools-container-tools \
-               rw.tools-scripts \
-               python-cinderclient \
-               libxml2-dev \
-               libxslt-dev
+       apt-get -y install \
+               rw.tools-container-tools=5.2.0.0.71033 \
+               rw.tools-scripts=5.2.0.0.71033
 
-RUN /usr/rift/container_tools/mkcontainer --modes build --modes ext --repo OSM
+RUN /usr/rift/container_tools/mkcontainer --modes UI-dev --repo OSM --rw-version 5.2.0.0.71033
 
-RUN chmod 777 /usr/rift /usr/rift/usr/share
-
-RUN rm -rf /tmp/npm-cache
+RUN chmod 777 /usr/rift
index 99cb405..ceb06c3 100644 (file)
@@ -29,4 +29,3 @@ node('docker') {
                            params.GERRIT_PATCHSET_REVISION,
                            params.TEST_INSTALL,
                            params.ARTIFACTORY_SERVER)
-}
index e69de29..ab4a981 100644 (file)
@@ -0,0 +1 @@
+ho
diff --git a/postinst b/postinst
new file mode 100755 (executable)
index 0000000..602b110
--- /dev/null
+++ b/postinst
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# 
+#   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.
+#
+
+# Post-install script for packaging
+
+/usr/rift/usr/share/rw.ui/skyquake/scripts/handle_plugin_node_modules
diff --git a/python-osmclient/.gitignore b/python-osmclient/.gitignore
deleted file mode 100644 (file)
index e09c6a0..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-deb_dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-#  Usually these files are written by a python script from a template
-#  before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-.hypothesis/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-# IPython Notebook
-.ipynb_checkpoints
-
-# pyenv
-.python-version
-
-# celery beat schedule file
-celerybeat-schedule
-
-# dotenv
-.env
-
-# virtualenv
-venv/
-ENV/
-
-# Spyder project settings
-.spyderproject
-
-# Rope project settings
-.ropeproject
diff --git a/python-osmclient/Makefile b/python-osmclient/Makefile
deleted file mode 100644 (file)
index 1a2e448..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-package:
-       @python setup.py --command-packages=stdeb.command bdist_deb > /dev/null 2>&1
diff --git a/python-osmclient/README.md b/python-osmclient/README.md
deleted file mode 100644 (file)
index 8d41f2f..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-# python-osmclient
-A python client for osm orchestration
-
-# Installation
-
-## Install dependencies
-```bash
-sudo apt-get install python-dev libcurl4-gnutls-dev python-pip libgnutls-dev python-prettytable   
-sudo pip install pycurl
-```
-
-# Setup
-Set the OSM_HOSTNAME variable to the host of the osm server.
-
-Example
-```bash
-localhost$ export OSM_HOSTNAME=<hostname>:8008
-```
-
-# Examples 
-
-## upload vnfd
-```bash
-localhost$ osm upload-package ubuntu_xenial_vnf.tar.gz
-{'transaction_id': 'ec12af77-1b91-4c84-b233-60f2c2c16d14'}
-localhost$ osm vnfd-list
-+--------------------+--------------------+
-| vnfd name          | id                 |
-+--------------------+--------------------+
-| ubuntu_xenial_vnfd | ubuntu_xenial_vnfd |
-+--------------------+--------------------+
-```
-
-## upload nsd
-```bash
-localhost$ osm upload-package ubuntu_xenial_ns.tar.gz
-{'transaction_id': 'b560c9cb-43e1-49ef-a2da-af7aab24ce9d'}
-localhost$ osm nsd-list
-+-------------------+-------------------+
-| nsd name          | id                |
-+-------------------+-------------------+
-| ubuntu_xenial_nsd | ubuntu_xenial_nsd |
-+-------------------+-------------------+
-```
-## vim-list
-
-```bash
-localhost$ osm vim-list
-+-------------+-----------------+--------------------------------------+
-| ro-account  | datacenter name | uuid                                 |
-+-------------+-----------------+--------------------------------------+
-| osmopenmano | openstack-site  | 2ea04690-0e4a-11e7-89bc-00163e59ff0c |
-+-------------+-----------------+--------------------------------------+
-```
-
-
-## instantiate ns
-```bash
-localhost$ osm ns-create ubuntu_xenial_nsd testns openstack-site
-{'success': ''}
-localhost$ osm ns-list
-+------------------+--------------------------------------+-------------------+--------------------+---------------+
-| ns instance name | id                                   | catalog name      | operational status | config status |
-+------------------+--------------------------------------+-------------------+--------------------+---------------+
-| testns           | 6b0d2906-13d4-11e7-aa01-b8ac6f7d0c77 | ubuntu_xenial_nsd | running            | configured    |
-+------------------+--------------------------------------+-------------------+--------------------+---------------+
-```
-
-# Bash Completion
-python-osmclient uses [click](http://click.pocoo.org/5/).  You can setup bash completion by putting this in your .bashrc:
-    
-    eval "$(_OSM_COMPLETE=source osm)"
-
diff --git a/python-osmclient/osmclient/__init__.py b/python-osmclient/osmclient/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/python-osmclient/osmclient/common/OsmAPI.py b/python-osmclient/osmclient/common/OsmAPI.py
deleted file mode 100644 (file)
index f85e872..0000000
+++ /dev/null
@@ -1,436 +0,0 @@
-from io import BytesIO 
-import pycurl
-import json
-import pprint
-import uuid
-from prettytable import PrettyTable
-import time
-
-class OsmAPI():
-    def __init__(self,host,upload_port=8443):
-        if host is None:
-            raise Exception('missing host specifier')
-         
-        self._user='admin'
-        self._password='admin'
-        self._host=host
-        self._upload_port=upload_port
-        self._url = 'https://{}/'.format(self._host)
-        self._upload_url = 'https://{}:{}/'.format(self._host.split(':')[0],self._upload_port)
-
-    def get_curl_cmd(self,url):
-        curl_cmd = pycurl.Curl()
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.URL, self._url + url )
-        curl_cmd.setopt(pycurl.SSL_VERIFYPEER,0)
-        curl_cmd.setopt(pycurl.SSL_VERIFYHOST,0)
-        curl_cmd.setopt(pycurl.USERPWD, '{}:{}'.format(self._user,self._password))
-        curl_cmd.setopt(pycurl.HTTPHEADER, ['Accept: application/vnd.yand.data+json','Content-Type": "application/vnd.yang.data+json'])
-        return curl_cmd
-
-    def get_curl_upload_cmd(self,filename):
-        curl_cmd = pycurl.Curl()
-        curl_cmd.setopt(pycurl.HTTPPOST,[(('package',(pycurl.FORM_FILE,filename)))])
-        curl_cmd.setopt(pycurl.URL, self._upload_url + 'composer/upload?api_server=https://localhost&upload_server=https://' + self._host.split(':')[0])
-        curl_cmd.setopt(pycurl.SSL_VERIFYPEER,0)
-        curl_cmd.setopt(pycurl.SSL_VERIFYHOST,0)
-        curl_cmd.setopt(pycurl.USERPWD, '{}:{}'.format(self._user,self._password))
-        return curl_cmd
-
-    def get_ns_opdata(self,id):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/operational/ns-instance-opdata/nsr/{}?deep'.format(id))
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        if data.getvalue():
-           return json.loads(data.getvalue().decode())
-        return None 
-
-    def get_ns_catalog(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/running/nsd-catalog/nsd')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        if data.getvalue():
-          resp = json.loads(data.getvalue().decode())
-          return resp
-        return {'nsd:nsd': []}
-
-    def get_config_agents(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/config/config-agent')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        if data.getvalue():
-          resp = json.loads(data.getvalue().decode())
-          if 'rw-config-agent:config-agent' in resp:
-            return resp['rw-config-agent:config-agent']['account']
-        return list()
-
-    def delete_config_agent(self,name):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/config/config-agent/account/'+name)
-        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def add_config_agent(self,name,account_type,server,user,secret):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/config/config-agent')
-        curl_cmd.setopt(pycurl.POST,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-
-        postdata={}
-        postdata['account'] = list()
-
-        account={}
-        account['name'] = name
-        account['account-type'] = account_type
-        account['juju'] = {}
-        account['juju']['user'] = user 
-        account['juju']['secret'] = secret
-        account['juju']['ip-address'] = server
-        postdata['account'].append(account)
-
-        jsondata=json.dumps(postdata)
-        curl_cmd.setopt(pycurl.POSTFIELDS,jsondata)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def get_ns_instance_list(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/running/ns-instance-config')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        return resp['nsr:ns-instance-config']
-
-    def get_vnf_catalog(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/running/vnfd-catalog/vnfd')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        if data.getvalue():
-          resp = json.loads(data.getvalue().decode())
-          return resp
-        return {'vnfd:vnfd': []}
-
-    def get_vcs_info(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/operational/vcs/info')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        if data.getvalue():
-          resp = json.loads(data.getvalue().decode())
-          return resp['rw-base:info']['components']['component_info']
-        return list()
-
-    def get_vnfr_catalog(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('v1/api/operational/vnfr-catalog/vnfr')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        if data.getvalue():
-          resp = json.loads(data.getvalue().decode())
-          return resp
-        return None 
-
-    def get_vnf(self,vnf_name):
-        vnfs=self.get_vnfr_catalog()
-        for vnf in vnfs['vnfr:vnfr']:
-            if vnf_name == vnf['name']:
-                return vnf
-        return None
-
-    def get_vnf_monitoring(self,vnf_name):
-        vnf=self.get_vnf(vnf_name)
-        if vnf is not None:
-            if 'monitoring-param' in vnf:
-                return vnf['monitoring-param'] 
-        return None
-
-    def get_ns_monitoring(self,ns_name):
-        ns=self.get_ns(ns_name)
-        if ns is None:
-            raise Exception('cannot find ns {}'.format(ns_name)) 
-
-        vnfs=self.get_vnfr_catalog()
-        if vnfs is None:
-            return None
-        mon_list={}
-        for vnf in vnfs['vnfr:vnfr']:
-            if ns['id'] == vnf['nsr-id-ref']:
-                if 'monitoring-param' in vnf:
-                    mon_list[vnf['name']] = vnf['monitoring-param']
-
-        return mon_list 
-
-    def list_key_pair(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('v1/api/config/key-pair?deep')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        if 'nsr:key-pair' in resp:
-            return resp['nsr:key-pair']
-        return list()
-
-    def list_ns_catalog(self):
-        resp = self.get_ns_catalog()
-        table=PrettyTable(['nsd name','id'])
-        for ns in resp['nsd:nsd']:
-            table.add_row([ns['name'],ns['id']])
-        table.align='l'
-        print(table)
-
-    def list_ns_instance(self):
-        resp = self.get_ns_instance_list()
-        table=PrettyTable(['ns instance name','id','catalog name','operational status','config status'])
-        if 'nsr' in resp:
-            for ns in resp['nsr']:
-                nsopdata=self.get_ns_opdata(ns['id'])
-                nsr=nsopdata['nsr:nsr']
-                table.add_row([nsr['name-ref'],nsr['ns-instance-config-ref'],nsr['nsd-name-ref'],nsr['operational-status'],nsr['config-status']])
-        table.align='l'
-        print(table)
-
-    def get_nsd(self,name):
-        resp = self.get_ns_catalog()
-        for ns in resp['nsd:nsd']:
-           if name == ns['name']:
-               return ns
-        return None 
-
-    def get_vnfd(self,name):
-        resp = self.get_vnf_catalog()
-        for vnf in resp['vnfd:vnfd']:
-           if name == vnf['name']:
-               return vnf 
-        return None 
-
-    def get_ns(self,name):
-        resp=self.get_ns_instance_list()
-        for ns in resp['nsr']:
-           if name == ns['name']:
-               return ns
-        return None 
-    
-    def instantiate_ns(self,nsd_name,nsr_name,account,vim_network_prefix=None,ssh_keys=None,description='default description',admin_status='ENABLED'):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/config/ns-instance-config/nsr')
-        curl_cmd.setopt(pycurl.POST,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-
-        postdata={}
-        postdata['nsr'] = list()
-        nsr={}
-        nsr['id']=str(uuid.uuid1())
-
-        nsd=self.get_nsd(nsd_name)
-        if nsd is None:
-            raise Exception('cannot find nsd {}'.format(nsd_name)) 
-        
-        datacenter=self.get_datacenter(account)
-        if datacenter is None:
-            raise Exception('cannot find datacenter account {}'.format(account)) 
-
-        nsr['nsd']=nsd
-        nsr['name']=nsr_name
-        nsr['short-name']=nsr_name
-        nsr['description']=description
-        nsr['admin-status']=admin_status
-        nsr['om-datacenter']=datacenter['uuid']
-
-        if ssh_keys is not None:
-            # ssh_keys is comma separate list
-            ssh_keys_format=[]
-            for key in ssh_keys.split(','):
-                ssh_keys_format.append({'key-pair-ref':key})
-
-            nsr['ssh-authorized-key']=ssh_keys_format
-
-        if vim_network_prefix is not None:
-            for index,vld in enumerate(nsr['nsd']['vld']):
-                network_name = vld['name']
-                nsr['nsd']['vld'][index]['vim-network-name'] = '{}-{}'.format(vim_network_prefix,network_name)
-
-        postdata['nsr'].append(nsr)
-        jsondata=json.dumps(postdata)
-        curl_cmd.setopt(pycurl.POSTFIELDS,jsondata)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def delete_nsd(self,nsd_name):
-        nsd=self.get_nsd(nsd_name)
-        if nsd is None:
-            raise Exception('cannot find nsd {}'.format(nsd_name)) 
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/running/nsd-catalog/nsd/'+nsd['id'])
-        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def delete_vnfd(self,vnfd_name):
-        vnfd=self.get_vnfd(vnfd_name)
-        if vnfd is None:
-            raise Exception('cannot find vnfd {}'.format(vnfd_name)) 
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/running/vnfd-catalog/vnfd/'+vnfd['id'])
-        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def terminate_ns(self,ns_name):
-        ns=self.get_ns(ns_name)
-        if ns is None:
-            raise Exception('cannot find ns {}'.format(ns_name)) 
-  
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/config/ns-instance-config/nsr/'+ns['id'])
-        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def upload_package(self,filename):
-        data = BytesIO()
-        curl_cmd=self.get_curl_upload_cmd(filename)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-      
-    def add_vim_account(self,name,user_name,secret,auth_url,tenant,mgmt_network,float_ip_pool,account_type='openstack'):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('api/config/cloud')
-        curl_cmd.setopt(pycurl.POST,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        vim_account={}
-        vim_account['account']={}
-
-        vim_account['account']['name'] = name
-        vim_account['account']['account-type'] = account_type
-        vim_account['account'][account_type] = {}
-        vim_account['account'][account_type]['key'] = user_name
-        vim_account['account'][account_type]['secret'] = secret
-        vim_account['account'][account_type]['auth_url'] = auth_url
-        vim_account['account'][account_type]['tenant'] = tenant
-        vim_account['account'][account_type]['mgmt-network'] = mgmt_network
-        vim_account['account'][account_type]['floating-ip-pool'] = float_ip_pool
-
-        jsondata=json.dumps(vim_account)
-        curl_cmd.setopt(pycurl.POSTFIELDS,jsondata)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        pprint.pprint(resp)
-
-    def list_vim_accounts(self):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('v1/api/operational/datacenters')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        datacenters=resp['rw-launchpad:datacenters']
-        if 'ro-accounts' in datacenters:
-            return datacenters['ro-accounts']
-        return list()
-
-    def get_datacenter(self,name):
-        data = BytesIO()
-        curl_cmd=self.get_curl_cmd('v1/api/operational/datacenters')
-        curl_cmd.setopt(pycurl.HTTPGET,1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        curl_cmd.perform() 
-        curl_cmd.close()
-        resp = json.loads(data.getvalue().decode())
-        datacenters=resp['rw-launchpad:datacenters']
-        if 'ro-accounts' in datacenters:
-            for roaccount in datacenters['ro-accounts']:
-                if not 'datacenters' in roaccount:
-                    continue
-                for datacenter in roaccount['datacenters']:
-                    if datacenter['name'] == name:
-                        return datacenter
-        return None
-
-    def show_ns(self,ns_name):
-        resp = self.get_ns_instance_list()
-        table=PrettyTable(['attribute','value'])
-
-        if 'nsr' in resp:
-            for ns in resp['nsr']:
-                if ns_name == ns['name']:
-                    # dump ns config
-                    for k,v in ns.items():
-                        table.add_row([k,json.dumps(v,indent=2)])
-
-                    nsopdata=self.get_ns_opdata(ns['id'])
-                    nsr_optdata=nsopdata['nsr:nsr']
-                    for k,v in nsr_optdata.items():
-                        table.add_row([k,json.dumps(v,indent=2)])
-        table.align='l'
-        print(table)
-
-    def show_ns_scaling(self,ns_name):
-        resp = self.get_ns_instance_list()
-
-        table=PrettyTable(['instance-id','operational status','create-time','vnfr ids'])
-
-        if 'nsr' in resp:
-            for ns in resp['nsr']:
-                if ns_name == ns['name']:
-                    nsopdata=self.get_ns_opdata(ns['id'])
-                    scaling_records=nsopdata['nsr:nsr']['scaling-group-record']
-                    for record in scaling_records:
-                        if 'instance' in record:
-                            instances=record['instance'] 
-                            for inst in instances:
-                                table.add_row([inst['instance-id'],inst['op-status'],time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(inst['create-time'])),inst['vnfrs']])
-        table.align='l'
-        print(table)
-
-    def list_vnfr(self):
-        return self.get_vnfr_catalog()
-
-    def list_vnf_catalog(self):
-        resp = self.get_vnf_catalog()
-        table=PrettyTable(['vnfd name','id'])
-        for ns in resp['vnfd:vnfd']:
-            table.add_row([ns['name'],ns['id']])
-        table.align='l'
-        print(table)
diff --git a/python-osmclient/osmclient/common/__init__.py b/python-osmclient/osmclient/common/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/python-osmclient/osmclient/scripts/__init__.py b/python-osmclient/osmclient/scripts/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/python-osmclient/osmclient/scripts/osm.py b/python-osmclient/osmclient/scripts/osm.py
deleted file mode 100755 (executable)
index d74580c..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-import click
-from osmclient.common import OsmAPI
-from prettytable import PrettyTable
-import pprint
-import textwrap
-
-@click.group()
-@click.option('--hostname',default=None,envvar='OSM_HOSTNAME',help='hostname of server.  Also can set OSM_HOSTNAME in environment')
-@click.pass_context
-def cli(ctx,hostname):
-    if hostname is None:
-        print("either hostname option or OSM_HOSTNAME environment variable needs to be specified") 
-        exit(1)
-    ctx.obj=OsmAPI.OsmAPI(hostname)
-
-@cli.command(name='ns-list')
-@click.pass_context
-def ns_list(ctx):
-    ctx.obj.list_ns_instance()
-
-@cli.command(name='nsd-list')
-@click.pass_context
-def nsd_list(ctx):
-    ctx.obj.list_ns_catalog()
-
-@cli.command(name='vnfd-list')
-@click.pass_context
-def vnfd_list(ctx):
-    ctx.obj.list_vnf_catalog()
-
-@cli.command(name='vnf-list')
-@click.pass_context
-def vnf_list(ctx):
-    resp=ctx.obj.list_vnfr()
-    table=PrettyTable(['vnf name','id','operational status','config Status','mgmt interface','nsr id'])
-    if resp is not None:
-        for vnfr in resp['vnfr:vnfr']:
-            if not 'mgmt-interface' in vnfr:
-                vnfr['mgmt-interface'] = {}
-                vnfr['mgmt-interface']['ip-address'] = None
-            table.add_row([vnfr['name'],vnfr['id'],vnfr['operational-status'],vnfr['config-status'],vnfr['mgmt-interface']['ip-address'],vnfr['nsr-id-ref']])
-        table.align='l'
-    print(table)
-
-@cli.command(name='vnf-monitoring-show')
-@click.argument('vnf_name')
-@click.pass_context
-def vnf_monitoring_show(ctx,vnf_name):
-    resp=ctx.obj.get_vnf_monitoring(vnf_name)
-    table=PrettyTable(['vnf name','monitoring name','value','units'])
-    if resp is not None:
-        for monitor in resp:
-            table.add_row([vnf_name,monitor['name'],monitor['value-integer'],monitor['units']])
-    table.align='l'
-    print(table)
-
-@cli.command(name='ns-monitoring-show')
-@click.argument('ns_name')
-@click.pass_context
-def ns_monitoring_show(ctx,ns_name):
-    resp=ctx.obj.get_ns_monitoring(ns_name)
-    table=PrettyTable(['vnf name','monitoring name','value','units'])
-    if resp is not None:
-        for key,val in resp.items():
-            for monitor in val:
-                table.add_row([key,monitor['name'],monitor['value-integer'],monitor['units']])
-    table.align='l'
-    print(table)
-
-@cli.command(name='ns-create')
-@click.argument('nsd_name')
-@click.argument('ns_name')
-@click.argument('vim_account')
-@click.option('--admin_status',default='ENABLED',help='administration status')
-@click.option('--ssh_keys',default=None,help='comma separated list of keys to inject to vnfs')
-@click.option('--vim_network_prefix',default=None,help='vim network name prefix')
-@click.pass_context
-def ns_create(ctx,nsd_name,ns_name,vim_account,admin_status,ssh_keys,vim_network_prefix):
-    ctx.obj.instantiate_ns(nsd_name,ns_name,vim_network_prefix=vim_network_prefix,ssh_keys=ssh_keys,account=vim_account)
-
-@cli.command(name='ns-delete')
-@click.argument('ns_name')
-@click.pass_context
-def ns_delete(ctx,ns_name):
-    ctx.obj.terminate_ns(ns_name)
-
-'''
-@cli.command(name='keypair-list')
-@click.pass_context
-def keypair_list(ctx):
-    resp=ctx.obj.list_key_pair()
-    table=PrettyTable(['key Name','key'])
-    for kp in resp:
-        table.add_row([kp['name'],kp['key']])
-    table.align='l'
-    print(table)
-'''
-
-@cli.command(name='upload-package')
-@click.argument('filename')
-@click.pass_context
-def upload_package(ctx,filename):
-    ctx.obj.upload_package(filename)
-
-@cli.command(name='ns-show')
-@click.argument('ns_name')
-@click.pass_context
-def ns_show(ctx,ns_name):
-    ctx.obj.show_ns(ns_name)
-
-@cli.command(name='ns-scaling-show')
-@click.argument('ns_name')
-@click.pass_context
-def show_ns_scaling(ctx,ns_name):
-    ctx.obj.show_ns_scaling(ns_name)
-
-@cli.command(name='nsd-delete')
-@click.argument('nsd_name')
-@click.pass_context
-def nsd_delete(ctx,nsd_name):
-    ctx.obj.delete_nsd(nsd_name)
-
-@cli.command(name='vnfd-delete')
-@click.argument('vnfd_name')
-@click.pass_context
-def nsd_delete(ctx,vnfd_name):
-    ctx.obj.delete_vnfd(vnfd_name)
-
-@cli.command(name='config-agent-list')
-@click.pass_context
-def config_agent_list(ctx):
-    table=PrettyTable(['name','account-type','details'])
-    for account in ctx.obj.get_config_agents():
-      table.add_row([account['name'],account['account-type'],account['juju']])
-    table.align='l'
-    print(table)
-
-@cli.command(name='config-agent-delete')
-@click.argument('name')
-@click.pass_context
-def config_agent_delete(ctx,name):
-    ctx.obj.delete_config_agent(name)
-
-@cli.command(name='config-agent-add')
-@click.argument('name')
-@click.argument('account_type')
-@click.argument('server')
-@click.argument('user')
-@click.argument('secret')
-@click.pass_context
-def config_agent_add(ctx,name,account_type,server,user,secret):
-    ctx.obj.add_config_agent(name,account_type,server,user,secret)
-
-'''
-@cli.command(name='vim-create')
-@click.argument('name')
-@click.argument('user')
-@click.argument('password')
-@click.argument('auth_url')
-@click.argument('tenant')
-@click.argument('mgmt_network')
-@click.argument('floating_ip_pool')
-@click.option('--account_type',default='openstack')
-@click.pass_context
-def vim_create(ctx,name,user,password,auth_url,tenant,mgmt_network,floating_ip_pool,account_type):
-    ctx.obj.add_vim_account(name,user,password,auth_url,tenant,mgmt_network,floating_ip_pool,account_type)
-'''
-
-@cli.command(name='vim-list')
-@click.pass_context
-def vim_list(ctx):
-    resp=ctx.obj.list_vim_accounts()
-    table=PrettyTable(['ro-account','datacenter name','uuid'])
-    for roaccount in resp:
-        for datacenter in roaccount['datacenters']:
-            table.add_row([roaccount['name'],datacenter['name'],datacenter['uuid']])
-    table.align='l'
-    print(table)
-
-@cli.command(name='vcs-list')
-@click.pass_context
-def vcs_list(ctx):
-    resp=ctx.obj.get_vcs_info()
-    table=PrettyTable(['component name','state'])
-    for component in resp:
-        table.add_row([component['component_name'],component['state']])
-    table.align='l'
-    print(table)
-
-if __name__ == '__main__':
-    cli()
diff --git a/python-osmclient/setup.py b/python-osmclient/setup.py
deleted file mode 100644 (file)
index 13b1771..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-from setuptools import setup,find_packages
-
-setup(
-    name='osmclient',
-    version='0.1',
-    author='Mike Marchetti',
-    author_email='mmarchetti@sandvine.com',
-    packages=find_packages(),
-    include_package_data=True,
-    install_requires=[
-        'Click','prettytable'
-    ],
-    entry_points='''
-        [console_scripts]
-        osm=osmclient.scripts.osm:cli
-        ''',
-)
diff --git a/python-osmclient/stdeb.cfg b/python-osmclient/stdeb.cfg
deleted file mode 100644 (file)
index eadf0fa..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-[DEFAULT]
-Depends: python-setuptools, python-pycurl, python-click, python-prettytable
index 42edf3c..d7168a9 100644 (file)
@@ -6,8 +6,12 @@ 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/FileManager');
+  require('../tests/stories/inputs');
   // require as many stories as you need.
 }
 
 configure(loadStories, module);
+
+
diff --git a/skyquake/framework/core/api_utils/auth.js b/skyquake/framework/core/api_utils/auth.js
new file mode 100644 (file)
index 0000000..7249ac7
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ *
+ *   Copyright 2017 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.
+ *
+ */
+
+/**
+ * Auth util for use across the api_server.
+ * @module framework/core/api_utils/auth
+ * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
+ */
+
+var jsonLoader = require('require-json');
+var passport = require('passport');
+var OpenIdConnectStrategy = require('passport-openidconnect').Strategy;
+var BearerStrategy = require('passport-http-bearer').Strategy;
+var OAuth2Strategy = require('passport-oauth2');
+var OAuth2RefreshTokenStrategy = require('passport-oauth2-middleware').Strategy;
+var openidConnectConfig = require('./openidconnect_config.json');
+var _ = require('lodash');
+var constants = require('./constants');
+var utils = require('./utils');
+var request = utils.request;
+var rp = require('request-promise');
+var nodeutil = require('util');
+
+
+var Authorization = function(openidConfig) {
+
+    var self = this;
+
+    self.passport = passport;
+
+    self.openidConnectConfig = openidConnectConfig;
+
+    var refreshStrategy = new OAuth2RefreshTokenStrategy({
+        refreshWindow: constants.REFRESH_WINDOW, // Time in seconds to perform a token refresh before it expires
+        userProperty: 'user', // Active user property name to store OAuth tokens
+        authenticationURL: '/login', // URL to redirect unauthorized users to
+        callbackParameter: 'callback' //URL query parameter name to pass a return URL
+    });
+
+    self.passport.use('main', refreshStrategy);
+
+    var openidConfigPrefix = openidConfig.idpServerProtocol + '://' + openidConfig.idpServerAddress + ':' + openidConfig.idpServerPortNumber;
+
+    self.openidConnectConfig.authorizationURL = openidConfigPrefix + self.openidConnectConfig.authorizationURL;
+    self.openidConnectConfig.tokenURL = openidConfigPrefix + self.openidConnectConfig.tokenURL;
+    self.openidConnectConfig.callbackURL = openidConfig.callbackServerProtocol + '://' + openidConfig.callbackAddress + ':' + openidConfig.callbackPortNumber + self.openidConnectConfig.callbackURL;
+
+    var userInfoURL = openidConfigPrefix + self.openidConnectConfig.userInfoURL;
+
+    function SkyquakeOAuth2Strategy(options, verify) {
+        OAuth2Strategy.call(this, options, verify);
+    }
+    nodeutil.inherits(SkyquakeOAuth2Strategy, OAuth2Strategy);
+
+    SkyquakeOAuth2Strategy.prototype.userProfile = function(access_token, done) {
+
+        var requestHeaders = {
+            'Authorization': 'Bearer ' + access_token
+        };
+
+        request({
+            url: userInfoURL,
+            type: 'GET',
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: constants.REJECT_UNAUTHORIZED
+        }, function(err, response, body) {
+            if (err) {
+                console.log('Error obtaining userinfo: ', err);
+                return done(null, {
+                    username: '',
+                    subject: ''
+                });
+            } else {
+                if (response.statusCode == constants.HTTP_RESPONSE_CODES.SUCCESS.OK) {
+                    try {
+                        var data = JSON.parse(response.body);
+                        var username = data['preferred_username'];
+                        var subject = data['sub'];
+                        var domain = data['user_domain'] || 'system';
+                        return done(null, {
+                            username: username,
+                            subject: subject,
+                            domain: domain
+                        })
+                    } catch (ex) {
+                        console.log('Error parsing userinfo data');
+                        return done(null, {
+                            username: '',
+                            subject: ''
+                        });
+                    }
+                }
+            }
+        })
+    };
+
+    var oauthStrategy = new SkyquakeOAuth2Strategy(self.openidConnectConfig,
+        refreshStrategy.getOAuth2StrategyCallback());
+
+    self.passport.use('oauth2', oauthStrategy);
+    refreshStrategy.useOAuth2Strategy(oauthStrategy);
+
+    self.passport.serializeUser(function(user, done) {
+        done(null, user);
+    });
+
+    self.passport.deserializeUser(function(obj, done) {
+        done(null, obj);
+    });
+
+};
+
+Authorization.prototype.configure = function(config) {
+       this.config = config;
+       // Initialize Passport and restore authentication state, if any, from the
+    // session.
+    if (this.config.app) {
+       this.config.app.use(this.passport.initialize());
+       this.config.app.use(this.passport.session());
+    } else {
+       console.log('FATAL error. Bad config passed into authorization module');
+    }
+};
+
+Authorization.prototype.invalidate_token = function(token) {
+
+};
+
+module.exports = Authorization;
index 0aac7d2..b27e7da 100644 (file)
@@ -73,11 +73,21 @@ constants.SOCKET_BASE_PORT = 3500;
 constants.SOCKET_POOL_LENGTH = 20;
 constants.SERVER_PORT = process.env.SERVER_PORT || 8000;
 constants.SECURE_SERVER_PORT = process.env.SECURE_SERVER_PORT || 8443;
+constants.REJECT_UNAUTHORIZED = false;
 
-constants.BASE_PACKAGE_UPLOAD_DESTINATION = 'upload/packages/';
+constants.BASE_PACKAGE_UPLOAD_DESTINATION = 'upload';
 constants.PACKAGE_MANAGER_SERVER_PORT = 4567;
 constants.PACKAGE_FILE_DELETE_DELAY_MILLISECONDS = 3 * 1000 * 60; //5 minutes
 constants.PACKAGE_FILE_ONBOARD_TRANSACTION_STATUS_CHECK_DELAY_MILLISECONDS = 2 * 1000; //2 seconds
 
+constants.REFRESH_WINDOW = 10; //Time in seconds to perform a token refresh before it expires
+constants.LAUNCHPAD_ADDRESS = 'localhost';
+constants.LAUNCHPAD_PORT = 8008;
+constants.IDP_SERVER_PROTOCOL = 'https';
+constants.IDP_PORT_NUMBER = 8009;
+constants.CALLBACK_SERVER_PROTOCOL = 'https';
+constants.CALLBACK_PORT_NUMBER = 8443;
+constants.CALLBACK_ADDRESS = 'localhost';
+constants.END_SESSION_PATH = 'end_session';
 
 module.exports = constants;
\ No newline at end of file
diff --git a/skyquake/framework/core/api_utils/csrf.js b/skyquake/framework/core/api_utils/csrf.js
new file mode 100644 (file)
index 0000000..62855f2
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ *
+ *   Copyright 2017 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.
+ *
+ */
+
+/**
+ * CSRF util for use across the api_server.
+ * @module framework/core/api_utils/csrf
+ * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
+ */
+
+var constants = require('./constants.js');
+var utils = require('./utils.js');
+
+var target = null;
+
+function configure(config) {
+       target = config.target;
+}
+
+function csrfCheck(req, res, next) {
+       var host = null;
+
+       if (req.headers.origin != 'null') {
+               host = utils.getHostNameFromURL(req.headers.origin);
+       } else if (req.headers.referer) {
+               host = utils.getHostNameFromURL(req.headers.referer);
+       } else {
+               var msg = 'Request did not contain an origin or referer header. Request terminated.';
+               var error = {};
+               error.statusCode = constants.HTTP_RESPONSE_CODES.ERROR.METHOD_NOT_ALLOWED;
+               error.errorMessage = {
+                       error: msg
+               }
+               return utils.sendErrorResponse(error, res);
+       }
+
+       if (!host || host != target) {
+               var msg = 'Request did not originate from authorized source (Potential CSRF attempt). Request terminated.';
+               var error = {};
+               error.statusCode = constants.HTTP_RESPONSE_CODES.ERROR.METHOD_NOT_ALLOWED;
+               error.errorMessage = {
+                       error: msg
+               }
+               return utils.sendErrorResponse(error, res);
+       } else {
+               return next();
+       }
+}
+
+module.exports = {
+       configure: configure,
+       csrfCheck: csrfCheck
+};
\ No newline at end of file
diff --git a/skyquake/framework/core/api_utils/openidconnect_config.json b/skyquake/framework/core/api_utils/openidconnect_config.json
new file mode 100644 (file)
index 0000000..63c4a1d
--- /dev/null
@@ -0,0 +1,9 @@
+{
+       "scope": "openid",
+    "clientID": "cncudWkub3BlbmlkY2xpZW50",
+    "clientSecret": "riftiorocks",
+    "authorizationURL": "/authorization",
+    "tokenURL": "/token",
+    "userInfoURL": "/userinfo",
+    "callbackURL": "/callback"
+}
\ No newline at end of file
index 5e0b25b..a86f5ed 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -33,6 +33,7 @@ var url = require('url');
 var sockjs = require('sockjs');
 var websocket_multiplex = require('websocket-multiplex');
 var utils = require('./utils.js');
+var configurationAPI = require('../modules/api/configuration.js');
 
 
 var Subscriptions = function() {
@@ -182,7 +183,7 @@ Subscriptions.prototype.socketInstance = function(url, req, wss, Type, channelId
       if (Type == PollingSocket) {
         Socket = new Type(url, req, 1000, req.body);
       } else {
-        Socket = new Type(url);
+        Socket = new Type(url, ['Bearer', req.session.passport.user.user['access_token']]);
       }
       console.log('Socket assigned for url', url);
     }
@@ -278,12 +279,12 @@ function PollingSocket(url, req, interval, config) {
   self.isClosed = false;
   var requestHeaders = {};
   _.extend(requestHeaders, {
-    'Authorization': req.get('Authorization')
+    Cookie: req.get('Cookie')
   });
 
   var pollServer = function() {
     Request({
-      url: url,
+      url: utils.projectContextUrl(req, url),
       method: config.method || 'GET',
       headers: requestHeaders,
       json: config.payload,
@@ -294,7 +295,11 @@ function PollingSocket(url, req, interval, config) {
         console.log('Error polling: ' + url);
       } else {
         if (!self.isClosed) {
-          self.poll = setTimeout(pollServer, 1000 || interval);
+          if(process.env.DISABLE_POLLING != "TRUE") {
+            self.poll = setTimeout(pollServer, 1000 || interval);
+          } else {
+            console.log('Polling is disabled. Finishing request.')
+          }
           var data = response.body;
           if (self.onmessage) {
             self.onmessage(data);
index 5b17279..cdf12fc 100644 (file)
@@ -49,6 +49,41 @@ var confdPort = function(api_server) {
        return api_server + ':' + CONFD_PORT;
 };
 
+var projectContextUrl = function(req, url) {
+       //NOTE: We need to go into the sessionStore because express-session
+       // does not reliably update the session.
+       // See https://github.com/expressjs/session/issues/450
+       var projectId = (req.session &&
+                                        req.sessionStore &&
+                                        req.sessionStore.sessions &&
+                                        req.sessionStore.sessions[req.session.id] &&
+                                        JSON.parse(req.sessionStore.sessions[req.session.id])['projectId']) ||
+                                        (null);
+       if (projectId) {
+               projectId = encodeURIComponent(projectId);
+               return url.replace(/(\/api\/operational\/|\/api\/config\/)(.*)/, '$1project/' + projectId + '/$2');
+       }
+       return url;
+}
+
+var addProjectContextToRPCPayload = function(req, url, inputPayload) {
+       //NOTE: We need to go into the sessionStore because express-session
+       // does not reliably update the session.
+       // See https://github.com/expressjs/session/issues/450
+       var projectId = (req.session &&
+                                        req.sessionStore &&
+                                        req.sessionStore.sessions &&
+                                        req.sessionStore.sessions[req.session.id] &&
+                                        JSON.parse(req.sessionStore.sessions[req.session.id])['projectId']) ||
+                                        (null);
+       if (projectId) {
+               if (url.indexOf('/api/operations/')) {
+                       inputPayload['project-name'] = projectId;
+               }
+       }
+       return inputPayload;
+}
+
 
 var validateResponse = function(callerName, error, response, body, resolve, reject) {
        var res = {};
@@ -61,12 +96,12 @@ var validateResponse = function(callerName, error, response, body, resolve, reje
                };
                reject(res);
                return false;
-       } else if (response.statusCode >= 400) {
+       } else if (response.statusCode >= CONSTANTS.HTTP_RESPONSE_CODES.ERROR.BAD_REQUEST) {
                console.log('Problem with "', callerName, '": ', response.statusCode, ':', body);
                res.statusCode = response.statusCode;
 
                // auth specific
-               if (response.statusCode == 401) {
+               if (response.statusCode == CONSTANTS.HTTP_RESPONSE_CODES.ERROR.UNAUTHORIZED) {
                        res.errorMessage = {
                                error: 'Authentication needed' + body
                        };
@@ -81,7 +116,7 @@ var validateResponse = function(callerName, error, response, body, resolve, reje
 
                reject(res);
                return false;
-       } else if (response.statusCode == 204) {
+       } else if (response.statusCode == CONSTANTS.HTTP_RESPONSE_CODES.SUCCESS.NO_CONTENT) {
                resolve({
                        statusCode: response.statusCode,
                        data: {}
@@ -95,7 +130,7 @@ var validateResponse = function(callerName, error, response, body, resolve, reje
 
 var checkAuthorizationHeader = function(req) {
        return new Promise(function(resolve, reject) {
-               if (req.get('Authorization') == null) {
+               if (req.session && req.session.authorization == null) {
                        reject();
                } else {
                        resolve();
@@ -119,12 +154,12 @@ if (process.env.LOG_REQUESTS) {
                                reject(res);
                                fs.appendFileSync(logFile, 'Request API: ' + response.request.uri.href + ' ; ' + 'Error: ' + error);
                                return false;
-                       } else if (response.statusCode >= 400) {
+                       } else if (response.statusCode >= CONSTANTS.HTTP_RESPONSE_CODES.ERROR.BAD_REQUEST) {
                                console.log('Problem with "', callerName, '": ', response.statusCode, ':', body);
                                res.statusCode = response.statusCode;
 
                                // auth specific
-                               if (response.statusCode == 401) {
+                               if (response.statusCode == CONSTANTS.HTTP_RESPONSE_CODES.ERROR.UNAUTHORIZED) {
                                        res.errorMessage = {
                                                error: 'Authentication needed' + body
                                        };
@@ -140,7 +175,7 @@ if (process.env.LOG_REQUESTS) {
                                reject(res);
                                fs.appendFileSync(logFile, 'Request API: ' + response.request.uri.href + ' ; ' + 'Error Body: ' + body);
                                return false;
-                       } else if (response.statusCode == 204) {
+                       } else if (response.statusCode == CONSTANTS.HTTP_RESPONSE_CODES.SUCCESS.NO_CONTENT) {
                                resolve();
                                fs.appendFileSync(logFile, 'Request API: ' + response.request.uri.href + ' ; ' + 'Response Body: ' + body);
                                return false;
@@ -162,6 +197,9 @@ if (process.env.LOG_REQUESTS) {
  * @param {Function} res - a handle to the express response function
  */
 var sendErrorResponse = function(error, res) {
+       if (!error.statusCode) {
+               console.error('Status Code has not been set in error object: ', error);
+       }
        res.status(error.statusCode);
        res.send(error);
 }
@@ -197,10 +235,10 @@ var passThroughConstructor = function(app) {
                }
                new Promise(function(resolve, reject) {
                        request({
-                               uri: uri,
+                               uri: projectContextUrl(req, uri),
                                method: 'GET',
                                headers: _.extend({}, CONSTANTS.HTTP_HEADERS.accept[type], {
-                                       'Authorization': req.get('Authorization'),
+                                       'Authorization': req.session && req.session.authorization,
                                        forever: CONSTANTS.FOREVER_ON,
                                        rejectUnauthorized: false,
                                })
@@ -226,6 +264,31 @@ var getPortForProtocol = function(protocol) {
   }
 }
 
+var buildRedirectURL = function(req, globalConfiguration, plugin, extra) {
+    var api_server = req.query['api_server'] || (req.protocol + '://' + globalConfiguration.get().api_server);
+    var download_server = req.query['dev_download_server'] || globalConfiguration.get().dev_download_server;
+       var url = '/';
+       url += plugin;
+       url += '/?api_server=' + api_server;
+       url += download_server ? '&dev_download_server=' + download_server : '';
+       url += extra || '';
+       return url;
+}
+
+var getHostNameFromURL = function(url) {
+    var match = url.match(/^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)([^?#]*)(\?[^#]*|)(#.*|)$/);
+    return match && match[3];
+}
+
+var dataToJsonSansPropNameNamespace = function(s) {
+       var a = JSON.parse(s);
+       var b = JSON.stringify(a);
+       var c = b.replace(/{"[-\w]+:/g, '{"');
+       var d = c.replace(/,"[-\w]+:/g, ',"');
+       var j = JSON.parse(d);
+       return j;
+}
+
 module.exports = {
        /**
         * Ensure confd port is on api_server variable.
@@ -244,5 +307,15 @@ module.exports = {
 
     passThroughConstructor: passThroughConstructor,
 
-    getPortForProtocol: getPortForProtocol
+    getPortForProtocol: getPortForProtocol,
+
+    projectContextUrl: projectContextUrl,
+
+       addProjectContextToRPCPayload: addProjectContextToRPCPayload,
+
+       buildRedirectURL: buildRedirectURL,
+
+       getHostNameFromURL: getHostNameFromURL,
+
+       dataToJsonSansPropNameNamespace: dataToJsonSansPropNameNamespace
 };
diff --git a/skyquake/framework/core/modules/api/appConfigAPI.js b/skyquake/framework/core/modules/api/appConfigAPI.js
new file mode 100644 (file)
index 0000000..ce93bdc
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 
+ *   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 Schema = {};
+var request = require('request');
+var Promise = require('promise');
+var constants = require('../../api_utils/constants');
+var utils = require('../../api_utils/utils');
+var _ = require('lodash');
+var cors = require('cors');
+var bodyParser = require('body-parser');
+var utils = require('../../api_utils/utils');
+var sessionAPI = require('./sessions.js');
+var configuration = require('./configuration');
+
+var router = require('express').Router();
+
+router.use(bodyParser.json());
+router.use(cors());
+router.use(bodyParser.urlencoded({
+    extended: true
+}));
+
+router.get('/app-config', cors(), function (req, res) {
+    getConfig(req).then(function (response) {
+        utils.sendSuccessResponse(response, res);
+    }, function (error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+var inactivityTimeout = process.env.UI_TIMEOUT_SECS || 600000;
+
+var versionPromise = null;
+
+var init = function () {
+    versionPromise = new Promise(
+        function (resolve, reject) {
+            sessionAPI.sessionPromise.then(
+                function (session) {
+                    request({
+                            url: configuration.getBackendURL() + '/api/operational/version',
+                            type: 'GET',
+                            headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                                'Authorization': session.authorization
+                            }),
+                            forever: constants.FOREVER_ON,
+                            rejectUnauthorized: false
+                        },
+                        function (error, response, body) {
+                            var data;
+                            if (utils.validateResponse('schema/version.get', error, response, body, resolve, reject)) {
+                                try {
+                                    data = JSON.parse(response.body)['rw-base:version'];
+                                    resolve(data.version);
+                                } catch (e) {
+                                    return reject({});
+                                }
+                            } else {
+                                console.log(error);
+                            }
+                        });
+                });
+        });
+}
+
+var getConfig = function (req) {
+    var api_server = req.query['api_server'];
+
+    var requests = [versionPromise];
+
+    return new Promise(function (resolve, reject) {
+        Promise.all(requests).then(
+            function (results) {
+                var data = {
+                    version: results[0],
+                    'api-server': configuration.getBackendURL,
+                    'inactivity-timeout': process.env.UI_TIMEOUT_SECS || 600000 
+                }
+                resolve({
+                    data: data,
+                    statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+                });
+            }).catch(
+            function (error) {
+                var response = {};
+                console.log('Problem with config.get', error);
+                response.statusCode = error.statusCode || 500;
+                response.errorMessage = {
+                    error: 'Failed to get config' + error
+                };
+                reject(response);
+            });
+    });
+};
+
+module.exports = {
+    getRouter: function () {
+        return router;
+    },
+    init: init
+};
\ No newline at end of file
index 3762643..d1fca27 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,7 +30,11 @@ var configurationAPI = {};
 var _ = require('lodash');
 var GLOBAL_CONFIGURATION = {
     api_server: 'localhost',
-    ssl_enabled: true
+    ssl_enabled: true,
+    api_server_port_number: constants.SECURE_SERVER_PORT,
+    idp_server_address: constants.LAUNCHPAD_ADDRESS,
+    idp_server_protocol: constants.IDP_SERVER_PROTOCOL,
+    idp_server_port_number: constants.IDP_PORT_NUMBER
 };
 
 /**
@@ -92,4 +96,18 @@ configurationAPI.globalConfiguration.get = function() {
     return GLOBAL_CONFIGURATION;
 };
 
+var backendURL = null;
+configurationAPI.getBackendURL = function () {
+       if (!backendURL) {
+               backendURL = GLOBAL_CONFIGURATION.api_server_protocol + '://' + GLOBAL_CONFIGURATION.api_server + ':' + GLOBAL_CONFIGURATION.api_server_port_number;
+       }
+       return backendURL;
+}
+
+configurationAPI.getBackendAPI = function () {
+       return configurationAPI.getBackendURL() + '/v2/api';
+}
+
+
+
 module.exports = configurationAPI;
index b0223b2..34d30b3 100644 (file)
@@ -36,7 +36,7 @@ ModelMeta.get = function(req) {
                 uri: utils.confdPort(api_server) + '/api/schema/nsd-catalog/nsd',
                 method: 'GET',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
@@ -46,7 +46,7 @@ ModelMeta.get = function(req) {
                 uri: utils.confdPort(api_server) + '/api/schema/vnfd-catalog/vnfd',
                 method: 'GET',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
diff --git a/skyquake/framework/core/modules/api/modelAPI.js b/skyquake/framework/core/modules/api/modelAPI.js
new file mode 100644 (file)
index 0000000..1f14cb7
--- /dev/null
@@ -0,0 +1,359 @@
+/*
+ * 
+ *   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 Schema = {};
+var rp = require('request-promise');
+var Promise = require('promise');
+var constants = require('../../api_utils/constants');
+var utils = require('../../api_utils/utils');
+var _ = require('lodash');
+var cors = require('cors');
+var bodyParser = require('body-parser');
+var utils = require('../../api_utils/utils');
+var configuration = require('./configuration');
+
+var router = require('express').Router();
+
+
+router.use(bodyParser.json());
+router.use(cors());
+router.use(bodyParser.urlencoded({
+    extended: true
+}));
+
+router.get('/model', cors(), function (req, res) {
+    get(req).then(function (response) {
+        utils.sendSuccessResponse(response, res);
+    }, function (error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+router.patch('/model', cors(), function (req, res) {
+    update(req).then(function (response) {
+        utils.sendSuccessResponse(response, res);
+    }, function (error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+router.put('/model', cors(), function (req, res) {
+    add(req).then(function (response) {
+        utils.sendSuccessResponse(response, res);
+    }, function (error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+router.delete('/model', cors(), function (req, res) {
+    remove(req).then(function (response) {
+        utils.sendSuccessResponse(response, res);
+    }, function (error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+module.exports = {
+    getRouter: function () {
+        return router;
+    },
+    init: function () {}
+};
+
+get = function (req) {
+    var backend = configuration.getBackendAPI();
+    var modelPath = req.query['path'];
+    var requestHeaders = _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+        'Authorization': req.session && req.session.authorization
+    })
+    return new Promise(function (resolve, reject) {
+        Promise.all([
+            rp({
+                uri: backend + '/config' + modelPath,
+                method: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+                // }),
+                // rp({
+                //     uri: utils.projectContextUrl(req, backend + '/api/operational' + modelPath),
+                //     method: 'GET',
+                //     headers: requestHeaders,
+                //     forever: constants.FOREVER_ON,
+                //     rejectUnauthorized: false,
+                //     resolveWithFullResponse: true
+            })
+        ]).then(function (results) {
+            var response = {
+                statusCode: results[0].statusCode || 200,
+                data: [null]
+            }
+            if (results[0].body && !results[0].error) {
+                var result = JSON.parse(results[0].body);
+                if (result.collection) {
+                    result = result.collection[Object.keys(result.collection)[0]];
+                    if (!result.length) {
+                        result = null;
+                    } else if (result.length === 1) {
+                        result = result[0];
+                    }
+                }
+                response.data = result;
+            }
+            resolve(response);
+
+        }).catch(function (error) {
+            var res = {};
+            console.log('Problem with model get', error);
+            res.statusCode = error.statusCode || 500;
+            res.errorMessage = {
+                error: 'Failed to get model: ' + error
+            };
+            reject(res);
+        });
+    });
+};
+
+add = function (req) {
+    var backend = configuration.getBackendAPI();
+    var modelPath = req.query['path'];
+    var targetProperty = modelPath.split('/').pop();
+    var newElement = {};
+    var data = {};
+    console.log(req.body);
+    _.forIn(req.body, function (value, key) {
+        if (_.isObject(value)) {
+            if (value.type === 'leaf_empty') {
+                if (value.data) {
+                    data[key] = ' ';
+                }
+            } else if (value.type === 'leaf_list') {
+                data[key] = value.data.add;
+            }
+        } else {
+            data[key] = value;
+        }
+    });
+    newElement[targetProperty] = [data];
+    console.log(newElement);
+    var target = backend + '/config' + modelPath;
+    var method = 'POST'
+    var requestHeaders = _.extend({},
+        constants.HTTP_HEADERS.accept.data, {
+            'Authorization': req.session && req.session.authorization
+        });
+    return new Promise(function (resolve, reject) {
+        rp({
+            uri: target,
+            method: method,
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            json: newElement,
+            rejectUnauthorized: false,
+            resolveWithFullResponse: true
+        }).then(function (results) {
+            var response = {};
+            response.data = {
+                path: modelPath,
+                data: data
+            };
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK;
+            console.log(response);
+            resolve(response);
+        }).catch(function (result) {
+            var response = {};
+            var error = {};
+            if (result.error['rpc-reply']) {
+                error.type = result.error['rpc-reply']['rpc-error']['error-tag'];
+                error.message = result.error['rpc-reply']['rpc-error']['error-message'];
+                error.rpcError = result.error['rpc-reply']['rpc-error']
+            } else {
+                error.type = 'api-error';
+                error.message = 'invalid api call';
+            }
+            console.log('Problem with model update', error);
+            response.statusCode = error.statusCode || 500;
+            response.error = error;
+            reject(response);
+        });
+    });
+};
+
+update = function (req) {
+    var backend = configuration.getBackendAPI();
+    var modelPath = req.query['path'];
+    var requestHeaders = _.extend({},
+        constants.HTTP_HEADERS.accept.data, {
+            'Authorization': req.session && req.session.authorization
+        });
+    var base = backend + '/config' + modelPath + '/';
+
+    function getUpdatePromise(name, value) {
+        var data = {};
+        data[name] = value;
+        return new Promise(function (resolve, reject) {
+            rp({
+                uri: base + name,
+                method: value ? 'PATCH' : 'DELETE',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                json: data,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            }).then(function (result) {
+                resolve({
+                    element: name,
+                    success: true,
+                    value: value
+                });
+            }).catch(function (result) {
+                var error = {};
+                if (result.error['rpc-reply']) {
+                    error.type = result.error['rpc-reply']['rpc-error']['error-tag'];
+                    error.message = result.error['rpc-reply']['rpc-error']['error-message'];
+                    error.rpcError = result.error['rpc-reply']['rpc-error']
+                } else {
+                    error.type = 'api-error';
+                    error.message = 'invalid api call';
+                }
+                resolve({
+                    element: name,
+                    success: false,
+                    error: error,
+                    value: value
+                });
+            })
+        })
+    }
+
+    function getDeletePromise(targetProp, item) {
+        if (item) {
+            targetProp = targetProp + '/' + item;
+        }
+        return getUpdatePromise(targetProp, '');
+    }
+
+    var updates = [];
+    _.forIn(req.body, function (value, key) {
+        var data = {};
+        if (_.isObject(value)) {
+            if (value.type === 'leaf_list') {
+                _.forEach(value.data.remove, function (v) {
+                    updates.push(getDeletePromise(key))
+                })
+                _.forEach(value.data.add, function (v) {
+                    updates.push(getUpdatePromise(key, v))
+                })
+            } else if (value.type === 'leaf_empty') {
+                if (value.data) {
+                    updates.push(getUpdatePromise(key, ' '))
+                } else {
+                    updates.push(getDeletePromise(key))
+                }
+            }
+        } else {
+            updates.push(getUpdatePromise(key, value))
+        }
+    })
+
+    return new Promise(function (resolve, reject) {
+        Promise.all(updates).then(function (results) {
+            var response = {};
+            var output = {};
+            var hasError = false;
+            _.forEach(results, function (result) {
+                var record = {};
+                if (output[result.element]) {
+                    if (_.isArray(output[result.element].value)) {
+                        output[result.element].value.push(result.value);
+                    } else {
+                        output[result.element].value = [output[result.element].value, result.value];
+                    }
+                } else {
+                    output[result.element] = result;
+                }
+                hasError = hasError || !result.success
+            })
+            response.data = {
+                result: output,
+                hasError: hasError
+            };
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK;
+            console.log(response);
+            resolve(response);
+        }).catch(function (result) {
+            var response = {};
+            var error = {};
+            if (result.error['rpc-reply']) {
+                error.type = result.error['rpc-reply']['rpc-error']['error-tag'];
+                error.message = result.error['rpc-reply']['rpc-error']['error-message'];
+                error.rpcError = result.error['rpc-reply']['rpc-error']
+            } else {
+                error.type = 'api-error';
+                error.message = 'invalid api call';
+            }
+            console.log('Problem with model update', error);
+            response.statusCode = error.statusCode || 500;
+            response.error = error;
+            reject(response);
+        });
+    });
+};
+
+remove = function (req) {
+    var backend = configuration.getBackendAPI();
+    var modelPath = req.query['path'];
+    var target = backend + '/config' + modelPath;
+    var requestHeaders = _.extend({},
+        constants.HTTP_HEADERS.accept.data,
+        constants.HTTP_HEADERS.content_type.data, {
+            'Authorization': req.session && req.session.authorization
+        })
+    return new Promise(function (resolve, reject) {
+        rp({
+            url: target,
+            method: 'DELETE',
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+        }).then(function (response) {
+            return resolve({
+                statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                data: modelPath
+            });
+        }).catch(function (result) {
+            var response = {};
+            var error = {};
+            if (result.error['rpc-reply']) {
+                error.type = result.error['rpc-reply']['rpc-error']['error-tag'];
+                error.message = result.error['rpc-reply']['rpc-error']['error-message'];
+                error.rpcError = result.error['rpc-reply']['rpc-error']
+            } else {
+                error.type = 'api-error';
+                error.message = 'invalid api call';
+            }
+            console.log('Problem with model update', error);
+            response.statusCode = error.statusCode || 500;
+            response.error = error;
+            reject(response);
+        });
+    })
+}
\ No newline at end of file
diff --git a/skyquake/framework/core/modules/api/projectManagementAPI.js b/skyquake/framework/core/modules/api/projectManagementAPI.js
new file mode 100644 (file)
index 0000000..d65e890
--- /dev/null
@@ -0,0 +1,297 @@
+/*
+ *
+ *   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 ProjectManagement = {};
+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');
+var API_VERSION = 'v2';
+ProjectManagement.get = function(req, fields) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    // by default just load basic info as this request is expensive
+    fields = fields || ['name', 'description', 'project-config'];
+    var select = fields.length ? '?fields=' + fields.join(';') : '';
+
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: `${utils.confdPort(api_server)}/${API_VERSION}/api/operational/project` + select,
+                method: 'GET',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            })
+        ]).then(function(result) {
+            var response = {};
+            response['data'] = {};
+            if (result[0].body) {
+                response['data']['project'] = JSON.parse(result[0].body)['rw-project:project'];
+            }
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+
+            resolve(response);
+        }).catch(function(error) {
+            var response = {};
+            console.log('Problem with ProjectManagement.get', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to get ProjectManagement' + error
+            };
+            reject(response);
+        });
+    });
+};
+
+ProjectManagement.create = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var data = req.body;
+    data = {
+        "project":[data]
+    }
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) + '/' + API_VERSION  + '/api/config/project',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.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 ProjectManagement.create', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to create user' + error
+            };
+            reject(response);
+        });
+    });
+};
+ProjectManagement.update = function(req) {
+    //"rw-project:project"
+    var self = this;
+    var api_server = req.query['api_server'];
+    var bodyData = req.body;
+    // oddly enough, if we do not encode this here letting the request below does so incorrectly
+    var projectName = encodeURIComponent(bodyData.name);
+    var descriptionData = {
+        "rw-project:project" : {
+            "name": bodyData.name,
+            "description": bodyData.description
+        }
+    }
+    var updateTasks = [];
+    var baseUrl = utils.confdPort(api_server) + '/' + API_VERSION  + '/api/config/project/' + projectName
+    var updateProjectConfig = rp({
+                uri: baseUrl + '/project-config',
+                method: 'PUT',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                json: {
+                    "project-config": bodyData['project-config']
+                },
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            });
+    updateTasks.push(updateProjectConfig);
+
+    var updateProjectDescription = rp({
+                uri: baseUrl + '/description',
+                method: 'PATCH',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                json: {"description": bodyData.description},
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            });
+    updateTasks.push(updateProjectDescription)
+    return new Promise(function(resolve, reject) {
+        Promise.all(
+            updateTasks
+        ).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 ProjectManagement.update', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to update project - ' + error
+            };
+            reject(response);
+        });
+    });
+};
+
+ProjectManagement.delete = function(req) {
+    var self = this;
+    var projectname = encodeURIComponent(req.params.projectname);
+    var api_server = req.query["api_server"];
+    var requestHeaders = {};
+    var url = utils.confdPort(api_server) + '/' + API_VERSION  + '/api/config/project/' + projectname
+    return new Promise(function(resolve, reject) {
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.data,
+            constants.HTTP_HEADERS.content_type.data, {
+                'Authorization': req.session && req.session.authorization
+            });
+        rp({
+            url: url,
+            method: 'DELETE',
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+        }, function(error, response, body) {
+            if (utils.validateResponse('ProjectManagement.DELETE', error, response, body, resolve, reject)) {
+                return resolve({
+                    statusCode: response.statusCode,
+                    data: JSON.stringify(response.body)
+                });
+            };
+        });
+    })
+}
+
+
+ProjectManagement.getPlatform = function(req, userId) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var user = req.params['userId'] || userId;
+    return new Promise(function(resolve, reject) {
+        var url = utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/operational/rbac-platform-config';
+        if(user) {
+            url = url + '/user/' + encodeURIComponent(user);
+        }
+        Promise.all([
+            rp({
+                uri: url,
+                method: 'GET',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            })
+        ]).then(function(result) {
+            var response = {};
+            response['data'] = {};
+            if (result[0].body) {
+                if(user) {
+                    response['data']['platform'] = JSON.parse(result[0].body)['rw-rbac-platform:user'];
+                } else {
+                    response['data']['platform'] = JSON.parse(result[0].body)['rw-rbac-platform:rbac-platform-config'];
+                }
+            }
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+
+            resolve(response);
+        }).catch(function(error) {
+            var response = {};
+            console.log('Problem with ProjectManagement.getPlatform', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to get ProjectManagement.getPlatform' + error
+            };
+            reject(response);
+        });
+    });
+};
+
+ProjectManagement.updatePlatform = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var bodyData = req.body;
+    data = bodyData;
+    data.user = JSON.parse(data.user)
+    var updateTasks = [];
+
+    var updatePlatform = rp({
+                uri: utils.confdPort(api_server) +   '/' + API_VERSION  + '/api/config/rbac-platform-config',
+                method: 'PUT',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                json: {
+                    "rw-rbac-platform:rbac-platform-config": data
+                },
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            });
+    updateTasks.push(updatePlatform)
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            updateTasks
+        ]).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 ProjectManagement.updatePlatform', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to update platform - ' + error
+            };
+            reject(response);
+        });
+    });
+};
+
+
+module.exports = ProjectManagement;
index 5ba0eb5..03f2721 100644 (file)
@@ -45,7 +45,7 @@ restconfAPI['streams'].get = function(req) {
             url: uri + url + '?deep',
             method: 'GET',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
diff --git a/skyquake/framework/core/modules/api/schemaAPI.js b/skyquake/framework/core/modules/api/schemaAPI.js
new file mode 100644 (file)
index 0000000..34c83be
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 
+ *   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 Schema = {};
+var rp = require('request-promise');
+var Promise = require('promise');
+var constants = require('../../api_utils/constants');
+var utils = require('../../api_utils/utils');
+var _ = require('lodash');
+var cors = require('cors');
+var bodyParser = require('body-parser');
+var utils = require('../../api_utils/utils');
+var configuration = require('./configuration');
+
+var router = require('express').Router();
+
+
+router.use(bodyParser.json());
+router.use(cors());
+router.use(bodyParser.urlencoded({
+    extended: true
+}));
+
+router.get('/schema', cors(), function (req, res) {
+    getSchema(req).then(function (response) {
+        utils.sendSuccessResponse(response, res);
+    }, function (error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+module.exports = {
+    getRouter: function () {
+        return router;
+    },
+    init: function () {}
+};
+
+getSchema = function (req) {
+    var schemaURI = configuration.getBackendURL() + '/api/schema/';
+    var schemaPaths = req.query['request'];
+    var paths = schemaPaths.split(',');
+
+    function getSchemaRequest(path) {
+        return rp({
+            uri: schemaURI + path,
+            method: 'GET',
+            headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
+                'Authorization': req.session && req.session.authorization
+            }),
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+            resolveWithFullResponse: true
+        })
+    }
+
+    var requests = _.map(paths, getSchemaRequest);
+
+    return new Promise(function (resolve, reject) {
+        Promise.all(requests).then(
+            function (results) {
+                var data = {
+                    schema: {}
+                }
+                _.forEach(results, function (result, index) {
+                    data.schema[paths[index]] = JSON.parse(result.body);
+                });
+                resolve({
+                    data: data,
+                    statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+                });
+            }).catch(
+            function (error) {
+                var response = {};
+                console.log('Problem with schema.get', error);
+                response.statusCode = error.statusCode || 500;
+                response.errorMessage = {
+                    error: 'Failed to get schema' + error
+                };
+                reject(response);
+            });
+    });
+};
\ No newline at end of file
diff --git a/skyquake/framework/core/modules/api/sessions.js b/skyquake/framework/core/modules/api/sessions.js
new file mode 100644 (file)
index 0000000..f168724
--- /dev/null
@@ -0,0 +1,294 @@
+/*
+ *
+ *   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.
+ *
+ */
+
+/**
+ * sessions api module. Provides API functions for sessions
+ * @module framework/core/modules/api/sessions
+ * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
+ */
+"use strict"
+var Promise = require('promise');
+var constants = require('../../api_utils/constants');
+var utils = require('../../api_utils/utils');
+var request = utils.request;
+var rp = require('request-promise');
+var sessionsAPI = {};
+var _ = require('lodash');
+var base64 = require('base-64');
+var APIVersion = '/v2';
+var configurationAPI = require('./configuration');
+var UserManagement = require('./userManagementAPI.js');
+var URL = require('url');
+
+// Used for determining what page a user should first go to.
+var Application = {
+    order: [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper",
+        "rw-project:project-admin",
+        "rw-project:project-oper",
+        "rw-project-mano:lcm-admin",
+        "rw-project-mano:lcm-oper",
+        "rw-project-mano:catalog-admin",
+        "rw-project-mano:catalog-oper",
+        "rw-project-mano:account-admin",
+        "rw-project-mano:account-oper"
+    ],
+    key: {
+        "rw-rbac-platform:super-admin": "user_management",
+        "rw-rbac-platform:platform-admin": "user_management",
+        "rw-rbac-platform:platform-oper": "user_management",
+        "rw-project:project-admin": "project_management",
+        "rw-project:project-oper": "project_management",
+        "rw-project-mano:catalog-admin": "composer",
+        "rw-project-mano:catalog-oper": "composer",
+        "rw-project-mano:lcm-admin": "launchpad",
+        "rw-project-mano:lcm-oper": "launchpad",
+        "rw-project-mano:account-admin": "accounts",
+        "rw-project-mano:account-oper": "accounts"
+    }
+};
+
+function logAndReject(mesg, reject, errCode) {
+    var res = {};
+    res.errorMessage = {
+        error: mesg
+    }
+    res.statusCode = errCode || constants.HTTP_RESPONSE_CODES.ERROR.BAD_REQUEST;
+    console.log(mesg);
+    reject(res);
+}
+
+function logAndRedirectToLogin(mesg, res, req, invalid) {
+    console.log(mesg);
+    if (!invalid) {
+        res.redirect(utils.buildRedirectURL(req, configurationAPI.globalConfiguration, 'login', '&referer=' + encodeURIComponent(req.headers.referer)));
+    }
+    res.end();
+}
+
+function logAndRedirectToEndSession(mesg, res, authorization, url) {
+    console.log(mesg);
+    res.set({
+        'Authorization': authorization
+    });
+    res.redirect(url);
+    res.end();
+}
+var sessionPromiseResolve = null;
+sessionsAPI.sessionPromise = new Promise(function(resolve, reject) {
+    sessionPromiseResolve = resolve;
+});
+
+sessionsAPI.create = function (req, res) {
+    if (!req.session.passport){
+        logAndRedirectToLogin("lost session", res, req);
+        return new Promise(function (resolve, reject){reject("lost session")});
+    }
+    var api_server = req.query['api_server'] || (req.protocol + '://' + configurationAPI.globalConfiguration.get().api_server);
+    var uri = utils.confdPort(api_server);
+    var username = req.session.passport.user['username'];
+    var authorization_header_string = 'Bearer ' + req.session.passport.user.user.access_token;
+    return new Promise(function (resolve, reject) {
+        req.session.authorization = authorization_header_string;
+        req.session.api_server = api_server;
+        req.session.api_protocal = req.protocol;
+        req.session.loggedIn = true;
+        req.session.userdata = {
+            username: username,
+        };
+        UserManagement.getUserInfo(req, req.session.passport.user.username).then(function (results) {
+            var project_list_for_user = null;
+            if (!req.session.projectId && results.data.project) {
+                project_list_for_user = Object.keys(results.data.project);
+                if (project_list_for_user.length > 0) {
+                    req.session.projectId = project_list_for_user.sort() && project_list_for_user[0];
+                }
+            }
+            sessionsAPI.setTopApplication(req);
+            req.session.isLCM = results.data.isLCM;
+
+            req.session['ui-state'] = results.data['ui-state'];
+            var lastActiveProject = req.session['ui-state'] && req.session['ui-state']['last-active-project'];
+            if (lastActiveProject) {
+                if (results.data.project.hasOwnProperty(lastActiveProject)) {
+                    req.session.projectId = lastActiveProject;
+                }
+
+            }
+
+            var successMsg = 'User => ' + username + ' successfully logged in.';
+            successMsg += req.session.projectId ? 'Project => ' + req.session.projectId + ' set as default.' : '';
+
+            console.log(successMsg);
+
+            req.session.save(function (err) {
+                if (err) {
+                    console.log('Error saving session to store', err);
+                }
+                // no response data, just redirect now that session data is set
+                if (req.session['ui-state'] && req.session['ui-state']['last-active-uri']) {
+                    var url = URL.parse(req.session['ui-state']['last-active-uri']);
+                    var host = req.headers.host;
+                    var path = url.path;
+                    var hash = url.hash;
+                    var protocol = url.protocol;
+                    var newUrl = protocol + '//' + host + path + (hash?hash:'');
+                    console.log('Redirecting to: ' + newUrl)
+                    res.redirect(newUrl)
+                } else {
+                    if(req.session.topApplication) {
+                        res.redirect(utils.buildRedirectURL(req, configurationAPI.globalConfiguration, req.session.topApplication));
+                    } else {
+                        res.redirect(utils.buildRedirectURL(req, configurationAPI.globalConfiguration, 'user_management', '#/user-profile'));
+                    }
+                }
+            })
+
+            sessionPromiseResolve(req.session);
+
+        }).catch(function (error) {
+            // Something went wrong - Redirect to /login
+            var errorMsg = 'Error logging in or getting list of projects. Error: ' + error;
+            console.log(errorMsg);
+            logAndRedirectToLogin(errorMsg, res, req);
+        });
+    })
+};
+
+sessionsAPI.addProjectToSession = function (req, res) {
+    return new Promise(function (resolve, reject) {
+        if (req.session && req.session.loggedIn == true) {
+            Promise.all([UserManagement.getProfile(req), UserManagement.updateActiveProject(req)]).then(function () {
+                req.session.projectId = req.params.projectId;
+                req.session.topApplication = null;
+                sessionsAPI.setTopApplication(req, req.query.app);
+                req.session.save(function (err) {
+                    if (err) {
+                        console.log('Error saving session to store', err);
+                        var errorMsg = 'Session does not exist or not logged in';
+                        logAndReject(errorMsg, reject, constants.HTTP_RESPONSE_CODES.ERROR.NOT_FOUND);
+                    } else {
+                        var successMsg = 'Added project ' + req.session.projectId + ' to session ' + req.sessionID;
+                        console.log(successMsg);
+                        var response = {
+                            statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                            data: JSON.stringify({
+                                status: successMsg
+                            })
+                        }
+                        return resolve(response);
+                    }
+                    // res.redirect('/');
+                });
+
+            })
+
+        }
+    });
+}
+
+sessionsAPI.delete = function (req, res) {
+    var idpServerAddress = configurationAPI.globalConfiguration.get().idp_server_address;
+    var idpServerProtocol = configurationAPI.globalConfiguration.get().idp_server_protocol;
+    var idpServerPortNumber = configurationAPI.globalConfiguration.get().idp_server_port_number;
+    var idpEndSessionPath = constants.END_SESSION_PATH;
+    var url = idpServerProtocol + '://' +
+        idpServerAddress + ':' +
+        idpServerPortNumber + '/' +
+        idpEndSessionPath;
+    var authorization = req.session.authorization;
+    return new Promise(function (resolve, reject) {
+        Promise.all([
+            UserManagement.updateActiveUri(req),
+            new Promise(function (success, failure) {
+                req.session.destroy(function (err) {
+                    if (err) {
+                        var errorMsg = 'Error deleting session. Error: ' + err;
+                        console.log(errorMsg);
+                        success({
+                            status: 'error',
+                            message: errorMsg
+                        });
+                    }
+
+                    var successMsg = 'Success deleting session';
+                    console.log(successMsg);
+
+                    success({
+                        status: 'success',
+                        message: successMsg
+                    });
+                });
+            })
+        ]).then(function (result) {
+            // assume the session was deleted!
+            var message = 'Session was deleted. Redirecting to end_session';
+            resolve({
+                statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                data: {
+                    url: url,
+                    message: message
+                }
+            });
+
+        }).catch(function (error) {
+            var message = "An error occured while deleting session";
+            resolve({
+                statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
+                data: {
+                    url: url,
+                    message: message
+                }
+            });
+        });
+    });
+}
+
+sessionsAPI.setTopApplication = function (req, suggestedPlugin) {
+    var selectedProject = req.session.projectId;
+    var userProject = selectedProject ? req.session.projectMap[selectedProject] : null;
+    if (userProject) {
+        if (suggestedPlugin) {
+            if (req.session.platformMap['rw-rbac-platform:super-admin']) {
+                topApplication = suggestedPlugin;
+            } else {
+                var roles = _.reduce(Object.keys(Application.key), function (accumulator, role) {
+                    if (Application.key[role] === suggestedPlugin) {
+                        accumulator.push(role);
+                    }
+                    return accumulator;
+                }, []);
+                if (_.some(roles, function (role){return userProject.role[role]})) {
+                    req.session.topApplication = suggestedPlugin;
+                    return;
+                }
+            }
+        }
+        _.some(Application.order, function (role) {
+            if (userProject.role[role] || req.session.platformMap.role[role]) {
+                req.session.topApplication = Application.key[role];
+                return true;
+            }
+            return false;
+        })
+    }
+}
+
+module.exports = sessionsAPI;
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..834df7e
--- /dev/null
@@ -0,0 +1,530 @@
+/*
+ *
+ *   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');
+var ProjectManagementAPI = require('./projectManagementAPI.js');
+var API_VERSION = 'v2';
+
+UserManagement.get = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+
+    return new Promise(function(resolve, reject) {
+        var userConfig = rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/operational/user-config/user',
+                method: 'GET',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            });
+        var userOp = rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/operational/user-state/user',
+                method: 'GET',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            })
+        Promise.all([
+            userConfig,
+            userOp
+        ]).then(function(result) {
+            var response = {};
+            var userConfig = [];
+            var userOpData = {};
+            response['data'] = {};
+            if (result[0].body) {
+                userConfig = JSON.parse(result[0].body)['rw-user:user'];
+            }
+            if (result[1].body) {
+                JSON.parse(result[1].body)['rw-user:user'].map(function(u) {
+                    userOpData[u['user-domain'] + ',' + u['user-name']] = u;
+                })
+            }
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+            response['data']['user'] = userConfig.map(function(u,i) {
+                var mergedData = _.merge(u, userOpData[u['user-domain'] + ',' + u['user-name']]);
+                mergedData.projects = {
+                    ids: [],
+                    data: {}
+                };
+                var projects = mergedData.projects;
+                mergedData.role && mergedData.role.map(function(r) {
+                    if ((r.role != "rw-project:user-self" )&& (r.role != "rw-rbac-platform:user-self")) {
+                        var projectId = r.keys.split(';')[0];
+                        if (projectId == "") {
+                            projectId = "platform"
+                        }
+                        if (!projects.data[projectId]) {
+                            projects.ids.push(projectId);
+                            projects.data[projectId] = [];
+                        }
+                        projects.data[projectId].push(r.role);
+                    }
+                })
+                return mergedData;
+            })
+            resolve(response);
+        }).catch(function(error) {
+            var response = {};
+            console.log('Problem with UserManagement.get', error);
+            response.statusCode = error.statusCode || constants.HTTP_RESPONSE_CODES.ERROR.INTERNAL_SERVER_ERROR;
+            response.errorMessage = {
+                error: 'Failed to get UserManagement' + error
+            };
+            reject(response);
+        });
+    });
+};
+
+
+UserManagement.getProfile = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    return new Promise(function(resolve, reject) {
+        var response = {};
+        try {
+            var userId = req.session.userdata.username
+            response['data'] = {
+                userId: userId,
+                projectId: req.session.projectId,
+                domain: req.session.passport.user.domain
+            };
+            UserManagement.getUserInfo(req, userId).then(function(result) {
+                response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK;
+                response.data.data = result.data
+                resolve(response);
+            }, function(error) {
+                console.log('Error retrieving getUserInfo');
+                response.statusCode = constants.HTTP_RESPONSE_CODES.ERROR.INTERNAL_SERVER_ERROR;
+                reject(response);
+            })
+        } catch (e) {
+            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.getUserInfo = function(req, userId, domain) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var id = req.params['userId'] || userId;
+    var domain = req.params['domainId'] || domain;
+    var response = {};
+    return new Promise(function(resolve, reject) {
+        if (id) {
+            var getProjects = ProjectManagementAPI.get(req, ['name', 'project-config']);
+            var getPlatformUser = ProjectManagementAPI.getPlatform(req, id);
+            var getUserUiState = UserManagement.getUserUiState(req);
+            Promise.all([
+                getProjects,
+                getPlatformUser,
+                getUserUiState
+            ]).then(function(result) {
+                var userData = {
+                    platform: {
+                        role: {
+
+                        }
+                    },
+                    //id/key values for each project
+                    projectId:[],
+                    project: {
+                        /**
+                         *  [projectId] : {
+                         *      data: [project object],
+                         *      role: {
+                         *          [roleId]: true
+                         *      }
+                         *  }
+                         */
+                    }
+                }
+                //Build UI state
+                var uiState = result[2].data && result[2].data['rw-user:user'];
+                userData['ui-state'] = uiState['ui-state'];
+                //Build platform roles
+                var platformRoles = result[1].data.platform && result[1].data.platform.role;
+                platformRoles && platformRoles.map(function(r) {
+                    userData.platform.role[r.role] = true
+                });
+                //Build project roles
+                var projects = result[0].data.project;
+                var userProjects = [];
+                projects && projects.map(function(p, i) {
+                    userData.project[p.name] = {
+                        data: p,
+                        role: {}
+                    }
+                    userData.projectId.push(p.name);
+                    if (userData.platform.role['rw-rbac-platform:super-admin']) {
+                        userData.project[p.name] = {
+                            data: p,
+                            role: {
+                                "rw-project:project-admin": true,
+                                "rw-project:project-oper": true,
+                                "rw-project-mano:account-admin": true,
+                                "rw-project-mano:account-oper": true,
+                                "rw-project-mano:catalog-admin": true,
+                                "rw-project-mano:catalog-oper": true,
+                                "rw-project-mano:lcm-admin": true,
+                                "rw-project-mano:lcm-oper": true
+                            }
+                        }
+                    } else {
+                        var users = p['project-config'] && p['project-config'].user;
+                        users && users.map(function(u) {
+                            if(u['user-name'] == id) {
+                                u.role && u.role.map(function(r) {
+                                    userData.project[p.name].role[r.role] = true;
+                                    if (r.role === 'rw-project:project-admin') {
+                                        userData.project[p.name].role["rw-project-mano:account-admin"] = true;
+                                        userData.project[p.name].role["rw-project-mano:catalog-admin"] = true;
+                                        userData.project[p.name].role["rw-project-mano:lcm-admin"] = true;
+                                        userData.isLCM = true;
+                                    } else if (r.role === 'rw-project:project-oper') {
+                                        userData.project[p.name].role["rw-project-mano:account-oper"] = true;
+                                        userData.project[p.name].role["rw-project-mano:catalog-oper"] = true;
+                                        userData.project[p.name].role["rw-project-mano:lcm-oper"] = true;
+                                        userData.isLCM = true;
+                                    }
+                                });
+                                u["rw-project-mano:mano-role"] && u["rw-project-mano:mano-role"] .map(function(r) {
+                                    userData.project[p.name].role[r.role] = true;
+                                    if (r.role.indexOf('rw-project-mano:lcm') > -1) {
+                                        userData.isLCM = true;
+                                    }
+                                });
+                            }
+                        })
+                    }
+                });
+                response.data = userData;
+                response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK;
+
+                req.session.projectMap = userData.project;
+                req.session.platformMap = userData.platform;
+                resolve(response);
+            })
+        } else {
+            var errorMsg = 'userId not specified in UserManagement.getUserInfo';
+            console.error(errorMsg);
+            response.statusCode = constants.HTTP_RESPONSE_CODES.ERROR.BAD_REQUEST;
+            response.error = errorMsg;
+            reject(response)
+        }
+
+    })
+}
+UserManagement.create = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var data = req.body;
+    data = {
+        "user":[data]
+    }
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/config/user-config',
+                method: 'POST',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.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.update = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var bodyData = req.body;
+    data = {
+        "rw-user:user": bodyData
+    }
+    var updateTasks = [];
+    if(bodyData.hasOwnProperty('old-password')) {
+        var changePW = rp({
+            uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/operations/change-password',
+            method: 'POST',
+            headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                'Authorization': req.session && req.session.authorization
+            }),
+            forever: constants.FOREVER_ON,
+            json: {
+                "input": {
+                    'user-name' : bodyData['user-name'],
+                    'user-domain' : bodyData['user-domain'],
+                    'old-password' : bodyData['old-password'],
+                    'new-password' : bodyData['new-password'],
+                    'confirm-password' : bodyData['confirm-password'],
+                }
+            },
+            rejectUnauthorized: false,
+            resolveWithFullResponse: true
+        });
+        updateTasks.push(changePW);
+    };
+    var updateUser = rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/config/user-config/user/' + encodeURIComponent(bodyData['user-name']) + ',' +  encodeURIComponent(bodyData['user-domain']),
+                method: 'PUT',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                json: data,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            });
+    updateTasks.push(updateUser)
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            updateTasks
+        ]).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.passwordChange', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to passwordChange 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_VERSION}/api/config/user-config/user/${encodeURIComponent(username)},${encodeURIComponent(domain)}`
+    return new Promise(function(resolve, reject) {
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.data,
+            constants.HTTP_HEADERS.content_type.data, {
+                'Authorization': req.session && req.session.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)
+                });
+            };
+        });
+    })
+};
+UserManagement.getUserUiState = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var user = req.session.passport.user;
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/config/user-config/user/'+encodeURIComponent(user.username) + ',' + encodeURIComponent(user.domain),
+                method: 'GET',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.authorization
+                }),
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                resolveWithFullResponse: true
+            })
+        ]).then(function(result) {
+            var response = {};
+            response['data'] = {};
+            if (result[0].body) {
+                response['data'] = JSON.parse(result[0].body);
+            }
+            response.statusCode = constants.HTTP_RESPONSE_CODES.SUCCESS.OK
+
+            resolve(response);
+        }).catch(function(error) {
+            var response = {};
+            console.log('Problem with UserManagement.getUserUiState', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to create user' + error
+            };
+            reject(response);
+        });
+    });
+};
+UserManagement.updateActiveProject = function(req) {
+    var self = this;
+    var api_server = req.query['api_server'];
+    var user = req.session.passport.user;
+    var data = {
+        "rw-user:user-config": {
+            "user":{
+                "user-name" : user.username,
+                "user-domain": user.domain,
+                "ui-state": {
+                     "last-active-project" : req.params.projectId
+                }
+            }
+        }
+    }
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/config/user-config',
+                method: 'PATCH',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.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.updateActiveProject', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to create user' + error
+            };
+            reject(response);
+        });
+    });
+};
+UserManagement.updateActiveUri = function(req) {
+    if (!req.session.passport) {
+        console.debug("passport gone before we got the save the active uri");
+        var response = {
+            statusCode: 500,
+            errorMessage: {
+                error: 'Failed to save active uri'
+            }};
+        return Promise.resolve(response);
+    }
+    var self = this;
+    var api_server = req.query['api_server'];
+    var user = req.session.passport.user;
+    var ref = req.headers.referer;
+    var hash = req.query.hash;
+    var data = {
+        "rw-user:user-config": {
+            "user":{
+                "user-name" : user.username,
+                "user-domain": user.domain,
+                "ui-state": {
+                     "last-active-uri" : ref + decodeURIComponent(hash)
+                }
+            }
+        }
+    }
+    return new Promise(function(resolve, reject) {
+        Promise.all([
+            rp({
+                uri: utils.confdPort(api_server) +  '/' + API_VERSION  + '/api/config/user-config',
+                method: 'PATCH',
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                    'Authorization': req.session && req.session.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.updateActiveProject', error);
+            response.statusCode = error.statusCode || 500;
+            response.errorMessage = {
+                error: 'Failed to create user' + error
+            };
+            reject(response);
+        });
+    });
+};
+module.exports = UserManagement;
index c85eba6..7d22394 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 {
@@ -69,6 +68,20 @@ function addLabel(plugin_name, label) {
        NAVIGATION[plugin_name].label = label || 'RW.UI Plugin';
 }
 
+function addAllow(plugin_name, allow) {
+       if (!NAVIGATION[plugin_name]) {
+               NAVIGATION[plugin_name] = {};
+       }
+       NAVIGATION[plugin_name].allow = allow || '*';
+}
+
+function addAdminFlag(plugin_name, admin_link) {
+       if (!NAVIGATION[plugin_name]) {
+               NAVIGATION[plugin_name] = {};
+       }
+       NAVIGATION[plugin_name].admin_link = admin_link || false;
+}
+
 function getNavigation() {
        return NAVIGATION;
 }
@@ -82,6 +95,8 @@ function onNavigationDiscovered(plugin_name, plugin) {
        addOrder(plugin_name, plugin.order);
        addPriority(plugin_name, plugin.priority);
        addLabel(plugin_name, plugin.name);
+       addAllow(plugin_name, plugin.allow);
+       addAdminFlag(plugin_name, plugin.admin_link);
 }
 
 function init() {
diff --git a/skyquake/framework/core/modules/routes/auth.js b/skyquake/framework/core/modules/routes/auth.js
new file mode 100644 (file)
index 0000000..c1df55a
--- /dev/null
@@ -0,0 +1,99 @@
+
+/*
+ *
+ *   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.
+ *
+ */
+
+/**
+ * auth routes module. Provides a RESTful API for this
+ * skyquake instance's auth state.
+ * @module framework/core/modules/routes/auth
+ * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
+ */
+
+var cors = require('cors');
+var bodyParser = require('body-parser');
+var Router = require('express').Router();
+var utils = require('../../api_utils/utils');
+var configurationAPI = require('../api/configuration');
+
+var auth = {};
+
+auth.routes = function(authManager) {
+       console.log('Configuring auth routes');
+       Router.use(bodyParser.json());
+       Router.use(cors());
+       Router.use(bodyParser.urlencoded({
+           extended: true
+       }));
+
+       // Define routes.
+    Router.get('/', function(req, res) {
+       var default_page = null;
+       var api_server = req.query['api_server'] || (req.protocol + '://' + configurationAPI.globalConfiguration.get().api_server);
+        if (req.session && req.session.topApplication) {
+            default_page = utils.buildRedirectURL(req, configurationAPI.globalConfiguration, req.session.topApplication);
+        } else {
+            default_page = utils.buildRedirectURL(req, configurationAPI.globalConfiguration, 'user_management', '#/user-profile');
+        }
+        if (!req.user) {
+            res.redirect('/login');
+        } else {
+            res.redirect(default_page);
+        }
+    });
+
+    Router.get('/login', cors(), function(req, res) {
+        // res.render('login.html');
+        res.redirect('/login/idp');
+    });
+
+    Router.get('/login/idp',
+        authManager.passport.authenticate('oauth2')
+    );
+
+    Router.get('/callback', function(req, res, next) {
+        authManager.passport.authenticate('oauth2', function(err, user, info) {
+            if (err) {
+                // Catch some errors specific to deployments (e.g. IDP unavailable)
+                if (err.oauthError && err.oauthError.code == 'ENOTFOUND') {
+                    return res.render('idpconnectfail.ejs', {
+                        callback_url: req.url
+                    });
+                }
+                return res.redirect('/login');
+            }
+            if (!user) {
+                return res.redirect('/login');
+            }
+            req.logIn(user, function(err) {
+                if (err) {
+                    return next(err);
+                }
+                return res.redirect('/session?redirectParams=' + req.url);
+            });
+        })(req, res, next);
+    });
+
+
+    Router.get('/login.html', cors(), function(req, res) {
+        res.render('login.html');
+    });
+}
+
+auth.router = Router;
+
+module.exports = auth;
index b789ff0..93bb88a 100644 (file)
@@ -56,38 +56,5 @@ Router.get('/server-configuration', cors(), function(req, res) {
     });
 });
 
-Router.get('/check-auth', function(req, res) {
-    console.log('testing auth')
-    var api_server = req.query["api_server"];
-    var uri = utils.confdPort(api_server) + '/api/config/';
-
-    checkAuth(uri, req).then(function(data) {
-        utils.sendSuccessResponse(data, res);
-    }, function(error) {
-        utils.sendErrorResponse(error, res);
-    });
-});
-
-function checkAuth(uri, req){
-    return new Promise(function(resolve, reject) {
-        request({
-            uri: uri,
-            method: 'GET',
-            headers: _.extend({}, {
-                'Authorization': req.get('Authorization'),
-                forever: CONSTANTS.FOREVER_ON,
-                rejectUnauthorized: false,
-            })
-        }, function(error, response, body) {
-            console.log(arguments)
-            if( response.statusCode == 401) {
-                reject({statusCode: 401, error: response.body});
-            } else {
-                resolve({statusCode:200, data:response.body})
-            }
-        });
-    });
-}
-
 
 module.exports = Router;
index 82c7ec5..37e86e4 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,6 +30,7 @@ var navAPI = require('../api/navigation');
 var Router = require('express').Router();
 var utils = require('../../api_utils/utils');
 var configurationAPI = require('../api/configuration');
+var csrfCheck = require('../../api_utils/csrf').csrfCheck;
 
 Router.use(bodyParser.json());
 Router.use(cors());
@@ -37,48 +38,64 @@ Router.use(bodyParser.urlencoded({
     extended: true
 }));
 
-Router.get('/', cors(), function(req, res, next) {
-       res.redirect('/launchpad/?api_server=' + req.protocol + '://' + configurationAPI.globalConfiguration.get().api_server + '&upload_server=' + req.protocol + '://' + (configurationAPI.globalConfiguration.get().upload_server || req.hostname));
+//Should have a way of adding excluded routes to this via plugin registry, instead of hard coding
+Router.use(/^(?!.*(login\/idp|session|composer\/upload|composer\/update)).*/, function(req, res, next) {
+    var api_server = req.query['api_server'] || (req.protocol + '://' + configurationAPI.globalConfiguration.get().api_server);
+    if (req.session && req.session.loggedIn) {
+        switch (req.method) {
+            case 'POST':
+            case 'PUT':
+                csrfCheck(req, res, next);
+                break;
+            default:
+                next();
+                break;
+        }
+    } else {
+        console.log('Redirect to login.html');
+        res.redirect(utils.buildRedirectURL(req, configurationAPI.globalConfiguration, 'login', '&referer=' + encodeURIComponent(req.headers.referer)));
+    }
 });
 
+
 Router.get('/nav', cors(), function(req, res) {
-       navAPI.get(req).then(function(data) {
-               utils.sendSuccessResponse(data, res);
-       }, function(error) {
-               utils.sendErrorResponse(error, res);
-       });
+    navAPI.get(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
 });
 
 Router.get('/nav/:plugin_id', cors(), function(req, res) {
-       navAPI.get(req).then(function(data) {
-               utils.sendSuccessResponse(data, res);
-       }, function(error) {
-               utils.sendErrorResponse(error, res);
-       });
+    navAPI.get(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
 });
 
 Router.post('/nav/:plugin_id', cors(), function(req, res) {
-       navAPI.create(req).then(function(data) {
-               utils.sendSuccessResponse(data, res);
-       }, function(error) {
-               utils.sendErrorResponse(error, res);
-       });
+    navAPI.create(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
 });
 
 Router.put('/nav/:plugin_id/:route_id', cors(), function(req, res) {
-       navAPI.update(req).then(function(data) {
-               utils.sendSuccessResponse(data, res);
-       }, function(error) {
-               utils.sendErrorResponse(error, res);
-       });
+    navAPI.update(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
 });
 
 Router.delete('/nav/:plugin_id/:route_id', cors(), function(req, res) {
-       navAPI.delete(req).then(function(data) {
-               utils.sendSuccessResponse(data, res);
-       }, function(error) {
-               utils.sendErrorResponse(error, res);
-       });
+    navAPI.delete(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
 });
 
 
diff --git a/skyquake/framework/core/modules/routes/projectManagement.js b/skyquake/framework/core/modules/routes/projectManagement.js
new file mode 100644 (file)
index 0000000..c106f30
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ *
+ *   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 ProjectManagementAPI = require('../api/projectManagementAPI.js');
+
+Router.use(bodyParser.json());
+Router.use(cors());
+Router.use(bodyParser.urlencoded({
+    extended: true
+}));
+
+Router.get('/project', cors(), function(req, res) {
+    ProjectManagementAPI.get(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+Router.post('/project', cors(), function(req, res) {
+    ProjectManagementAPI.create(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+Router.put('/project', cors(), function(req, res) {
+    ProjectManagementAPI.update(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+Router.delete('/project/:projectname', cors(), function(req, res) {
+    ProjectManagementAPI.delete(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+Router.put('/platform', cors(), function(req, res) {
+    ProjectManagementAPI.updatePlatform(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+Router.get('/platform', cors(), function(req, res) {
+    ProjectManagementAPI.getPlatform(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+module.exports = Router;
+
+
+
diff --git a/skyquake/framework/core/modules/routes/sessions.js b/skyquake/framework/core/modules/routes/sessions.js
new file mode 100644 (file)
index 0000000..f84ca11
--- /dev/null
@@ -0,0 +1,74 @@
+
+/*
+ *
+ *   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.
+ *
+ */
+
+/**
+ * Node sessions routes module.
+ * Provides a RESTful API to manage sessions.
+ * @module framework/core/modules/routes/sessions
+ * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
+ */
+
+var cors = require('cors');
+var bodyParser = require('body-parser');
+var sessionsAPI = require('../api/sessions');
+var Router = require('express').Router();
+var utils = require('../../api_utils/utils');
+var CONSTANTS = require('../../api_utils/constants.js');
+var request = require('request');
+var _ = require('lodash');
+
+var sessions = {};
+
+sessions.routes = function(sessionsConfig) {
+    Router.use(bodyParser.json());
+    Router.use(cors());
+    Router.use(bodyParser.urlencoded({
+        extended: true
+    }));
+
+    // Overloaded get method to handle OpenIDConnect redirect to establish a session.
+    Router.get('/session*', cors(), /*sessionsConfig.authManager.passport.authenticate('main', {
+        noredirect: false
+    }), */function(req, res) {
+        req.query['api_server'] = sessionsConfig.api_server_protocol + '://' + sessionsConfig.api_server;
+        sessionsAPI.create(req, res).then(function(data) {
+            utils.sendSuccessResponse(data, res);
+        });
+    });
+
+    // For project switcher UI
+    Router.put('/session/:projectId', cors(), function(req, res) {
+        sessionsAPI.addProjectToSession(req, res).then(function(data) {
+            utils.sendSuccessResponse(data, res);
+        }, function(error) {
+            utils.sendErrorResponse(error, res);
+        });
+    });
+
+    Router.delete('/session', cors(), function(req, res) {
+        sessionsAPI.delete(req, res).then(function(data) {
+            utils.sendSuccessResponse(data, res);
+        });
+    });
+}
+
+sessions.router = Router;
+
+
+module.exports = sessions;
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..22a2d74
--- /dev/null
@@ -0,0 +1,85 @@
+
+/*
+ *
+ *   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.get('/user-profile', cors(), function(req, res) {
+    UserManagementAPI.getProfile(req).then(function(response) {
+        utils.sendSuccessResponse(response, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+Router.get('/user-data/:userId/:domain?', cors(), function(req, res) {
+    UserManagementAPI.getUserInfo(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.put('/user', cors(), function(req, res) {
+    UserManagementAPI.update(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;
+
+
+
diff --git a/skyquake/framework/core/views/home.ejs b/skyquake/framework/core/views/home.ejs
new file mode 100644 (file)
index 0000000..2cb2f80
--- /dev/null
@@ -0,0 +1,5 @@
+<% if (!user) { %>
+       <p>Welcome! Please <a href="/login">log in</a>.</p>
+<% } else { %>
+       <p>Hello, <%= user.username %>. View your <a href="<%= default_page %>">default page</a>. TODO: Update link to dashboard</p>
+<% } %>
\ No newline at end of file
diff --git a/skyquake/framework/core/views/idpconnectfail.ejs b/skyquake/framework/core/views/idpconnectfail.ejs
new file mode 100644 (file)
index 0000000..7847b3d
--- /dev/null
@@ -0,0 +1,43 @@
+<style type="text/css">
+       
+html {
+       background: #f1f1f1;
+}
+
+body {
+       background: #f1f1f1;
+}
+#idpfail {
+       display: flex;
+       flex-direction: column;
+       text-align: center;
+       align-items: center;
+}
+#idpfail .message {
+       display: flex;
+       margin-bottom: 20px;
+       font-size: 1.625rem;
+       font-weight: 400;
+       text-decoration: none;
+       text-transform: uppercase;
+       font-family: roboto-thin, Helvetica, Arial, sans-serif;
+       color: #CD5C5C;
+}
+#retrylink {
+       margin-top:40px;
+       font-family: roboto-thin, Helvetica, Arial, sans-serif;
+}
+
+
+</style>
+<div id="idpfail">
+       <p class="message">
+               We are having trouble connecting to the Identity Provider.
+       </p>
+       <p class="message">
+               Please check that it is running and reachable.
+       </p>
+       <p id="retrylink">
+               Once you have resolved the connectivity issues, <a href="<%= callback_url %>">please click here to retry.</a>
+       </p>
+</div>
\ No newline at end of file
diff --git a/skyquake/framework/plugin-index.html b/skyquake/framework/plugin-index.html
new file mode 100644 (file)
index 0000000..b704a3c
--- /dev/null
@@ -0,0 +1,13 @@
+<style>
+    html {
+        display: none;
+    }
+</style>
+<script>
+    if (self == top) {
+        document.documentElement.style.display = 'block';
+    } else {
+        top.location = self.location;
+    }
+</script>
+<div id="app"></div>
\ No newline at end of file
diff --git a/skyquake/framework/source/SourceCache.js b/skyquake/framework/source/SourceCache.js
new file mode 100644 (file)
index 0000000..ded3c9d
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 
+ *   Copyright 2017 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 appConfiguration from '../utils/appConfiguration'
+
+let versionKeyPrefix = null;
+
+const localCache = new class {
+    get(key) {
+        let valueAsString = localStorage.getItem(key);
+        return valueAsString ? JSON.parse(valueAsString) : undefined;
+    }
+    set(key, val) {
+        localStorage.setItem(key, typeof val === 'string' ? val : JSON.stringify(val));
+    }
+}();
+
+let objCache = new Map();
+
+const storeCache = new class {
+    get(key) {
+        if (objCache[key]) {
+            objCache[key].timerId && clearTimeout(objCache[key].timerId)
+            objCache[key].timerId = setTimeout((key) => delete objCache[key], 2000)
+            return objCache[key].value;
+        } 
+        const obj = localCache.get(key);
+        if (obj) {
+            objCache[key] = {
+                value: obj,
+                timerId: setTimeout((key) => delete objCache[key], 2000)
+            }
+            return obj;
+        }
+    }
+    set(key, obj) {
+        setTimeout(localCache.set, 100, key, obj);
+        objCache[key] = {
+            value: obj,
+            timerId: setTimeout((key) => delete objCache[key], 2000)
+        }
+    }
+    init(version) {
+        versionKeyPrefix = 's-v-' + version;
+        const currentStoreVersion = localStorage.getItem('store-version');
+        if (currentStoreVersion !== version) {
+            let removeItems = [];
+            for (let i = 0; i < localStorage.length; ++i) {
+                let key = localStorage.key(i);
+                if (key.startsWith('s-v-')) {
+                    removeItems.push(key);
+                }
+            }
+            removeItems.forEach((key) => localStorage.removeItem(key));
+            localStorage.setItem('store-version', version);
+        }
+    }
+}();
+
+class StoreCache {
+    constructor(name) {
+        this.name = 's-v-' + name;
+    }
+    get(key) {
+        return storeCache.get(this.name + key);
+    }
+    set(key, obj) {
+        storeCache.set(this.name + key, obj);
+    }
+    init() {
+        return versionKeyPrefix ? Promise.resolve() : new Promise(
+            (resolve, reject) => {
+                appConfiguration.get().then((config) => {
+                    storeCache.init(config.version);
+                    resolve();
+                })
+            }
+        )
+    }
+}
+
+module.exports = StoreCache;
\ No newline at end of file
diff --git a/skyquake/framework/source/model/index.js b/skyquake/framework/source/model/index.js
new file mode 100644 (file)
index 0000000..019ada0
--- /dev/null
@@ -0,0 +1,7 @@
+import modelActions from './modelActions'
+import modelSource from './modelSource'
+
+export {
+    modelSource,
+    modelActions
+}
\ No newline at end of file
diff --git a/skyquake/framework/source/model/modelActions.js b/skyquake/framework/source/model/modelActions.js
new file mode 100644 (file)
index 0000000..41dbc00
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 
+ *   Copyright 2017 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+import Alt from 'widgets/skyquake_container/skyquakeAltInstance';
+
+class Actions {
+
+       constructor() {
+               this.generateActions(
+            'loadModel',
+            'processRequestSuccess',
+            'processRequestInitiated',
+            'processRequestFailure');
+       }
+}
+
+export default Alt.createActions(Actions);
diff --git a/skyquake/framework/source/model/modelSource.js b/skyquake/framework/source/model/modelSource.js
new file mode 100644 (file)
index 0000000..3779aa5
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * 
+ *   Copyright 2017 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 rw from 'utils/rw'
+import modelActions from './modelActions'
+import Utils from 'utils/utils'
+import $ from 'jquery';
+
+const source = {
+    loadModel: function () {
+        return {
+            remote: function (state, path) {
+                return new Promise(function (resolve, reject) {
+                    $.ajax({
+                        url: '/model?path=' + path,
+                        type: 'GET',
+                        success: function (result) {
+                            resolve(result);
+                        },
+                        error: function (xhr, errorType, errorMessage) {
+                            console.log("There was an error updating the model: ", errorType, errorMessage, xhr);
+                            reject({response: xhr.responseJSON, errorType, errorMessage});
+                        }
+                    });
+                })
+            },
+            success: modelActions.processRequestSuccess,
+            loading: modelActions.processRequestInitiated,
+            error: modelActions.processRequestFailure
+        }
+    },
+    updateModel: function () {
+        return {
+            remote: function (state, path, data) {
+                const url = path.reduce((url, node) => {
+                    url += node[0] !== '[' ? '/' : '';
+                    return url + node
+                }, `/model?path=/${state.path}`);                
+                return new Promise(function (resolve, reject) {
+                    $.ajax({
+                        url: url,
+                        type: 'PATCH',
+                        data: data,
+                        success: function (result) {
+                            resolve(result);
+                        },
+                        error: function (xhr, errorType, errorMessage) {
+                            console.log("There was an error updating the model: ", errorType, errorMessage, xhr);
+                            reject({response: xhr.responseJSON, errorType, errorMessage});
+                        }
+                    });
+                })
+            },
+            success: modelActions.processRequestSuccess,
+            loading: modelActions.processRequestInitiated,
+            error: modelActions.processRequestFailure
+        }
+    },
+    createModel: function () {
+        return {
+            remote: function (state, path, data) {
+                const url = path.reduce((url, node) => {
+                    url += node[0] !== '[' ? '/' : '';
+                    return url + node
+                }, `/model?path=/${state.path}`);                
+                return new Promise(function (resolve, reject) {
+                    $.ajax({
+                        url: url,
+                        type: 'PUT',
+                        data: data,
+                        success: function (result) {
+                            resolve(result);
+                        },
+                        error: function (xhr, errorType, errorMessage) {
+                            console.log("There was an error updating the model: ", errorType, errorMessage, xhr);
+                            reject({response: xhr.responseJSON, errorType, errorMessage});
+                        }
+                    });
+                })
+            },
+            success: modelActions.processRequestSuccess,
+            loading: modelActions.processRequestInitiated,
+            error: modelActions.processRequestFailure
+        }
+    },
+
+    deleteModel: function () {
+        return {
+            remote: function (state, path) {
+                const url = path.reduce((url, node) => {
+                    url += node[0] !== '[' ? '/' : '';
+                    return url + node
+                }, `/model?path=/${state.path}`);                
+                return new Promise(function (resolve, reject) {
+                    $.ajax({
+                        url: url,
+                        type: 'DELETE',
+                        success: function (result) {
+                            resolve(result);
+                        },
+                        error: function (xhr, errorType, errorMessage) {
+                            console.log("There was an error updating the model: ", errorType, errorMessage, xhr);
+                            reject({response: xhr.responseJSON, errorType, errorMessage});
+                        }
+                    });
+                })
+            },
+            success: modelActions.processRequestSuccess,
+            loading: modelActions.processRequestInitiated,
+            error: modelActions.processRequestFailure
+        }
+    }
+}
+module.exports = source;
\ No newline at end of file
diff --git a/skyquake/framework/source/schema/index.js b/skyquake/framework/source/schema/index.js
new file mode 100644 (file)
index 0000000..95b152e
--- /dev/null
@@ -0,0 +1,7 @@
+import schemaActions from './schemaActions'
+import schemaSource from './schemaSource'
+
+export {
+    schemaSource,
+    schemaActions
+}
\ No newline at end of file
diff --git a/skyquake/framework/source/schema/schemaActions.js b/skyquake/framework/source/schema/schemaActions.js
new file mode 100644 (file)
index 0000000..7e36cca
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 
+ *   Copyright 2017 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+import Alt from 'widgets/skyquake_container/skyquakeAltInstance';
+
+class Actions {
+
+       constructor() {
+               this.generateActions(
+            'loadSchema',
+            'loadSchemaSuccess',
+            'loadSchemaLoading',
+            'loadSchemaFail');
+       }
+}
+
+export default Alt.createActions(Actions);
diff --git a/skyquake/framework/source/schema/schemaSource.js b/skyquake/framework/source/schema/schemaSource.js
new file mode 100644 (file)
index 0000000..2c7bd5b
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 
+ *   Copyright 2017 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 rw from 'utils/rw'
+import schemaActions from './schemaActions'
+import Utils from 'utils/utils'
+import $ from 'jquery';
+import StoreCache from '../SourceCache'
+
+const storeCache = new StoreCache('schema');
+storeCache.init(); // get the ball rolling
+
+function getCachedSchema(request) {
+    const cachedSchema = {};
+    const requestSchema = [];
+    request.forEach((path) => {
+        let schema = storeCache.get(path);
+        if (schema) {
+            cachedSchema[path] = schema
+        } else {
+            requestSchema.push(path);
+        }
+    });
+    return {
+        cachedSchema,
+        requestSchema
+    };
+}
+
+const schemaSource = {
+    loadSchema: function () {
+        return {
+            local: function (state, request) {
+                request = Array.isArray(request) ? request : [request];
+                const results = getCachedSchema(request);
+                if (!results.requestSchema.length) {
+                    return(Promise.resolve(results.cachedSchema));
+                }
+            }, 
+
+            remote: function (state, request) {
+                return new Promise(function (resolve, reject) {
+                    storeCache.init().then(() => {
+                        request = Array.isArray(request) ? request : [request];
+                        const results = getCachedSchema(request);
+                        if (!results.requestSchema.length) {
+                            resolve({
+                                schema: results.cachedSchema
+                            });
+                        } else {
+                            $.ajax({
+                                url: '/schema?request=' + results.requestSchema.join(','),
+                                type: 'GET',
+                                success: function ({
+                                    schema
+                                }) {
+                                    for (let path in schema) {
+                                        storeCache.set(path, schema[path]);
+                                    }
+                                    resolve(getCachedSchema(request).cachedSchema);
+                                },
+                                error: function (error) {
+                                    console.log("There was an error getting the schema: ", error);
+                                    reject(error);
+                                }
+                            }).fail(function (xhr) {
+                                console.log("There was an error getting the schema: ", xhr);
+                                Utils.checkAuthentication(xhr.status);
+                                reject("There was an error getting the schema.")
+                            });
+                        }
+                    })
+                })
+            },
+            success: schemaActions.loadSchemaSuccess,
+            loading: schemaActions.loadSchemaLoading,
+            error: schemaActions.loadSchemaFail
+        }
+    },
+}
+export default schemaSource;
\ No newline at end of file
index 9378984..a01c293 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;
@@ -66,11 +66,12 @@ $neutral-light-3: hsl(360, 100%, 50%);
 $neutral-light-4: hsl(360, 100%, 50%);
 $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-0: hsl(0, 0%, 80.7%);
+$neutral-dark-1: hsl(0, 0%, 63.7%);
+$neutral-dark-2: hsl(0, 0%, 56.7%);
+$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 01b728b..e9d5de9 100644 (file)
@@ -16,8 +16,6 @@
  *
  */
 /*@import "./vendor/css-reset-2.0/css-reset.css";*/
-@import "../../node_modules/reset-css/reset.css";
-
 .has-drop-shadow {
     box-shadow: 2px 2px rgba(0, 0, 0, .15)
 }
@@ -2023,7 +2021,7 @@ a[role=button] {
     margin-top: 150px;
     margin-bottom: 20px;
     background-size: 154px 102px;
-    /*background-image: url(./img/header-logo.png)*/
+       /*background-image: url(./img/header-logo.png)*/
     background-image: url(./img/svg/osm-logo_color_rgb.svg);
 }
 .login-cntnr .riftio {
index 53a9500..c67dca3 100644 (file)
Binary files a/skyquake/framework/style/img/osm_header.png and b/skyquake/framework/style/img/osm_header.png differ
index 1005431..3055122 100644 (file)
Binary files a/skyquake/framework/style/img/osm_header_253x50.png and b/skyquake/framework/style/img/osm_header_253x50.png differ
index 7ece845..4b7a344 100644 (file)
Binary files a/skyquake/framework/style/img/osm_header_506x100.png and b/skyquake/framework/style/img/osm_header_506x100.png differ
diff --git a/skyquake/framework/utils/appConfiguration.js b/skyquake/framework/utils/appConfiguration.js
new file mode 100644 (file)
index 0000000..2882309
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ *
+ *   Copyright 2017 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 $ from 'jquery';
+
+const configPromise = new Promise((resolve, reject) => {
+    $.ajax({
+        url: '/app-config',
+        type: 'GET',
+        success: function (data) {
+            if (data.version.endsWith('.1')){
+                data.version = '' + Date.now();
+            }
+            resolve(data);
+        },
+        error: function (error) {
+            console.log("There was an error getting config: ", error);
+            reject(error);
+        }
+    }).fail(function (xhr) {
+        console.log('There was an xhr error getting the config', xhr);
+        reject(xhr);
+    });
+});
+
+module.exports = {
+    get: () => configPromise
+};
\ No newline at end of file
diff --git a/skyquake/framework/utils/roleConstants.js b/skyquake/framework/utils/roleConstants.js
new file mode 100644 (file)
index 0000000..5ac4e64
--- /dev/null
@@ -0,0 +1,31 @@
+var c = {};
+
+c.PLATFORM = {
+  OPER: "rw-rbac-platform:platform-oper",
+  ADMIN: "rw-rbac-platform:platform-admin",
+  SUPER: "rw-rbac-platform:super-admin"
+}
+
+c.PROJECT = {
+    TYPE: {
+      "rw-project-mano:catalog-oper": "rw-project-mano",
+      "rw-project-mano:catalog-admin": "rw-project-mano",
+      "rw-project-mano:lcm-oper": "rw-project-mano",
+      "rw-project-mano:lcm-admin": "rw-project-mano",
+      "rw-project-mano:account-oper": "rw-project-mano",
+      "rw-project-mano:account-admin": "rw-project-mano",
+      "rw-project:project-oper": "rw-project",
+      "rw-project:project-admin": "rw-project"
+    },
+    CATALOG_OPER: "rw-project-mano:catalog-oper",
+    CATALOG_ADMIN: "rw-project-mano:catalog-admin",
+    LCM_OPER: "rw-project-mano:lcm-oper",
+    LCM_ADMIN: "rw-project-mano:lcm-admin",
+    ACCOUNT_OPER: "rw-project-mano:account-oper",
+    ACCOUNT_ADMIN: "rw-project-mano:account-admin",
+    PROJECT_OPER: "rw-project:project-oper",
+    PROJECT_ADMIN: "rw-project:project-admin"
+
+}
+
+module.exports = c;
index 7b93fd5..88ef939 100644 (file)
  *   limitations under the License.
  *
  */
-//Login needs to be refactored. Too many cross dependencies
-var AuthActions = require('../widgets/login/loginAuthActions.js');
-var $ = require('jquery');
-import rw from './rw.js';
+import AuthActions from '../widgets/login/loginAuthActions';
+import $ from 'jquery';
+import rw from './rw';
+import appConfiguration from './appConfiguration'
+import SockJS from 'sockjs-client';
+
 var API_SERVER = rw.getSearchParams(window.location).api_server;
 let NODE_PORT = rw.getSearchParams(window.location).api_port || ((window.location.protocol == 'https:') ? 8443 : 8000);
-var SockJS = require('sockjs-client');
 
 var Utils = {};
 
@@ -29,25 +30,6 @@ Utils.DescriptorModelMeta = null;
 
 var INACTIVITY_TIMEOUT = 600000;
 
-Utils.getInactivityTimeout = function() {
-    return new Promise(function(resolve, reject) {
-        $.ajax({
-            url: '/inactivity-timeout',
-            type: 'GET',
-            success: function(data) {
-                resolve(data);
-            },
-            error: function(error) {
-                console.log("There was an error getting the inactivity-timeout: ", error);
-                reject(error);
-            }
-        }).fail(function(xhr) {
-            console.log('There was an xhr error getting the inactivity-timeout', xhr);
-            reject(xhr);
-        });
-    });
-};
-
 Utils.isMultiplexerLoaded = function() {
     if (window.multiplexer) {
         return true;
@@ -75,14 +57,19 @@ Utils.setupMultiplexClient = function() {
     loadChecker();
 };
 
-Utils.checkAndResolveSocketRequest = function(data, resolve, reject) {
+Utils.checkAndResolveSocketRequest = function(data, resolve, reject, successCallback) {
     const checker = () => {
         if (!Utils.isMultiplexerLoaded()) {
             setTimeout(() => {
                 checker();
             }, 500);
         } else {
-            resolve(data.id);
+            if (!successCallback) {
+                resolve(data.id);
+            } else {
+                //resolve handled in callback
+                successCallback(data.id)
+            }
         }
     };
 
@@ -90,9 +77,8 @@ Utils.checkAndResolveSocketRequest = function(data, resolve, reject) {
 };
 
 Utils.bootstrapApplication = function() {
-    var self = this;
-    return new Promise(function(resolve, reject) {
-        Promise.all([self.getInactivityTimeout()]).then(function(results) {
+    return new Promise((resolve, reject) => {
+        Promise.all([appConfiguration.get()]).then(function(results) {
             INACTIVITY_TIMEOUT = results[0]['inactivity-timeout'];
             resolve();
         }, function(error) {
@@ -129,8 +115,9 @@ Utils.getDescriptorModelMeta = function() {
 }
 
 Utils.addAuthorizationStub = function(xhr) {
-    var Auth = window.sessionStorage.getItem("auth");
-    xhr.setRequestHeader('Authorization', 'Basic ' + Auth);
+    // NO-OP now that we are dealing with it on the server
+    // var Auth = window.sessionStorage.getItem("auth");
+    // xhr.setRequestHeader('Authorization', 'Basic ' + Auth);
 };
 
 Utils.getByteDataWithUnitPrefix = function(number, precision) {
@@ -183,36 +170,30 @@ Utils.getPacketDataWithUnitPrefix = function(number, precision) {
     }
 }
 Utils.loginHash = "#/login";
-Utils.setAuthentication = function(username, password, cb) {
-    var self = this;
-    var AuthBase64 = btoa(username + ":" + password);
-    window.sessionStorage.setItem("auth", AuthBase64);
-    self.detectInactivity();
-    $.ajax({
-            url: '//' + window.location.hostname + ':' + window.location.port + '/check-auth?api_server=' + API_SERVER,
-            type: 'GET',
-            beforeSend: Utils.addAuthorizationStub,
-            success: function(data) {
-              //console.log("LoggingSource.getLoggingConfig success call. data=", data);
-                if (cb) {
-                    cb();
-                };
-            },
-            error: function(data) {
-                Utils.clearAuthentication();
-            }
-          });
-}
-Utils.clearAuthentication = function(callback) {
+
+Utils.clearAuthentication = function() {
     var self = this;
     window.sessionStorage.removeItem("auth");
     AuthActions.notAuthenticated();
     window.sessionStorage.setItem("locationRefHash", window.location.hash);
-    if (callback) {
-        callback();
-    } else {
-        window.location.hash = Utils.loginHash;
-    }
+    var reloadURL = '';
+    $.ajax({
+        url: '//' + window.location.hostname + ':' + window.location.port + '/session?api_server=' + API_SERVER + '&hash=' + encodeURIComponent(window.location.hash),
+        type: 'DELETE',
+        success: function(data) {
+            console.log('User logged out');
+            reloadURL = data['url'] + '?post_logout_redirect_uri=' +
+                 window.location.protocol + '//' +
+                 window.location.hostname + ':' +
+                 window.location.port +
+                 '/?api_server=' + API_SERVER;
+
+            window.location.replace(reloadURL);
+        },
+        error: function(data) {
+            console.log('Problem logging user out');
+        }
+    });
 }
 Utils.isNotAuthenticated = function(windowLocation, callback) {
     var self = this;
@@ -322,4 +303,24 @@ Utils.parseError = (error) => {
     return displayMsg
 }
 
+Utils.rpcError = (rpcResult) => {
+    try {
+        let info = JSON.parse(rpcResult);
+        let rpcError = info.body || info.errorMessage.body || info.errorMessage.error;
+        if (rpcError) {
+            if (typeof rpcError === 'string') {
+                const index = rpcError.indexOf('{');
+                if (index >= 0) {
+                    return JSON.parse(rpcError.substr(index));
+                }
+            } else {
+                return rpcError;
+            }
+        }
+    } catch (e) {
+    }
+    console.log('invalid rpc error: ', rpcResult);
+    return null;
+}
+
 module.exports = Utils;
index c972e14..043a769 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,8 +110,9 @@ button{
 
   /* Focus */
   &:focus {
-    // box-shadow: $focus-shadow;
-    border: 1px solid red;
+    /* box-shadow: $focus-shadow;*/
+    border: 1px solid;
+    border-color: darken($normalHoverBackground, 10%);
   }
 
   /* SIZES
@@ -256,11 +260,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 f38719a..461a783 100644 (file)
@@ -26,355 +26,7 @@ export default {
 //  Histogram: Histogram,
   Multicomponent: require('./multicomponent/multicomponent.js'),
   Mixins: require('./mixins/ButtonEventListener.js'),
-  // Gauge: require('./gauge/gauge.js'),
+//  Gauge: require('./gauge/gauge.js'),
   Bullet: require('./bullet/bullet.js')
 };
 
-// require('../../assets/js/n3-line-chart.js');
-// var Gauge = require('../../assets/js/gauge-modified.js');
-// var bulletController = function($scope, $element) {
-//     this.$element = $element;
-//     this.vertical = false;
-//     this.value = 0;
-//     this.min = 0;
-//     this.max = 100;
-//     //this.range = this.max - this.min;
-//     //this.percent = (this.value - this.min) / this.range;
-//     this.displayValue = this.value;
-//     this.isPercent = (this.units == '')? true:false;
-//     this.bulletColor = "#6BB814";
-//     this.fontsize = 28;
-//     this.radius = 4;
-//     this.containerMarginX = 0;
-//     this.containerMarginY = 0;
-//     this.textMarginX = 5;
-//     this.textMarginY = 42;
-//     this.bulletMargin = 0;
-//     this.width = 512;
-//     this.height = 64;
-//     this.markerX = -100; // puts it off screen unless set
-//     var self = this;
-//     if (this.isPercent) {
-//         this.displayValue + "%";
-//     }
-//     $scope.$watch(
-//       function() {
-//         return self.value;
-//       },
-//       function() {
-//         self.valueChanged();
-//       }
-//     );
-
-//   }
-
-//   bulletController.prototype = {
-
-//     valueChanged: function() {
-//       var range = this.max - this.min;
-//       var normalizedValue = (this.value - this.min) / range;
-//       if (this.isPercent) {
-//         this.displayValue = String(Math.round(normalizedValue * 100)) + "%";
-//       } else {
-//         this.displayValue = this.value;
-//       }
-//       // All versions of IE as of Jan 2015 does not support inline CSS transforms on SVG
-//       if (platform.name == 'IE') {
-//         this.bulletWidth = Math.round(100 * normalizedValue) + '%';
-//       } else {
-//         this.bulletWidth = this.width - (2 * this.containerMarginX);
-//         var transform = 'scaleX(' + normalizedValue + ')';
-//         var bullet = $(this.$element).find('.bullet2');
-//         bullet.css('transform', transform);
-//         bullet.css('-webkit-transform', transform);
-//       }
-//     },
-
-//     markerChanged: function() {
-//       var range = this.max - this.min;
-//       var w = this.width - (2 * this.containerMarginX);
-//       this.markerX = this.containerMarginX + ((this.marker - this.min) / range ) * w;
-//       this.markerY1 = 7;
-//       this.markerY2 = this.width - 7;
-//     }
-//   }
-
-// angular.module('components', ['n3-line-chart'])
-//     .directive('rwBullet', function() {
-//       return {
-//         restrict : 'E',
-//         templateUrl: 'modules/views/rw.bullet.tmpl.html',
-//         bindToController: true,
-//         controllerAs: 'bullet',
-//         controller: bulletController,
-//         replace: true,
-//         scope: {
-//           min : '@?',
-//           max : '@?',
-//           value : '@',
-//           marker: '@?',
-//           units: '@?',
-//           bulletColor: '@?',
-//           label: '@?'
-//         }
-//       };
-//     })
-//     .directive('rwSlider', function() {
-//       var controller = function($scope, $element, $timeout) {
-//         // Q: is there a way to force attributes to be ints?
-//         $scope.min = $scope.min || "0";
-//         $scope.max = $scope.max || "100";
-//         $scope.step = $scope.step || "1";
-//         $scope.height = $scope.height || "30";
-//         $scope.orientation = $scope.orientation || 'horizontal';
-//         $scope.tooltipInvert = $scope.tooltipInvert || false;
-//         $scope.percent = $scope.percent || false;
-//         $scope.kvalue = $scope.kvalue || false;
-//         $scope.direction = $scope.direction || "ltr";
-//         $($element).noUiSlider({
-//           start: parseInt($scope.value),
-//           step: parseInt($scope.step),
-//           orientation: $scope.orientation,
-//           range: {
-//             min: parseInt($scope.min),
-//             max: parseInt($scope.max)
-//           },
-//           direction: $scope.direction
-//         });
-//         //$(".no-Ui-target").Link('upper').to('-inline-<div class="tooltip"></div>')
-//         var onSlide = function(e, value) {
-//           $timeout(function(){
-//             $scope.value = value;
-//           })
-
-//         };
-//         $($element).on({
-//           change: onSlide,
-//           slide: onSlide,
-//           set: $scope.onSet({value: $scope.value})
-//         });
-//         var val = String(Math.round($scope.value));
-//         if ($scope.percent) {
-//             val += "%"
-//         } else if ($scope.kvalue) {
-//             val += "k"
-//         }
-//         $($element).height($scope.height);
-//         if ($scope.tooltipInvert) {
-//             $($element).find('.noUi-handle').append("<div class='tooltip' style='position:relative;right:20px'>" + val + "</div>");
-//         } else {
-//             $($element).find('.noUi-handle').append("<div class='tooltip' style='position:relative;left:-20px'>" + val + "</div>");
-//         }
-//         $scope.$watch('value', function(value) {
-//         var val = String(Math.round($scope.value));
-//         if ($scope.percent) {
-//             val += "%"
-//         } else if($scope.kvalue) {
-//             val += "k"
-//         }
-//           $($element).val(value);
-//           $($element).find('.tooltip').html(val);
-//         if ($scope.tooltipInvert) {
-//             $($element).find('.tooltip').css('right', $($element).find('.tooltip').innerWidth() * -1);
-//         } else {
-//             $($element).find('.tooltip').css('left', $($element).find('.tooltip').innerWidth() * -1);
-//         }
-//         });
-//       };
-
-//       return {
-//         restrict : 'E',
-//         template: '<div></div>',
-//         controller : controller,
-//         replace: true,
-//         scope: {
-//           min : '@',
-//           max : '@',
-//           width: '@',
-//           height: '@',
-//           step : '@',
-//           orientation : '@',
-//           tooltipInvert: '@',
-//           percent: '@',
-//           kvalue: '@?',
-//           onSet:'&?',
-//           direction: '@?',
-//           value:'=?'
-//         }
-//       };
-//     })
-// .directive('rwGauge', function() {
-//     return {
-//         restrict: 'AE',
-//         template: '<canvas class="rwgauge" style="width:100%;height:100%;max-width:{{width}}px;max-height:240px;"></canvas>',
-//         replace: true,
-//         scope: {
-//             min: '@?',
-//             max: '@?',
-//             size: '@?',
-//             color: '@?',
-//             value: '@?',
-//             resize: '@?',
-//             isAggregate: '@?',
-//             units: '@?',
-//             valueFormat: '=?',
-//             width: '@?'
-//         },
-//         bindToController: true,
-//         controllerAs: 'gauge',
-//         controller: function($scope, $element) {
-//             var self = this;
-//             this.gauge = null;
-//             this.min = this.min || 0;
-//             this.max = this.max || 100;
-//             this.nSteps = 14;
-//             this.size = this.size || 300;
-//             this.units = this.units || '';
-//             $scope.width = this.width || 240;
-//             this.color = this.color || 'hsla(212, 57%, 50%, 1)';
-//             if (!this.valueFormat) {
-//                 if (this.max > 1000 || this.value) {
-//                     self.valueFormat = {
-//                         "int": 1,
-//                         "dec": 0
-//                     };
-//                 } else {
-//                     self.valueFormat = {
-//                         "int": 1,
-//                         "dec": 2
-//                     };
-//                 }
-//             }
-//             this.isAggregate = this.isAggregate || false;
-//             this.resize = this.resize || false;
-//             if (this.format == 'percent') {
-//                 self.valueFormat = {
-//                     "int": 3,
-//                     "dec": 0
-//                 };
-//             }
-//             $scope.$watch(function() {
-//                 return self.max;
-//             }, function(n, o) {
-//                 if(n !== o) {
-//                     renderGauge();
-//                 }
-//             });
-//             $scope.$watch(function() {
-//                 return self.valueFormat;
-//             }, function(n, o) {
-//                 if(n != 0) {
-//                     renderGauge();
-//                 }
-//             });
-//             $scope.$watch(function() {
-//                 return self.value;
-//             }, function() {
-//                 if (self.gauge) {
-//                     // w/o rounding gauge will unexplainably thrash round.
-//                     self.valueFormat = determineValueFormat(self.value);
-//                     self.gauge.setValue(Math.ceil(self.value * 100) / 100);
-//                     //self.gauge.setValue(Math.round(self.value));
-//                 }
-//             });
-//             angular.element($element).ready(function() {
-//                 console.log('rendering')
-//                 renderGauge();
-//             })
-//             window.testme = renderGauge;
-//             function determineValueFormat(value) {
-
-//                     if (value > 999 || self.units == "%") {
-//                         return {
-//                             "int": 1,
-//                             "dec": 0
-//                         }
-//                     }
-
-//                     return {
-//                         "int": 1,
-//                         "dec": 2
-//                     }
-//                 }
-//             function renderGauge(calcWidth) {
-//                 if (self.max == self.min) {
-//                     self.max = 14;
-//                 }
-//                 var range = self.max - self.min;
-//                 var step = Math.round(range / self.nSteps);
-//                 var majorTicks = [];
-//                 for (var i = 0; i <= self.nSteps; i++) {
-//                     majorTicks.push(self.min + (i * step));
-//                 };
-//                 var redLine = self.min + (range * 0.9);
-//                 var config = {
-//                     isAggregate: self.isAggregate,
-//                     renderTo: angular.element($element)[0],
-//                     width: calcWidth || self.size,
-//                     height: calcWidth || self.size,
-//                     glow: false,
-//                     units: self.units,
-//                     title: false,
-//                     minValue: self.min,
-//                     maxValue: self.max,
-//                     majorTicks: majorTicks,
-//                     valueFormat: determineValueFormat(self.value),
-//                     minorTicks: 0,
-//                     strokeTicks: false,
-//                     highlights: [],
-//                     colors: {
-//                         plate: 'rgba(0,0,0,0)',
-//                         majorTicks: 'rgba(15, 123, 182, .84)',
-//                         minorTicks: '#ccc',
-//                         title: 'rgba(50,50,50,100)',
-//                         units: 'rgba(50,50,50,100)',
-//                         numbers: '#fff',
-//                         needle: {
-//                             start: 'rgba(255, 255, 255, 1)',
-//                             end: 'rgba(255, 255, 255, 1)'
-//                         }
-//                     }
-//                 };
-//                 var min = config.minValue;
-//                 var max = config.maxValue;
-//                 var N = 1000;
-//                 var increment = (max - min) / N;
-//                 for (i = 0; i < N; i++) {
-//                     var temp_color = 'rgb(0, 172, 238)';
-//                     if (i > 0.5714 * N && i <= 0.6428 * N) {
-//                         temp_color = 'rgb(0,157,217)';
-//                     } else if (i >= 0.6428 * N && i < 0.7142 * N) {
-//                         temp_color = 'rgb(0,142,196)';
-//                     } else if (i >= 0.7142 * N && i < 0.7857 * N) {
-//                         temp_color = 'rgb(0,126,175)';
-//                     } else if (i >= 0.7857 * N && i < 0.8571 * N) {
-//                         temp_color = 'rgb(0,122,154)';
-//                     } else if (i >= 0.8571 * N && i < 0.9285 * N) {
-//                         temp_color = 'rgb(0,96,133)';
-//                     } else if (i >= 0.9285 * N) {
-//                         temp_color = 'rgb(0,80,112)';
-//                     }
-//                     config.highlights.push({
-//                         from: i * increment,
-//                         to: increment * (i + 2),
-//                         color: temp_color
-//                     })
-//                 }
-//                 var updateSize = _.debounce(function() {
-//                     config.maxValue = self.max;
-//                     var clientWidth = self.parentNode.parentNode.clientWidth / 2;
-//                     var calcWidth = (300 > clientWidth) ? clientWidth : 300;
-//                     self.gauge.config.width = self.gauge.config.height = calcWidth;
-//                     self.renderGauge(calcWidth);
-//                 }, 500);
-//                 if (self.resize) $(window).resize(updateSize)
-//                 if (self.gauge) {
-//                     self.gauge.updateConfig(config);
-//                 } else {
-//                     self.gauge = new Gauge(config);
-//                     self.gauge.draw();
-//                 }
-//             };
-//         },
-//     }
-// });
diff --git a/skyquake/framework/widgets/form_controls/formControls.jsx b/skyquake/framework/widgets/form_controls/formControls.jsx
new file mode 100644 (file)
index 0000000..e50fb7e
--- /dev/null
@@ -0,0 +1,113 @@
+import './formControls.jsx';
+
+import React from 'react'
+import SelectOption from 'widgets/form_controls/selectOption.jsx';
+import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
+import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
+import TextInput from 'widgets/form_controls/textInput.jsx';
+import Input from 'widgets/form_controls/input.jsx';
+
+export 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: ''
+}
+
+/**
+ * AddItemFn:
+ */
+export class InputCollection extends React.Component {
+    constructor(props) {
+        super(props);
+        this.collection = props.collection;
+    }
+    buildTextInput(onChange, v, i) {
+        return (
+            <Input
+                readonly={this.props.readonly}
+                style={{flex: '1 1'}}
+                key={i}
+                value={v}
+                onChange= {onChange.bind(null, i)}
+            />
+        )
+    }
+    buildSelectOption(initial, options, onChange, v, i) {
+        return (
+            <SelectOption
+                readonly={this.props.readonly}
+                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)}
+                            {
+                                props.readonly ? null : <span onClick={props.RemoveItemFn.bind(null, i)} className="removeInput"><img src={imgRemove} />Remove</span>}
+                        </div>
+                    )
+                })}
+                { props.readonly ? null : <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}`)
+    }
+}
index 4a88435..ad95add 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,7 +15,7 @@
  *   limitations under the License.
  *
  */
-@import 'style/_colors.scss';
+@import '../../style/_colors.scss';
 
 .sqTextInput {
     display: -ms-flexbox;
@@ -32,9 +32,9 @@
         color:$darker-gray;
         text-transform:uppercase;
     }
-    input, .readonly, textarea {
+    input, textarea {
         height: 35px;
-        box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.39), 0 -1px 1px #ffffff, 0 1px 0 #ffffff;
+        /* box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.39), 0 -1px 1px #ffffff, 0 1px 0 #ffffff;*/
         font-size: 1rem;
         display: block;
         background: white !important;
@@ -49,6 +49,7 @@
     .readonly {
         line-height: 35px;
         box-shadow:none;
+        background:none !important;
     }
     textarea {
         -ms-flex-align: stretch;
         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;
+            }
+    }
+    .invalid {
+        color: red;
+        font-weight:strong;
+    }
+     input:invalid {
+        border: 2px solid red;
+        &:after {
+            content: 'Invalid Value'
+        }
+    }
+}
+
+.sqCheckBox {
+    display:-ms-flexbox;
+    display:flex;
+    label {
+        display:-ms-flexbox;
+        display:flex;
+        -ms-flex-align: center;
+        align-items: center;
+        input {
+            box-shadow: none;
+            height: auto;
+            margin: 0 0.25rem;
+        }
+    }
+}
+
+.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 {
+
+    }
 }
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..ca31c13
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ *
+ *   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 CheckSVG from '../../../node_modules/open-iconic/svg/check.svg'
+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 tester = null;
+        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;
+            tester = new RegExp(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;
+            case 'radiogroup':
+                inputType = buildRadioButtons(this.props);
+                break;
+            default:
+                inputType = <input key={props.key} type={props.type} {...inputProperties} onChange={props.onChange} placeholder={props.placeholder}/>;
+        }
+        let displayedValue;
+        if(value === null) {
+            displayedValue = null;
+        } else {
+            displayedValue = value.toString();
+        }
+        if( props.readonly && props.type == "checkbox" && props.checked ) {
+            displayedValue = <img src={CheckSVG} />
+        }
+
+        if( props.readonly && props.type == "radiogroup" && props.readonlydisplay ) {
+            displayedValue = props.readonlydisplay
+        }
+
+        let html = (
+            <label className={className} style={props.style}>
+              <span> { label } {isRequired}</span>
+              {
+                !props.readonly ? inputType : <div className="readonly">{displayedValue}</div>
+              }
+              {
+                 !props.readonly && tester && value && !tester.test(value) ? <span className="invalid">The Value you entered is invalid</span> : null
+              }
+            </label>
+        );
+        return html;
+    }
+}
+
+
+function buildRadioButtons(props) {
+    let className = 'sqCheckBox';
+    return(
+       <div className={className}>
+            {
+                props.options.map((o,i) => {
+                    let label = o.label || o;
+                    let value = o.value || o;
+                    return (
+                        <label key={i}>
+                            {label}
+                            <input type="radio" checked={props.value == value} value={value} onChange={props.onChange} />
+                        </label>
+                    )
+                })
+            }
+       </div>
+
+    )
+}
+
+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..067d1d5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,32 +28,65 @@ export default class SelectOption extends React.Component {
   render() {
     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 options =  this.props.options && this.props.options.map(function(op, i) {
+    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>
-            {this.props.label}
-            <select className={this.props.className} onChange={this.handleOnChange} defaultValue={JSON.stringify(defaultValue)} >
-                {
-                 options
-                }
-            </select>
+        <label key={this.props.key} className={this.props.className}>
+            <span>{this.props.label}</span>
+            {
+              this.props.readonly ? defaultValue
+              : (
+                  <select
+                    className={this.props.className}
+                    onChange={this.handleOnChange}
+                    value={JSON.stringify(this.props.value)}
+                    defaultValue={JSON.stringify(defaultValue)}>
+                      {
+                       options
+                      }
+                  </select>
+                )
+            }
         </label>
     );
     return html;
   }
 }
 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,
+  readonly: 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 5e1e717..6a2e56c 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
  */
 
 header.header-app-component {
-    padding: 20px 0px;
+    padding: 10px 0px;
+    display:-ms-flexbox;
     display:flex;
-    flex-direction:column;
+    -ms-flex-direction:column;
+        flex-direction:column;
     .header-app-main {
+        display:-ms-flexbox;
         display:flex;
-        flex-direction:row;
-        justify-content:space-between;
-        align-items:center;
+        -ms-flex-direction:row;
+            flex-direction:row;
+        -ms-flex-pack:justify;
+            justify-content:space-between;
+        -ms-flex-align:center;
+            align-items:center;
     }
     h1 {
         /*background: url('../../style/img/header-logo.png') no-repeat;*/
@@ -39,15 +45,19 @@ header.header-app-component {
         font-size: 1.625rem;
         font-weight: 400;
         position:relative;
-        flex: 1 0 auto;
+        -ms-flex: 1 0 auto;
+            flex: 1 0 auto;
 
     }
     ul {
+            display:-ms-flexbox;
             display:flex;
         }
         li {
+            display:-ms-flexbox;
             display:flex;
-             flex:1 1 auto;
+             -ms-flex:1 1 auto;
+                 flex:1 1 auto;
              border-right:1px solid #e5e5e5;
              padding: 0 1rem;
             &:last-child {
@@ -55,12 +65,13 @@ header.header-app-component {
             }
             a {
                 cursor:pointer;
-                // padding: 0.125rem;
-                // border-bottom:1px solid black;
+                /* padding: 0.125rem;*/
+                /* border-bottom:1px solid black;*/
                 text-decoration:underline;
             }
         }
     .header-app-nav {
+        display:-ms-flexbox;
         display:flex;
         margin-left: 0.25rem;
         a,span {
@@ -81,8 +92,11 @@ header.header-app-component {
         }
     }
     nav {
+        display:-ms-flexbox;
         display:flex;
-        flex:0 1 auto;
-        align-items:center;
+        -ms-flex:0 1 auto;
+            flex:0 1 auto;
+        -ms-flex-align:center;
+            align-items:center;
     }
 }
index ba77a79..0e12b6e 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -101,7 +101,7 @@ class ThrottledMessage extends React.Component {
   }
   render() {
     if(!this.props.hasFailed) {
-      return (<span className='throttledMessageText'>{this.state.displayMessage}</span>)
+      return (<span className='throttledMessageText' style={{margin:'1rem'}}>{this.state.displayMessage}</span>)
     } else {
       return (<span> </span>)
     }
index 4ef7c89..03877b0 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}
@@ -51,13 +59,23 @@ Panel.defaultProps = {
 
 export class PanelWrapper extends Component {
     render() {
+        let wrapperClass = 'skyquakePanelWrapper';
+        let {className, column, style, ...props} = this.props;
+        if(className) {
+            wrapperClass = `${wrapperClass} ${className}`
+        }
+        if(column) {
+            style.flexDirection = 'column';
+        }
         return (
-        <div className={'skyquakePanelWrapper ' + this.props.className} style={this.props.style}>
+        <div className={wrapperClass} style={style} {...props}>
             {this.props.children}
         </div>)
     }
 }
-
+PanelWrapper.defaultProps = {
+    style: {}
+}
 export default Panel;
 
 
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 7df4e3e..471efd1 100644 (file)
  *
  */
 
- /**
 * EventCenter module to display a list of events from the system
 * @module framework/widgets/skyquake_container/EventCenter
 * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
 *
 */
+/**
+ * EventCenter module to display a list of events from the system
+ * @module framework/widgets/skyquake_container/EventCenter
+ * @author Kiran Kashalkar <kiran.kashalkar@riftio.com>
+ *
+ */
 
 import React from 'react';
 import { Link } from 'react-router';
@@ -31,6 +31,8 @@ import TreeView from 'react-treeview';
 import _isEqual from 'lodash/isEqual';
 import _merge from 'lodash/merge';
 import _indexOf from 'lodash/indexOf';
+import _isArray from 'lodash/isArray';
+import _sortBy from 'lodash/sortBy';
 import '../../../node_modules/react-treeview/react-treeview.css';
 import './eventCenter.scss';
 
@@ -44,7 +46,6 @@ export default class EventCenter extends React.Component {
        componentWillReceiveProps(props) {
                let stateObject = {};
 
-               let notificationList = sessionStorage.getItem('notificationList');
                let latestNotification = sessionStorage.getItem('latestNotification');
 
                if (props.newNotificationEvent && props.newNotificationMsg) {
@@ -67,21 +68,16 @@ export default class EventCenter extends React.Component {
                        stateObject.newNotificationEvent = false;
                        stateObject.newNotificationMsg = null;
                }
+               stateObject.notifications = props.notifications;
 
-               if (notificationList) {
-                       stateObject.notifications = _merge(notificationList, props.notifications);
-               } else {
-                       stateObject.notifications = props.notifications;
-               }
-               sessionStorage.setItem('notifications', JSON.stringify(stateObject.notifications));
                this.setState(stateObject);
        }
 
        newNotificationReset = () => {
-        this.setState({
-            newNotificationEvent: false
-        });
-    }
+               this.setState({
+                       newNotificationEvent: false
+               });
+       }
 
        onClickToggleOpenClose(event) {
                this.props.onToggle();
@@ -92,13 +88,13 @@ export default class EventCenter extends React.Component {
        constructTree(details) {
                let markup = [];
                for (let key in details) {
-                       if (typeof(details[key]) == "object") {
-                           let html = (
-                               <TreeView key={key} nodeLabel={key}>
-                                 {this.constructTree(details[key])}
-                               </TreeView>
-                           );
-                           markup.push(html);
+                       if (typeof (details[key]) == "object") {
+                               let html = (
+                                       <TreeView key={key} nodeLabel={key}>
+                                               {this.constructTree(details[key])}
+                                       </TreeView>
+                               );
+                               markup.push(html);
                        } else {
                                markup.push((<div key={key} className="info">{key} = {details[key].toString()}</div>));
                        }
@@ -142,40 +138,42 @@ export default class EventCenter extends React.Component {
                        );
                }
 
-               this.state.notifications && this.state.notifications.map((notification, notifIndex) => {
-                       let notificationFields = {};
-
-                       notificationFields = this.getNotificationFields(notification);
-
-                       displayNotifications.push(
-                               <tr key={notifIndex} className='notificationItem'>
-                                       <td className='source column'> {notificationFields.source} </td>
-                                       <td className='timestamp column'> {notificationFields.eventTime} </td>
-                                       <td className='event column'> {notificationFields.eventKey} </td>
-                                       <td className='details column'>
-                                               <TreeView key={notifIndex + '-detail'} nodeLabel='Event Details'>
-                                                       {this.constructTree(notificationFields.details)}
-                                               </TreeView>
-                                       </td>
-                               </tr>
-                       );
-               });
+               this.state.notifications &&
+                       _isArray(this.state.notifications) &&
+                       this.state.notifications.map((notification, notifIndex) => {
+                               let notificationFields = {};
+
+                               notificationFields = this.getNotificationFields(notification);
+
+                               displayNotifications.push(
+                                       <tr key={notifIndex} className='notificationItem'>
+                                               <td className='source column'> {notificationFields.source} </td>
+                                               <td className='timestamp column'> {notificationFields.eventTime} </td>
+                                               <td className='event column'> {notificationFields.eventKey} </td>
+                                               <td className='details column'>
+                                                       <TreeView key={notifIndex + '-detail'} nodeLabel='Event Details'>
+                                                               {this.constructTree(notificationFields.details)}
+                                                       </TreeView>
+                                               </td>
+                                       </tr>
+                               );
+                       });
 
                let openedClassName = this.state.isOpen ? 'open' : 'closed';
                html = (
                        <div className={'eventCenter ' + openedClassName}>
 
-                        <div className='notification'>
-                            <Crouton
-                                id={Date.now()}
-                                message={this.getNotificationFields(this.state.newNotificationMsg).eventKey +
-                                       ' notification received. Check Event Center for more details.'}
-                                type={'info'}
-                                hidden={!(this.state.newNotificationEvent && this.state.newNotificationMsg)}
-                                onDismiss={this.newNotificationReset}
-                                timeout={this.props.dismissTimeout}
-                            />
-                        </div>
+                               <div className='notification'>
+                                       <Crouton
+                                               id={Date.now()}
+                                               message={this.getNotificationFields(this.state.newNotificationMsg).eventKey +
+                                                       ' notification received. Check Event Center for more details.'}
+                                               type={'info'}
+                                               hidden={!(this.state.newNotificationEvent && this.state.newNotificationMsg)}
+                                               onDismiss={this.newNotificationReset}
+                                               timeout={this.props.dismissTimeout}
+                                       />
+                               </div>
                                <h1 className='title' data-open-close-icon={this.state.isOpen ? 'open' : 'closed'}
                                        onClick={this.onClickToggleOpenClose.bind(this)}>
                                        EVENT CENTER
index 2869560..b6f688f 100644 (file)
@@ -7,15 +7,18 @@ html, body {
 
 .crouton {
     span {
-        white-space: pre;
+        /* white-space: pre;*/
     }
 }
 
 .skyquakeApp {
     display: -ms-flexbox;
+    display: -webkit-box;
     display: flex;
     -ms-flex-direction: column;
-        flex-direction: column;
+        -webkit-box-orient: vertical;
+        -webkit-box-direction: normal;
+            flex-direction: column;
     height: 100%;
     background: $gray-lightest;
     h1 {
@@ -28,6 +31,7 @@ html, body {
     }
     .skyquakeNav {
         display: -ms-flexbox;
+        display: -webkit-box;
         display: flex;
         color:white;
         background:black;
@@ -36,11 +40,14 @@ html, body {
         font-size:0.75rem;
         .secondaryNav {
             -ms-flex: 1 1 auto;
-                flex: 1 1 auto;
+                -webkit-box-flex: 1;
+                    flex: 1 1 auto;
             display: -ms-flexbox;
+            display: -webkit-box;
             display: flex;
             -ms-flex-pack: end;
-                justify-content: flex-end;
+                -webkit-box-pack: end;
+                    justify-content: flex-end;
         }
         .app {
             position:relative;
@@ -48,9 +55,11 @@ html, body {
                 font-size:0.75rem;
                 border-right: 1px solid black;
                 display: -ms-flexbox;
+                display: -webkit-box;
                 display: flex;
                 -ms-flex-align: center;
-                    align-items: center;
+                    -webkit-box-align: center;
+                        align-items: center;
                 .oi {
                     padding-right: 0.5rem;
                 }
@@ -60,6 +69,11 @@ html, body {
                 display:none;
                 z-index:2;
                 width: 100%;
+                &.project {
+                    a {
+                        text-transform:none;
+                    }
+                }
             }
             &:first-child{
                 h2 {
@@ -102,12 +116,13 @@ html, body {
         &:before {
             content: '';
             height:1.85rem;
-            width:5.5rem;
-            /*margin:0 1rem;*/
-            /*padding:0 0.125rem;*/
+            width:2.5rem;
+            margin:auto 1rem;
+            padding:0 0.125rem;
+            /* background: url('../../style/img/header-logo.png') no-repeat center center white;*/
             /*background: url('../../style/img/svg/riftio_logo_white.svg') no-repeat center center;*/
             background: url('../../style/img/svg/osm-logo_color_rgb_white_text.svg') no-repeat center center;
-            background-size: contain;
+            background-size:contain;
         }
     }
     .skyquakeContainerWrapper {
@@ -127,7 +142,8 @@ html, body {
         text-align:left;
         position:relative;
         -ms-flex: 1 0 auto;
-            flex: 1 0 auto;
+            -webkit-box-flex: 1;
+                flex: 1 0 auto;
         }
     }
     .corner-accent {
index 893a4c1..cd489ac 100644 (file)
@@ -7,7 +7,7 @@ export default function(Component) {
         this.actions = context.flux.actions.global;
     }
     render(props) {
-        return <Component {...this.props} router={this.router} actions={this.actions} flux={this.context.flux} />
+        return <Component {...this.props} router={this.router} actions={this.actions} flux={this.context.flux}/>
     }
   }
   SkyquakeComponent.contextTypes = {
index d83c836..5c4c97e 100644 (file)
 import React from 'react';
 import AltContainer from 'alt-container';
 import Alt from './skyquakeAltInstance.js';
-import SkyquakeNav from './skyquakeNav.jsx';
+ import _cloneDeep from 'lodash/cloneDeep';
+import SkyquakeNav from '../skyquake_nav/skyquakeNav.jsx';
 import EventCenter from './eventCenter.jsx';
 import SkyquakeContainerActions from './skyquakeContainerActions.js'
 import SkyquakeContainerStore from './skyquakeContainerStore.js';
 // import Breadcrumbs from 'react-breadcrumbs';
 import Utils from 'utils/utils.js';
 import Crouton from 'react-crouton';
+import SkyquakeNotification from '../skyquake_notification/skyquakeNotification.jsx';
 import ScreenLoader from 'widgets/screen-loader/screenLoader.jsx';
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
 import './skyquakeApp.scss';
 // import 'style/reset.css';
 import 'style/core.css';
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
 export default class skyquakeContainer extends React.Component {
     constructor(props) {
         super(props);
@@ -39,13 +45,21 @@ export default class skyquakeContainer extends React.Component {
         this.state.eventCenterIsOpen = false;
         this.state.currentPlugin = SkyquakeContainerStore.currentPlugin;
     }
-
+    getChildContext() {
+        return {
+          userProfile: this.state.user
+        };
+    }
+    getUserProfile() {
+        return this.state.user;
+    }
     componentWillMount() {
         let self = this;
 
         Utils.bootstrapApplication().then(function() {
             SkyquakeContainerStore.listen(self.listener);
             SkyquakeContainerStore.getNav();
+            SkyquakeContainerStore.getUserProfile();
             SkyquakeContainerStore.getEventStreams();
         });
 
@@ -82,12 +96,14 @@ export default class skyquakeContainer extends React.Component {
     }
 
     render() {
-        const {displayNotification, notificationMessage, displayScreenLoader, notificationType, ...state} = this.state;
+        const {displayNotification, notificationData, displayScreenLoader,...state} = this.state;
+        const User = this.state.user || {};
+        const rbacValid = isRBACValid(User, [PLATFORM.SUPER, PLATFORM.ADMIN, PLATFORM.OPER]);
         var html;
-
+        let nav = _cloneDeep(this.state.nav);
         if (this.matchesLoginUrl()) {
             html = (
-                <AltContainer>
+                <AltContainer flux={Alt}>
                     <div className="skyquakeApp">
                         {this.props.children}
                     </div>
@@ -100,29 +116,33 @@ export default class skyquakeContainer extends React.Component {
             html = (
                 <AltContainer flux={Alt}>
                     <div className="skyquakeApp wrap">
-                        <Crouton
-                            id={Date.now()}
-                            message={notificationMessage}
-                            type={notificationType}
-                            hidden={!(displayNotification && notificationMessage)}
+                        <SkyquakeNotification
+                            data={this.state.notificationData}
+                            visible={displayNotification}
                             onDismiss={SkyquakeContainerActions.hideNotification}
-                            timeout= {5000}
                         />
                         <ScreenLoader show={displayScreenLoader}/>
-                        <SkyquakeNav nav={this.state.nav}
+                        <SkyquakeNav nav={nav}
                             currentPlugin={this.state.currentPlugin}
-                            store={SkyquakeContainerStore} />
+                            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}
                         </div>
-                        <EventCenter className="eventCenter"
-                            notifications={this.state.notifications}
-                            newNotificationEvent={this.state.newNotificationEvent}
-                            newNotificationMsg={this.state.newNotificationMsg}
-                            onToggle={this.onToggle} />
+                        {
+                            rbacValid ?
+                                <EventCenter className="eventCenter"
+                                    notifications={this.state.notifications}
+                                    newNotificationEvent={this.state.newNotificationEvent}
+                                    newNotificationMsg={this.state.newNotificationMsg}
+                                    onToggle={this.onToggle} />
+                            : null
+                        }
                     </div>
                 </AltContainer>
             );
@@ -130,6 +150,9 @@ export default class skyquakeContainer extends React.Component {
         return html;
     }
 }
+skyquakeContainer.childContextTypes = {
+  userProfile: React.PropTypes.object
+};
 skyquakeContainer.contextTypes = {
     router: React.PropTypes.object
   };
index 773978b..1124658 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,9 +25,13 @@ export default Alt.generateActions(
        'getEventStreamsSuccess',
        'getEventStreamsError',
     //Notifications
+    'handleServerReportedError',
     'showNotification',
     'hideNotification',
     //Screen Loader
     'showScreenLoader',
-    'hideScreenLoader'
+    'hideScreenLoader',
+    'openProjectSocketSuccess',
+    'getUserProfileSuccess',
+    'selectActiveProjectSuccess'
 );
index da7080f..f02e7ba 100644 (file)
@@ -79,7 +79,7 @@ export default {
             remote: function(state, location, streamSource) {
                 return new Promise((resolve, reject) => {
                     $.ajax({
-                        url: '//' + window.location.hostname + ':' + window.location.port + '/socket-polling?api_server=' + API_SERVER,
+                        url: '//' + window.location.hostname + ':' + window.location.port + '/socket-polling',
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         data: {
@@ -114,6 +114,79 @@ export default {
             success: SkyquakeContainerActions.openNotificationsSocketSuccess,
             error: SkyquakeContainerActions.openNotificationsSocketError
         }
+    },
+    openProjectSocket() {
+            return {
+                remote: function(state) {
+                    return new Promise(function(resolve, reject) {
+                        //If socket connection already exists, eat the request.
+                        if(state.socket) {
+                            return resolve(false);
+                        }
+                        $.ajax({
+                            url: '/socket-polling',
+                            type: 'POST',
+                            beforeSend: Utils.addAuthorizationStub,
+                            data: {
+                                url: '/project?api_server=' + API_SERVER
+                            },
+                            success: function(data, textStatus, jqXHR) {
+                                Utils.checkAndResolveSocketRequest(data, resolve, reject);
+                            }
+                            })
+                        .fail(function(xhr){
+                            //Authentication and the handling of fail states should be wrapped up into a connection class.
+                            Utils.checkAuthentication(xhr.status);
+                        });;
+                    });
+                },
+            success: SkyquakeContainerActions.openProjectSocketSuccess
+        }
+    },
+
+    getUserProfile() {
+        return {
+            remote: function(state, recordID) {
+                return new Promise(function(resolve, reject) {
+                    $.ajax({
+                        url: '/user-profile?api_server=' + API_SERVER,
+                        type: 'GET',
+                        beforeSend: Utils.addAuthorizationStub,
+                        success: function(data) {
+                            resolve(data);
+                        }
+                    }).fail(function(xhr) {
+                        //Authentication and the handling of fail states should be wrapped up into a connection class.
+                        Utils.checkAuthentication(xhr.status);
+                    });;
+                });
+            },
+            loading: Alt.actions.global.showScreenLoader,
+            success: SkyquakeContainerActions.getUserProfileSuccess
+        }
+    },
+
+    selectActiveProject() {
+        return {
+            remote: function(state, projectId) {
+                const {currentPlugin} = state;
+                const encodedProjectId = encodeURIComponent(projectId);
+                return new Promise(function(resolve, reject) {
+                    $.ajax({
+                        url: `/session/${encodedProjectId}?api_server=${API_SERVER}&app=${currentPlugin}`,
+                        type: 'PUT',
+                        beforeSend: Utils.addAuthorizationStub,
+                        success: function(data) {
+                            resolve(projectId);
+                        }
+                    }).fail(function(xhr) {
+                        //Authentication and the handling of fail states should be wrapped up into a connection class.
+                        Utils.checkAuthentication(xhr.status);
+                    });;
+                });
+            },
+            success: SkyquakeContainerActions.selectActiveProjectSuccess
+        }
     }
 }
 
index 56ebdda..e9ba78e 100644 (file)
 import Alt from './skyquakeAltInstance.js';
 import SkyquakeContainerSource from './skyquakeContainerSource.js';
 import SkyquakeContainerActions from './skyquakeContainerActions';
+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
 import rw from 'utils/rw.js';
 
 var API_SERVER = rw.getSearchParams(window.location).api_server;
-var UPLOAD_SERVER = rw.getSearchParams(window.location).upload_server;
+const MAX_STORED_EVENTS = 20;
 
 class SkyquakeContainerStore {
     constructor() {
         this.currentPlugin = getCurrentPlugin();
         this.nav = {};
-        this.notifications = [];
+               let notificationList = null;
+               try {notificationList = JSON.parse(sessionStorage.getItem('notifications'));} catch (e) {}
+               this.notifications = notificationList || [];
         this.socket = null;
+        this.projects = null;
+        this.user = {};
         //Notification defaults
         this.notificationMessage = '';
         this.displayNotification = false;
@@ -96,6 +102,9 @@ class SkyquakeContainerStore {
         ws.onmessage = (socket) => {
             try {
                 var data = JSON.parse(socket.data);
+                if(data.hasOwnProperty('map')) {
+                    data = [];
+                }
                 if (!data.notification) {
                     console.warn('No notification in the received payload: ', data);
                 } else {
@@ -105,12 +114,14 @@ class SkyquakeContainerStore {
                         // newly appreared event.
                         // Add to the notifications list and setState
                         self.notifications.unshift(data.notification);
+                        (self.notifications.length > MAX_STORED_EVENTS) && self.notifications.pop();
                         self.setState({
                             newNotificationEvent: true,
                             newNotificationMsg: data.notification,
                             notifications: self.notifications,
                             isLoading: false
                         });
+                        sessionStorage.setItem('notifications', JSON.stringify(self.notifications));
                     }
                 }
             } catch(e) {
@@ -137,6 +148,7 @@ class SkyquakeContainerStore {
         console.log('Found streams: ', streams);
         let self = this;
 
+        streams &&
         streams['ietf-restconf-monitoring:streams'] &&
         streams['ietf-restconf-monitoring:streams']['stream'] &&
         streams['ietf-restconf-monitoring:streams']['stream'].map((stream) => {
@@ -162,23 +174,53 @@ class SkyquakeContainerStore {
         })
     }
 
-    //Notifications
-    showNotification = (data) => {
-        let state = {
-                displayNotification: true,
-                notificationMessage: data,
-                notificationType: 'error',
-                displayScreenLoader: false
-            }
-        if(typeof(data) == 'string') {
-
-        } else {
-            state.notificationMessage = data.msg;
-            if(data.type) {
-                state.notificationType = data.type;
+    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 (!data.project || !_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);
             }
-        }
-        this.setState(state);
+        };
+    }
+    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.href = window.location.origin;
+    }
+    //Notifications
+    handleServerReportedError = (result) => {
+        this.hideScreenLoader();
+        this.alt.actions.global.showNotification.defer(result);
+    }
+    showNotification = (notificationData) => {
+        this.setState({
+            notificationData,
+            displayNotification: true
+        })
     }
     hideNotification = () => {
         this.setState({
@@ -211,7 +253,7 @@ function decorateAndTransformNav(nav, currentPlugin) {
             if (k != currentPlugin)  {
                 nav[k].routes.map(function(route, i) {
                     if (API_SERVER) {
-                        route.route = '/' + k + '/index.html?api_server=' + API_SERVER + '&upload_server=' + UPLOAD_SERVER + '#' + route.route;
+                        route.route = '/' + k + '/index.html?api_server=' + API_SERVER + '#' + route.route;
                     } else {
                         route.route = '/' + k + '/#' + route.route;
                     }
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeNav.jsx b/skyquake/framework/widgets/skyquake_container/skyquakeNav.jsx
deleted file mode 100644 (file)
index 8814a18..0000000
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * 
- *   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';
-
-//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>
-        );
-    }
-}
-
-
-//
-// 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 = '';
-    }
-    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)}
-            </nav>
-
-            </div>
-        )
-        return html;
-    }
-}
-skyquakeNav.defaultProps = {
-    nav: {}
-}
-/**
- * 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) {
-    let ref;
-    let route = link.route;
-    if(link.isExternal) {
-        ref = (
-            <a href={route}>{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}>
-                {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) {
-    let navList = [];
-    let navListHTML = [];
-    let secondaryNav = [];
-    let self = this;
-    self.hasSubNav = {};
-    let secondaryNavHTML = (
-        <div className="secondaryNav" key="secondaryNav">
-        {secondaryNav}
-            <LogoutAppMenuItem />
-        </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
-            });
-            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;
-            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;
-}
index fc3231d..7d8daa5 100644 (file)
@@ -22,10 +22,24 @@ import {
 from 'react-router';
 import SkyquakeContainer from 'widgets/skyquake_container/skyquakeContainer.jsx';
 
+/**
+ * This is the Skyquake App wrapper that all plugins use to be a Skyquake plugin. This 
+ * is not a react component although it does return a react component that will be the
+ * app.
+ * This function will also set the title into the <head>
+ * 
+ * @export
+ * @param {any} config 
+ * @param {any} context 
+ * @returns a react component to be rendered manually.
+ */
 export default function(config, context) {
     let routes = [];
     let index = null;
     let components = null;
+    
+    document.title = config.name || "OpenMANO";
+
     if (config && config.routes) {
         routes =  buildRoutes(config.routes)
         function buildRoutes(routes) {
@@ -83,7 +97,6 @@ export default function(config, context) {
             },
             childRoutes: routes
         }
-
         return((
             <Router history={hashHistory} routes={rootRoute}>
             </Router>
diff --git a/skyquake/framework/widgets/skyquake_nav/skyquakeNav.jsx b/skyquake/framework/widgets/skyquake_nav/skyquakeNav.jsx
new file mode 100644 (file)
index 0000000..7986d5e
--- /dev/null
@@ -0,0 +1,377 @@
+/*
+ *
+ *   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 DOWNLOAD_SERVER = rw.getSearchParams(window.location).dev_download_server;
+
+//
+// Internal classes/functions
+//
+
+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 hasProjects = props.projects;
+        let userAssignedProjects = hasProjects && (props.projects.length > 0)
+        return (
+            <div className="app">
+                <h2>
+                    <a style={{textTransform:'none'}}>
+                        {
+                            hasProjects ?
+                            (userAssignedProjects ? 'PROJECT: ' + props.currentProject : 'No Projects Assigned')
+                            : 'Projects Loading...'
+                        }
+                    </a>
+                    {
+                        userAssignedProjects ? <span className="oi" data-glyph="caret-bottom"></span> : null
+                    }
+                </h2>
+                {
+                    userAssignedProjects ?
+                        <ul className="project menu">
+                            {
+                                props.projects.map(function (p, k) {
+                                    return <li key={k} onClick={props.onSelectProject.bind(null, p.name)}><a>{p.name}</a></li>
+                                })
+                            }
+                        </ul>
+                        : null
+                }
+            </div>
+        )
+    }
+}
+
+/*
+
+ <SelectOption
+                        options={projects}
+                        value={currentValue}
+                        defaultValue={currentValue}
+                        onChange={props.onSelectProject}
+                        className="projectSelect" />
+
+ */
+
+
+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 = null;
+        this.props.nav['user_management'] && this.props.nav['user_management'].routes.map((r) => {
+            if (r.unique) {
+                userProfileLink = r;
+            }
+        })
+        return !userProfileLink ? null : (
+            <div className="app">
+                <h2 className="username">
+                    USERNAME: {returnLinkItem(userProfileLink, props.currentUser)}
+                    <span className="oi" data-glyph="caret-bottom"></span>
+                </h2>
+                <ul className="menu">
+                    <li>
+                        {returnLinkItem(userProfileLink, "My Profile")}
+                    </li>
+                    <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();
+    }
+    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(navData, currentPlugin, props) {
+    let navList = [];
+    let navListHTML = [];
+    let secondaryNav = [];
+    let adminNav = [];
+    //For monitoring when admin panel is active
+    let adminNavList = [];
+    let self = this;
+    const User = this.context.userProfile;
+    //The way the nav is sorting needs to be refactored.
+    let navArray = navData && Object.keys(navData).sort((a, b) => navData[a].order - navData[b].order)
+    self.hasSubNav = {};
+    for (let i = 0; i < navArray.length; i++) {
+        let k = navArray[i];
+        if (navData.hasOwnProperty(k)) {
+            self.hasSubNav[k] = false;
+            let header = null;
+            let navClass = "app";
+            let routes = navData[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 + '' + (DOWNLOAD_SERVER ? '&dev_download_server=' + DOWNLOAD_SERVER : '')
+                    : '';
+            } else {
+                route = routes[0].isExternal ? '/' + k + '/' : '';
+            }
+            if(navData[k].route) {
+                route = route + navData[k].route;
+            }
+            let dashboardLink = returnLinkItem({
+                isExternal: routes[0].isExternal,
+                pluginName: navData[k].pluginName,
+                label: navData[k].label || k,
+                route: route
+            });
+            let shouldAllow = navData[k].allow || ['*'];
+            if (navData[k].pluginName == currentPlugin) {
+                navClass += " active";
+            }
+            NavList = navData[k].routes.filter((r) => {
+                const User = self.context.userProfile;
+                const shouldAllow = r.allow || ['*'];
+                return isRBACValid(User, shouldAllow);
+            }).map(buildNavListItem.bind(self, k));
+            navItem.priority = navData[k].priority;
+            navItem.order = navData[k].order;
+            if (navData[k].admin_link) {
+                if (isRBACValid(User, shouldAllow)) {
+                    adminNavList.push(navData[k].pluginName);
+                    adminNav.push((
+                        <li key={navData[k].pluginName}>
+                            {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.map(function (n) {
+        if ((n.priority < 2)) {
+            return n.html;
+        } else {
+            secondaryNav.push(n.html);
+        }
+    });
+    if (adminNav.length) {
+            navListHTML.push(
+                <div key="Adminstration" className={"app " + (adminNavList.indexOf(currentPlugin) > -1 ? 'active' : '')}>
+                    <h2>
+                        <a>
+                            ADMINISTRATION
+                        </a>
+                        <span className="oi" data-glyph="caret-bottom"></span>
+                    </h2>
+                    <ul className="menu">
+                        {
+                            adminNav
+                        }
+                    </ul>
+                </div>
+            );
+    }
+    let secondaryNavHTML = (
+        <div className="secondaryNav" key="secondaryNav">
+            {secondaryNav}
+            <SelectProject
+                onSelectProject={props.store.selectActiveProject}
+                projects={props.projects}
+                currentProject={props.currentProject} />
+            <UserNav
+                currentUser={props.currentUser}
+                nav={navData} />
+        </div>
+    )
+    // console.log("app admin " + (adminNavList.indexOf(currentPlugin) > -1 ? 'active' : ''))
+    navListHTML.push(secondaryNavHTML);
+    return navListHTML;
+}
diff --git a/skyquake/framework/widgets/skyquake_nav/skyquakeNav.scss b/skyquake/framework/widgets/skyquake_nav/skyquakeNav.scss
new file mode 100644 (file)
index 0000000..8a312ff
--- /dev/null
@@ -0,0 +1,127 @@
+@import '../../style/_colors.scss';
+.active {
+        background-color: $brand-blue!important;
+        border-color: $brand-blue!important;
+        color: #fff!important
+    }
+    .skyquakeNav {
+        display: -ms-flexbox;
+        display: -webkit-box;
+        display: flex;
+        color:white;
+        background:black;
+        position:relative;
+        z-index: 10;
+        font-size:0.75rem;
+        padding-right:1rem;
+        .secondaryNav {
+            -ms-flex: 1 1 auto;
+            -webkit-box-flex: 1;
+            flex: 1 1 auto;
+            display: -ms-flexbox;
+            display: -webkit-box;
+            display: flex;
+            -ms-flex-pack: end;
+            -webkit-box-pack: end;
+            justify-content: flex-end;
+
+            .username a{
+                text-transform: none;
+            }
+        }
+        .app {
+            display: -ms-flexbox;
+            display: block;
+            position:relative;
+            margin: auto 0.5rem;
+            h2 {
+                font-size:0.75rem;
+                border-right: 1px solid black;
+                display: -ms-flexbox;
+                display: -webkit-box;
+                display: flex;
+                -ms-flex-pack: start;
+                -ms-flex-pack: start;
+                -webkit-box-pack: start;
+                        justify-content: flex-start;
+                -ms-flex-align: center;
+                -webkit-box-align: center;
+                        align-items: center;
+                .oi {
+                    padding-right: 0.5rem;
+                }
+            }
+            .menu {
+                position:absolute;
+                display:none;
+                z-index:2;
+                width: 100%;
+                li {
+                    text-align:left;
+                }
+            }
+            &:first-child{
+                h2 {
+                    border-left: 1px solid black;
+                }
+            }
+            &:hover {
+                a {
+                    color:$brand-blue-light;
+                    cursor:pointer;
+                }
+                .menu {
+                    display:block;
+                    background:black;
+                    a {
+                        color:white;
+                    }
+                    li:hover {
+                        a {
+                            color:$brand-blue-light;
+                        }
+                    }
+                }
+            }
+            &.active {
+                color:white;
+                background:black;
+                a {
+                    color:white;
+                }
+            }
+        }
+        a{
+            display:block;
+            padding:0.5rem 1rem;
+            text-decoration:none;
+            text-transform:uppercase;
+            text-align:left;
+            color:white;
+        }
+        &:before {
+            content: '';
+            min-width: 5.5rem;
+            margin: 0.125rem 1rem;
+            /*background: url('../../style/img/svg/riftio_logo_white.svg') no-repeat center center;*/
+            background: url('../../style/img/svg/osm-logo_color_rgb_white_text.svg') no-repeat center center;
+            background-size: contain;
+        }
+        .userSection {
+            display:-ms-flexbox;
+            display:-webkit-box;
+            display:flex;
+            -ms-flex-align:center;
+            -webkit-box-align:center;
+                    align-items:center;
+            padding-left: 1rem;
+            text-transform:uppercase;
+            text-align: left;
+            .projectSelect {
+                padding: 0 0.5rem;
+                font-size: 1rem;
+                /* min-width: 75%;*/
+                height: 25px;
+            }
+        }
+    }
diff --git a/skyquake/framework/widgets/skyquake_notification/netConfErrors.js b/skyquake/framework/widgets/skyquake_notification/netConfErrors.js
new file mode 100644 (file)
index 0000000..cde7b5d
--- /dev/null
@@ -0,0 +1,58 @@
+const NETCONF_ERRORS = {
+    'in-use' : {
+        description: 'The request requires a resource that already is in use.'
+    },
+    'invalid-value' : {
+        description: 'The request specifies an unacceptable value for one or more parameters.'
+    },
+    'too-big' : {
+        description: 'The request or response (that would be generated) is too large for the implementation to handle.'
+    },
+    'missing-attribute' : {
+        description: 'An expected attribute is missing.'
+    },
+    'bad-attribute' : {
+        description: 'An attribute value is not correct; e.g., wrong type, out of range, pattern mismatch.'
+    },
+    'unknown-attribute' : {
+        description: 'An unexpected attribute is present.'
+    },
+    'missing-element' : {
+        description: 'An expected element is missing.'
+    },
+    'bad-element' : {
+        description: 'An element value is not correct; e.g., wrong type, out of range, pattern mismatch.'
+    },
+    'unknown-element' : {
+        description: 'An unexpected element is present.'
+    },
+    'unknown-namespace' : {
+        description: 'An unexpected namespace is present.'
+    },
+    'access-denied' : {
+        description: 'Access to the requested protocol operation or data model is denied because authorization failed.'
+    },
+    'lock-denied' : {
+        description: 'Access to the requested lock is denied because the lock is currently held by another entity.'
+    },
+    'resource-denied' : {
+        description: 'Request could not be completed because of insufficient resources.'
+    },
+    'rollback-failed' : {
+        description: 'Request to roll back some configuration change (via rollback-on-error or <discard-changes> operations) was not completed for some reason.'
+    },
+    'data-exists' : {
+        description: 'Request could not be completed because the relevant data model content already exists.  For example, a "create" operation was attempted on data that already exists.'
+    },
+    'data-missing' : {
+        description: 'Request could not be completed because the relevant data model content does not exist.  For example, a "delete" operation was attempted on data that does not exist.'
+    },
+    'operation-not-supported' : {
+        description: 'Request could not be completed because the requested operation is not supported by this implementation.'
+    },
+    'operation-failed' : {
+        description: 'Request could not be completed because the requested operation failed for some reason not covered by any other error condition.'
+    }
+}
+
+export default NETCONF_ERRORS;
diff --git a/skyquake/framework/widgets/skyquake_notification/skyquakeNotification.jsx b/skyquake/framework/widgets/skyquake_notification/skyquakeNotification.jsx
new file mode 100644 (file)
index 0000000..c8ce157
--- /dev/null
@@ -0,0 +1,89 @@
+import React from 'react';
+import Crouton from 'react-crouton';
+import NETCONF_ERRORS from './netConfErrors.js';
+
+class SkyquakeNotification extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {};
+        this.state.displayNotification = props.visible;
+        this.state.notificationMessage = '';
+        this.state.notificationType = 'error';
+    }
+    componentWillReceiveProps(props) {
+        if(props.visible) {
+            this.processMessage(props.data);
+        } else {
+            this.setState({displayNotification: props.visible});
+        }
+    }
+    buildNetconfError(data) {
+        let error = data;
+        try {
+            let info = JSON.parse(data);
+            let rpcError = info.body || info.errorMessage.body || info.errorMessage.error;
+            if (rpcError && typeof rpcError === 'string') {
+                const index = rpcError.indexOf('{');
+                if (index >= 0) {
+                    rpcError = JSON.parse(rpcError.substr(index));
+                } else {
+                    return rpcError;
+                }
+            }
+            if (!rpcError) {
+                return error;
+            }
+            info = rpcError["rpc-reply"]["rpc-error"];
+            let errorTag = info['error-tag']
+            error = `
+                ${NETCONF_ERRORS[errorTag] && NETCONF_ERRORS[errorTag].description || 'Unknown NETCONF Error'}
+                PATH: ${info['error-path']}
+                INFO: ${JSON.stringify(info['error-info'])}
+            `
+        } catch (e) {
+            console.log('Unexpected string sent to buildNetconfError: ', e);
+        }
+        return error;
+    }
+    processMessage(data) {
+        let state = {
+                displayNotification: true,
+                notificationMessage: data,
+                notificationType: 'error',
+                displayScreenLoader: false
+            }
+        if(typeof(data) == 'string') {
+            //netconf errors will be json strings
+            state.notificationMessage = this.buildNetconfError(data);
+        } else {
+            let message = data.msg || '';
+            if(data.type) {
+                state.notificationType = data.type;
+            }
+            if(data.rpcError){
+                message += " " + this.buildNetconfError(data.rpcError);
+            }
+            state.notificationMessage = message;
+        }
+        console.log('NOTIFICATION: ', state.notificationMessage)
+        this.setState(state);
+    }
+    render() {
+        const {displayNotification, notificationMessage, notificationType, ...state} = this.state;
+        return (
+            <Crouton
+                id={Date.now()}
+                message={notificationMessage}
+                type={notificationType}
+                hidden={!(displayNotification && notificationMessage)}
+                onDismiss={this.props.onDismiss}
+                timeout={10000}
+            />
+        )
+    }
+}
+SkyquakeNotification.defaultProps = {
+    data: {},
+    onDismiss: function(){}
+}
+export default SkyquakeNotification;
diff --git a/skyquake/framework/widgets/skyquake_rbac/skyquakeRBAC.jsx b/skyquake/framework/widgets/skyquake_rbac/skyquakeRBAC.jsx
new file mode 100644 (file)
index 0000000..9097b4e
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ *
+ *   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 ROLES from 'utils/roleConstants.js';
+const PLATFORM = ROLES.PLATFORM;
+
+export function isRBACValid(User, allow, Project){
+  const UserData = User && User.data;
+  if(UserData) {
+      const PlatformRole = UserData.platform.role;
+      const isPlatformSuper = PlatformRole[PLATFORM.SUPER];
+      const isPlatformAdmin = PlatformRole[PLATFORM.ADMIN];
+      const isPlatformOper = PlatformRole[PLATFORM.OPER];
+      const hasRoleAccess =  checkForRoleAccess(UserData.project[Project || User.projectId], PlatformRole, allow)//false//(this.props.roles.indexOf(userProfile.projectRole) > -1)
+      if (isPlatformSuper) {
+        return true;
+      } else {
+        if (hasRoleAccess) {
+          return true;
+        }
+      }
+    }
+  return false;
+}
+
+export default class SkyquakeRBAC extends React.Component {
+    constructor(props, context) {
+        super(props);
+    }
+    render() {
+      const User = this.context.userProfile;
+      const UserData = User.data;
+      const Project = this.props.project;
+      let HTML = null;
+      // If user object has platform property then it has been populated by the back end.
+      if(isRBACValid(User, this.props.allow, Project)) {
+        HTML = this.props.children;
+      }
+      return (<div className={this.props.className} style={this.props.style}>{HTML}</div>)
+    }
+}
+SkyquakeRBAC.defaultProps = {
+  allow: [],
+  project: false
+}
+SkyquakeRBAC.contextTypes = {
+  userProfile: React.PropTypes.object
+}
+
+function checkForRoleAccess(project, PlatformRole, allow) {
+    if (allow.indexOf('*') > -1) return true;
+    for (let i = 0; i<allow.length; i++) {
+      if((project && project.role[allow[i]])|| PlatformRole[allow[i]]) {
+        return true
+      }
+    }
+    return false;
+  }
+
+
+
+// export default function(Component) {
+//   class SkyquakeRBAC extends React.Component {
+//     constructor(props, context) {
+//         super(props);
+//             }
+//     render(props) {
+//       console.log(this.context.userProfile)
+//       const User = this.context.userProfile.data;
+//       // If user object has platform property then it has been populated by the back end.
+//       if(User) {
+//         const PlatformRole = User.platform.role;
+//         const HTML = <Component {...this.props} router={this.router} actions={this.actions} flux={this.context.flux}/>;
+//         const isPlatformSuper = PlatformRole[PLATFORM.SUPER];
+//         const isPlatformAdmin = PlatformRole[PLATFORM.ADMIN];
+//         const isPlatformOper = PlatformRole[PLATFORM.OPER];
+//         const hasRoleAccess =  false//(this.props.roles.indexOf(userProfile.projectRole) > -1)
+//         if (isPlatformSuper || isPlatformOper || isPlatformAdmin) {
+//           return HTML
+//         } else {
+//           if (hasRoleAccess) {
+//             return HTML
+//           } else {
+//             return null;
+//           }
+//         }
+//       }
+//       else {
+//         return null;
+
+//       }
+//     }
+//   }
+//   SkyquakeRBAC.defaultProps = {
+
+//   }
+//   SkyquakeRBAC.contextTypes = {
+//     userProfile: React.PropTypes.object,
+//     allowedRoles: []
+//   };
+//   return SkyquakeRBAC;
+// }
index 0e8daeb..df83041 100644 (file)
   "dependencies": {
     "alt": "^0.18.3",
     "alt-container": "^1.0.2",
+    "base-64": "^0.1.0",
     "bluebird": "^3.4.1",
     "body-parser": "^1.14.2",
     "cors": "^2.7.1",
     "d3": "^3.5.16",
+    "debug": "^3.0.1",
     "ejs": "^2.3.4",
     "express": "^4.13.3",
     "express-session": "^1.13.0",
     "jquery": "^2.2.1",
     "json2yaml": "^1.1.0",
     "lodash": "^4.0.0",
+    "lusca": "^1.5.0",
     "minimist": "^1.2.0",
     "open-iconic": "^1.1.1",
+    "passport": "^0.3.2",
+    "passport-http-bearer": "^1.0.1",
+    "passport-oauth2": "^1.4.0",
+    "passport-oauth2-middleware": "^1.0.2",
+    "passport-openidconnect": "^0.0.2",
     "prismjs": "^1.3.0",
     "promise": "^7.1.1",
     "react": "^0.14.6",
@@ -39,7 +47,6 @@
     "request-promise": "^3.0.0",
     "require-json": "0.0.1",
     "require-reload": "^0.2.2",
-    "reset-css": "^2.0.20160720",
     "sockjs": "^0.3.17",
     "sockjs-client": "^1.1.1",
     "underscore": "^1.8.3",
index acfe863..2b14afa 100644 (file)
@@ -1,4 +1,4 @@
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,7 +15,7 @@
 #
 # Author(s): Kiran Kashalkar
 # Creation Date: 08/18/2015
-# 
+#
 
 ##
 # DEPENDENCY ALERT
@@ -42,13 +42,16 @@ set(
   subdirs
     about
     composer
-    config
     debug
+    project_management
+    user_management
 #    goodbyworld
 #    helloworld
     launchpad
     accounts
     logging
+    redundancy
+    admin
   )
 rift_add_subdirs(
   SUBDIR_LIST
index d77a43a..82cbafb 100644 (file)
@@ -32,7 +32,7 @@ About.getVCS = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
     request({
         url: utils.confdPort(api_server) + APIVersion + '/api/operational/vcs/info?deep',
@@ -64,7 +64,7 @@ About.getVersion = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
     request({
         url: utils.confdPort(api_server) + APIVersion + '/api/operational/version?deep',
@@ -116,7 +116,7 @@ About.uptime = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
     request({
         url: utils.confdPort(api_server) + APIVersion + '/api/operational/uptime/uptime',
index 1100c71..da8f697 100644 (file)
@@ -2,8 +2,14 @@
     "root": "public",
     "name": "About",
     "dashboard": "./about.jsx",
-    "order": 99,
+    "order": 15,
     "priority":2,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper"
+    ],
      "routes" : [{
         "label": "Dashboard",
         "route": "/",
index 3fd9253..403cb6a 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash
 
 # 
-#   Copyright 2016 RIFT.IO Inc
+#   Copyright 2016-2017 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.
 
 PLUGIN_NAME=about
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
\ No newline at end of file
index 7936913..b73dc46 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,6 +44,6 @@ 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
+tar -cf $bcache_dir/plugins/$plugin/node_modules.tar node_modules package.json -C $source_dir
 #cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
 
index 1a43aa4..c02afc5 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -120,10 +120,8 @@ class About extends React.Component {
     if (this.state != null) {
       var html = (
               <div className="table-container-wrapper">
-                {fossInfoComponent}
-                {uptimeComponent}
                 <div className="table-container">
-                  <h2> Version Info </h2>
+                  <h2>RIFT.ware Version Info </h2>
                   <table>
                     <thead>
                       <tr>
@@ -153,6 +151,7 @@ class About extends React.Component {
                     </tbody>
                   </table>
                 </div>
+                {uptimeComponent}
                 <div className="table-container">
                   <h2> Component Info </h2>
                   <table>
@@ -174,6 +173,36 @@ class About extends React.Component {
                     </tbody>
                   </table>
                 </div>
+                {fossInfoComponent}
+                <div className="table-container">
+                  <h2>RIFT.ware Copyright </h2>
+                  <table>
+                    <tbody>
+                      <tr>
+                        <td>
+                          Open Source Version
+                        </td>
+                        <td>
+                          (c) Copyright 2014 â€“ {new Date().getUTCFullYear()} RIFT.io Inc., All rights reserved
+                        </td>
+                        <td>
+                          <a href="https://support.riftio.com" target="_blank">https://open.riftio.com/</a>
+                        </td>
+                      </tr>
+                      <tr>
+                        <td>
+                          Commercial Version
+                        </td>
+                        <td>
+                          (c) Copyright 2014 â€“ {new Date().getUTCFullYear()} RIFT.io Inc., All rights reserved
+                        </td>
+                        <td>
+                          <a href="https://riftio.com/contact/" target="_blank">https://riftio.com/contact/</a>
+                        </td>
+                      </tr>
+                    </tbody>
+                  </table>
+                </div>
               </div>
               );
     } else {
index 4a2aa12..a2fa842 100644 (file)
@@ -27,6 +27,7 @@ var CompressionPlugin = require("compression-webpack-plugin");
 
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -67,8 +68,8 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
-            templateContent: '<div id="app"></div>'
+            filename: '../' + htmlFilename
+            template: frameworkPath + '/plugin-index.html'
         })
     ]
 };
index ec74f51..e0b4e3d 100644 (file)
@@ -29,8 +29,10 @@ var Accounts = {};
 var nameSpace = {
     cloud: 'cloud',
     sdn: 'sdn',
-    'config-agent': 'config-agent'
+    'config-agent': 'config-agent',
+    'resource-orchestrator': 'ro-account'
 };
+var APIVersion = '/v2'
 Accounts.get = function(req) {
     return new Promise(function(resolve, reject) {
         if (req.params.type || req.params.name) {
@@ -43,8 +45,8 @@ Accounts.get = function(req) {
                         })
                     });
                 }, function(reason) {
-            reject(reason);
-        })
+                    reject(reason);
+                })
         } else {
             getAll(req, resolve, reject);
         }
@@ -54,12 +56,14 @@ Accounts.get = function(req) {
         Promise.all([
             Cloud.get(req),
             Sdn.get(req),
-            ConfigAgent.get(req)
+            ConfigAgent.get(req),
+            getResourceOrchestrator(req)
         ]).then(function(result) {
             var ReturnData = {
                 cloud: result[0],
                 sdn: result[1],
-                'config-agent': result[2]
+                'config-agent': result[2],
+                'resource-orchestrator': result[3]
             };
             ReturnData.cloud.type = 'cloud';
             ReturnData.sdn.type = 'sdn';
@@ -78,6 +82,7 @@ Accounts.update = updateAccount;
 Accounts.create = updateAccount;
 Accounts.delete = deleteAccount;
 Accounts.refreshAccountConnectionStatus = refreshAccountConnectionStatus
+
 function getAccount(req) {
     return new Promise(function(resolve, reject) {
         var self = this;
@@ -87,18 +92,18 @@ function getAccount(req) {
         var type = nameSpace[req.params.type];
         var url = utils.confdPort(api_server) + '/api/operational/' + type + '/account';
         if (id) {
-            url += '/' + id;
+            url += '/' + encodeURIComponent(id);
         }
 
         _.extend(
             requestHeaders,
             id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
 
         request({
-                url: url + '?deep',
+                url: utils.projectContextUrl(req, url + '?deep'),
                 type: 'GET',
                 headers: requestHeaders,
                 forever: constants.FOREVER_ON,
@@ -141,16 +146,19 @@ function updateAccount(req) {
     var data = req.body;
     var requestHeaders = {};
     var createData = {};
-    var url = utils.confdPort(api_server) + '/api/config/' + type;
+    var url = utils.confdPort(api_server) + '/api/config/' + type //+ '/account';
     var method = 'POST'
     if (!id) {
-        createData = {
-            'account': Array.isArray(data) ? data : [data]
+        createData = {}
+        if (type == 'ro-account') {
+            createData['rw-ro-account:account'] = Array.isArray(data) ? data : [data]
+        } else {
+            createData['account'] = Array.isArray(data) ? data : [data]
         }
         console.log('Creating ' + type + ' account: ', createData);
     } else {
         method = 'PUT';
-        url += '/account/' + id;
+        url += '/account/' + encodeURIComponent(id);
         createData['rw-' + type + ':account'] = Array.isArray(data) ? data : [data];
     }
 
@@ -160,10 +168,10 @@ function updateAccount(req) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
         request({
-            url: url,
+            url: utils.projectContextUrl(req, url),
             method: method,
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -189,15 +197,15 @@ function deleteAccount(req) {
     var requestHeaders = {};
     var createData = {};
     var url = utils.confdPort(api_server) + '/api/config/' + type;
-    url += '/account/' + id;
+    url += '/account/' + encodeURIComponent(id);
     return new Promise(function(resolve, reject) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
         request({
-            url: url,
+            url: utils.projectContextUrl(req, url),
             method: 'DELETE',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -213,7 +221,7 @@ function deleteAccount(req) {
     })
 }
 
-function refreshAccountConnectionStatus (req) {
+function refreshAccountConnectionStatus(req) {
     var api_server = req.query['api_server'];
     var Name = req.params.name;
     var Type = req.params.type;
@@ -232,19 +240,27 @@ function refreshAccountConnectionStatus (req) {
         cloud: {
             label: 'cloud-account',
             rpc: 'update-cloud-status'
+        },
+        'resource-orchestrator': {
+            label: 'ro-account',
+            rpc: 'update-ro-account-status'
         }
     }
     jsonData.input[rpcInfo[Type].label] = Name;
     var headers = _.extend({},
         constants.HTTP_HEADERS.accept.data,
         constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         }
     );
+    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/' + rpcInfo[Type].rpc);
+
+    jsonData['input'] = utils.addProjectContextToRPCPayload(req, uri, jsonData['input']);
+
     return new Promise(function(resolve, reject) {
 
         request({
-            uri: utils.confdPort(api_server) + '/api/operations/' + rpcInfo[Type].rpc,
+            uri: uri,
             method: 'POST',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -264,4 +280,117 @@ function refreshAccountConnectionStatus (req) {
     });
 };
 
+function getResourceOrchestrator(req, id) {
+    var self = this;
+    var api_server = req.query["api_server"];
+    var accountID = req.params.id || req.params.name;
+
+    return new Promise(function(resolve, reject) {
+        var requestHeaders = {};
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.collection, {
+                'Authorization': req.session && req.session.authorization
+            }
+        );
+        var urlOp =  utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ro-account/account');
+        var urlConfig =  utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ro-account-state/account');
+        if(accountID) {
+            urlOp = url + '/' + encodeURIComponent(accountID);
+            urlConfig = url + '/' + encodeURIComponent(accountID);
+        }
+        var allRequests = [];
+        var roOpData = new Promise(function(resolve, reject) {
+            request({
+                url: urlOp,
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+                },
+                function(error, response, body) {
+                    var data;
+                    if (utils.validateResponse('RoAccount.get', error, response, body, resolve, reject)) {
+                        try {
+                            data = JSON.parse(response.body).collection['rw-ro-account:account']
+                        } catch (e) {
+                            console.log('Problem with "RoAccount.get"', e);
+                            var err = {};
+                            err.statusCode = 500;
+                            err.errorMessage = {
+                                error: 'Problem with "RoAccount.get": ' + e // + e.toString()
+                            }
+                            return reject(err);
+                        }
+                        return resolve({
+                            statusCode: response.statusCode,
+                            data: data
+                        });
+                    };
+                }
+            );
+        });
+        var roConfigData = new Promise(function(resolve, reject){
+            request({
+                url: urlConfig,
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+                },
+                function(error, response, body) {
+                    var data;
+                    if (utils.validateResponse('RoAccount.get', error, response, body, resolve, reject)) {
+                        try {
+                            data = JSON.parse(response.body).collection['rw-ro-account:account']
+                        } catch (e) {
+                            console.log('Problem with "RoAccount.get"', e);
+                            var err = {};
+                            err.statusCode = 500;
+                            err.errorMessage = {
+                                error: 'Problem with "RoAccount.get": ' + e // + e.toString()
+                            }
+                            return reject(err);
+                        }
+                        return resolve({
+                            statusCode: response.statusCode,
+                            data: data
+                        });
+                    };
+                }
+            );
+        });
+
+        allRequests.push(roOpData);
+        allRequests.push(roConfigData);
+        Promise.all(allRequests).then(function(data) {
+            var state = data[1].data;
+            var op = data[0].data;
+            var result = [];
+            var dict = {};
+            if (!accountID) {
+                state.map && state.map(function(s){
+                    if(s.name != "rift") {
+                        dict[s.name] = s;
+                    }
+                });
+                op.map && op.map(function(o) {
+                    if(o.name != "rift") {
+                        dict[o.name] = _.extend(dict[o.name], o);
+                    }
+                });
+                Object.keys(dict).map(function(d) {
+                    result.push(dict[d]);
+                })
+            } else {
+                result = _.extend(op, state);
+            }
+            resolve({
+                statusCode: 200,
+                data: result
+            })
+        })
+
+    })
+}
+
 module.exports = Accounts;
index 5475750..ade5861 100644 (file)
@@ -40,11 +40,11 @@ Cloud.get = function(req) {
       var requestHeaders = {};
       _.extend(requestHeaders,
         constants.HTTP_HEADERS.accept.collection, {
-          'Authorization': req.get('Authorization')
+          'Authorization': req.session && req.session.authorization
         });
 
       request({
-          url: utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account',
+          url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account'),
           type: 'GET',
           headers: requestHeaders,
           forever: constants.FOREVER_ON,
@@ -77,11 +77,11 @@ Cloud.get = function(req) {
       var requestHeaders = {};
       _.extend(requestHeaders,
         constants.HTTP_HEADERS.accept.data, {
-          'Authorization': req.get('Authorization')
+          'Authorization': req.session && req.session.authorization
         });
 
       request({
-          url: utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account/' + id,
+          url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account/' + id),
           type: 'GET',
           headers: requestHeaders,
           forever: constants.FOREVER_ON,
@@ -128,11 +128,11 @@ Cloud.create = function(req) {
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data,
       constants.HTTP_HEADERS.content_type.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-      url: utils.confdPort(api_server) + APIVersion + '/api/config/cloud',
+      url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/cloud'),
       method: 'POST',
       headers: requestHeaders,
       forever: constants.FOREVER_ON,
@@ -167,11 +167,11 @@ Cloud.update = function(req) {
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data,
       constants.HTTP_HEADERS.content_type.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-      url: utils.confdPort(api_server) + APIVersion + '/api/config/cloud/account/' + id,
+      url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/cloud/account/' + id),
       method: 'PUT',
       headers: requestHeaders,
       forever: constants.FOREVER_ON,
@@ -209,10 +209,10 @@ Cloud.delete = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
     request({
-      url: utils.confdPort(api_server) + APIVersion + '/api/config/cloud/account/' + id,
+      url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/cloud/account/' + id),
       method: 'DELETE',
       headers: requestHeaders,
       forever: constants.FOREVER_ON,
@@ -237,11 +237,11 @@ Cloud.getResources = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-        url: utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account/' + cloudAccount + '/resources?deep',
+        url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account/' + cloudAccount + '/resources?deep'),
         type: 'GET',
         headers: requestHeaders,
         forever: constants.FOREVER_ON,
@@ -280,11 +280,11 @@ Cloud.getPools = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-        url: utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account/' + cloudAccount + '/pools',
+        url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/cloud/account/' + cloudAccount + '/pools'),
         type: 'GET',
         headers: requestHeaders,
         forever: constants.FOREVER_ON,
index 8d1734b..ebca59a 100644 (file)
@@ -37,11 +37,11 @@ ConfigAgentAccount.get = function(req) {
             var requestHeaders = {};
             _.extend(requestHeaders,
                 constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 });
 
             request({
-                    url: utils.confdPort(api_server) + '/api/operational/config-agent/account',
+                    url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operational/config-agent/account'),
                     type: 'GET',
                     headers: requestHeaders,
                     forever: constants.FOREVER_ON,
@@ -77,11 +77,11 @@ ConfigAgentAccount.get = function(req) {
             var requestHeaders = {};
             _.extend(requestHeaders,
                 constants.HTTP_HEADERS.accept.data, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 });
 
             request({
-                    url: utils.confdPort(api_server) + '/api/operational/config-agent/account/' + id,
+                    url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operational/config-agent/account/' + id),
                     type: 'GET',
                     headers: requestHeaders,
                     forever: constants.FOREVER_ON,
@@ -130,11 +130,11 @@ ConfigAgentAccount.create = function(req) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
 
         request({
-            url: utils.confdPort(api_server) + '/api/config/config-agent',
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/config-agent'),
             method: 'POST',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -169,11 +169,11 @@ ConfigAgentAccount.update = function(req) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
 
         request({
-            url: utils.confdPort(api_server) + '/api/config/config-agent/account/' + id,
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/config-agent/account/' + id),
             method: 'PUT',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -211,10 +211,10 @@ ConfigAgentAccount.delete = function(req) {
         var requestHeaders = {};
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
         request({
-            url: utils.confdPort(api_server) + '/api/config/config-agent/account/' + id,
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/config-agent/account/' + id),
             method: 'DELETE',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
index 6757c28..6d928ca 100644 (file)
@@ -38,10 +38,10 @@ Sdn.get = function(req) {
       var requestHeaders = {};
       _.extend(requestHeaders,
         constants.HTTP_HEADERS.accept.collection, {
-          'Authorization': req.get('Authorization')
+          'Authorization': req.session && req.session.authorization
         });
       request({
-          url: utils.confdPort(api_server) + '/api/operational/sdn/account?deep',
+          url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operational/sdn/account?deep'),
           type: 'GET',
           headers: requestHeaders,
           forever: constants.FOREVER_ON,
@@ -77,11 +77,11 @@ Sdn.get = function(req) {
       var requestHeaders = {};
       _.extend(requestHeaders,
         constants.HTTP_HEADERS.accept.data, {
-          'Authorization': req.get('Authorization')
+          'Authorization': req.session && req.session.authorization
         });
 
       request({
-          url: utils.confdPort(api_server) + '/api/operational/sdn/account/' + id + '?deep',
+          url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operational/sdn/account/' + id + '?deep'),
           type: 'GET',
           headers: requestHeaders,
           forever: constants.FOREVER_ON,
@@ -130,11 +130,11 @@ Sdn.create = function(req) {
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data,
       constants.HTTP_HEADERS.content_type.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-      url: utils.confdPort(api_server) + '/api/config/sdn/account',
+      url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/sdn/account'),
       method: 'POST',
       headers: requestHeaders,
       forever: constants.FOREVER_ON,
@@ -168,11 +168,11 @@ Sdn.update = function(req) {
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data,
       constants.HTTP_HEADERS.content_type.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-      url: utils.confdPort(api_server) + '/api/config/sdn/account/' + id,
+      url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/sdn/account/' + id),
       method: 'PUT',
       headers: requestHeaders,
       forever: constants.FOREVER_ON,
@@ -211,11 +211,11 @@ Sdn.delete = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
 
     request({
-      url: utils.confdPort(api_server) + '/api/config/sdn/account/' + id,
+      url: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/sdn/account/' + id),
       method: 'DELETE',
       headers: requestHeaders,
       forever: constants.FOREVER_ON,
index 81bb702..47e8e95 100644 (file)
@@ -2,11 +2,18 @@
     "root": "public",
     "name": "Accounts",
     "dashboard": "./account/accountsDashboard.jsx",
-    "order": 1,
+    "order": 3,
     "priority":1,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-project:project-admin",
+        "rw-project:project-oper",
+        "rw-project-mano:account-oper",
+        "rw-project-mano:account-admin"
+    ],
     "routes": [
     {
-        "label": "Accounts Dashboard",
+        "label": "Dashboard",
         "route": "accounts",
         "component": "./account/accountsDashboard.jsx",
         "path": "accounts",
diff --git a/skyquake/plugins/accounts/config_routes.js b/skyquake/plugins/accounts/config_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/accounts/images/brocade.png b/skyquake/plugins/accounts/images/brocade.png
new file mode 100644 (file)
index 0000000..4d667ae
Binary files /dev/null and b/skyquake/plugins/accounts/images/brocade.png differ
index ab831e5..8c9d5b0 100644 (file)
@@ -18,8 +18,8 @@
 
 var app = require('express').Router();
 var cors = require('cors');
-var utils = require('../../framework/core/api_utils/utils.js')
-var accountsAPI = require('./api/accounts.js')
+var utils = require('../../framework/core/api_utils/utils.js');
+var accountsAPI = require('./api/accounts.js');
  // Begin Accounts API
     app.get('/all', cors(), function(req, res) {
         accountsAPI.get(req).then(function(data) {
@@ -70,6 +70,8 @@ var accountsAPI = require('./api/accounts.js')
             utils.sendErrorResponse(error, res);
         });
     })
+    //RO config routes
+
     utils.passThroughConstructor(app);
 
 module.exports = app;
index f8dcff1..e1b8cd8 100755 (executable)
 
 PLUGIN_NAME=accounts
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index 846dad7..bfd0c50 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,5 +44,5 @@ 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
+tar -cf $bcache_dir/plugins/$plugin/node_modules.tar node_modules package.json -C $source_dir
 #cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
index 41a3046..2aacf70 100644 (file)
@@ -19,6 +19,7 @@
 import React from 'react';
 import Button from 'widgets/button/rw.button.js';
 import _cloneDeep from 'lodash/cloneDeep';
+import _isEmpty from 'lodash/isEmpty';
 import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
 import Crouton from 'react-crouton';
 import TextInput from 'widgets/form_controls/textInput.jsx';
@@ -26,21 +27,41 @@ import {AccountConnectivityStatus} from '../account_sidebar/accountSidebar.jsx';
 import Utils from 'utils/utils.js';
 import 'style/common.scss';
 import './account.scss';
+
+function isAccessDenied (error) {
+    const rpcResult = Utils.rpcError(error);
+    return rpcResult && rpcResult['rpc-reply']['rpc-error']['error-tag'] === 'access-denied';
+}
+
 class Account extends React.Component {
     constructor(props) {
         super(props);
-        this.state = {};
-        this.state.account = {};
+
+        // console.log(this.state.account)
     }
     storeListener = (state) => {
-        if(!state.account) {
-            this.setUp(this.props)
+        if(state.socket) {
+            if((!state.account || _isEmpty(state.account)) && state.userProfile) {
+                this.setUp(this.props, state.savedData)
+            }
+            state.account && state.account.params && this.setState({
+                account: state.account,
+                accountType: state.accountType,
+                types: state.types,
+                sdnOptions: state.sdnOptions,
+                savedData: state.savedData,
+                userProfile: state.userProfile
+            })
         }
-        state.account && this.setState({account: state.account,accountType: state.accountType, types: state.types, sdnOptions: state.sdnOptions})
     }
     componentWillMount() {
+        this.state = this.props.store.getState();
         this.props.store.listen(this.storeListener);
-        this.setUp(this.props);
+    }
+    componentWillUpdate(nextProps, nextState, nextContext) {
+        if(!_isEmpty(nextContext.userProfile) && !nextState.userProfile) {
+            this.props.store.getTransientAccountForUser(nextContext.userProfile)
+        }
     }
     componentWillReceiveProps(nextProps) {
         if(JSON.stringify(nextProps.params) != JSON.stringify(this.props.params)){
@@ -48,13 +69,22 @@ class Account extends React.Component {
         }
     }
     componentWillUnmount() {
+        console.log('unmounting')
+        // this.setState({account: null, accountType: null, types: []})
         this.props.store.unlisten(this.storeListener);
     }
-    setUp(props){
-        if(props.params.name != 'create') {
-            this.props.store.viewAccount({type: props.params.type, name: props.params.name});
+    setUp(props, savedData){
+        console.log('Setting up');
+        var SD = savedData || this.state.savedData;
+        if(props.params.name && props.params.name != 'create') {
+            if (SD && SD == props.params.name) {
+                this.props.store.viewAccount({type: props.params.type, name: props.params.name}, SD);
+            } else {
+                this.props.store.viewAccount({type: props.params.type, name: props.params.name});
+            }
+
         } else {
-            this.props.store.setAccountTemplate(props.params.type);
+            this.props.store.setAccountTemplate(props.params.type, null, SD);
         }
     }
     create(e) {
@@ -66,69 +96,74 @@ class Account extends React.Component {
             self.props.flux.actions.global.showNotification("Please give the account a name");
             return;
         } else {
-            var type = Account['account-type'];
-            var params = Account.params;
-
-            if(params) {
-                for (var i = 0; i < params.length; i++) {
-                    var param = params[i].ref;
-                    if (typeof(Account[type]) == 'undefined' || typeof(Account[type][param]) == 'undefined' || Account[type][param] == "") {
-                        if (!params[i].optional) {
-                            self.props.flux.actions.global.showNotification("Please fill all account details");
-                            return;
-                        }
-                    }
-                }
-            }
-
-            let nestedParams = Account.nestedParams && Account.nestedParams;
-            if (nestedParams && nestedParams.params) {
-                for (let i = 0; i < nestedParams.params.length; i++) {
-                    let nestedParam = nestedParams.params[i].ref;
-                    if (typeof(Account[type]) == 'undefined' || typeof(Account[type][nestedParams['container-name']][nestedParam]) == 'undefined' || Account[type][nestedParams['container-name']][nestedParam] == "") {
-                        if (!nestedParams.params[i].optional) {
-                            self.props.flux.actions.global.showNotification("Please fill all account details");
-                            return;
-                        }
-                    }
-                }
+            if(!wasAllDetailsFilled(Account)) {
+                self.props.flux.actions.global.showNotification("Please fill all account details");
+                return;
             }
         }
 
         let newAccount = _cloneDeep(removeTrailingWhitespace(Account));
+
         delete newAccount.params;
+
         newAccount.nestedParams &&
-            newAccount.nestedParams['container-name'] &&
-            delete newAccount[newAccount.nestedParams['container-name']];
+        newAccount.nestedParams['container-name'] &&
+        delete newAccount[newAccount.nestedParams['container-name']];
+
         delete newAccount.nestedParams;
 
+        if(AccountType == 'resource-orchestrator') {
+            newAccount['ro-account-type'] = newAccount['account-type'] || newAccount['ro-account-type'];
+            delete newAccount['account-type'];
+        }
+        if(AccountType == 'cloud' && self.props.vduInstanceTimeout != '') {
+            newAccount['vdu-instance-timeout'] = self.props.vduInstanceTimeout;
+        }
         this.props.flux.actions.global.showScreenLoader();
-        this.props.store.create(newAccount, AccountType).then(function() {
-            self.props.router.push({pathname:'accounts'});
-            self.props.flux.actions.global.hideScreenLoader.defer();
-        },
-         function(error) {
-            self.props.flux.actions.global.showNotification(Utils.parseError(error));
-            self.props.flux.actions.global.hideScreenLoader.defer();
-         });
+        this.props.store.create(newAccount, AccountType).then(
+            function() {
+                self.props.router.push({pathname:'accounts'});
+                self.props.flux.actions.global.hideScreenLoader.defer();
+            },
+            function(error) {
+                self.props.flux.actions.global.showNotification(error);
+                self.props.flux.actions.global.hideScreenLoader.defer();
+            }
+        );
     }
     update(e) {
         e.preventDefault();
         var self = this;
         var Account = this.state.account;
         let AccountType = this.state.accountType;
-        this.props.flux.actions.global.showScreenLoader();
-        this.props.store.update(Account, AccountType).then(function() {
-            self.props.router.push({pathname:'accounts'});
-             self.props.flux.actions.global.hideScreenLoader();
-        },
-        function() {
 
-        });
+        if(!wasAllDetailsFilled(Account)) {
+            self.props.flux.actions.global.showNotification("Please fill all account details");
+            return;
+        }
+
+        if(AccountType == 'cloud' && self.props.vduInstanceTimeout != '') {
+            Account['vdu-instance-timeout'] = self.props.vduInstanceTimeout;
+        }
+        this.props.flux.actions.global.showScreenLoader();
+        this.props.store.update(Account, AccountType).then(
+            function() {
+                self.props.router.push({pathname:'accounts'});
+                self.props.flux.actions.global.hideScreenLoader();
+            },
+            function(error){
+                let msg = isAccessDenied(error) ?
+                    "Update of account failed. No authorization to modify accounts."
+                    : "Update of account failed. This could be because the account is in use or no longer exists.";
+                self.props.flux.actions.global.hideScreenLoader.defer();
+                self.props.flux.actions.global.showNotification.defer(msg);
+            }
+        );
     }
     cancel = (e) => {
         e.preventDefault();
         e.stopPropagation();
+        this.props.flux.actions.global.handleCancelAccount();
         this.props.router.push({pathname:'accounts'});
     }
     handleDelete = () => {
@@ -136,22 +171,27 @@ class Account extends React.Component {
         let msg = 'Preparing to delete "' + self.state.account.name + '"' +
         ' Are you sure you want to delete this ' + self.state.accountType + ' account?"';
         if (window.confirm(msg)) {
-            this.props.store.delete(self.state.accountType, self.state.account.name).then(function() {
-                self.props.flux.actions.global.hideScreenLoader();
-                self.props.router.push({pathname:'accounts'});
-            }, function(){
-                // self.props.flux.actions.global.hideScreenLoader.defer();
-                // console.log('Delete Account Fail');
-            });
-        } else {
-           self.props.flux.actions.global.hideScreenLoader();
+            this.props.flux.actions.global.showScreenLoader();
+            this.props.store.delete(self.state.accountType, self.state.account.name).then(
+                function() {
+                    self.props.flux.actions.global.hideScreenLoader();
+                    self.props.router.push({pathname:'accounts'});
+                },
+                function(error){
+                    let msg = isAccessDenied(error) ?
+                        "Deletion of account failed. No authorization to delete accounts."
+                        : "Deletion of account failed. This could be because the account is in use or has already been deleted.";
+                    self.props.flux.actions.global.hideScreenLoader.defer();
+                    self.props.flux.actions.global.showNotification.defer(msg);
+                }
+            );
         }
     }
     handleNameChange(event) {
        this.props.store.handleNameChange(event);
     }
-    handleAccountTypeChange(node, event) {
-        this.props.store.handleAccountTypeChange(node, event);
+    handleAccountTypeChange(node, isRo, event) {
+        this.props.store.handleAccountTypeChange(node, isRo, event);
     }
     handleSelectSdnAccount = (e) => {
         var tmp = this.state.account;
@@ -164,6 +204,9 @@ class Account extends React.Component {
         }
         console.log(e, tmp)
     }
+    updateVduInstanceTimeout(event) {
+        this.props.store.updateVduTimeout(event)
+    }
     preventDefault = (e) => {
         e.preventDefault();
         e.stopPropagation();
@@ -185,11 +228,11 @@ class Account extends React.Component {
         let {store, ...props} = this.props;
         // This section builds elements that only show up on the create page.
         // var name = <label>Name <input type="text" onChange={this.handleNameChange.bind(this)} style={{'textAlign':'left'}} /></label>;
-        var name = <TextInput label="Name"  onChange={this.handleNameChange.bind(this)} required={true} />;
+        let Account = this.state.account || {};
+        var name = <TextInput label="Name"  onChange={this.handleNameChange.bind(this)} required={true} value={Account &&Account.name || ''} />;
         let params = null;
         let selectAccount = null;
         let resfreshStatus = null;
-        let Account = this.state.account;
         // AccountType is for the view, not the data account-type value;
         let AccountType = this.state.accountType;
         let Types = this.state.types;
@@ -197,9 +240,9 @@ class Account extends React.Component {
         var buttons;
         let cloudResources = Account['cloud-resources-state'] && Account['cloud-resources-state'][Account['account-type']];
         let cloudResourcesStateHTML = null;
-
         // Account Type Radio
         var selectAccountStack = [];
+        let setVduTimeout = null;
         if (!isEdit) {
             buttons = [
                 <Button key="0" onClick={this.cancel} className="cancel light" label="Cancel"></Button>,
@@ -207,14 +250,15 @@ class Account extends React.Component {
             ]
             for (var i = 0; i < Types.length; i++) {
                 var node = Types[i];
-                var isSelected = (Account['account-type'] == node['account-type']);
+                const isRo = node.hasOwnProperty('ro-account-type');
+                var isSelected = (Account['account-type'] || Account['ro-account-type']) == (node['account-type'] || node['ro-account-type']);
                 selectAccountStack.push(
                   <label key={i} className={"accountSelection " + (isSelected ? "accountSelection--isSelected" : "")}>
                     <div className={"accountSelection-overlay" + (isSelected ? "accountSelection-overlay--isSelected" : "")}></div>
                     <div className="accountSelection-imageWrapper">
-                        <img src={store.getImage(node['account-type'])}/>
+                        <img src={store.getImage(node['account-type'] || node['ro-account-type'])}/>
                     </div>
-                    <input type="radio" name="account" onChange={this.handleAccountTypeChange.bind(this, node)} defaultChecked={node.name == Types[0].name} value={node['account-type']} />{node.name}
+                    <input type="radio" name="account" onChange={this.handleAccountTypeChange.bind(this, node, isRo)} defaultChecked={node.name == Types[0].name} value={node['account-type'] || node['ro-account-type']} />{node.name}
                   </label>
                 )
             }
@@ -249,23 +293,67 @@ class Account extends React.Component {
             }
         }
          //END Cloud Account: SDN Account Option
+
+         setVduTimeout = <TextInput
+                        className="accountForm-input"
+                        label="VIM Instantiation Timeout (Seconds)"
+                        onChange={this.updateVduInstanceTimeout.bind(this)}
+                        value={this.props.vduInstanceTimeout}
+                        readonly={self.props.readonly}
+                        placeholder={300}
+                        />
+
          //
         // This sections builds the parameters for the account details.
         if (Account.params) {
             var paramsStack = [];
             var optionalField = '';
+            var isRo = Account.hasOwnProperty('ro-account-type');
             for (var i = 0; i < Account.params.length; i++) {
                 var node = Account.params[i];
                 var value = ""
-                if (Account[Account['account-type']]) {
-                    value = Account[Account['account-type']][node.ref]
+                if(isRo) {
+                    if (Account[Account['ro-account-type']] && Account[Account['ro-account-type']][node.ref]) {
+                        value = Account[Account['ro-account-type']][node.ref]
+                    }
+                } else  {
+                    if (Account[Account['account-type']] && Account[Account['account-type']][node.ref]) {
+                        value = Account[Account['account-type']][node.ref]
+                    }
                 }
                 if (this.props.edit && Account.params) {
                     value = Account.params[node.ref];
                 }
-                paramsStack.push(
-                    <TextInput key={node.label} className="accountForm-input" label={node.label} required={!node.optional}  onChange={this.props.store.handleParamChange(node)} value={value} />
-                );
+                if(node.ref == "password" || node.ref == "secret") {
+                    let displayValue = value;
+                    if (self.props.readonly) {
+                        displayValue = new Array(value.length + 1).join( '*' );
+                    }
+                    paramsStack.push(
+                        <TextInput
+                            key={node.label}
+                            className="accountForm-input"
+                            label={node.label}
+                            required={!node.optional}
+                            onChange={this.props.store.handleParamChange(node, isRo)}
+                            value={displayValue}
+                            readonly={self.props.readonly}
+                            type="password"
+                        />
+                    );
+                } else {
+                    paramsStack.push(
+                        <TextInput
+                            key={node.label}
+                            className="accountForm-input"
+                            label={node.label}
+                            required={!node.optional}
+                            onChange={this.props.store.handleParamChange(node, isRo)}
+                            value={value}
+                            readonly={self.props.readonly}
+                        />
+                    );
+                }
             }
 
             let nestedParamsStack = null;
@@ -287,9 +375,37 @@ class Account extends React.Component {
                     //       <input className="create-fleet-pool-input" type="text" onChange={this.props.store.handleNestedParamChange(Account.nestedParams['container-name'], node)} value={value}/>
                     //     </label>
                     // );
-                    nestedParamsStack.push(
-                          <TextInput key={node.label} label={node.label} required={!node.optional} className="create-fleet-pool-input" type="text" onChange={this.props.store.handleNestedParamChange(Account.nestedParams['container-name'], node)} value={value}/>
-                    );
+                    if(node.ref == "password" || node.ref == "secret") {
+                        let displayValue = value;
+                        if (self.props.readonly) {
+                            displayValue = new Array(value.length + 1).join( '*' );
+                        }
+                        nestedParamsStack.push(
+                              <TextInput
+                                key={node.label}
+                                label={node.label}
+                                required={!node.optional}
+                                className="create-fleet-pool-input"
+                                type="password"
+                                onChange={this.props.store.handleNestedParamChange(Account.nestedParams['container-name'], node)}
+                                value={displayValue}
+                                readonly={self.props.readonly}
+                                />
+                        );
+                    } else {
+                            nestedParamsStack.push(
+                              <TextInput
+                                key={node.label}
+                                label={node.label}
+                                required={!node.optional}
+                                className="create-fleet-pool-input"
+                                type="text"
+                                onChange={this.props.store.handleNestedParamChange(Account.nestedParams['container-name'], node)}
+                                value={value}
+                                readonly={self.props.readonly}
+                                />
+                        );
+                    }
                 }
             }
 
@@ -331,7 +447,9 @@ class Account extends React.Component {
                             <AccountConnectivityStatus status={Account['connection-status'].status} />
                             {Account['connection-status'] && Account['connection-status'].status &&  Account['connection-status'].status.toUpperCase()}
                         </div>
-                            <Button className="refreshList light" onClick={this.props.store.refreshAccount.bind(this, Account.name, AccountType)} label="REFRESH STATUS"></Button>
+                        { self.props.readonly ? null :
+                            <Button is-disabled={self.props.readonly} className="refreshList light" onClick={this.props.store.refreshAccount.bind(this, Account.name, AccountType)} label="REFRESH STATUS" />
+                        }
                     </div>
                     {
                         (Account['connection-status'] && Account['connection-status'].status && Account['connection-status'].status.toUpperCase()) === 'FAILURE' ?
@@ -339,25 +457,7 @@ class Account extends React.Component {
                     }
                 </div>
             ) : null;
-            // cloudResourcesStateHTML = (
-            //     <div className="accountForm">
-            //         <h3 className="accountForm-title">Resources Status</h3>
-            //         <div className="accountForm-content" >
-            //         <ul>
-            //             {
-            //                 cloudResources && props.AccountMeta.resources[Account['account-type']].map(function(r, i) {
-
-            //                     return (
-            //                         <li key={i}>
-            //                             {r}: {cloudResources[r]}
-            //                         </li>
-            //                     )
-            //                 }) || 'No Additional Resources'
-            //             }
-            //         </ul>
-            //         </div>
-            //     </div>
-            // )
+
         }
 
         var html = (
@@ -369,15 +469,23 @@ class Account extends React.Component {
                 <div className="associateSdnAccount accountForm">
                     <h3 className="accountForm-title">Account</h3>
                     <div className="accountForm-content" style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
-                        <h4 style={{flex: '1'}}>{name}</h4>
+                        {name}
                         { isEdit ?
                             (
                                 <div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
-                                    <img src={store.getImage(Account['account-type'])}/> {props.AccountMeta.labelByType[Account['account-type']]}
+                                    <img src={store.getImage(Account['account-type'] || Account['ro-account-type'])}/> {props.AccountMeta.labelByType[Account['account-type'] || Account['ro-account-type']]}
                                 </div>)
                             : null
                         }
                     </div>
+                     {
+                        AccountType == 'cloud' ? (
+                                <div className="accountForm-content">
+                                    {setVduTimeout}
+                                </div>
+                            )
+                        : null
+                    }
                 </div>
 
                   {selectAccount}
@@ -388,14 +496,19 @@ class Account extends React.Component {
                       {params}
                   </ol>
                   <div className="form-actions">
-                      {buttons}
+                      {!self.props.readonly ? buttons : null}
                   </div>
               </form>
         )
-        return html;
+        return Types.length ? html : null;
     }
 }
 
+Account.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+}
+
 function displayFailureMessage(msg) {
     return (
         <div className="accountForm-content" style={{maxWidth: '600px'}}>
@@ -436,8 +549,37 @@ SelectOption.defaultProps = {
   }
 }
 
+function wasAllDetailsFilled(Account) {
+    var type = Account['account-type'] || Account['ro-account-type'];
+    var params = Account.params;
+
+    if(params) {
+        for (var i = 0; i < params.length; i++) {
+            var param = params[i].ref;
+            if (typeof(Account[type]) == 'undefined' || typeof(Account[type][param]) == 'undefined' || Account[type][param] == "") {
+                if (!params[i].optional) {
+                    return false;
+                }
+            }
+        }
+    }
+
+    let nestedParams = Account.nestedParams && Account.nestedParams;
+    if (nestedParams && nestedParams.params) {
+        for (let i = 0; i < nestedParams.params.length; i++) {
+            let nestedParam = nestedParams.params[i].ref;
+            if (typeof(Account[type]) == 'undefined' || typeof(Account[type][nestedParams['container-name']][nestedParam]) == 'undefined' || Account[type][nestedParams['container-name']][nestedParam] == "") {
+                if (!nestedParams.params[i].optional) {
+                    return false;
+                }
+            }
+        }
+    }
+    return true;
+}
+
 function removeTrailingWhitespace(Account) {
-             var type = Account['account-type'];
+            var type = Account['account-type'] || Account['ro-account-type'];
             var params = Account.params;
 
             if(params) {
index 15c48f7..28b4c9e 100644 (file)
@@ -32,6 +32,7 @@ module.exports = function(Alt) {
                                        'deleteAccountLoading',
                                        'deleteAccountFail',
                                        'viewAccount',
-                                       'getResourceOrchestratorSuccess'
+                                       'getResourceOrchestratorSuccess',
+                                       'handleCancelAccount'
                                        );
 }
index 45da0fb..6ab9a3d 100644 (file)
@@ -38,7 +38,7 @@ module.exports = function(Alt) {
                 return resolve(false);
               }
                $.ajax({
-                url: '/socket-polling?api_server=' + API_SERVER,
+                url: '/socket-polling',
                 type: 'POST',
                 beforeSend: Utils.addAuthorizationStub,
                 data: {
@@ -76,7 +76,7 @@ module.exports = function(Alt) {
                     }
 
                     function refreshStatus(type, name) {
-                        let url = type + '/' + name + '/refresh?api_server=' + API_SERVER;
+                        let url = type + '/' + encodeURIComponent(name) + '/refresh?api_server=' + API_SERVER;
                         setTimeout(function(){
                           Refreshing.next();
                         },100);
@@ -102,7 +102,7 @@ module.exports = function(Alt) {
           remote: function(state, name, type) {
             return new Promise(function(resolve, reject) {
               $.ajax({
-                url: type + '/' + name + '/refresh?api_server=' + API_SERVER,
+                url: type + '/' + encodeURIComponent(name) + '/refresh?api_server=' + API_SERVER,
                 type: 'POST',
                 beforeSend: Utils.addAuthorizationStub,
                 success: function(account) {
@@ -117,12 +117,21 @@ module.exports = function(Alt) {
         create: {
           remote: function(state, account, type, cb) {
             delete account['connection-status'];
+            var payload = account;
+            var accountKey = payload.hasOwnProperty('account-type') ? 'account-type' : 'ro-account-type';
+            var payloadKeys = Object.keys(payload[payload[accountKey]]);
+            var accountData = payload[payload[accountKey]];
+            payloadKeys.map(function(k) {
+              if (!accountData[k] || accountData[k].trim() == '') {
+                delete payload[payload[accountKey]][k];
+              }
+            });
             return new Promise(function(resolve, reject) {
               $.ajax({
                 url: type + '?api_server=' + API_SERVER,
                 type:'POST',
                 beforeSend: Utils.addAuthorizationStub,
-                data: JSON.stringify(account),
+                data: JSON.stringify(payload),
                 contentType: "application/json",
                 success: function(data) {
                   resolve({data, cb});
@@ -146,9 +155,12 @@ module.exports = function(Alt) {
         update: {
           remote: function(state, account, type, cb) {
             var payload = Object.assign({}, account);
+            delete payload["instance-ref-count"]
             delete payload['connection-status'];
             delete payload['params'];
             delete payload['pools'];
+            delete payload['datacenters'];
+            delete payload['config-data'];
             (
               (payload.nestedParams == null) &&
               delete payload.nestedParams
@@ -160,10 +172,17 @@ module.exports = function(Alt) {
               delete payload.nestedParams
             );
 
+            var payloadKeys = Object.keys(payload[payload['account-type']]);
+            var accountData = payload[payload['account-type']];
+            payloadKeys.map(function(k) {
+              if (!accountData[k] || accountData[k].trim() == '') {
+                delete payload[payload['account-type']][k];
+              }
+            });
 
             return new Promise(function(resolve, reject) {
               $.ajax({
-                url: type + '/' + account.name + '?api_server=' + API_SERVER,
+                url: type + '/' + encodeURIComponent(account.name) + '?api_server=' + API_SERVER,
                 type:'PUT',
                 beforeSend: Utils.addAuthorizationStub,
                 data: JSON.stringify(payload),
@@ -173,7 +192,6 @@ module.exports = function(Alt) {
                 },
                 error: function(error) {
                   console.log("There was an error updating the account: ", arguments);
-
                 }
               }).fail(function(xhr){
                 //Authentication and the handling of fail states should be wrapped up into a connection class.
@@ -188,13 +206,13 @@ module.exports = function(Alt) {
           }),
           success: Alt.actions.global.createAccountSuccess,
           loading: Alt.actions.global.createAccountLoading,
-          error: Alt.actions.global.showNotification
+          error: Alt.actions.global.createAccountFail
       },
         delete: {
           remote: function(state, type, name, cb) {
             return new Promise(function(resolve, reject) {
               $.ajax({
-                url: type + '/' + name + '/?api_server=' + API_SERVER,
+                url: type + '/' + encodeURIComponent(name) + '/?api_server=' + API_SERVER,
                 type:'DELETE',
                 dataType : 'html',
                 beforeSend: Utils.addAuthorizationStub,
@@ -213,43 +231,8 @@ module.exports = function(Alt) {
             'error': 'Something went wrong while trying to delete the account. Check the error logs for more information' }),
           success: Alt.actions.global.deleteAccountSuccess,
           loading: Alt.actions.global.deleteAccountLoading,
-          error: Alt.actions.global.showNotification
-      },
-        getResourceOrchestrator: {
-          remote: function() {
-              return new Promise(function(resolve, reject) {
-                $.ajax({
-                  url: 'passthrough/data/api/running/resource-orchestrator' + '?api_server=' + API_SERVER,
-                  type: 'GET',
-                  beforeSend: Utils.addAuthorizationStub,
-                  contentType: "application/json",
-                  success: function(data) {
-                    let returnedData;
-                    if (data.hasOwnProperty("rw-launchpad:resource-orchestrator")) {
-                      returnedData = data;
-                    } else {
-                      returnedData = {};
-                    }
-                    resolve(returnedData);
-                  },
-                  error: function(error) {
-                    console.log("There was an error updating the account: ", arguments);
-
-                  }
-                }).fail(function(xhr){
-                  //Authentication and the handling of fail states should be wrapped up into a connection class.
-                  Utils.checkAuthentication(xhr.status);
-                  return reject('error');
-                });
-              });
-          },
-          interceptResponse: interceptResponse({
-            'error': 'There was an error retrieving the resource orchestrator information.'
-          }),
-          success: Alt.actions.global.getResourceOrchestratorSuccess,
-          loading: Alt.actions.global.showScreenLoader,
-          error: Alt.actions.global.showNotification
-        },
+          error: Alt.actions.global.deleteAccountFail
+      }
     }
 }
 
index def033a..265a74f 100644 (file)
@@ -21,6 +21,7 @@ import AccountSource from './accountSource.js';
 var Utils = require('utils/utils.js');
 var rw = require('utils/rw.js');
 var altImage = rw.getSearchParams(window.location).alt_image;
+var _ = require('lodash');
 
 let Params = {
     //Config Agent
@@ -30,8 +31,27 @@ let Params = {
 }
 
 
-
 let AccountMeta = {
+    'resource-orchestrator': {
+        defaultType: 'openmano',
+        types: [
+        {
+            name: "OpenMANO",
+            'ro-account-type': 'openmano'
+        }],
+        params: {
+            'openmano' : [{
+                label: "Host",
+                ref: 'host'
+            }, {
+                label: "Port",
+                ref: 'port'
+            }, {
+                label: "Tenant ID",
+                ref: 'tenant-id'
+            }]
+        }
+    },
     'config-agent': {
 
         defaultType: 'juju',
@@ -70,11 +90,78 @@ let AccountMeta = {
             }, {
                 label: "URL",
                 ref: 'url'
-            }]
+            }],
+
+            "openstack": [{
+                label: "Key",
+                ref: 'key'
+            },{
+                label: "Secret",
+                ref: 'secret'
+            },{
+                label: "Authentication URL",
+                ref: 'auth_url'
+            },{
+                label: "Tenant",
+                ref: 'tenant'
+            },{
+                label: "User domain",
+                ref: 'user-domain',
+                optional: true
+            },{
+                label: "Project domain",
+                ref: 'project-domain',
+                optional: true
+            },{
+                label: "Region",
+                ref: 'region',
+                optional: true
+            }
+            // ,{
+            //     label: "admin",
+            //     ref: 'admin',
+            //     default: false,
+            //     optional: true
+            // }
+            // ,{
+            //    label: "Management Network",
+            //    ref: 'mgmt-network'
+            // }
+            // ,{
+            //     label: "Plugin Name",
+            //     ref: 'plugin-name',
+            //     optional: true
+            // },{
+            //     label: "Security Groups",
+            //     ref: 'security-groups',
+            //     type: 'list',
+            //     optional: true
+            // },{
+            //     label: "Dynamic Flavor Support ",
+            //     ref: 'dynamic-flavor-support',
+            //     type: 'boolean',
+            //     optional: true
+            // }
+            //, {
+            //    label: "Floating IP Pool",
+            //    ref: 'floating-ip-pool',
+            //    optional: true
+            // }
+            // ,{
+            //     label: "Certificate Validation",
+            //     ref: 'cert-validate',
+            //     type: 'boolean',
+            //     optional: true
+            // }
+            ]
+
         },
         types: [{
             "name": "ODL",
             "account-type": "odl",
+        },{
+            "name": "OpenStack",
+            "account-type": "openstack",
         }]
     },
     'cloud': {
@@ -119,10 +206,11 @@ let AccountMeta = {
                 label: "Tenant",
                 ref: 'tenant'
             }, {
-                label: 'Management Network',
-                ref: 'mgmt-network'
+                label: 'Default Management Network',
+                ref: 'mgmt-network',
+                optional: true
             }, {
-                label: 'Floating IP Pool Network Name',
+                label: 'Default Floating IP Pool Network Name',
                 ref: 'floating-ip-pool',
                 optional: true
             }, {
@@ -151,7 +239,30 @@ let AccountMeta = {
                 label: "Port",
                 ref: 'port',
                 optional: true
-            }]
+            }],
+            "prop_cloud1": [{
+                label: "Host",
+                ref: "host"
+            }, {
+                label: "Username",
+                ref: "username"
+            }, {
+                label: "Password",
+                ref: "password"
+            }, {
+                label: "Management Network",
+                ref: "mgmt-network"
+            }, {
+                label: "Public IP pool",
+                ref: "public-ip-pool"
+            }, {
+                label: "WAN Interface",
+                ref: "wan-interface"
+            }, {
+                label: "Firewall",
+                ref: "firewall",
+                optional: true
+             }]
         },
         nestedParams: {
             "openvim": {
@@ -182,7 +293,10 @@ let AccountMeta = {
         }, {
             "name": "Open VIM",
             "account-type": "openvim"
-        }]
+        }, {
+            "name": "Brocade",
+            "account-type": "prop_cloud1"
+         }]
     },
     resources: {
     },
@@ -192,28 +306,40 @@ let AccountMeta = {
         "openstack": require("../../images/openstack.png"),
         "cloudsim_proxy": require("../../images/riftio.png"),
         "odl": require("../../images/OpenDaylight_logo.png"),
-        "juju": require("../../images/juju.svg")
+        "juju": require("../../images/juju.svg"),
+        "prop_cloud1": require("../../images/brocade.png"),
+        "openmano": require("../../images/openmano.png")
 
     },
     labelByType: {
         "aws": "AWS",
         "openvim": "Open VIM",
         "openstack": "OpenStack",
-        "cloudsim_proxy": "Cloudsim"
+        "cloudsim_proxy": "Cloudsim",
+        "prop_cloud1": "Brocade",
+        "openmano": "OpenStack"
     }
 }
 
 export default class AccountStore {
     constructor() {
+        // const savedData = JSON.parse(window.sessionStorage.getItem('account'));
+        const savedData = null;
+        this.saveAccountToSessionStorage(null, true)
         this.cloud = [];
         this['config-agent'] = [];
+        this['resource-orchestrator'] = [];
         this.sdn = [];
+        this.savedData = savedData;
         this.account = null;
-        this.types = [];
+        // this.account = savedData.account;
+        // this.accountType = savedData.accountType;
+        this.types = this.accountType ? AccountMeta[this.accountType].types : [];
         this.refreshingAll = false;
         this.sdnOptions = [];
         this.AccountMeta = AccountMeta;
         this.showVIM = true;
+        this.vduInstanceTimeout = '';
         this.bindActions(AccountActions(this.alt));
         this.registerAsync(AccountSource);
         this.exportPublicMethods({
@@ -225,7 +351,10 @@ export default class AccountStore {
             updateAccount: this.updateAccount,
             viewAccount: this.viewAccount,
             handleNestedParamChange: this.handleNestedParamChange,
-            getImage: this.getImage
+            getImage: this.getImage,
+            saveAccountToSessionStorage: this.saveAccountToSessionStorage,
+            updateVduTimeout: this.updateVduTimeout,
+            getTransientAccountForUser: this.getTransientAccountForUser
         })
     }
     refreshAllAccountsSuccess = () => {
@@ -246,16 +375,9 @@ export default class AccountStore {
     }
     refreshCloudAccountSuccess = () => {
 
-    }
-    getResourceOrchestratorSuccess = (data) => {
-        this.alt.actions.global.hideScreenLoader.defer();
-        if(data['rw-launchpad:resource-orchestrator'] && (data['rw-launchpad:resource-orchestrator']['account-type'] == 'openmano')) {
-            this.setState({
-                showVIM: false
-            })
-        }
     }
     deleteAccountSuccess = (response) => {
+        this.saveAccountToSessionStorage(null, true);
         this.setState({
             currentAccount: false,
             account: {}
@@ -284,16 +406,19 @@ export default class AccountStore {
                     cloud: data.cloud.data,
                     'config-agent': data['config-agent'].data,
                     sdn: data.sdn.data,
+                    'resource-orchestrator': data['resource-orchestrator'].data,
                     sdnOptions: SdnOptions
                 };
 
                 //If account is selected, updated connection status only
-                if(self.currentAccount) {
+                if(self.currentAccount && (self.account || self.savedData)) {
                     let Account = self.getAccountFromStream(data[self.currentAccount.type].data, self.currentAccount.name);
-                    newState.account = self.account;
-                    newState.account['connection-status'] = Account['connection-status']
+                    newState.account = self.changedData ? self.account : self.savedData || self.account;
+
+                    newState.account['connection-status'] = Account && Account['connection-status'];
+                    newState.savedData = null;
                 }
-                self.setState(newState)
+                self.setState(newState);
             } catch(error) {
                 console.log('Hit at exception in openAccountSocketSuccess', error)
             }
@@ -311,7 +436,8 @@ export default class AccountStore {
             socket: null
         })
     }
-    setAccountTemplate = (AccountType, type) => {
+    setAccountTemplate = (AccountType, type, savedData) => {
+        console.log('Setting Account Template')
         let account = {
             name: '',
             'account-type': type || AccountMeta[AccountType].defaultType,
@@ -324,47 +450,68 @@ export default class AccountStore {
 
         account[type || AccountMeta[AccountType].defaultType] = {}
         this.setState({
-            account: account,
+            account: savedData || account,
             accountType: AccountType,
             types: AccountMeta[AccountType].types,
-            currentAccount: null
+            currentAccount: null,
+            changedData: true,
+            savedData: null
         })
     }
     getAccountFromStream(data, name) {
         let result = null;
-        data.map(function(a) {
+        data && _.isArray(data) && data.map(function(a) {
             if(a.name == name) {
                 result = a;
             }
         });
         return result;
     }
-    viewAccount = ({type, name}) => {
+    viewAccount = ({type, name}, savedData) => {
+        console.log('Viewing account')
         var data = null;
         var accounts = null;
-        if(this && this[type].length) {
+        let vduInstanceTimeout = '';
+        if(this && this[type] && this[type].length) {
             accounts = this[type];
             data = this.getAccountFromStream(accounts, name);
+            const isRo = data.hasOwnProperty('ro-account-type');
             if(data) {
                 let accountParams = {
-                    params: AccountMeta[type].params[data['account-type']]
+                    params: AccountMeta[type].params[
+                        isRo ? data['ro-account-type'] : data['account-type']
+                    ]
                 };
 
                 let accountNestedParams = {
-                    nestedParams: AccountMeta[type].nestedParams?AccountMeta[type].nestedParams[data['account-type']]:null
+                    nestedParams: AccountMeta[type].nestedParams?AccountMeta[type].nestedParams[
+                        isRo ? data['ro-account-type'] : data['account-type']
+                    ]:null
                 };
-
+                if (data.hasOwnProperty('vdu-instance-timeout')) {
+                    vduInstanceTimeout = data['vdu-instance-timeout'];
+                }
                 this.setState({
                     currentAccount: {type, name},
-                    account: Object.assign(data, accountParams, accountNestedParams),
-                    accountType: type
+                    account: (savedData ? savedData : Object.assign(data, accountParams, accountNestedParams)) || null,
+                    accountType: type,
+                    types: AccountMeta[type].types,
+                    vduInstanceTimeout: vduInstanceTimeout,
+                    savedData: null
                 })
             }
+        } else {
+            this.setState({
+                    currentAccount: {type, name},
+                    account: null,
+                    accountType: type,
+                    types: AccountMeta[type].types,
+                })
         }
     }
     generateOptionsByName(data) {
         let results = [];
-        if (data && data.constructor.name == "Array") {
+        if (data && _.isArray(data)) {
           data.map(function(d) {
               results.push({
                   label: d.name,
@@ -375,32 +522,56 @@ export default class AccountStore {
         return results;
     }
     updateAccount = (account) => {
-        this.setState({account:account})
+        this.saveAccountToSessionStorage(account);
+        this.setState({account:account,
+            changedData: true})
     }
     handleNameChange = (event) => {
         var account = this.account;
         account.name = event.target.value;
+        this.saveAccountToSessionStorage(account)
         this.setState(
              {
-                account:account
+                account:account,
+                changedData: true
              }
         );
     }
-    handleAccountTypeChange = (node, event) => {
+    updateVduTimeout = (event) => {
+        var vduInstanceTimeout = event.target.value;
+        this.setState(
+             {
+                vduInstanceTimeout:vduInstanceTimeout
+             }
+        );
+    }
+    handleAccountTypeChange = (node, isRo, event) => {
         var temp = {};
         temp.name = this.account.name;
-        temp['account-type'] = event.target.value;
+        if (isRo) {
+            temp['ro-account-type'] = event.target.value;
+        } else {
+            temp['account-type'] = event.target.value;
+        }
         temp.params= AccountMeta[this.accountType].params[event.target.value];
-        temp.nestedParams = AccountMeta[this.accountType]?AccountMeta[this.accountType].nestedParams[event.target.value]:null;
+        temp.nestedParams = (AccountMeta[this.accountType] && AccountMeta[this.accountType].nestedParams )?AccountMeta[this.accountType].nestedParams[event.target.value]:null;
         temp[event.target.value] = {};
+        this.saveAccountToSessionStorage(temp)
         this.setState({
-            account: temp
+            account: temp,
+            changedData: true
         });
     }
-    handleParamChange(node, event) {
+    handleParamChange(node, isRo, event) {
         return function(event) {
             var account = this.state.account;
-            account[account['account-type']][node.ref] = event.target.value;
+            if (isRo) {
+                account[account['ro-account-type']][node.ref] = event.target.value;
+
+            } else {
+                account[account['account-type']][node.ref] = event.target.value;
+
+            }
             account.params[node.ref] = event.target.value;
             this.updateAccount(account);
         }.bind(this);
@@ -419,5 +590,44 @@ export default class AccountStore {
     getImage = (type) => {
         return AccountMeta.image[type];
     }
+    createAccountSuccess = () => {
+        this.setState({account: {}})
+        this.saveAccountToSessionStorage(null, true)
+    }
+    handleCancelAccount = () => {
+        this.setState({account: {}, currentAccount: null, savedData: null, accountType: null, types: []})
+        this.saveAccountToSessionStorage(null, true)
+    }
+    saveAccountToSessionStorage = (account, clear) => {
+        const userProfile = this.userProfile;
+        if(userProfile) {
+             if(clear) {
+
+                window.sessionStorage.removeItem(userProfile.userId + '@' + userProfile.domain + ':account');
+                // this.setState({
+                //     account:null,
+                //     accountType: null
+                // })
+            } else {
+                const state = account || this.account;
+                window.sessionStorage.setItem(userProfile.userId + '@' + userProfile.domain + ':account', JSON.stringify(state));
+            }
+        }
+
+    }
+    getTransientAccountForUser = (userProfile) => {
+        let userProfileTransientAccount = window.sessionStorage.getItem(userProfile.userId + '@' + userProfile.domain + ':account') || null;
+        var transientAccount = null;
+        if (userProfileTransientAccount) {
+            transientAccount = JSON.parse(userProfileTransientAccount);
+        };
+        this.saveAccountToSessionStorage(null, true);
+        if(!this.userProfile) {
+            this.setState({
+                savedData:  transientAccount,
+                userProfile: userProfile
+            })
+        }
+    }
 }
 
index 7b56011..afa9ab1 100644 (file)
@@ -21,6 +21,14 @@ import AppHeader from 'widgets/header/header.jsx';
 import AccountStore from './accountStore.js';
 import AccountSidebar from '../account_sidebar/accountSidebar.jsx';
 import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
+//Delete this line after testing is done
+// PROJECT_ROLES.ACCOUNT_ADMIN = '';
 import 'style/layout.scss';
 
 class AccountsDashboard extends React.Component {
@@ -32,7 +40,6 @@ class AccountsDashboard extends React.Component {
     componentWillMount() {
         this.Store.listen(this.updateState);
         this.Store.openAccountsSocket();
-        this.Store.getResourceOrchestrator();
     }
     componentWillUnmount() {
         this.Store.closeSocket();
@@ -44,11 +51,12 @@ class AccountsDashboard extends React.Component {
     render() {
         let self = this;
         let html;
+        let READONLY = !isRBACValid(this.context.userProfile, [PROJECT_ROLES.ACCOUNT_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]);
         html = (<div className="launchpad-account-dashboard content-wrapper">
                     <div className="flex">
-                      <AccountSidebar {...this.state} store={this.Store}/>
+                      <AccountSidebar {...this.state} readonly={READONLY} store={this.Store}/>
                       <div>
-                        { this.props.children ? React.cloneElement(this.props.children, {store: self.Store, ...self.state}) : 'Edit or Create New Accounts'
+                        { this.props.children ? React.cloneElement(this.props.children, {readonly: READONLY, store: self.Store, ...self.state}) : 'Edit or Create New Accounts'
                         }
                       </div>
                     </div>
@@ -57,7 +65,8 @@ class AccountsDashboard extends React.Component {
     }
 }
 AccountsDashboard.contextTypes = {
-    router: React.PropTypes.object
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
 };
 
 export default SkyquakeComponent(AccountsDashboard);
index 723d893..fbcafff 100644 (file)
@@ -42,6 +42,7 @@ class AccountSidebar extends React.Component{
     }
     render() {
         let html;
+        let self = this;
         let {store, ...props} = this.props;
         //[this.props.cloud,this.props.sdn,this.props['config-agent']]
         let AccountData = [
@@ -56,9 +57,36 @@ class AccountSidebar extends React.Component{
             {
                 type: 'config',
                 data: this.props['config-agent']
+            },
+            {
+                type: 'resource-orchestrator',
+                data: this.props['resource-orchestrator']
             }
         ];
         let refreshStatus = (<div>Check All Connectivity Status</div>)
+        let resourceOrchestrators = (this.props['resource-orchestrator'].length > 0) ? this.props['resource-orchestrator'].map(function(orchestrator, index) {
+            let status = null;
+            if (orchestrator) {
+                if (orchestrator['connection-status']) {
+                    status = orchestrator['connection-status'].status;
+                }
+                return (
+                    <DashboardCard  key={index} className='pool-card accountSidebarCard'>
+                    <header>
+                    <Link to={'accounts/resource-orchestrator/' + encodeURIComponent(orchestrator.name)}>
+                            <div className="accountSidebarCard--content">
+                                <img className="accountSidebarCard--logo" src={store.getImage(orchestrator['ro-account-type'])} />
+                                <h3 title="Edit Resource Orchestrator(RO) Account">
+                                    <span className="accountSidebarCard--name" title={orchestrator.name}>{orchestrator.name}</span>
+                                    <AccountConnectivityStatus status={status}/>
+                                </h3>
+                            </div>
+                    </Link>
+                    </header>
+                    </DashboardCard>
+                );
+            }
+        }) : null;
         let cloudAccounts = (this.props.cloud.length > 0) ? this.props.cloud.map(function(account, index) {
             let status = null;
             if (account) {
@@ -68,11 +96,11 @@ class AccountSidebar extends React.Component{
                 return (
                     <DashboardCard  key={index} className='pool-card accountSidebarCard'>
                     <header>
-                    <Link to={'accounts/cloud/' + account.name}>
+                    <Link to={'accounts/cloud/' + encodeURIComponent(account.name)} onClick={self.props.actions.handleCancelAccount}>
                             <div className="accountSidebarCard--content">
                                 <img className="accountSidebarCard--logo" src={store.getImage(account['account-type'])} />
                                 <h3 title="Edit Account">
-                                    {account.name}
+                                    <span className="accountSidebarCard--name" title={account.name}>{account.name}</span>
                                     <AccountConnectivityStatus status={status}/>
                                 </h3>
                             </div>
@@ -90,10 +118,10 @@ class AccountSidebar extends React.Component{
             return (
                 <DashboardCard key={index} className='pool-card accountSidebarCard'>
                      <header>
-                        <Link to={'accounts/sdn/' + account.name} title="Edit Account">
+                        <Link to={'accounts/sdn/' + encodeURIComponent(account.name)} title="Edit Account">
                          <div className="accountSidebarCard--content">
                             <img className="accountSidebarCard--logo" src={store.getImage(account['account-type'])} />
-                            <h3>{account.name}<AccountConnectivityStatus status={status}/></h3>
+                            <h3><span className="accountSidebarCard--name" title={account.name}>{account.name}</span><AccountConnectivityStatus status={status}/></h3>
                         </div>
                 </Link>
                     </header>
@@ -108,11 +136,11 @@ class AccountSidebar extends React.Component{
             return (
                 <DashboardCard key={index} className='pool-card accountSidebarCard'>
                 <header>
-                    <Link to={'accounts/config-agent/' + account.name} title="Edit Account">
+                    <Link to={'accounts/config-agent/' + encodeURIComponent(account.name)} title="Edit Account">
                         <div className="accountSidebarCard--content">
                             <img className="accountSidebarCard--logo" src={store.getImage(account['account-type'])} />
                             <h3 title="Edit Account">
-                                {account.name}
+                                <span className="accountSidebarCard--name" title={account.name}>{account.name}</span>
                                 <AccountConnectivityStatus status={status}/>
                             </h3>
                         </div>
@@ -123,47 +151,82 @@ class AccountSidebar extends React.Component{
         }) : null;
         html = (
             <div className='accountSidebar'>
-                 <Button className="refreshList light" onClick={this.props.store.refreshAll.bind(this, AccountData)} label={this.props.refreshingAll ? 'Checking Connectivity Status...' : refreshStatus}></Button>
+                {
+                    self.props.readonly ? null :
+                        <Button className="refreshList light" onClick={this.props.store.refreshAll.bind(this, AccountData)} label={this.props.refreshingAll ? 'Checking Connectivity Status...' : refreshStatus}/>
+                }
+                 <div>
+                        <h1>RO Accounts</h1>
+                        {resourceOrchestrators}
+                        {
+                            !self.props.readonly ?
+                                <DashboardCard className="accountSidebarCard">
+                                    <Link
+                                        to={{pathname: '/accounts/resource-orchestrator/create'}}
+                                        title="Create Resource Orchestrator(RO) Account"
+                                        className={'accountSidebarCard_create'}
+                                        onClick={self.props.actions.handleCancelAccount} >
+                                            Add RO Account
+                                            <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
+                                    </Link>
+                                </DashboardCard>
+                            :  <div style={{margin:'1rem'}}></div>
+                        }
+                    </div>
                 {props.showVIM ? (
                     <div>
                         <h1>VIM Accounts</h1>
                         {cloudAccounts}
-                        <DashboardCard className="accountSidebarCard">
-                                <Link
-                                to={{pathname: '/accounts/cloud/create'}}
-                                title="Create Cloud Account"
-                                className={'accountSidebarCard_create'}
-                            >
-                                    Add VIM Account
-                                    <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
-                                </Link>
-                        </DashboardCard>
+                        {
+                            !self.props.readonly ?
+                                <DashboardCard className="accountSidebarCard">
+                                    <Link
+                                        to={{pathname: '/accounts/cloud/create'}}
+                                        title="Create Cloud Account"
+                                        className={'accountSidebarCard_create'}
+                                        onClick={self.props.actions.handleCancelAccount} >
+                                            Add VIM Account
+                                            <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
+                                    </Link>
+                                </DashboardCard>
+                            :  <div style={{margin:'1rem'}}></div>
+                        }
                     </div>)
                 : null}
                 <h1>SDN Accounts</h1>
                 {sdnAccounts}
-                <DashboardCard className="accountSidebarCard">
-                        <Link
-                        to={{pathname: '/accounts/sdn/create'}}
-                        title="Create Sdn Account"
-                        className={'accountSidebarCard_create'}
-                    >
-                            Add SDN Account
-                            <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
-                        </Link>
-                </DashboardCard>
+                {
+                    !self.props.readonly ?
+                        <DashboardCard className="accountSidebarCard">
+                            <Link
+                            to={{pathname: '/accounts/sdn/create'}}
+                            title="Create Sdn Account"
+                            className={'accountSidebarCard_create'}
+                            onClick={self.props.actions.handleCancelAccount}>
+                                Add SDN Account
+                                <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
+                            </Link>
+
+                        </DashboardCard>
+                    : <div style={{margin:'1rem'}}></div>
+                }
                 <h1>Config Agent Accounts</h1>
                 {configAgentAccounts}
-                <DashboardCard className="accountSidebarCard">
-                    <Link
-                        to={{pathname: '/accounts/config-agent/create'}}
-                        title="Create Config Agent Account"
-                        className={'accountSidebarCard_create'}
-                    >
-                            Add Config Agent Account
-                            <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
-                        </Link>
-                </DashboardCard>
+                {
+                    !self.props.readonly ?
+                        <DashboardCard className="accountSidebarCard">
+                            <Link
+                                to={{pathname: '/accounts/config-agent/create'}}
+                                title="Create Config Agent Account"
+                                className={'accountSidebarCard_create'}
+                                onClick={self.props.actions.handleCancelAccount}
+                            >
+                                Add Config Agent Account
+                                <img src={require("style/img/launchpad-add-fleet-icon.png")}/>
+                            </Link>
+                        </DashboardCard>
+                    :  <div style={{margin:'1rem'}}></div>
+                }
             </div>
                 );
         return html;
@@ -173,7 +236,8 @@ class AccountSidebar extends React.Component{
 AccountSidebar.defaultProps = {
     cloud: [],
     sdn: [],
-    'config-agent': []
+    'config-agent': [],
+    ro: []
 }
 
 
index cf25899..2768a97 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,7 +18,8 @@
 @import 'style/_colors.scss';
 .accountSidebar {
     -ms-flex:1 1 100%;
-        flex:1 1 100%;
+        -webkit-box-flex:1;
+            flex:1 1 100%;
     max-width:300px;
     h1 {
       margin-left:1rem;
             background:white;
             padding-left:0.5rem;
             display:-ms-flexbox;
+            display:-webkit-box;
             display:flex;
                 -ms-flex-align:start;
-                    align-items:flex-start;
+                    -webkit-box-align:start;
+                        align-items:flex-start;
             text-transform:uppercase;
             h3 {
                 display:-ms-flexbox;
+                display:-webkit-box;
                 display:flex;
                 -ms-flex:1;
-                    flex:1;
+                    -webkit-box-flex:1;
+                        flex:1;
                 -ms-flex-pack:justify;
-                    justify-content:space-between;
+                    -webkit-box-pack:justify;
+                        justify-content:space-between;
                 -ms-flex-align: center;
-                    align-items: center;
+                    -webkit-box-align: center;
+                        align-items: center;
                  padding-left:0.5rem;
             }
         }
+        &--name {
+            text-overflow: ellipsis;
+            overflow: hidden;
+            max-width: 170px;
+        }
         &--content {
             display:-ms-flexbox;
+            display:-webkit-box;
             display:flex;
             -ms-flex-pack:start;
-            justify-content:flex-start;
+            -webkit-box-pack:start;
+                    justify-content:flex-start;
             -ms-flex-align: center;
-                align-items: center;
+                -webkit-box-align: center;
+                    align-items: center;
             height:70px;
         }
         &--logo{
             a.link-item {
                 background-color: $dark-gray;
                 display: -ms-flexbox;
+                display: -webkit-box;
                 display: flex;
                 -ms-flex-direction: row;
-                    flex-direction: row;
+                    -webkit-box-orient: horizontal;
+                    -webkit-box-direction: normal;
+                        flex-direction: row;
                 padding: 1rem;
                 -ms-flex-pack: start;
-                    justify-content: flex-start;
+                    -webkit-box-pack: start;
+                        justify-content: flex-start;
                 text-decoration: none;
                 color: black;
             }
             a.empty-pool {
                 margin-top:0.125rem;
                 display:-ms-flexbox;
+                display:-webkit-box;
                 display:flex;
                 -ms-flex-pack: justify;
-                    justify-content: space-between;
+                    -webkit-box-pack: justify;
+                        justify-content: space-between;
                 img {
                     height:1rem;
                 }
         }
         &_create {
             display:-ms-flexbox;
+            display:-webkit-box;
             display:flex;
             -ms-flex-pack:justify;
-                justify-content:space-between;
+                -webkit-box-pack:justify;
+                    justify-content:space-between;
             padding:1rem;
             font-size: 0.85rem;
             text-transform:uppercase;
         margin-left: 1rem;
         margin-bottom:1rem;
         display:-ms-flexbox;
+        display:-webkit-box;
         display:flex;
         -ms-flex-align:center;
-        align-items:center;
+        -webkit-box-align:center;
+                align-items:center;
         padding: 0.5rem;
         cursor: pointer;
         text-transform:uppercase;
index 6356ddb..48ec049 100644 (file)
@@ -26,6 +26,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CompressionPlugin = require("compression-webpack-plugin");
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -66,8 +67,8 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
-            templateContent: '<div id="app"></div>'
+            filename: '../' + htmlFilename
+            template: frameworkPath + '/plugin-index.html'
         })
     ]
 };
diff --git a/skyquake/plugins/admin/CMakeLists.txt b/skyquake/plugins/admin/CMakeLists.txt
new file mode 100644 (file)
index 0000000..4a7dfec
--- /dev/null
@@ -0,0 +1,52 @@
+# 
+#   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.
+#
+# Author(s): Kiran Kashalkar
+# Creation Date: 08/18/2015
+# 
+
+##
+# 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(
+  admin
+  DEPENDS skyquake
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
+  CONFIGURE_COMMAND echo
+  BUILD_COMMAND
+    ${CMAKE_CURRENT_BINARY_DIR}/admin/admin-build/scripts/build.sh
+  INSTALL_COMMAND
+    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/install.sh
+    ${CMAKE_CURRENT_BINARY_DIR}/admin/admin-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/admin/api/admin.js b/skyquake/plugins/admin/api/admin.js
new file mode 100644 (file)
index 0000000..82cbafb
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * 
+ *   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.
+ *
+ */
+
+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 APIVersion = '/v1';
+var About = {};
+
+About.getVCS = function(req) {
+  var api_server = req.query["api_server"];
+
+  return new Promise(function(resolve, reject) {
+    var requestHeaders = {};
+    _.extend(requestHeaders,
+      constants.HTTP_HEADERS.accept.data, {
+        'Authorization': req.session && req.session.authorization
+      });
+    request({
+        url: utils.confdPort(api_server) + APIVersion + '/api/operational/vcs/info?deep',
+        type: 'GET',
+        headers: requestHeaders,
+        forever: constants.FOREVER_ON,
+        rejectUnauthorized: false,
+      },
+      function(error, response, body) {
+        var data;
+        console.log(error);
+        if (utils.validateResponse('About/vcs.get', error, response, body, resolve, reject)) {
+          try {
+            data = JSON.parse(response.body)["rw-base:info"]
+          } catch (e) {
+            return reject({});
+          }
+
+          return resolve(data);
+        }
+      });
+  });
+}
+
+About.getVersion = function(req) {
+  var api_server = req.query["api_server"];
+
+  return new Promise(function(resolve, reject) {
+    var requestHeaders = {};
+    _.extend(requestHeaders,
+      constants.HTTP_HEADERS.accept.data, {
+        'Authorization': req.session && req.session.authorization
+      });
+    request({
+        url: utils.confdPort(api_server) + APIVersion + '/api/operational/version?deep',
+        type: 'GET',
+        headers: requestHeaders,
+        forever: constants.FOREVER_ON,
+        rejectUnauthorized: false,
+      },
+      function(error, response, body) {
+        var data;
+        console.log(error);
+        if (utils.validateResponse('About/version.get', error, response, body, resolve, reject)) {
+          try {
+            data = JSON.parse(response.body)['rw-base:version']
+          } catch (e) {
+            return reject({});
+          }
+
+          return resolve(data);
+        }
+      });
+  });
+}
+
+About.get = function(req) {
+
+  var api_server = req.query["api_server"];
+
+  return new Promise(function(resolve, reject) {
+    Promise.all([
+        About.getVCS(req),
+        About.getVersion(req)
+      ])
+      .then(function(results) {
+        var AboutObject = {};
+        AboutObject.vcs = results[0];
+        AboutObject.version = results[1];
+        resolve(AboutObject);
+      }, function(error) {
+        console.log('error getting vcs data', error);
+        reject(error)
+      });
+  });
+};
+
+About.uptime = function(req) {
+  var api_server = req.query["api_server"];
+  return new Promise(function(resolve, reject) {
+    var requestHeaders = {};
+    _.extend(requestHeaders,
+      constants.HTTP_HEADERS.accept.data, {
+        'Authorization': req.session && req.session.authorization
+      });
+    request({
+        url: utils.confdPort(api_server) + APIVersion + '/api/operational/uptime/uptime',
+        type: 'GET',
+        headers: requestHeaders,
+        forever: constants.FOREVER_ON,
+        rejectUnauthorized: false
+      },
+      function(error, response, body) {
+        if (utils.validateResponse('About.uptime', error, response, body, resolve, reject)) {
+          try {
+            data = JSON.parse(response.body);
+          } catch (e) {
+            return reject({});
+          }
+          return resolve(data)
+
+        }
+      })
+  })
+}
+module.exports = About;
diff --git a/skyquake/plugins/admin/config.json b/skyquake/plugins/admin/config.json
new file mode 100644 (file)
index 0000000..d4b6336
--- /dev/null
@@ -0,0 +1,28 @@
+{
+    "root": "public",
+    "name": "Configuration",
+    "dashboard": "./admin.jsx",
+    "order": 5,
+    "priority": 2,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper"
+    ],
+    "routes": [{
+        "label": "Dashboard",
+        "route": "",
+        "component": "./admin.jsx",
+        "path": "",
+        "type": "internal",
+        "routes": [
+            {
+                "route": ":ref",
+                "component": "./admin.jsx",
+                "path": ":ref",
+                "type": "internal"
+            }
+        ]
+    }]
+}
\ No newline at end of file
diff --git a/skyquake/plugins/admin/package.json b/skyquake/plugins/admin/package.json
new file mode 100644 (file)
index 0000000..db65c03
--- /dev/null
@@ -0,0 +1,58 @@
+{
+  "name": "admin",
+  "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": {
+    "bluebird": "^3.4.1",
+    "change-case": "^3.0.1",
+    "express": "^4.13.3",
+    "history": "^1.17.0",
+    "jquery": "^2.2.1",
+    "json-loader": "^0.5.4",
+    "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-dialog": "^1.0.1",
+    "react-dom": "^0.14.6",
+    "react-modal": "^2.2.4",
+    "react-open-iconic-svg": "^1.0.4",
+    "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",
+    "validator": "^8.1.0"
+  },
+  "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",
+    "compression-webpack-plugin": "^0.3.2",
+    "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.10.1"
+  }
+}
diff --git a/skyquake/plugins/admin/routes.js b/skyquake/plugins/admin/routes.js
new file mode 100644 (file)
index 0000000..68321d1
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 
+ *   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.
+ *
+ */
+var router = require('express').Router();
+var cors = require('cors');
+var utils = require('../../framework/core/api_utils/utils.js')
+var Admin = require('./api/admin.js');
+router.get('/api/', cors(), function(req, res) {
+    Admin.get(req).then(function(data) {
+        res.send(data);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+module.exports = router;
diff --git a/skyquake/plugins/admin/scripts/build.sh b/skyquake/plugins/admin/scripts/build.sh
new file mode 100755 (executable)
index 0000000..9eec1ab
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# 
+#   Copyright 2016-2017 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.
+#
+
+PLUGIN_NAME=admin
+# change to the directory of this script
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
\ No newline at end of file
diff --git a/skyquake/plugins/admin/scripts/install.sh b/skyquake/plugins/admin/scripts/install.sh
new file mode 100755 (executable)
index 0000000..01a6d30
--- /dev/null
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+#
+#   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.
+#
+
+plugin=admin
+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 node_modules package.json -C $source_dir
+#cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
+
diff --git a/skyquake/plugins/admin/server.js b/skyquake/plugins/admin/server.js
new file mode 100644 (file)
index 0000000..03b53fb
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 
+ *   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.
+ *
+ */
+
+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);
+});
diff --git a/skyquake/plugins/admin/src/AdminStore.js b/skyquake/plugins/admin/src/AdminStore.js
new file mode 100644 (file)
index 0000000..889fb72
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+import Alt from 'widgets/skyquake_container/skyquakeAltInstance';
+
+const NSD_SCHEMA_PATH = 'nsd-catalog/nsd';
+const VNFD_SCHEMA_PATH = 'vnfd-catalog/vnfd';
+
+import {
+    modelActions
+} from 'source/model'
+
+class AdminStore {
+    constructor() {
+        this.adminActions = this.alt.generateActions(
+            'openModel'
+        );
+        this.state = {
+            modelList: [
+                'openidc-provider-config'
+            ]
+        }
+        this.bindActions(modelActions);
+        
+    }
+
+    get actions() {
+        return this.adminActions
+    }
+    processRequestFailure(result){
+        console.debug('processRequestFailure');
+        Alt.actions.global.showNotification.defer(result.response.error.message);
+    }
+    processRequestSuccess(data){
+        console.debug('processRequestSuccess');
+    }
+    processRequestInitiated(data){
+        console.debug('processRequestInitiated');
+    }
+}
+
+export default AdminStore
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/admin.jsx b/skyquake/plugins/admin/src/admin.jsx
new file mode 100644 (file)
index 0000000..708a136
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ *
+ *   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 './admin.scss';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import AppHeader from 'widgets/header/header.jsx';
+import AdminStore from './AdminStore'
+import ModelStore from './store/ModelStore'
+import ModelExplorer from './components/ModelExplorer'
+
+import 'style/layout.scss';
+
+class Element {
+  constructor(explorerModel) {
+    this.explorerModel = explorerModel;
+    this.node = explorerModel.dataModel.path.split('/').pop().split(':').pop();
+    this.dataModel = explorerModel.dataModel;
+    if (this.dataModel.data && this.dataModel.schema[this.node].type === 'list' && !Array.isArray(this.dataModel.data)) {
+      this.dataModel.data = [this.dataModel.data];
+    }
+  }
+  get name() { return this.dataModel.path.split(':').pop() }
+  get type() { return this.dataModel.schema[this.node].type }
+  get value() { return this.dataModel.data }
+  get schema() { return this.dataModel.schema[this.node] }
+  getItemInfo(item) {
+    return ModelExplorer.getItemInfo(this.dataModel.schema[this.node].key, item);
+  }
+  // explore model methods
+  getElement = path => path.length > 1 ? this.explorerModel.getElement(path.slice(1)) : this;
+}
+
+
+class Admin extends React.Component {
+  constructor(props) {
+    super(props)
+    this.adminStore = props.flux.stores['AdminStore'] || props.flux.createStore(AdminStore, 'AdminStore');
+    this.adminStore.listen(this.updateAdminState);
+    this.state = { explorerModels: {}, ...this.adminStore.getState() };
+  }
+
+  componentDidMount() {
+    const storeList = this.state.modelList ? this.state.modelList.map((path, index) => {
+      const store = this.props.flux.stores[path] || this.props.flux.createStore(ModelStore, path, path);
+      store.listen(this.updateModelState);
+      store.get();
+      return store;
+    }) : [];
+    this.setState({ storeList });
+  }
+
+  componentWillUnmount() {
+    this.adminStore.unlisten(this.updateSchema);
+    this.state.storeList.forEach((store) => store.unlisten(this.updateModelState));
+  }
+
+  updateModelState = model => {
+    const explorerModels = Object.assign({}, this.state.explorerModels);
+    explorerModels[model.path] = new Element(ModelExplorer.getExplorerModel(model));
+    this.setState({ explorerModels });
+  }
+
+  updateAdminState = admin => {
+    // if storeList has changed then handle that
+  }
+
+  updateModel = (path, operation, data) => {
+    const store = this.props.flux.stores[path[1]] || this.props.flux.createStore(ModelStore, path[1]);
+    store[operation](path.slice(2), data);
+    return operation === 'delete'; // close column?
+  }
+
+  adminExplorerModel = new class {
+    constructor(admin) {
+      this.admin = admin;
+    }
+
+    getElement(path) {
+      if (path.length > 2) {
+        return this.admin.state.explorerModels[path[1]].getElement(path.slice(1))
+      } else if (path.length > 1) {
+        return this.admin.state.explorerModels[path[1]]
+      } else {
+        const data = Object.values(this.admin.state.explorerModels);
+        const properties = data.length ? 
+          data.map(e => ({
+            name: e.dataModel.path, 
+            type: ((e.dataModel.schema && e.dataModel.schema[e.node].type) || 'loading')
+          }))
+          : [];
+        const schema = {
+          name: "Configuration", 
+          type: 'container', 
+          properties
+        }
+        return {
+          schema,
+          value: data,
+          type: schema.type,
+          name: schema.name
+        }
+      }
+    }
+  }(this);
+
+  render() {
+    const { flux } = this.props;
+    const { storeList } = this.state;
+    const modelList = storeList && storeList.map(store => store.getState())
+    return (
+      <div className="admin-container">
+        <ModelExplorer model={this.adminExplorerModel} onUpdate={this.updateModel} />
+      </div>
+    )
+  }
+}
+export default SkyquakeComponent(Admin);
diff --git a/skyquake/plugins/admin/src/admin.scss b/skyquake/plugins/admin/src/admin.scss
new file mode 100644 (file)
index 0000000..50d08ba
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 
+ *   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.
+ *
+ */
+.admin-container {
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: stretch;
+  align-content: stretch;
+  width: 100%;
+  flex: 1 1 100%;
+  overflow-y: auto;
+
+  .table-container-wrapper {
+    flex: 1;
+    .table-container {
+      padding:40px 20px 10px 20px;
+      font-family: 'roboto-thin', Helvetica, Arial, sans-serif;
+    }
+  }
+
+  table {
+    border-collapse: separate;
+    border-spacing: 0.0625rem solid;
+    width: 100%;
+  
+    thead  {
+      th {
+        background-color: #e5e5e5;
+        border-right: 1px solid #dadada;
+        padding: 1.125rem 1.125rem;
+        text-align:left;
+        font-size: .8rem;
+        font-weight: bold;
+        text-transform: uppercase;
+      }
+      td:last-child {
+        border: 0;
+      }
+    }
+    tbody {
+      td {
+        border-bottom: 1px solid #e5e5e5;
+        padding: .75rem 0rem .75rem 1.125rem;
+        text-align: left;
+        width: 40%;
+        font-size: .8rem;
+      }
+      tr:nth-child(even) td {
+        background-color: #e5e5e5;
+      }
+    }
+  }
+  h2 {
+    background-color: #ffffff;
+    margin-bottom: 0.125rem;
+    font-size: 0.9rem;
+    padding: 1.125rem;
+    font-weight: bold;
+    text-transform: uppercase;
+  }
+}
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/adminActions.js b/skyquake/plugins/admin/src/adminActions.js
new file mode 100644 (file)
index 0000000..2d97dfe
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 
+ *   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.
+ *
+ */
+class Actions {
+
+       constructor() {
+               this.generateActions(
+            'openModel');
+       }
+}
+export default Actions
diff --git a/skyquake/plugins/admin/src/adminSource.js b/skyquake/plugins/admin/src/adminSource.js
new file mode 100644 (file)
index 0000000..5bf439e
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 
+ *   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 adminActions from './adminActions.js';
+import Utils from 'utils/utils.js';
+import $ from 'jquery';
+var adminSource = {
+    get: function () {
+        return {
+            remote: function (state) {
+                return new Promise(function (resolve, reject) {
+                    $.ajax({
+                        url: 'api/?api_server=' + API_SERVER,
+                        type: 'GET',
+                        contentType: "application/json",
+                        success: function (data) {
+                            resolve(data);
+                        },
+                        error: function (error) {
+                            console.log("There was an error getting the crash details: ", error);
+                            reject(error);
+                        }
+                    }).fail(function (xhr) {
+                        console.log(xhr)
+                    });
+
+                }).catch(function (e) {
+                        console.log(e)
+                });
+            },
+            success: adminActions.getAdminSuccess,
+            loading: adminActions.getAdminLoading,
+            error: adminActions.getAdminFail
+        }
+    },
+}
+module.exports = adminSource;
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/ActionBar.jsx b/skyquake/plugins/admin/src/components/ActionBar.jsx
new file mode 100644 (file)
index 0000000..87e62d1
--- /dev/null
@@ -0,0 +1,30 @@
+import React from 'react'
+import { TrashIcon } from 'react-open-iconic-svg';
+import { PencilIcon } from 'react-open-iconic-svg';
+import { PlusIcon } from 'react-open-iconic-svg';
+import { CircleXIcon } from 'react-open-iconic-svg';
+
+const actionIcon = {
+    'create': {icon: PlusIcon, title: "Add"},
+    'update': {icon: PencilIcon, title: "Edit"},
+    'delete': {icon: TrashIcon, title: "Delete"},
+    'close': {icon: CircleXIcon, title: "Close Panel"}
+}
+
+export default (props) => {
+    const { isHidden, actions, handler } = props;
+    const buttons = [];
+    actions && actions.forEach((action => buttons.push(
+        <div key={action} style={{ visibility: isHidden ? 'hidden' : 'visible', display: 'inline-block', padding: '.15rem .5rem' }}>
+            <div title={actionIcon[action].title} style={{ display: 'inline-block', cursor: 'pointer' }}>
+                {React.createElement(actionIcon[action].icon, {width: '12', height: '12', onClick: () => handler(action)})}
+            </div>
+        </div>
+    )));
+    return (
+        <div className='button-bar' style={{ height: '17px', width: '100%', textAlign: 'right' }} >
+            {buttons}
+        </div>
+    )
+}
+
diff --git a/skyquake/plugins/admin/src/components/ChoiceCard.jsx b/skyquake/plugins/admin/src/components/ChoiceCard.jsx
new file mode 100644 (file)
index 0000000..9623429
--- /dev/null
@@ -0,0 +1,32 @@
+import React from 'react'
+import ColumnCard from './ColumnCard'
+import { ExcerptIcon } from 'react-open-iconic-svg';
+
+export default class extends React.Component {
+    render() {
+        try {
+            const { model, path, isSelected, openElement } = this.props;
+            const element = model.getElement(path)
+            const name = element.name;
+            let choiceName = "none";
+            if (element.value) {
+                const selectedCase = element.schema.properties.find(c =>
+                    c.properties && c.properties.some(p => element.value[p.name]));
+                if (selectedCase) {
+                    choiceName = selectedCase.name;
+                }
+            }
+            console.debug(`ChoiceCard: ${name}`);
+            return (
+                <ColumnCard path={path} isSelected={isSelected} className='chioce-card'>
+                    <ExcerptIcon />
+                    <div className='list-card' style={{ cursor: 'pointer' }} onClick={() => openElement({ path })}>
+                        <span style={{ paddingRight: '8px' }} >{`${name}`}</span><span style={{ fontSize: 'small', verticalAlign: 'top' }}>{`(${choiceName})`}</span>
+                    </div>
+                </ColumnCard>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ChoiceColumn.jsx b/skyquake/plugins/admin/src/components/ChoiceColumn.jsx
new file mode 100644 (file)
index 0000000..41f0960
--- /dev/null
@@ -0,0 +1,22 @@
+import React from 'react'
+import ContainerColumn from './ContainerColumn'
+
+export default class extends ContainerColumn {
+    constructor(props) {
+        super(props);
+        const element = props.model.getElement(props.path);
+        const selectedCase = element.value ?
+            element.schema.properties.find(c => c.properties && c.properties.some(p => element.value[p.name]))
+            : null;
+        if (selectedCase) {
+            this.state.actions = ['delete'];
+            this.state.properties = selectedCase.properties;
+        } else {
+            this.state.actions = ['create'];
+        }
+    }
+    render() {
+        return super.render();
+    }
+}
+
diff --git a/skyquake/plugins/admin/src/components/ColumnCard.jsx b/skyquake/plugins/admin/src/components/ColumnCard.jsx
new file mode 100644 (file)
index 0000000..55fc9d2
--- /dev/null
@@ -0,0 +1,23 @@
+import React from 'react'
+
+export default class extends React.Component {
+    render() {
+        let { isSelected, path, children } = this.props;
+        const bgColor = ((isSelected && '#00acee !important') || 'inherit');
+        const className = ['card'];
+        isSelected && className.push('--selected');
+        const id = path.join('-') + className.join('-');
+        if (isSelected) {
+            children = (
+                <div style={{ backgroundColor: '#00acee !important', width: '100%' }} >
+                    {children}
+                </div>
+            )
+        }
+        return (
+            <div className={className.join(' ')} style={{ margin: '1px', padding: '3px' }} >
+                {children}
+            </div>
+        )
+    }
+}
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/ContainerCard.jsx b/skyquake/plugins/admin/src/components/ContainerCard.jsx
new file mode 100644 (file)
index 0000000..497bd9b
--- /dev/null
@@ -0,0 +1,23 @@
+import React from 'react'
+import ColumnCard from './ColumnCard'
+import { FolderIcon } from 'react-open-iconic-svg';
+
+export default class extends React.Component {
+    render() {
+        try {
+            const { model, path, isSelected, openElement } = this.props;
+            const element = model.getElement(path)
+            const name = element.name;
+            console.debug(`ContainerCard: ${name}`);
+            return (
+                <ColumnCard path={path} isSelected={isSelected} className='container-card'>
+                    <FolderIcon/>
+                    <div style={{cursor: 'pointer'}} onClick={() => openElement({ path })}>{name}</div>
+                </ColumnCard>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
+
diff --git a/skyquake/plugins/admin/src/components/ContainerColumn.jsx b/skyquake/plugins/admin/src/components/ContainerColumn.jsx
new file mode 100644 (file)
index 0000000..99abb78
--- /dev/null
@@ -0,0 +1,63 @@
+import React from 'react'
+import LeafGroup from './LeafGroup'
+import ListStack from './ListStack'
+import ExplorerColumn from './ExplorerColumn'
+import yang from '../yang/leaf-utils'
+
+export default class extends React.Component {
+    constructor(props){
+        super(props);
+        this.state = {actions: [], properties: null};
+    }
+
+    render() {
+        try {
+            const { model, path, isLast, selected, isReadonly, openElement, editElement, columnCloser } = this.props;
+            const element = model.getElement(path)
+            const name = element.name;
+            console.debug(`ContainerColumn: ${name}`);
+            const data = element.value;
+            const leaves = [];
+            const choices = [];
+            const containers = [];
+            const lists = [];
+            const loading = [];
+            const dataDivided = {
+                leaf: leaves,
+                leaf_list: leaves,
+                container: containers,
+                list: lists,
+                choice: choices,
+                loading: loading
+            };
+            const properties = this.state.properties || element.schema.properties;
+            properties.forEach((property, index) => {
+                const list = dataDivided[property.type]
+                list && list.push(property)
+                !list && console.debug(`ContainerColumn - unhandled property : ${path}/${property.name} - ${property.type}`)
+            });
+            choices.forEach(choice => choice.properties.forEach(c => c.properties.forEach(p => leaves.push(p))));
+            const items = [];
+            leaves.length && items.push(<LeafGroup key='leaves' model={model} path={path} isReadonly={!isLast} properties={leaves} editElement={editElement} />);
+            containers.length && items.push(<ListStack key='containers' model={model} path={path} properties={containers} selected={selected} openElement={openElement} />);
+            lists.length && items.push(<ListStack key='list' model={model} path={path} properties={lists} selected={selected} openElement={openElement} />);
+            loading.length && items.push(<ListStack key='loading' model={model} path={path} properties={loading} openElement={openElement} />);
+            const actions = this.state.actions.slice();
+            !isLast || isReadonly || !leaves.length || leaves.every(p => yang.isKey(p)) || actions.push('update');
+            function invokeAction(action) {
+                editElement(path, action);
+            }
+            return (
+                <ExplorerColumn title={name || path} isLast={isLast} actions={actions} handler={invokeAction} columnCloser={columnCloser} >
+                    <div className='container-column' >
+                        <div>
+                            {items}
+                        </div>
+                    </div>
+                </ExplorerColumn>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/EditorDialog.jsx b/skyquake/plugins/admin/src/components/EditorDialog.jsx
new file mode 100644 (file)
index 0000000..3d9309d
--- /dev/null
@@ -0,0 +1,249 @@
+import React from 'react'
+import Modal from 'react-modal'
+import Dialog from 'react-dialog'
+import LeafField from './editor/LeafField'
+import yang from '../yang/leaf-utils.js'
+import changeCase from 'change-case'
+
+const Button = ({ name, onClick, disabled }) => (
+    <button disabled={disabled}
+        style={{ padding: '.25rem .5rem', margin: '0 0.25rem', boxShadow: '1px 1px rgba(0, 0, 0, 0.15)' }}
+        onClick={onClick}>{name}</button>
+);
+
+const ButtonBar = ({ submitName, submitDisabled, onSubmit, onCancel }) => (
+    <div className='button-bar' style={{ width: '100%', textAlign: 'right', marginTop: '18px' }} >
+        <Button name="Cancel" onClick={onCancel} />
+        <Button name={submitName} onClick={onSubmit} disabled={submitDisabled} />
+    </div>
+);
+
+function initErrorInfo(leaves, container) {
+    return leaves.reduce((errorInfo, p) => {
+        const value = container && container[p.name];
+        const readOnly = p.isKey && !operation.isCreate;
+        if (!readOnly && p.mandatory && !value) {
+            errorInfo[p.name] = 'required';
+        }
+        return errorInfo;
+    }, {})
+}
+
+function buildChoiceProperty(choiceProperty) {
+    let description = choiceProperty.description + ' ';
+    const choices = choiceProperty.properties.reduce((o, p) => {
+        o[p.name] = { value: p.name };
+        description = `${description} ${p.name} - ${p.description}`;
+        return o;
+    }, {});
+    const choicePicker = Object.assign({}, choiceProperty);
+    choicePicker['type'] = 'leaf';
+    choicePicker['description'] = description;
+    choicePicker['data-type'] = {
+        enumeration: {
+            enum: choices
+        }
+    }
+    return choicePicker;
+}
+
+function getIntialState(props) {
+    const { isOpen, model, path, operation } = props;
+    const baseLeaves = [];
+    const choices = [];
+    const dataSet = {};
+    const errorInfo = {};
+    let shadowErrorInfo = {};
+    let leaves = baseLeaves;
+    if (isOpen && path && !operation.isDelete) {
+        const element = model.getElement(path);
+        const dataDivided = {
+            leaf: baseLeaves,
+            leaf_list: baseLeaves,
+            choice: choices,
+        };
+        element.schema.properties.forEach((property, index) => {
+            const list = dataDivided[property.type]
+            list && list.push(property)
+        });
+        if (!operation.isCreate) {
+            // we are not going to prompt for choices so add in appropriate files now
+            choices.forEach((choice) => {
+                const caseSelected = choice.properties.find(c => c.properties && c.properties.some(p => element.value[p.name]));
+                if (caseSelected) {
+                    caseSelected.properties.forEach(p => yang.isLeafOrLeafList(p) && baseLeaves.push(p));
+                }
+            });
+        } else {
+            // on create we first propmt for "features"
+            leaves = choices.map(choice => buildChoiceProperty(choice));
+        }
+        shadowErrorInfo = initErrorInfo(leaves, element.value);
+    }
+    return { dataSet, shadowErrorInfo, errorInfo, baseLeaves, leaves, choices };
+}
+
+export default class extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = getIntialState(props);
+        this.state.showHelp = true;
+    }
+
+    handleCloseEditor = () => {
+    };
+
+    componentWillReceiveProps(nextProps) {
+        if (this.props.isOpen !== nextProps.isOpen) {
+            this.setState(getIntialState(nextProps));
+        }
+    }
+
+    render() {
+        try {
+            const { isOpen, model, isReadonly, properties, operation, onSave, onCancel } = this.props;
+            if (!isOpen) {
+                return null;
+            }
+            let dataPath = this.props.path.slice();
+            const element = model.getElement(dataPath);
+            const container = element.value;
+            let editors = null;
+            let submitHandler = () => {
+                if (Object.keys(this.state.errorInfo).length === 0) {
+                    onSave(this.state.dataSet);
+                }
+            }
+            const checkForSubmitKey = (e) => {
+                if (e.keyCode == 13) {
+                    e.preventDefault();
+                    e.stopPropagation();
+                    submitHandler();
+                }
+            };
+            let submitButtonLabel = operation.isCreate ? "Add" : "Save";
+            let submitDisabled = true;
+            let headerText = null;
+            if (operation.isDelete) {
+                const id = dataPath[dataPath.length - 1];
+                const deletePrompt = `Delete ${id}?`;
+                submitButtonLabel = "Delete";
+                submitDisabled = false;
+                editors = (<div style={{ paddingBottom: '10px' }}>{deletePrompt}</div>);
+            } else {
+                let { leaves, choices } = this.state;
+                if (choices.length) {
+                    if (operation.isCreate) {
+                        headerText = `Select feature(s) for ${element.name}`
+                        submitButtonLabel = "Continue";
+                        submitHandler = () => {
+                            const { dataSet, baseLeaves } = this.state;
+                            const leaves = choices.reduce((leafList, choice) => {
+                                const caseSelected = dataSet[choice.name].value;
+                                delete dataSet[choice.name];
+                                if (caseSelected) {
+                                    return leafList.concat(choice.properties.find((p) => p.name === caseSelected).properties.reduce((list, p) => {
+                                        yang.isLeafOrLeafList(p) && list.push(p);
+                                        return list;
+                                    }, []));
+                                }
+                                return leafList;
+                            }, baseLeaves.slice());
+                            const shadowErrorInfo = initErrorInfo(leaves, element.value);
+                            this.setState({ dataSet, leaves, choices: [], errorInfo: {}, shadowErrorInfo });
+                        }
+                    }
+                }
+                submitDisabled = (Object.keys(this.state.shadowErrorInfo).length
+                    || (!operation.isCreate && Object.keys(this.state.dataSet).length === 0));
+                // process the named field value change
+                function processFieldValueChange(property, currentValue, value) {
+                    console.debug(`processed change for -- ${name} -- with value -- ${value}`);
+                    const dataSet = this.state.dataSet;
+                    const name = property.name;
+                    if ((currentValue && currentValue !== value) || (!currentValue && value)) {
+                        dataSet[name] = { property, value, currentValue };
+                    } else {
+                        delete dataSet[name];
+                    }
+                    const { errorInfo, shadowErrorInfo } = this.state;
+                    delete errorInfo[name];
+                    delete shadowErrorInfo[name];
+                    this.setState({ dataSet, errorInfo, shadowErrorInfo });
+                }
+
+                function onErrorHandler(name, message) {
+                    const { errorInfo, shadowErrorInfo } = this.state;
+                    errorInfo[name] = message;
+                    shadowErrorInfo[name] = message;
+                    this.setState({ errorInfo, shadowErrorInfo });
+                }
+
+                editors = leaves.reduce((editors, property, index) => {
+                    const itemPath = dataPath.slice();
+                    itemPath.push(property.name);
+                    const props = { model, 'path': itemPath };
+                    const value = container && container[property.name];
+                    let readOnly = isReadonly;
+                    let extraHelp = null;
+                    if (!isReadonly) {
+                        if (yang.isKey(property) && !operation.isCreate) {
+                            extraHelp = "Id fields are not modifiable.";
+                            readOnly = true;
+                        } else if (yang.isLeafList(property)) {
+                            extraHelp = "Enter a comma separated list of values."
+                        }
+                    }
+                    editors.push(
+                        <LeafField
+                            key={property.name}
+                            container={container}
+                            property={property}
+                            path={dataPath}
+                            value={value}
+                            showHelp={this.state.showHelp}
+                            onChange={processFieldValueChange.bind(this, property, value)}
+                            onError={onErrorHandler.bind(this, property.name)}
+                            readOnly={readOnly}
+                            extraHelp={extraHelp}
+                            errorMessage={this.state.errorInfo[property.name]}
+                        />
+                    );
+                    return editors;
+                }, [])
+            }
+            const customStyles = {
+                content: {
+                    top: '50%',
+                    left: '50%',
+                    right: 'auto',
+                    bottom: 'auto',
+                    marginRight: '-50%',
+                    transform: 'translate(-50%, -50%)'
+                }
+            };
+            const dlgHeader = headerText ? (<div style={{ paddingBottom: '16px' }} >{headerText}</div>) : null;
+            return (
+                <Modal
+                    isOpen={isOpen}
+                    contentLabel="Edit"
+                    onRequestClose={onCancel}
+                    shouldCloseOnOverlayClick={false}
+                    style={customStyles}
+                >
+                    <div className='leaf-group' style={{ maxWidth: '400px', maxHeight: '600px' }} onKeyUp={checkForSubmitKey} >
+                        {dlgHeader}
+                        {editors}
+                        <ButtonBar
+                            submitName={submitButtonLabel}
+                            submitDisabled={submitDisabled}
+                            onSubmit={submitHandler}
+                            onCancel={onCancel} />
+                    </div>
+                </Modal >
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/ExplorerColumn.jsx b/skyquake/plugins/admin/src/components/ExplorerColumn.jsx
new file mode 100644 (file)
index 0000000..20232a6
--- /dev/null
@@ -0,0 +1,23 @@
+import React from 'react'
+import ActionBar from './ActionBar'
+
+export default ({ title, children, columnCloser, isLast, actions, handler }) => {
+    let actionBar = null;
+    if (isLast) {
+        if (columnCloser) {
+            actions = actions ? actions.slice() : [];
+            actions.push('close');
+        }
+        actionBar = (<ActionBar actions={actions} handler={(operation) => operation === 'close' ? columnCloser() : handler(operation)} />);
+    } else {
+        actionBar = (<ActionBar />);
+    }
+    return (
+        <div className='column' style={{ backgroundColor: 'gainsboro', height: '100%', display: 'flex', flexDirection: 'column', marginLeft: '4px', marginRight: '4px' }} >
+            <h2 style={{ marginBottom: '4px' }} >{title}</h2>
+            {actionBar}
+            {children}
+        </div>
+    )
+}
+
diff --git a/skyquake/plugins/admin/src/components/LeafGroup.jsx b/skyquake/plugins/admin/src/components/LeafGroup.jsx
new file mode 100644 (file)
index 0000000..13a54cf
--- /dev/null
@@ -0,0 +1,45 @@
+import React from 'react'
+import ColumnCard from './ColumnCard'
+import yang from '../yang/leaf-utils.js'
+
+export default class extends React.Component {
+
+    render() {
+        try {
+            const { model, path, isReadonly, properties, editElement } = this.props;
+            const element = model.getElement(path);
+            console.debug(`LeafGroup: ${element.name}`);
+            const container = element.value;
+            const leaves = properties.reduce((leaves, property, index) => {
+                const itemPath = path.slice();
+                itemPath.push(property.name);
+                const props = { model, 'path': itemPath };
+                let value = (container && container[property.name]);
+                let valueIsSet = yang.isValueSet(property, value);
+                if (!valueIsSet) {
+                    value = yang.getDefaultValue(property);
+                    valueIsSet = yang.isValueSet(property, value);
+                }
+                if (valueIsSet) {
+                    value = yang.getDisplayValue(property, value);
+                }
+                valueIsSet && leaves.push(
+                    <div key={property.name} className='leaf-group-leaf' style={{ maxWidth: '400px' }} >
+                        <div style={{ fontSize: 'small', color: 'gray' }} >{`${property.name}`}</div>
+                        <div style={{paddingLeft: '4px'}} >{value}</div>
+                    </div>
+                );
+                return leaves;
+            }, [])
+            return (
+                <ColumnCard path={path}>
+                    <div className='leaf-group'>
+                        {leaves}
+                    </div>
+                </ColumnCard>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ListCard.jsx b/skyquake/plugins/admin/src/components/ListCard.jsx
new file mode 100644 (file)
index 0000000..29b1715
--- /dev/null
@@ -0,0 +1,25 @@
+import React from 'react'
+import ColumnCard from './ColumnCard'
+import { ListIcon } from 'react-open-iconic-svg';
+
+export default class extends React.Component {
+    render() {
+        try {
+            const { model, path, isSelected, openElement } = this.props;
+            const element = model.getElement(path);
+            const name = element.name;
+            const numItems = element.value ? Array.isArray(element.value) ? element.value.length : 1 : 0;
+            console.debug(`ListCard: ${name}`);
+            return (
+                <ColumnCard path={path} isSelected={isSelected} >
+                    <ListIcon />
+                    <div className='list-card' style={{cursor: 'pointer'}} onClick={() => openElement({ path })}>
+                        <span style={{ paddingRight: '8px' }} >{`${name}`}</span><span style={{ fontSize: 'small', verticalAlign: 'top' }}>{`(${numItems})`}</span>
+                    </div>
+                </ColumnCard>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ListColumn.jsx b/skyquake/plugins/admin/src/components/ListColumn.jsx
new file mode 100644 (file)
index 0000000..d2b1d8e
--- /dev/null
@@ -0,0 +1,45 @@
+import React from 'react'
+import ListStack from './ListStack'
+import ListEntryCard from './ListEntryCard'
+import ExplorerColumn from './ExplorerColumn'
+import changeCase from 'change-case'
+
+export default class extends React.Component {
+
+    render() {
+        try {
+            const { model, path, isLast, selected, openElement, editElement, columnCloser } = this.props;
+            const element = model.getElement(path);
+            const name = element.name;
+            const shortName = changeCase.titleCase(name).split(' ').pop();
+            const buttonName = `Add ${shortName}`;
+            console.debug(`ListColumn: ${name}`);
+            const list = element.value ? Array.isArray(element.value) ? element.value : [element.value] : null;
+            const cards = list && list.map((item, index) => {
+                const itemInfo = element.getItemInfo(item);
+                const isSelected = selected ? selected.every((p,i) => p === itemInfo.path[i]) : false;
+                const itemPath = path.slice();
+                itemPath.push(itemInfo.path);
+                const props = { model, 'path': itemPath, 'openElement': openElement.bind(this, itemPath) };
+                return (
+                    <ListEntryCard key={itemInfo.path}
+                        model={model}
+                        path={itemPath}
+                        name={itemInfo.name}
+                        isSelected={isSelected}
+                        openElement={openElement.bind(this, itemPath)}
+                        deleteElement={(path) => editElement(path, 'delete')} />
+                )
+            })
+            return (
+                <ExplorerColumn title={name || path} isLast={isLast} actions={['create']} handler={() => editElement(path, 'create')} columnCloser={columnCloser} >
+                    <div className='list-column' >
+                        {cards}
+                    </div>
+                </ExplorerColumn>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ListEntryCard.jsx b/skyquake/plugins/admin/src/components/ListEntryCard.jsx
new file mode 100644 (file)
index 0000000..783eaa9
--- /dev/null
@@ -0,0 +1,23 @@
+import React from 'react'
+import ColumnCard from './ColumnCard'
+import { DocumentIcon } from 'react-open-iconic-svg';
+
+export default class extends React.Component {
+
+    render() {
+        try {
+            const { model, path, name, isSelected, openElement } = this.props;
+            console.debug(`ListEntryCard: ${name}`);
+            return (
+                <ColumnCard path={path} isSelected={isSelected} >
+                    <DocumentIcon />
+                    <div className='list-entry-card' style={{cursor: 'pointer'}} onClick={() => openElement({ path })}>
+                        <div>{`${name}`}</div>
+                    </div>
+                </ColumnCard>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ListEntryColumn.jsx b/skyquake/plugins/admin/src/components/ListEntryColumn.jsx
new file mode 100644 (file)
index 0000000..12d2afa
--- /dev/null
@@ -0,0 +1,9 @@
+import React from 'react'
+import ContainerColumn from './ContainerColumn'
+
+export default class extends ContainerColumn {
+    constructor(props){
+        super(props);
+        this.state.actions = ['delete'];
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ListStack.jsx b/skyquake/plugins/admin/src/components/ListStack.jsx
new file mode 100644 (file)
index 0000000..38179f1
--- /dev/null
@@ -0,0 +1,41 @@
+import React from 'react'
+import ListCard from './ListCard'
+import ContainerCard from './ContainerCard'
+import ChoiceCard from './ChoiceCard'
+import LoadingCard from './LoadingCard'
+
+const cardComponent = {
+    list: ListCard,
+    container: ContainerCard,
+    choice: ChoiceCard,
+    loading: LoadingCard
+}
+export default class extends React.Component {
+
+    render() {
+        try {
+            const { model, path, properties, selected, openElement } = this.props;
+            const element = model.getElement(path);
+            const container = element.value;
+            console.debug(`PropertiesStack: ${properties.length}`);            
+            const cards = properties.map((property, index) => {
+                const isSelected = selected ? selected === property.name : false;
+                const itemPath = path.slice();
+                itemPath.push(property.name);
+                const props = { model, 'path': itemPath, isSelected, 'openElement': openElement.bind(this, itemPath) };
+                return (
+                    <div key={property.name}>
+                        {React.createElement(cardComponent[property.type], props)}
+                    </div>
+                )
+            })
+            return (
+                <div>
+                    {cards}
+                </div>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/LoadingCard.jsx b/skyquake/plugins/admin/src/components/LoadingCard.jsx
new file mode 100644 (file)
index 0000000..c3ce4cd
--- /dev/null
@@ -0,0 +1,21 @@
+import React from 'react'
+import ColumnCard from './ColumnCard'
+
+class LoadingCard extends React.Component {
+    render() {
+        try {
+            const { model, path } = this.props;
+            const name = model.getElement(path).name;
+            console.debug(`LoadingCard: ${name}`);
+            return (
+                <ColumnCard path={path}>
+                    <div>{`Loading ${name}`}</div>
+                </ColumnCard>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
+
+export default LoadingCard
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/LoadingColumn.jsx b/skyquake/plugins/admin/src/components/LoadingColumn.jsx
new file mode 100644 (file)
index 0000000..a97e9b4
--- /dev/null
@@ -0,0 +1,21 @@
+import React from 'react'
+import ExplorerColumn from './ExplorerColumn'
+
+export default class extends React.Component {
+
+    render() {
+        try {
+            const { model, path, isLast, openElement } = this.props;
+            console.debug(`LoadingColumn: ${path.join()}`);
+            return (
+                <ExplorerColumn title={name || path} isLast={isLast} >
+                    <div className='loading-column'>
+                        <h3>Loading</h3>
+                    </div>
+                </ExplorerColumn>
+            )
+        } catch (e) {
+            console.error("component render", e);
+        }
+    }
+}
diff --git a/skyquake/plugins/admin/src/components/ModelExplorer.jsx b/skyquake/plugins/admin/src/components/ModelExplorer.jsx
new file mode 100644 (file)
index 0000000..b8e800f
--- /dev/null
@@ -0,0 +1,181 @@
+import React from 'react'
+import ModalDialog from 'react-modal'
+import _set from 'lodash/set';
+import _get from 'lodash/get';
+import ContainerColumn from './ContainerColumn'
+import ChoiceColumn from './ChoiceColumn'
+import ListColumn from './ListColumn'
+import LoadingColumn from './LoadingColumn'
+import ListEntryColumn from './ListEntryColumn'
+import LoadingCard from './LoadingCard'
+import ListCard from './ListCard'
+import ContainerCard from './ContainerCard'
+import EditorDialog from './EditorDialog'
+
+function findItemInList(list, key, keyValue) {
+    const keyPath = Array.isArray(keyValue) ? keyValue : key.length > 1 ? JSON.parse(keyValue) : [keyValue];
+    if (key.length > 1) {
+        return list.find(item => {
+            return key.every((k, i) => item[k] === keyPath[i]);
+        });
+    } else {
+        const leaf = key[0];
+        const match = keyPath[0];
+        return list.find(item => {
+            return item[leaf] === match;
+        });
+    }
+}
+function getItemInfoFunction(schema) {
+    return function (item) {
+        return ModelExplorer.getItemInfo(schema.key, item);
+    }
+}
+
+function makeNameFromKeyPath(key, path, name) {
+    return path.length > 1 ? `${path[0]} (${path.slice(1).join(" ,")})` : name || path[0];
+}
+
+class ExplorerModel {
+    constructor(dataModel) {
+        this.dataModel = dataModel;
+        this.topNode = dataModel.path.split('/').pop();
+    }
+    getElement(path) {
+        const dataModel = this.dataModel;
+        if (dataModel.isLoading) {
+            return null;
+        }
+        if (dataModel.updatingPath && dataModel.updatingPath.every((p, i) => path[i] === p)) {
+            return { type: 'loading' }
+        }
+        return path.reduce((parent, node, index) => {
+            const element = Object.assign({}, parent);
+            element.path.push(node);
+            if (parent.type === 'list') {
+                element.type = 'list-entry'
+                element.value = findItemInList(parent.value, parent.schema.key, node);
+                element.keyValue = parent.schema.key.map(leaf => element.value[leaf]);
+                element.name = makeNameFromKeyPath(parent.schema.key, element.keyValue, element.value['name']);
+                element.getItemInfo = getItemInfoFunction(parent.schema);
+            } else {
+                element.schema = parent.schema.properties.find(property => property.name === node)
+                element.type = element.schema.type;
+                element.value = element.type === 'choice' ? parent.value : parent.value && parent.value[node];
+                element.name = node.split(':').pop();
+                if (element.type === 'list') {
+                    element.getItemInfo = getItemInfoFunction(element.schema);
+                }
+            }
+            return element;
+        }, {
+                schema: dataModel.schema[this.topNode],
+                getItemInfo: getItemInfoFunction(this.schema),
+                value: dataModel.data,
+                type: dataModel.schema[this.topNode].type,
+                path: dataModel.path.split('/')
+            }
+        )
+    }
+}
+
+const columnComponent = {
+    'list': ListColumn,
+    'container': ContainerColumn,
+    'list-entry': ListEntryColumn,
+    'choice': ChoiceColumn,
+    'loading': LoadingColumn
+}
+
+class ModelExplorer extends React.Component {
+    static getExplorerModel(dataModel) {
+        return new ExplorerModel(dataModel);
+    }
+    static getItemInfo(keyDef, item) {
+        const path = keyDef.map(leaf => item[leaf]);
+        const name = makeNameFromKeyPath(keyDef, path, item.name);
+        return {
+            path,
+            name
+        };
+    }
+
+    constructor(props) {
+        super(props);
+        const columns = props.columns || [['/']];
+        this.state = { columns };
+    }
+
+    componentWillReceiveProps(nextProps) {
+        if (!this.state.columns && nextProps.model) {
+            this.setState({ columns: [[Array.isArray(nextProps.model) ? '' : '/']] })
+        }
+    }
+
+    render() {
+        const { model, onUpdate } = this.props;
+        let { columns, isEditMode, editPath, editOperation } = this.state;
+
+        const openElement = (col, path) => {
+            columns = columns.slice(0, col + 1);
+            columns.push(path);
+            this.setState({ columns })
+        }
+
+        const closeLastColumn = () => {
+            columns = columns.slice();
+            columns.pop();
+            this.setState({ columns })
+        }
+
+        const lastCol = columns.length - 1;
+        const modelColumns = columns && columns.map((path, col) => {
+            const open = openElement.bind(this, col);
+            const props = {
+                key: path.join('/'),
+                model,
+                path,
+                selected: col < lastCol ? columns[col + 1][path.length] : null,
+                isLast: col === lastCol,
+                openElement: openElement.bind(this, col),
+                columnCloser: col === lastCol && col && closeLastColumn,
+                editElement: (path, op) => this.setState({
+                    isEditMode: true,
+                    editOperation: op || 'update',
+                    editPath: path
+                })
+            }
+            return React.createElement(columnComponent[model.getElement(path).type], props)
+        })
+
+        const updateModel = (data) => {
+            let { columns, isEditMode, editPath, editOperation } = this.state;
+            onUpdate(editPath, editOperation, data) && columns.pop();
+            this.setState({
+                columns,
+                isEditMode: false,
+                editOperation: null,
+                editPath: null
+            });
+        }
+
+        return (
+            <div className='model-explorer'>
+                <div style={{ width: '100%', display: 'flex', flexDirection: 'row', overflow: 'scroll' }}>
+                    {modelColumns}
+                </div>
+                <EditorDialog
+                    isOpen={isEditMode}
+                    operation={{
+                        isCreate: editOperation === 'create',
+                        isDelete: editOperation === 'delete'}}
+                    model={model}
+                    path={editPath}
+                    onCancel={() => this.setState({ isEditMode: false })}
+                    onSave={updateModel} />
+            </div>
+        )
+    }
+}
+
+export default ModelExplorer 
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/editor/LeafEditor.jsx b/skyquake/plugins/admin/src/components/editor/LeafEditor.jsx
new file mode 100644 (file)
index 0000000..46e9db0
--- /dev/null
@@ -0,0 +1,300 @@
+/*
+ *
+ *   Copyright 2016-2017 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 _debounce from 'lodash/debounce';
+import React from 'react'
+import ClassNames from 'classnames'
+import changeCase from 'change-case'
+import _isInt from 'validator/lib/isInt'
+import _toInt from 'validator/lib/toInt'
+import _isFloat from 'validator/lib/isFloat'
+import _toFloat from 'validator/lib/toFloat'
+import _trim from 'validator/lib/trim'
+import _isIP from 'validator/lib/isIP'
+import yang from '../../yang/leaf-utils.js'
+import resolveLeafRefPaths from './resolveLeafRef'
+import Select from './Select'
+
+function validateRequired(isRequired, value) {
+       value = value.trim();
+       return isRequired && !value ? { success: false, message: "A value is required." } : { success: true, value: null };
+}
+
+function editorExitHandler(isValueRequired, onExit, onError, event) {
+       const value = event.target.value;
+       const result = validateRequired(isValueRequired, value);
+       onExit && onExit(result);
+}
+
+function Enumeration(props) {
+       const { id, property, title, readOnly, onChange, onError, onExit } = props;
+       let value = props.value;
+       const enumDef = property['data-type'].enumeration.enum;
+       const enumeration = typeof enumDef === 'string' ?
+               [{ name: enumDef, value: enumDef, isSelected: String(value) === enumDef }]
+               : Object.keys(enumDef).map(enumName => {
+                       let enumValue = enumName;
+                       return { name: enumName, value: enumValue, isSelected: String(enumValue) === String(value) };
+               });
+       const hasDefaultValue = !!yang.getDefaultValue(property);
+       const required = yang.isRequired(property) || hasDefaultValue;
+       if (!value && hasDefaultValue) {
+               value = yang.getDefaultValue(property);
+       }
+       return (
+               <Select
+                       id={id}
+                       value={value}
+                       options={enumeration}
+                       title={title}
+                       placeholder={property.name}
+                       onChange={onChange}
+                       onExit={editorExitHandler.bind(null, required, onExit, onError)}
+                       required={required} r
+                       readOnly={readOnly} />
+       );
+}
+
+function Reference(props) {
+       const { id, property, title, value, path, model, readOnly, onChange, onError, onExit } = props;
+
+       function getLeafRef(property = {}, path, value, model) {
+               const leafRefPath = property['data-type']['leafref']['path'];
+
+               let leafRefPathValues = []; //resolveLeafRefPath(model, path, leafRefPath);
+
+               let leafRefObjects = [];
+
+               leafRefPathValues && leafRefPathValues.map((leafRefPathValue) => {
+                       leafRefObjects.push({
+                               name: leafRefPathValue,
+                               value: leafRefPathValue,
+                               isSelected: String(leafRefPathValue) === String(value)
+                       });
+               });
+
+               return leafRefObjects;
+       }
+       const leafRefPathValues = getLeafRef(property, path, value, model);
+       const required = yang.isRequired(property);
+
+       return (
+               <Select
+                       id={id}
+                       value={value}
+                       options={leafRefPathValues}
+                       title={title}
+                       placeholder={property.name}
+                       onChange={onChange}
+                       onExit={editorExitHandler.bind(null, required, onExit, onError)}
+                       required={required}
+                       readOnly={readOnly} />
+       );
+}
+
+function Boolean(props) {
+       const { id, property, title, readOnly, onChange, onError, onExit } = props;
+       let value = props.value;
+       const typeOfValue = typeof value;
+       if (typeOfValue === 'number' || typeOfValue === 'boolean') {
+               value = value ? 'TRUE' : 'FALSE';
+       } else if (value) {
+               value = value.toUpperCase();
+       }
+       const options = [
+               { name: "TRUE", value: 'TRUE' },
+               { name: "FALSE", value: 'FALSE' }
+       ]
+       const hasDefaultValue = !!yang.getDefaultValue(property);
+       const required = yang.isRequired(property) || hasDefaultValue;
+       if (!value && hasDefaultValue) {
+               value = yang.getDefaultValue(property);
+       }
+       return (
+               <Select
+                       id={id}
+                       value={value}
+                       options={options}
+                       title={title}
+                       placeholder={property.name}
+                       onChange={onChange}
+                       onExit={editorExitHandler.bind(null, required, onExit, onError)}
+                       required={required}
+                       readOnly={readOnly} />
+       );
+}
+
+function Empty(props) {
+       // 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
+       const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
+       const { id, property, value, title, readOnly, onChange } = props;
+       let isEmptyLeafPresent = !!value;
+       let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
+       const options = [
+               { name: "Enabled", value: EMPTY_LEAF_PRESENT }
+       ]
+
+       return (
+               <Select
+                       id={id}
+                       value={present}
+                       placeholder={"Not Enabled"}
+                       options={options}
+                       title={title}
+                       onChange={onChange}
+                       readOnly={readOnly} />
+       );
+}
+
+function getValidator(property) {
+       function validateInteger(constraints, value) {
+               return _isInt(value, constraints) ? { success: true, value: _toInt(value) } :
+                       { success: false, value, message: "The value is not an integer or does not meet the property constraints." };
+       }
+       function validateDecimal(constraints, value) {
+               return _isFloat(value, constraints) ? { success: true, value: _toFloat(value) } :
+                       { success: false, value, message: "The value is not a decimal number or does not meet the property constraints." };
+       }
+       function validateProperty(validator, errorMessage, value) {
+               return validator(value) ? { success: true, value } :
+                       { success: false, value, message: errorMessage };
+       }
+       const name = property.name;
+       if (name === 'ip-address' || name.endsWith('-ip-address')) {
+               return validateProperty.bind(null, _isIP, "The value is not a valid ip address.")
+       }
+       switch (property['data-type']) {
+               case 'int8':
+                       return validateInteger.bind(null, { min: -128, max: 127 });
+               case 'int16':
+                       return validateInteger.bind(null, { min: -32768, max: 32767 });
+               case 'int32':
+                       return validateInteger.bind(null, { min: -2147483648, max: 2147483647 });
+               case 'int64':
+                       return validateInteger.bind(null, null);
+               case 'uint8':
+                       return validateInteger.bind(null, { min: 0, max: 255 });
+               case 'uint16':
+                       return validateInteger.bind(null, { min: 0, max: 65535 });
+               case 'uint32':
+                       return validateInteger.bind(null, { min: 0, max: 4294967295 });
+               case 'uint64':
+                       return validateInteger.bind(null, { min: 0 });
+               case 'decimal64':
+                       return validateDecimal.bind(null, null)
+               case 'string':
+               default:
+                       return function (value) { return { success: true, value } };
+       }
+}
+
+function messageTemplate(strings, ...keys) {
+       return (function (...vars) {
+               let helpInfo = vars.reduce((o, info) => Object.assign(o, info), {});
+               return keys.reduce((s, key, i) => {
+                       return s + helpInfo[key] + strings[i + 1];
+               }, strings[0]);
+       });
+}
+
+const errorMessage = messageTemplate`"${'value'}" is ${'error'}. ${'message'}`;
+
+const inputDemensionStyle = { maxWidth: '100%', minWidth: '100%' };
+
+class Input extends React.Component {
+       constructor(props) {
+               super(props);
+               let originalValue = yang.isValueSet(props.property, props.value) ? props.value : null; // normalize empty value
+               this.state = { originalValue };
+       }
+
+       componentWillReceiveProps(nextProps) {
+               const { value } = nextProps
+               if (value !== this.state.originalValue) {
+                       let originalValue = value ? value : null; // normalize empty value
+                       this.setState({ originalValue })
+               }
+       }
+
+       render() {
+               const { id, property, value, title, readOnly, onChange, onError, onExit } = this.props;
+               const { originalValue } = this.state;
+               const placeholder = property.name;
+               const required = yang.isRequired(property);
+               const className = ClassNames(property.name + '-input', { '-is-required': required });
+
+               const validator = getValidator(property);
+               function handleValueChanged(newValue) {
+                       newValue = newValue.trim();
+                       const result = !newValue ? validateRequired(required, newValue) : validator(newValue);
+                       result.success ? onChange(result.value) : onError(result.message);
+               }
+               const changeHandler = _debounce(handleValueChanged, 2000);
+               function onInputChange(e) {
+                       e.preventDefault();
+                       changeHandler(_trim(e.target.value));
+               }
+               function onBlur(e) {
+                       changeHandler.cancel();
+                       const value = _trim(e.target.value);
+                       const result = !value ? validateRequired(required, value) : validator(value);
+                       // just in case we missed it by cancelling the debouncer
+                       result.success ? onChange(result.value) : onError(result.message);
+                       onExit(result);
+               }
+               // if (!yang.isKey(property) && yang.isString(property)) {
+               //      return (
+               //              <textarea
+               //                      cols="5"
+               //                      id={id}
+               //                      defaultValue={value}
+               //                      placeholder={placeholder}
+               //                      className={className}
+               //                      onChange={onInputChange}
+               //                      onBlur={onBlur}
+               //                      required={required} 
+               //                      readOnly={readOnly} 
+               //                      style={inputDemensionStyle}/>
+               //      );
+               // }
+               return (
+                       <input
+                               id={id}
+                               type="text"
+                               defaultValue={value}
+                               className={className}
+                               placeholder={placeholder}
+                               onChange={onInputChange}
+                               onBlur={onBlur}
+                               required={required}
+                               readOnly={readOnly}
+                               style={inputDemensionStyle}
+                       />
+               );
+       }
+}
+
+export {
+       Input,
+       Empty,
+       Boolean,
+       Reference,
+       Enumeration
+};
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/editor/LeafField.jsx b/skyquake/plugins/admin/src/components/editor/LeafField.jsx
new file mode 100644 (file)
index 0000000..c5c47a7
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ *
+ *   Copyright 2017 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 ClassNames from 'classnames'
+import changeCase from 'change-case'
+import yang from '../../yang/leaf-utils.js'
+import { Input, Empty, Boolean, Reference, Enumeration } from './LeafEditor'
+
+// import '../../styles/EditDescriptorModelProperties.scss'
+
+function buildEditor(model, path, property, value, readOnly, id, onChange, onError, onExit) {
+       const title = path.join('.');
+
+       let editor = null;
+       id = id || property.name;
+       if (yang.isEnumeration(property)) {
+               editor = <Enumeration
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (yang.isLeafRef(property)) {
+               editor = <Reference
+                       model={model}
+                       property={property}
+                       path={path}
+                       value={value}
+                       id={id}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (yang.isBoolean(property)) {
+               editor = <Boolean
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (yang.isLeafEmpty(property)) {
+               editor = <Empty
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else {
+               editor = <Input
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       }
+       return editor;
+}
+
+export default class LeafField extends React.Component {
+       constructor(props) {
+               super(props);
+               this.state = { showHelp: props.showHelp, isInError: props.errorMessage };
+       }
+       componentWillReceiveProps(nextProps) {
+               const { showHelp, errorMessage } = nextProps
+               if (showHelp !== this.state.showHelp && !this.state.isInError) {
+                       this.setState({ showHelp })
+               }
+               if (errorMessage !== this.state.errorMessage) {
+                       this.setState({ showHelp: !!errorMessage || showHelp, isInError: errorMessage })
+               }
+       }
+       render() {
+               const { model, path, property, value, id, readOnly, extraHelp, onChange, onError } = this.props;
+               let title = changeCase.titleCase(property.name);
+               const showHelp = this.state.showHelp;
+               const description = (extraHelp ? extraHelp + ' ' : '') + property.description;
+               const helpText = this.state.isInError ? this.state.isInError + " " + description : description;
+               if (property.mandatory && !readOnly) {
+                       title = "* " + title;
+               }
+               const errorHandler = (message) => {
+                       this.setState({ showHelp: true, isInError: message });
+               }
+               const changeHandler = (value) => {
+                       this.setState({ showHelp: this.props.showHelp, isInError: false });
+                       onChange && onChange(value);
+               }
+               const exitHandler = (exitResult) => {
+                       if (!exitResult.success) {
+                               // errorHandler(exitResult.message);
+                               onError && onError(exitResult.message);
+                       }
+               }
+
+               const editor = buildEditor(
+                       model, path, property, value, readOnly, id,
+                       changeHandler, errorHandler, exitHandler);
+               const helpStyle = {
+                       display: showHelp ? 'inline-block' : 'none',
+                       paddingTop: '2px',
+                       paddingLeft: '8px',
+                       color: this.state.isInError ? 'red' : 'darkgray',
+                       fontSize: 'small'
+               };
+               return (
+                       <div className={ClassNames('leaf-property -is-leaf property')} style={{paddingBottom: '10px'}} >
+                               <h3 className="property-label">
+                                       <label htmlFor={id}>
+                                               <span className={'leaf-name name info'}>{title}</span>
+                                       </label>
+                               </h3>
+                               <div className={ClassNames('property-content')}>
+                                       {editor}
+                               </div>
+                               <span className={'leaf-description description'}
+                                       style={helpStyle}>{helpText}</span>
+                       </div>
+               );
+       }
+}
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/components/editor/Select.jsx b/skyquake/plugins/admin/src/components/editor/Select.jsx
new file mode 100644 (file)
index 0000000..4734fd5
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ *
+ *   Copyright 2016-2017 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 ClassNames from 'classnames'
+import changeCase from 'change-case'
+
+// import '../../styles/EditDescriptorModelProperties.scss'
+
+export default function Select(props) {
+       const { id, label, value, options, title, required, readOnly, onChange } = props;
+       const selectOptions = options.map((o, i) => <option key={':' + i} value={o.value}>{o.name}</option>);
+       if (!value || !required) {
+               let placeholder = props.placeholder || " ";
+               placeholder = changeCase.title(placeholder);
+               const noValueDisplayText = placeholder;
+               selectOptions.unshift(<option key={'(value-not-set)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
+       }
+       function onSelectChange(e) {
+               e.preventDefault();
+               onChange(e.target.value);
+       }
+       return (
+               <select
+                       id={id}
+                       style={{minWidth: '100%', maxWidth: '100%'}}
+                       defaultValue={value}
+                       title={title}
+                       onChange={onSelectChange}
+                       required={required} 
+                       disabled={readOnly}>
+                       {selectOptions}
+               </select>
+       );
+}
diff --git a/skyquake/plugins/admin/src/components/editor/resolveLeafRef.js b/skyquake/plugins/admin/src/components/editor/resolveLeafRef.js
new file mode 100644 (file)
index 0000000..5a8e8fc
--- /dev/null
@@ -0,0 +1,284 @@
+function isRelativePath(path) {
+    if (path.split('/')[0] == '..') {
+        return true;
+    }
+    return false;
+}
+
+function getResults(topLevelObject, pathArray) {
+    let objectCopy = _cloneDeep(topLevelObject);
+    let i = pathArray.length;
+    let results = [];
+
+    while (pathArray[pathArray.length - i]) {
+        if (_isArray(objectCopy[pathArray[pathArray.length - i]])) {
+            if (i == 2) {
+                results = _map(objectCopy[pathArray[pathArray.length - i]], pathArray[pathArray.length - 1]);
+            } else {
+                objectCopy = objectCopy[pathArray[pathArray.length - i]];
+            }
+        } else if (_isArray(objectCopy)) {
+            objectCopy.map((object) => {
+                if (_isArray(object[pathArray[pathArray.length - i]])) {
+                    if (i == 2) {
+                        results = results.concat(_map(object[pathArray[pathArray.length - i]], pathArray[pathArray.length - 1]));
+                    }
+                }
+            })
+        }
+        i--;
+    }
+
+    return results;
+}
+
+function getAbsoluteResults(topLevelObject, pathArray) {
+    let i = pathArray.length;
+    let objectCopy = _cloneDeep(topLevelObject);
+    let results = [];
+
+    let fragment = pathArray[pathArray.length - i]
+
+    while (fragment) {
+        if (i == 1) {
+            // last fragment
+            if (_isArray(objectCopy)) {
+                // results will be obtained from a map
+                results = _map(objectCopy, fragment);
+            } else {
+                // object
+                if (fragment.match(/\[.*\]/g)) {
+                    // contains a predicate
+                    // shouldn't reach here
+                    console.log('Something went wrong while resolving a leafref. Reached a leaf with predicate.');
+                } else {
+                    // contains no predicate
+                    if (!objectCopy) {
+                        break;
+                    }
+                    results.push(objectCopy[fragment]);
+                }
+            }
+        } else {
+            if (_isArray(objectCopy)) {
+                // is array
+                objectCopy = _map(objectCopy, fragment);
+
+                // If any of the deeper object is an array, flatten the entire list.
+                // This would usually be a bad leafref going out of its scope.
+                // Log it too
+                for (let i = 0; i < objectCopy.length; i++) {
+                    if (_isArray(objectCopy[i])) {
+                        objectCopy = _flatten(objectCopy);
+                        console.log('This might be a bad leafref. Verify with backend team.')
+                        break;
+                    }
+                }
+            } else {
+                // is object
+                if (fragment.match(/\[.*\]/g)) {
+                    // contains a predicate
+                    let predicateStr = fragment.match(/\[.*\]/g)[0];
+                    // Clip leading [ and trailing ]
+                    predicateStr = predicateStr.substr(1, predicateStr.length - 2);
+                    const predicateKeyValue = predicateStr.split('=');
+                    const predicateKey = predicateKeyValue[0];
+                    const predicateValue = predicateKeyValue[1];
+                    // get key for object to search into
+                    let key = fragment.split('[')[0];
+                    let searchObject = {};
+                    searchObject[predicateKey] = predicateValue;
+                    let found = _find(objectCopy[key], searchObject);
+                    if (found) {
+                        objectCopy = found;
+                    } else {
+                        // check for numerical value
+                        if (predicateValue != "" &&
+                            predicateValue != null &&
+                            predicateValue != NaN &&
+                            predicateValue != Infinity &&
+                            predicateValue != -Infinity) {
+                            let numericalPredicateValue = _toNumber(predicateValue);
+                            if (_isNumber(numericalPredicateValue)) {
+                                searchObject[predicateKey] = numericalPredicateValue;
+                                found = _find(objectCopy[key], searchObject);
+                            }
+                        }
+                        if (found) {
+                            objectCopy = found;
+                        } else {
+                            return [];
+                        }
+                    }
+                } else {
+                    // contains no predicate
+                    if (!objectCopy) {
+                        break;
+                    }
+                    objectCopy = objectCopy[fragment];
+                    if (!objectCopy) {
+                        // contains no value
+                        break;
+                    }
+                }
+            }
+        }
+        i--;
+        fragment = pathArray[pathArray.length - i];
+    }
+
+    return results;
+}
+
+function resolveCurrentPredicate(leafRefPath, container, pathCopy) {
+    if (leafRefPath.indexOf('current()') != -1) {
+        // contains current
+
+        // Get the relative path from current
+        let relativePath = leafRefPath.match("current\\(\\)\/(.*)\]");
+        let relativePathArray = relativePath[1].split('/');
+
+        while (relativePathArray[0] == '..') {
+            pathCopy.pop();
+            relativePathArray.shift();
+        }
+
+        // Supports only one relative path up
+        // To support deeper paths, will need to massage the string more
+        // i.e. change '/'' to '.'
+        const searchPath = pathCopy.join('.').concat('.', relativePathArray[0]);
+        const searchValue = resolvePath(container.model, searchPath);
+
+        const predicateStr = leafRefPath.match("(current.*)\]")[1];
+        leafRefPath = leafRefPath.replace(predicateStr, searchValue);
+    }
+    return leafRefPath;
+}
+
+function cleanupFieldKeyArray (fieldKeyArray) {
+    fieldKeyArray.map((fieldKey, fieldKeyIndex) => {
+        fieldKeyArray[fieldKeyIndex] = fieldKey.replace(/.*\/(.*)/, '$1');
+    });
+    return fieldKeyArray;
+}
+
+export default function resolveLeafRefPath(catalogs, leafRefPath, fieldKey, path, container) {
+    let pathCopy = _clone(path);
+    // Strip any prefixes
+    let leafRefPathCopy = leafRefPath.replace(/[\w\d]*:/g, '');
+    // Strip any spaces
+    leafRefPathCopy = leafRefPathCopy.replace(/\s/g, '');
+
+    // resolve any current paths
+    leafRefPathCopy = resolveCurrentPredicate(leafRefPathCopy, container, pathCopy);
+
+    // Split on delimiter (/)
+    const pathArray = leafRefPathCopy.split('/');
+
+    let fieldKeyArray = fieldKey.split(':');
+
+    // strip prepending qualifiers from fieldKeys
+    fieldKeyArray = cleanupFieldKeyArray(fieldKeyArray);
+    let results = [];
+
+    // Check if relative path or not
+    // TODO: Below works but
+    // better to convert the pathCopy to absolute/rooted path 
+    // and use the absolute module instead
+    if (isRelativePath(leafRefPathCopy)) {
+        let i = pathArray.length;
+        while (pathArray[pathArray.length - i] == '..') {
+            fieldKeyArray.splice(-1, 1);
+            if (!isNaN(Number(fieldKeyArray[fieldKeyArray.length - 1]))) {
+                // found a number, so an index. strip it
+                fieldKeyArray.splice(-1, 1);
+            }
+            i--;
+        }
+
+        // traversed all .. - now traverse down
+        if (fieldKeyArray.length == 1) {
+            for (let key in catalogs) {
+                for (let subKey in catalogs[key]) {
+                    let found = _find(catalogs[key][subKey], {
+                        id: fieldKeyArray[0]
+                    });
+                    if (found) {
+                        results = getAbsoluteResults(found, pathArray.splice(-i, i));
+                        return results;
+                    }
+                }
+            }
+        } else if (fieldKeyArray.length == 2) {
+            for (let key in catalogs) {
+                for (let subKey in catalogs[key]) {
+                    console.log(key, subKey);
+                    var found = _find(catalogs[key][subKey], {
+                        id: fieldKeyArray[0]
+                    });
+                    if (found) {
+                        for (let foundKey in found) {
+                            if (_isArray(found[foundKey])) {
+                                let topLevel = _find(found[foundKey], {
+                                    id: fieldKeyArray[1]
+                                });
+                                if (topLevel) {
+                                    results = getAbsoluteResults(topLevel, pathArray.splice(-i, i));
+                                    return results;
+                                }
+                            } else {
+                                if (foundKey == fieldKeyArray[1]) {
+                                    results = getAbsoluteResults(found[foundKey], pathArray.splice(-i, i));
+                                    return results;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } else if (fieldKeyArray.length == 3) {
+            for (let key in catalogs) {
+                for (let subKey in catalogs[key]) {
+                    let found = _find(catalogs[key][subKey], {
+                        id: fieldKeyArray[0]
+                    });
+                    if (found) {
+                        for (let foundKey in found) {
+                            if (_isArray(found[foundKey])) {
+                                let topLevel = _find(found[foundKey], {
+                                    id: fieldKeyArray[1]
+                                });
+                                if (topLevel) {
+                                    results = getAbsoluteResults(topLevel, pathArray.splice(-i, i));
+                                    return results;
+                                }
+                            } else {
+                                if (foundKey == fieldKeyArray[1]) {
+                                    results = getAbsoluteResults(found[foundKey], pathArray.splice(-i, i));
+                                    return results;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            // not supported - too many levels deep ... maybe some day
+            console.log('The relative path is from a node too many levels deep from root. This is not supported at the time');
+        }
+    } else {
+        // absolute path
+        if (pathArray[0] == "") {
+            pathArray.splice(0, 1);
+        }
+
+        let catalogKey = pathArray[0];
+        let topLevelObject = {};
+
+        topLevelObject[catalogKey] = catalogs[catalogKey];
+
+        results = getAbsoluteResults(topLevelObject, pathArray);
+
+        return results;
+    }
+}
diff --git a/skyquake/plugins/admin/src/main.js b/skyquake/plugins/admin/src/main.js
new file mode 100644 (file)
index 0000000..b10dc1d
--- /dev/null
@@ -0,0 +1,11 @@
+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/admin/src/store/ModelStore.js b/skyquake/plugins/admin/src/store/ModelStore.js
new file mode 100644 (file)
index 0000000..555689b
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * 
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+import Alt from 'widgets/skyquake_container/skyquakeAltInstance';
+import yang from '../yang/leaf-utils';
+
+import {
+    schemaActions,
+    schemaSource
+} from 'source/schema'
+import {
+    modelActions,
+    modelSource
+} from 'source/model'
+import adminActions from '../adminActions'
+
+function pullOutWantedProperty(path, result) {
+    if (!result) {
+        return null;
+    }
+    const nodes = path.split('/');
+    if (nodes[0] === 'project') {
+        nodes.shift(); // get rid of top level as it was not sent back
+        result = nodes.reduce((data, node) => data[node], result);
+    }
+    return result;
+}
+
+function findItemIndex(list, key, keyValue) {
+    const keyPath = Array.isArray(keyValue) ? keyValue : key.length > 1 ? JSON.parse(keyValue) : [keyValue];
+    if (key.length > 1) {
+        return list.findIndex(item => {
+            return key.every((k, i) => item[k] === keyPath[i]);
+        });
+    } else {
+        const leaf = key[0];
+        const match = keyPath[0];
+        return list.findIndex(item => {
+            return item[leaf] === match;
+        });
+    }
+}
+
+function getItemFromList(list, key, keyValue) {
+    return list[findItemIndex(list, key, keyValue)];
+}
+
+function getElement(path, store) {
+    return path.reduce((parent, node, index) => {
+        const element = Object.assign({}, parent);
+        if (parent.type === 'list') {
+            element.type = 'list-entry'
+            element.value = getItemFromList(parent.value, parent.schema.key, node);
+        } else {
+            element.schema = parent.schema.properties.find(property => property.name === node)
+            element.type = element.schema.type;
+            element.value = (parent.value && parent.value[node]) || (parent.value[node] = {});
+        }
+        return element;
+    }, {
+        value: store.data,
+        schema: store.schema[store.path],
+        type: store.schema[store.path].type
+    });
+}
+
+function massageData(data, schema){
+    return Object.keys(data).reduce((o, name) => {
+        let input = data[name];
+        let output = null;
+        if (yang.isLeafEmpty(input.property)) {
+            output = {type: 'leaf_empty', data: input.value.length ? 'set' : ''}
+        } else if (yang.isLeafList(input.property)) {
+            const newList = Array.isArray(input.value) ? input.value : input.value.split(',');
+            const oldList = input.currentValue ? (Array.isArray(input.currentValue) ? input.currentValue : input.currentValue.split(',')) : [];
+            const addList = newList.reduce((list, v) => {
+                v = v.trim();
+                if (v) {
+                    const i = oldList.indexOf(v);
+                    if (i === -1) {
+                        list.push(v);
+                    } else {
+                        oldList.splice(i, 1);
+                    }
+                }
+                return list;
+            }, [])
+            output = {type: 'leaf_list', data: {}};
+            addList.length && (output.data.add = addList);
+            oldList.length && (output.data.remove = oldList);
+        } else {
+            output = input.value;
+        }
+        o[name] = output;
+        return o
+    }, {})
+}
+
+class ModelStore {
+    constructor(path) {
+        this.state = {
+            path,
+            isLoading: false
+        };
+        this.bindActions(adminActions);
+        this.registerAsync(modelSource);
+        this.bindActions(modelActions);
+        this.registerAsync(schemaSource);
+        this.bindActions(schemaActions);
+        this.exportPublicMethods({
+            get: this.get,
+            update: this.update,
+            create: this.create,
+            'delete': this.remove
+        })
+    }
+
+    get = () => {
+        Promise.all([
+            new Promise((resolve, reject) => {
+                this.getInstance().loadSchema(this.state.path)
+                    .then(result => resolve(result))
+                    .catch(error => reject(error))
+            }),
+            new Promise((resolve, reject) => {
+                const result = this.getInstance().loadModel(this.state.path)
+                    .then(result => resolve(pullOutWantedProperty(this.state.path, result)))
+                    .catch(error => reject(error))
+            })
+        ]).then((results) => {
+            this.setState({
+                isLoading: false,
+                path: this.state.path,
+                schema: results[0][this.state.path],
+                data: results[1] || {}
+            });
+        }).catch((errors) => {
+            this.setState({
+                isLoading: false,
+                error: {
+                    get: errors
+                }
+            })
+        })
+        this.setState({
+            isLoading: true,
+            path: this.state.path
+        })
+    }
+    update = (path, obj) => {
+        const e = getElement(path, this.state);
+        obj = massageData(obj, e.schema);
+        this.getInstance().updateModel(path, obj)
+            .then((response) => {
+                const errors = [];
+                const target = getElement(path, this.state);
+                for (const name in response.result) {
+                    if (response.result[name].success) {
+                        target.value[name] = response.result[name].value
+                    } else {
+                        errors.push(response.result[name]);
+                    }
+                }
+                this.setState({
+                    updatingPath: null,
+                    data: this.state.data
+                });
+                errors && this.setState({
+                    error: {
+                        update: errors
+                    }
+                });
+            }).catch((errors) => {
+                this.setState({
+                    updatingPath: null,
+                    error: {
+                        update: errors
+                    }
+                })
+            })
+        this.setState({
+            updatingPath: path,
+        })
+    }
+
+    create = (path, obj) => {
+        const e = getElement(path, this.state);
+        obj = massageData(obj, e.schema);
+        this.getInstance().createModel(path, obj)
+            .then((response) => {
+                const createList = () => {
+                    const parentPath = path.slice();
+                    const name = parentPath.pop();
+                    const list = [];
+                    const parent = getElement(parentPath, this.state)
+                    if (parent.value) parent.value[name] = [];
+                    return parent.value[name];
+                }
+                const errors = [];
+                const target = getElement(path, this.state);
+                const list = target.value || createList();
+                list.unshift(response.data);
+                this.setState({
+                    updatingPath: null,
+                    data: this.state.data
+                });
+                errors && this.setState({
+                    error: {
+                        update: errors
+                    }
+                });
+            }).catch((errors) => {
+                this.setState({
+                    updatingPath: null,
+                    error: {
+                        update: errors
+                    }
+                })
+            })
+        this.setState({
+            updatingPath: path,
+        })
+    }
+
+    remove = (path, obj) => {
+        this.getInstance().deleteModel(path)
+            .then((response) => {
+                path = path.slice();
+                const id = path.pop();
+                const list = getElement(path, this.state);
+                const index = findItemIndex(list.value, list.schema.key, id);
+                list.value.splice(index, 1);
+                this.setState({
+                    updatingPath: null,
+                    data: this.state.data
+                });
+            }).catch((errors) => {
+                this.setState({
+                    updatingPath: null,
+                    error: {
+                        'delete': errors
+                    }
+                })
+            })
+        this.setState({
+            updatingPath: path,
+        })
+    }
+}
+
+export default ModelStore
\ No newline at end of file
diff --git a/skyquake/plugins/admin/src/yang/leaf-utils.js b/skyquake/plugins/admin/src/yang/leaf-utils.js
new file mode 100644 (file)
index 0000000..0deae4c
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 1/27/16.
+ *
+ * This class provides utility methods for interrogating an instance of model uiState object.
+ */
+
+export default {
+       isString(property = {}) {
+               return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'string')
+       },
+       isBoolean(property = {}) {
+               return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'boolean')
+       },
+       isLeafEmpty(property = {}) {
+               return (typeof(property['data-type']) == 'string') && (property['data-type'].toLowerCase() == 'empty')
+       },
+       isLeaf(property = {}) {
+        return property.type === 'leaf';
+       },
+       isLeafOrLeafList(property = {}) {
+        return property.type === 'leaf' || property.type === 'leaf_list';
+       },
+       isLeafList(property = {}) {
+        return property.type === 'leaf_list';
+       },
+       isList(property = {}) {
+        return property.type === 'list';
+       },
+       isLeafRef(property = {}) {
+               const type = property['data-type'] || {};
+               return type.hasOwnProperty('leafref');
+       },
+       isEnumeration(property = {}) {
+               const type = property['data-type'] || {};
+               return type.hasOwnProperty('enumeration');
+       },
+       isRequired(property = {}) {
+               return property.mandatory === 'true';
+       },
+       isKey(property = {}) {
+               return property['is-key'] === 'true';
+       },
+       isJSON(property = {}) {
+               property.name.match(/(vnfd|nsd):meta$/)
+       },
+       getDefaultValue(property = {}) {
+               if (property['default-value']) {
+                       return property['default-value'];
+               }
+               return null;
+       },
+       getEnumeration(property = {}, value) {
+               const enumeration = property['data-type'].enumeration.enum;
+               if (typeof enumeration === 'string') {
+                       return [{name: enumeration, value: enumeration, isSelected: String(value) === enumeration}];
+               }
+               return Object.keys(enumeration).map(enumName => {
+                       let enumValue = enumName;
+                       // warn we only support named enums and systematically ignore enum values
+                       //const enumObj = enumeration[enumName];
+                       //if (enumObj) {
+                       //      enumValue = enumObj.value || enumName;
+                       //}
+                       return {name: enumName, value: enumValue, isSelected: String(enumValue) === String(value)};
+               });
+       },
+       getDisplayValue(property = {}, rawValue) {
+               if (this.isLeafEmpty(property) && rawValue !== undefined && rawValue !== null) {
+                       return "Enabled";
+               } if (this.isLeafList(property) && Array.isArray(rawValue)) {
+                       return rawValue.join(", ");
+               }
+               return rawValue;
+       },
+
+       isValueSet(property = {}, rawValue) {
+               return !(
+                          (typeof rawValue === 'undefined')
+                       || (rawValue == null)
+                       || (typeof rawValue === 'string' && rawValue.length === 0)
+               );
+       }
+}
diff --git a/skyquake/plugins/admin/src/yang/property-utils.js b/skyquake/plugins/admin/src/yang/property-utils.js
new file mode 100644 (file)
index 0000000..e705d08
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 1/27/16.
+ *
+ * This class provides utility methods for interrogating an instance of model uiState object.
+ */
+
+import _includes from 'lodash/includes'
+import _isArray from 'lodash/isArray'
+import changeCase from 'change-case'
+import utils from '../utils'
+
+export default {
+       isLeaf(property = {}) {
+               return /leaf|leaf_list/.test(property.type);
+       },
+       isList(property = {}) {
+               return property.type === 'list';
+       },
+       isContainer(property = {}) {
+               return property.type === 'container';
+       },
+       isChoice(property = {}) {
+               return property.type === 'choice';
+       },
+       isLeafList(property = {}) {
+               return property.type === 'leaf_list';
+       },
+}
\ No newline at end of file
diff --git a/skyquake/plugins/admin/webpack.production.config.js b/skyquake/plugins/admin/webpack.production.config.js
new file mode 100644 (file)
index 0000000..f34eab7
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * 
+ *   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.
+ *
+ */
+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 CompressionPlugin = require("compression-webpack-plugin");
+
+// Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
+process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
+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',
+            'source': path.resolve(frameworkPath) + '/source',
+            '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: /node_modules/,
+                loader: 'babel-loader',
+                query: {
+                    presets: ["es2015", "stage-0", "react"]
+                }
+            }, {
+                test: /\.css$/,
+                loader: 'style!css'
+            }, {
+                test: /\.scss/,
+                loader: 'style!css!sass'
+            }
+        ]
+    },
+    plugins: [
+        new HtmlWebpackPlugin({
+            filename: '../' + htmlFilename, 
+            template: frameworkPath + '/plugin-index.html'
+        })
+    ]
+};
+
+if (process.argv.indexOf('--optimize-minimize') !== -1) {
+    // we are going to output a gzip file in the production process
+    config.output.filename = "gzip-" + config.output.filename;
+    config.plugins.push(new webpack.DefinePlugin({ // <-- key to reducing React's size
+      'process.env': {
+        'NODE_ENV': JSON.stringify('production')
+      }
+    }));
+    config.plugins.push(new CompressionPlugin({
+        asset: "[path]", // overwrite js file with gz file
+        algorithm: "gzip",
+        test: /\.(js)$/
+    }));
+}
+module.exports = config;
index e9e4be1..5a5cdc5 100644 (file)
@@ -35,33 +35,34 @@ var DataCenters = {};
 Composer.get = function(req) {
     var api_server = req.query['api_server'];
     var results = {}
+    var projectPrefix = req.session.projectId ? "project-" : "";
     return new Promise(function(resolve, reject) {
         Promise.all([
             rp({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/config/nsd-catalog/nsd?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/nsd-catalog/nsd?deep'),
                 method: 'GET',
                 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
             }),
             rp({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd?deep'),
                 method: 'GET',
                 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
             }),
             rp({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata?deep'),
                 method: 'GET',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
@@ -74,7 +75,7 @@ Composer.get = function(req) {
             //   headers: _.extend({},
             //     constants.HTTP_HEADERS.accept.collection,
             //     {
-            //       'Authorization': req.get('Authorization')
+            //       'Authorization': req.session && req.session.authorization
             //     }),
             //   forever: constants.FOREVER_ON,
             // rejectUnauthorized: false,
@@ -122,11 +123,10 @@ Composer.get = function(req) {
                 "descriptors": []
             }];
             if (result[0].body) {
-                response[0].descriptors = JSON.parse(result[0].body).collection['nsd:nsd'];
+                response[0].descriptors = utils.dataToJsonSansPropNameNamespace(result[0].body).collection['nsd'];
                 if (result[2].body) {
-                    var data = JSON.parse(result[2].body);
-                    if (data && data["nsr:ns-instance-opdata"] && data["nsr:ns-instance-opdata"]["rw-nsr:nsd-ref-count"]) {
-                        var nsdRefCountCollection = data["nsr:ns-instance-opdata"]["rw-nsr:nsd-ref-count"];
+                    var data = utils.dataToJsonSansPropNameNamespace(result[2].body);
+                    if (data && data["nsr:ns-instance-opdata"]) {
                         response[0].descriptors.map(function(nsd) {
                             if (!nsd["meta"]) {
                                 nsd["meta"] = {};
@@ -134,19 +134,13 @@ Composer.get = function(req) {
                             if (typeof nsd['meta'] == 'string') {
                                 nsd['meta'] = JSON.parse(nsd['meta']);
                             }
-                            nsd["meta"]["instance-ref-count"] = _.findWhere(nsdRefCountCollection, {
-                                "nsd-id-ref": nsd.id
-                            })["instance-ref-count"];
                         });
                     }
                 }
             };
             if (result[1].body) {
-                response[1].descriptors = JSON.parse(result[1].body).collection['vnfd:vnfd'];
+                response[1].descriptors = utils.dataToJsonSansPropNameNamespace(result[1].body).collection['vnfd'];
             };
-            // if (result[2].body) {
-            //   response[2].descriptors = JSON.parse(result[2].body).collection['pnfd:pnfd'];
-            // };
             resolve({
                 statusCode: response.statusCode || 200,
                 data: JSON.stringify(response)
@@ -171,10 +165,10 @@ Composer.delete = function(req) {
     console.log('Deleting', catalogType, id, 'from', api_server);
     return new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog/' + catalogType + '/' + id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog/' + catalogType + '/' + encodeURIComponent(id)),
             method: 'DELETE',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
@@ -190,7 +184,7 @@ Composer.delete = function(req) {
 Composer.getVNFD = function(req) {
     var api_server = req.query['api_server'];
     var vnfdID = req.body.data;
-    var authorization = req.get('Authorization');
+    var authorization = req.session && req.session.authorization;
     var VNFDs = [];
     if (typeof(vnfdID) == "object" && vnfdID.constructor.name == "Array") {
         vnfdID.map(function(id) {
@@ -217,9 +211,9 @@ Composer.getVNFD = function(req) {
 
     function requestVNFD(id) {
         return new Promise(function(resolve, reject) {
-            var url = utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd' + (id ? '/' + id : '') + '?deep';
+            var url = utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd' + (id ? '/' + encodeURIComponent(id) : '') + '?deep';
             request({
-                uri: url,
+                uri: utils.projectContextUrl(req, url),
                 method: 'GET',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
                     'Authorization': authorization
@@ -255,10 +249,10 @@ Composer.create = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + '/api/config/' + catalogType + '-catalog',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/' + catalogType + '-catalog'),
             method: 'POST',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -285,10 +279,10 @@ Composer.updateSave = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog' + '/' + catalogType + '/' + id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog' + '/' + catalogType + '/' + encodeURIComponent(id)),
             method: 'PUT',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -316,24 +310,40 @@ PackageManager.upload = function(req) {
         download_host = req.protocol + '://' + req.get('host');//req.api_server + ':' + utils.getPortForProtocol(req.protocol);
     }
 
+    var input = {
+        'external-url': download_host + '/composer/' + constants.BASE_PACKAGE_UPLOAD_DESTINATION + '/' + 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);
+
+    var authorization = {};
+    if (req.session && req.session.authorization) {
+        authorization = {
+            'Authorization': req.session && req.session.authorization
+        };
+    } else {
+        // For RIFT-16894: onboard_pkg script | Upload packages through command line
+        authorization = {
+            'Authorization': req.get('Authorization')
+        };
+    }
+
     return new Promise(function(resolve, reject) {
         Promise.all([
             rp({
-                uri: utils.confdPort(api_server) + '/api/operations/package-create',
+                uri: uri,
                 method: 'POST',
-                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
-                }),
+                headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, authorization),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
                 resolveWithFullResponse: true,
                 json: true,
                 body: {
-                    input: {
-                        'external-url': download_host + '/composer/upload/' + req.file.filename,
-                        'package-type': 'VNFD',
-                        'package-id': uuid()
-                    }
+                    input: input
                 }
             })
         ]).then(function(result) {
@@ -341,7 +351,7 @@ PackageManager.upload = function(req) {
             data['transaction_id'] = result[0].body['output']['transaction-id'];
 
             // Add a status checker on the transaction and then to delete the file later
-            PackageFileHandler.checkCreatePackageStatusAndHandleFile(req, data['transaction_id']);
+            PackageFileHandler.checkCreatePackageStatusAndHandleFile(req, data['transaction_id'], 'create');
 
             // Return status to composer UI to update the status.
             resolve({
@@ -372,17 +382,22 @@ PackageManager.update = function(req) {
         download_host = req.protocol + '://' + req.get('host');//api_server + ':' + utils.getPortForProtocol(req.protocol);
     }
     var input = {
-        'external-url': download_host + '/composer/update/' + req.file.filename,
+        'external-url': download_host + '/composer/' + constants.BASE_PACKAGE_UPLOAD_DESTINATION + '/' + req.file.filename,
         'package-type': 'VNFD',
         'package-id': uuid()
-    }
+    };
+
+    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({
-                uri: utils.confdPort(api_server) + '/api/operations/package-update',
+                uri: uri,
                 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,
@@ -397,7 +412,7 @@ PackageManager.update = function(req) {
             data['transaction_id'] = result[0].body['output']['transaction-id'];
 
             // Add a status checker on the transaction and then to delete the file later
-            PackageFileHandler.checkCreatePackageStatusAndHandleFile(req, data['transaction_id'], true);
+            PackageFileHandler.checkCreatePackageStatusAndHandleFile(req, data['transaction_id'], 'update');
 
             // Return status to composer UI to update the status.
             resolve({
@@ -419,19 +434,22 @@ PackageManager.update = function(req) {
 PackageManager.export = function(req) {
     // /api/operations/package-export
     var api_server = req.query['api_server'];
+    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: utils.confdPort(api_server) + '/api/operations/package-export',
+                uri: uri,
                 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": req.body}
+                body: { "input": input }
             })
         ]).then(function(result) {
             var data = {};
@@ -454,19 +472,23 @@ PackageManager.export = 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-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.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
                 resolveWithFullResponse: true,
                 json: true,
-                body: { "input": req.body}
+                body: { "input": input}
             })
         ]).then(function(result) {
             var data = {};
@@ -487,41 +509,57 @@ PackageManager.copy = function(req) {
 }
 
 /**
- * This methods retrieves the status of package operations. It takes an optional 
+ * These 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) {
+PackageManager.getJobStatus = function(req, jobType) {
     var api_server = req.query["api_server"];
     var uri = utils.confdPort(api_server);
     var id = req.params['id'];
-    var url = uri + '/api/operational/copy-jobs' + (id ? '/job/' + id : '');
+    var url = utils.projectContextUrl(req, uri + '/api/operational/' + jobType + (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')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false
         }, function(error, response, body) {
-            if (utils.validateResponse('restconfAPI.streams', error, response, body, resolve, reject)) {
+            if (utils.validateResponse('PackageManager.getJobStatus', error, response, body, resolve, reject)) {
                 var returnData;
+                var jsonResponse = JSON.parse(response.body);
                 if (id) {
-                    returnData = JSON.parse(response.body)['rw-pkg-mgmt:job'];
+                    returnData = jsonResponse['rw-pkg-mgmt:job'];
+                    if (!returnData){
+                        returnData = {
+                            status: 'failure',
+                            errors: [{value: "Internal error"}]
+                        }
+                    }
                 } else {
-                    var data = JSON.parse(response.body)['rw-pkg-mgmt:copy-jobs'];
+                    var data = jsonResponse['rw-pkg-mgmt:' + jobType];
                     returnData = (data && data.job) || [];
                 }
                 resolve({
                     statusCode: response.statusCode,
                     data: returnData
-                })
-            };
+                });
+            }
         })
     })
 }
+PackageManager.getCopyJobStatus = function(req) {
+    return PackageManager.getJobStatus(req, 'copy-jobs');
+}
+PackageManager.getImportJobStatus = function(req) {
+    return PackageManager.getJobStatus(req, 'create-jobs');
+}
+PackageManager.getUpdateJobStatus = function(req) {
+    return PackageManager.getJobStatus(req, 'update-jobs');
+}
 
 function makeAssetTypeParamName (type) {
     return type.toLowerCase() + '-file-type';
@@ -533,24 +571,31 @@ FileManager.addFile = function(req) {
     var package_id = req.query['package_id'];
     var package_type = req.query['package_type'].toUpperCase();
     var package_path = req.query['package_path'];
-     if (!download_host) {
+    if (!download_host) {
         download_host = req.protocol + '://' + req.get('host');//api_server + ':' + utils.getPortForProtocol(req.protocol);
     }
+    var filename = req.file.filename;
+    var assetName = req.file.originalname;
     var input = {
-        'external-url': download_host + '/composer/upload/' + req.query['package_id'] + '/' + req.file.filename,
+        'external-url': download_host + '/composer/' + constants.BASE_PACKAGE_UPLOAD_DESTINATION + '/' + filename,
         'package-type': package_type,
         'package-id': package_id,
-        'package-path': package_path ? package_path + '/' + req.file.filename : req.file.filename
-    }
+        'package-path': package_path ? package_path + '/' + assetName : assetName
+    };
+
+    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/package-file-add');
+
+    input = utils.addProjectContextToRPCPayload(req, uri, input);
+
     var assetType = req.query['asset_type'].toUpperCase();
     input[makeAssetTypeParamName(package_type)] = assetType;
     return new Promise(function(resolve, reject) {
         Promise.all([
             rp({
-                uri: utils.confdPort(api_server) + '/api/operations/package-file-add',
+                uri: uri,
                 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,
@@ -563,6 +608,8 @@ FileManager.addFile = function(req) {
         ]).then(function(result) {
             var data = {};
             data['transaction_id'] = result[0].body['output']['task-id'];
+            // Add a status checker on the transaction and then to delete the file later
+            PackageFileHandler.checkCreatePackageStatusAndHandleFile(req, data['transaction_id'], 'download');
             resolve({
                 statusCode: constants.HTTP_RESPONSE_CODES.SUCCESS.OK,
                 data: data
@@ -582,13 +629,13 @@ FileManager.addFile = function(req) {
 FileManager.get = function(req) {
     var api_server = req.query['api_server'];
     var type = req.query['package_type'] && req.query['package_type'].toUpperCase();
-    var id = req.query['package_id'];
+    var packageId = req.query['package_id'];
     var downloadUrl = req.query['url'];
     var path = req.query['package_path'];
     var assetType = req.query['asset_type'];
     var input = {
         "package-type": type,
-        "package-id": id
+        "package-id": packageId
     }
     var payload = {input: input};
     if(req.method == 'GET') {
@@ -608,12 +655,14 @@ FileManager.get = function(req) {
     }
 
     function deleteFile(payload) {
+        var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/rw-pkg-mgmt:package-file-delete');
+        payload.input = utils.addProjectContextToRPCPayload(req, uri, payload.input);
         return new Promise(function(resolve, reject) {
             rp({
-                uri: utils.confdPort(api_server) + '/api/operations/rw-pkg-mgmt:package-file-delete',
+                uri: uri,
                 method: 'POST',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 json: payload,
                 forever: constants.FOREVER_ON,
@@ -630,12 +679,14 @@ FileManager.get = function(req) {
         })
     }
     function download(payload) {
+        var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/rw-pkg-mgmt:package-file-add');
+        payload.input = utils.addProjectContextToRPCPayload(req, uri, payload.input);
         return new Promise(function(resolve, reject) {
             rp({
-                uri: utils.confdPort(api_server) + '/api/operations/rw-pkg-mgmt:package-file-add',
+                uri: uri,
                 method: 'POST',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 json: payload,
                 forever: constants.FOREVER_ON,
@@ -652,12 +703,14 @@ FileManager.get = function(req) {
         })
     }
     function list(payload) {
+        var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/operations/get-package-endpoint');
+        payload.input = utils.addProjectContextToRPCPayload(req, uri, payload.input);
         return new Promise(function(resolve, reject) {
             rp({
-                uri: utils.confdPort(api_server) + '/api/operations/get-package-endpoint',
+                uri: uri,
                 method: 'POST',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 json: payload,
                 forever: constants.FOREVER_ON,
@@ -675,10 +728,10 @@ FileManager.get = function(req) {
                     }
                     parsedEndpoint = URL.parse(endpoint);
                     rp({
-                        uri: api_server + ':' + parsedEndpoint.port + parsedEndpoint.path,
+                        uri: utils.projectContextUrl(req, api_server + ':' + parsedEndpoint.port + parsedEndpoint.path),
                         method: 'GET',
                         headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                            'Authorization': req.get('Authorization')
+                            'Authorization': req.session && req.session.authorization
                         }),
                         forever: constants.FOREVER_ON,
                         rejectUnauthorized: false,
@@ -692,7 +745,12 @@ FileManager.get = function(req) {
                         }
                     }).catch(function(err) {
                         console.log('Failed to retrieve FileManager.list')
-                        resolve(err);
+                        reject({
+                                statusCode: constants.HTTP_RESPONSE_CODES.ERROR.INTERNAL_SERVER_ERROR,
+                                error: {
+                                    errorMessage: JSON.stringify(err)
+                                }
+                        });
                     })
                 }
             })
@@ -706,15 +764,15 @@ FileManager.job = function(req) {
     var id = req.params['id'];
     return new Promise(function(resolve, reject) {
         request({
-            url: uri + url + '?deep',
+            url: utils.projectContextUrl(req, uri + url + '?deep'),
             method: 'GET',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
         }, function(error, response, body) {
-            if (utils.validateResponse('restconfAPI.streams', error, response, body, resolve, reject)) {
+            if (utils.validateResponse('FileManager.job', error, response, body, resolve, reject)) {
                 var data = JSON.parse(response.body)['rw-pkg-mgmt:download-jobs'];
                 var returnData = [];
                 data && data.job.map(function(d) {
index cc9cca7..868a588 100644 (file)
@@ -6,26 +6,30 @@ var constants = require('../../../framework/core/api_utils/constants.js');
 var fs = require('fs');
 var _ = require('lodash');
 
-PackageFileHandler = {};
+var PackageFileHandler = {};
 
 function deleteFile(filename) {
        setTimeout(function() {
-               fs.unlinkSync(constants.BASE_PACKAGE_UPLOAD_DESTINATION + filename);
+               try {
+                       fs.unlinkSync(constants.BASE_PACKAGE_UPLOAD_DESTINATION + '/' + filename);
+               } catch (e) {
+                       console.log("file delete error", e);
+               }
        }, constants.PACKAGE_FILE_DELETE_DELAY_MILLISECONDS);
 };
 
-function checkStatus(req, transactionId, isUpdate) {
-       var upload_server = req.query['upload_server'];
-       var headers = _.extend({},
-        {
-            'Authorization': req.get('Authorization')
-        }
-    );
-    var type = isUpdate ? 'update' : 'upload';
+function checkStatus(req, transactionId, jobType) {
+    var api_server = req.query["api_server"];
+    var uri = utils.confdPort(api_server);
+       var id = req.params['id'];
+       var jobName = jobType + '-jobs';
+    var url = utils.projectContextUrl(req, uri + '/api/operational/' + jobName + (transactionId ? '/job/' + transactionId : ''));
        request({
-               url: upload_server + ':' + constants.PACKAGE_MANAGER_SERVER_PORT + '/api/' + type + '/' + transactionId + '/state',
-               type: 'GET',
-               headers: headers,
+               url: url,
+               method: 'GET',
+               headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
+                       'Authorization': req.session && req.session.authorization
+               }),
                forever: constants.FOREVER_ON,
                rejectUnauthorized: false
        }, function(error, response, body) {
@@ -35,25 +39,25 @@ function checkStatus(req, transactionId, isUpdate) {
                } else {
                        var jsonStatus = null;
                        if (typeof body == 'string' || body instanceof String) {
-                               jsonStatus = JSON.parse(body);
+                               try {jsonStatus = JSON.parse(body)['rw-pkg-mgmt:job'];} catch(e) {jsonStatus = {status: 'failure'}}
                        } else {
                                jsonStatus = body;
                        }
 
-                       if (jsonStatus.status && (jsonStatus.status == 'success' || jsonStatus.status == 'failure')) {
+                       if (jsonStatus && jsonStatus.status && (jsonStatus.status == 'COMPLETED' || jsonStatus.status == 'FAILED')) {
                                console.log('Transaction ', transactionId, ' completed with status ', jsonStatus.status ,'. Will delete file', req.file.filename, ' in ', constants.PACKAGE_FILE_DELETE_DELAY_MILLISECONDS ,' milliseconds');
                                deleteFile(req.file.filename);
                        } else {
                                setTimeout(function() {
-                                       checkStatus(req, transactionId, isUpdate);
+                                       checkStatus(req, transactionId, jobType);
                                }, constants.PACKAGE_FILE_ONBOARD_TRANSACTION_STATUS_CHECK_DELAY_MILLISECONDS);
                        }
                }
        });
 };
 
-PackageFileHandler.checkCreatePackageStatusAndHandleFile = function(req, transactionId, isUpdate) {
-       checkStatus(req, transactionId, isUpdate);
+PackageFileHandler.checkCreatePackageStatusAndHandleFile = function(req, transactionId, jobType) {
+       checkStatus(req, transactionId, jobType);
 };
 
 
index 80a2a0c..a56a038 100644 (file)
@@ -5,6 +5,12 @@
     "name": "Catalog",
     "dashboard" : "./src/components/ComposerApp.js",
     "order": 2,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-project:project-admin",
+        "rw-project:project-oper",
+        "rw-project-mano:catalog-oper",
+        "rw-project-mano:catalog-admin"],
     "routes" : [{
         "label": "Catalog",
         "route": "/",
index f75330d..7698f81 100644 (file)
@@ -38,6 +38,7 @@
     "react": "^0.14.7",
     "react-addons-css-transition-group": "^0.14.7",
     "react-addons-pure-render-mixin": "^0.14.7",
+    "react-addons-shallow-compare": "^0.14.7",
     "react-crouton": "^0.2.7",
     "react-dom": "^0.14.3",
     "react-popout": "^0.4.0",
@@ -45,7 +46,8 @@
     "react-treeview": "0.4.2",
     "request-promise": "^3.0.0",
     "require-json": "0.0.1",
-    "uuid": "^3.0.0"
+    "uuid": "^3.0.0",
+    "validator": "^7.0.0"
   },
   "devDependencies": {
     "babel-core": "^6.4.5",
@@ -64,7 +66,6 @@
     "grunt-contrib-clean": "^0.7.0",
     "grunt-contrib-connect": "^0.11.2",
     "grunt-contrib-copy": "^0.8.2",
-    "grunt-karma": "^0.12.1",
     "grunt-open": "^0.2.3",
     "grunt-version": "^1.0.0",
     "grunt-webpack": "^1.0.11",
     "jasmine-core": "^2.4.1",
     "json-loader": "^0.5.4",
     "json2yaml": "^1.1.0",
-    "karma": "^0.13.15",
-    "karma-chrome-launcher": "^0.2.2",
-    "karma-es6-shim": "^0.2.3",
-    "karma-jasmine": "^0.3.6",
-    "karma-phantomjs-launcher": "^0.2.1",
-    "karma-phantomjs-shim": "^1.2.0",
-    "karma-script-launcher": "^0.1.0",
-    "karma-sourcemap-loader": "^0.3.7",
-    "karma-webpack": "^1.7.0",
     "load-grunt-tasks": "^3.3.0",
     "node-sass": "^3.4.2",
     "npm": "^3.7.1",
-    "phantomjs": "^1.9.19",
     "react-addons-test-utils": "^0.14.7",
     "react-hot-loader": "^1.3.0",
     "react-inlinesvg": "^0.5.3",
+    "react-open-iconic-svg": "^1.0.4",
     "sass-loader": "^3.1.2",
     "style-loader": "^0.13.0",
     "url-loader": "^0.5.7",
index b3641aa..db0b7aa 100644 (file)
@@ -33,27 +33,18 @@ var storage = multer.diskStorage({
     // destination: 'upload/packages/',
     destination: function(req, file, cb) {
         var dir = constants.BASE_PACKAGE_UPLOAD_DESTINATION;
-        if (req.query['package_id']) {
-            dir += req.query['package_id'] + '/';
-        }
         if (!fs.existsSync(dir)){
-            mkdirp(dir, function(err) {
+            mkdirp(dir, function(err) { 
                 if (err) {
                     console.log('Error creating folder for uploads. All systems FAIL!');
                     throw err;
                 }
-                cb(null, dir);
             });
-        } else {
-            cb(null, dir);
         }
+        cb(null, dir);
     },
     filename: function (req, file, cb) {
-        if (req.query['package_id']) {
-            cb(null, file.originalname);
-        } else  {
-            cb(null, Date.now() + '_' + file.fieldname + '_' + file.originalname);
-        }
+        cb(null, Date.now() + '_' + file.fieldname + '_' + file.originalname);
     },
     // limits: {
     //     fieldNameSize: 100,
@@ -105,7 +96,7 @@ router.put('/api/catalog/:catalogType/:id', cors(), function(req, res) {
     });
 });
 
-router.post('/api/file-manager', cors(), upload.single('package'), function (req, res, next) {
+router.post('/api/file-manager', cors(), upload.single('file'), function (req, res, next) {
     FileManager.addFile(req).then(function(data) {
         utils.sendSuccessResponse(data, res);
     }, function(error) {
@@ -144,7 +135,10 @@ router.post('/upload', cors(), upload.single('package'), function (req, res, nex
         utils.sendErrorResponse(error, res);
     });
 });
-router.use('/upload', cors(), express.static('upload/packages'));
+router.use('/upload', cors(), function(req, res, next) {
+    console.log('Received request for ', req.originalUrl, ' from ', req.ip);
+    next();
+}, express.static(constants.BASE_PACKAGE_UPLOAD_DESTINATION));
 
 router.post('/update', cors(), upload.single('package'), function (req, res, next) {
     PackageManager.update(req).then(function(data) {
@@ -169,8 +163,22 @@ router.post('/api/package-copy', cors(), function (req, res, next) {
         utils.sendErrorResponse(error, res);
     });
 });
-router.get('/api/package-manager/jobs/:id', cors(), function (req, res, next) {
-    PackageManager.getJobStatus(req).then(function(data) {
+router.get('/api/package-copy/jobs/:id', cors(), function (req, res, next) {
+    PackageManager.getCopyJobStatus(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+router.get('/api/package-import/jobs/:id', cors(), function (req, res, next) {
+    PackageManager.getImportJobStatus(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+router.get('/api/package-update/jobs/:id', cors(), function (req, res, next) {
+    PackageManager.getUpdateJobStatus(req).then(function(data) {
         utils.sendSuccessResponse(data, res);
     }, function(error) {
         utils.sendErrorResponse(error, res);
index ae23fac..258282d 100755 (executable)
 
 PLUGIN_NAME=composer
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index 0362a38..c09436f 100644 (file)
-// /*
-//  * 
-//  *   Copyright 2016 RIFT.IO Inc
-//  *
-//  *   Licensed under the Apache License, Version 2.0 (the "License");
-//  *   you may not use this file except in compliance with the License.
-//  *   You may obtain a copy of the License at
-//  *
-//  *       http://www.apache.org/licenses/LICENSE-2.0
-//  *
-//  *   Unless required by applicable law or agreed to in writing, software
-//  *   distributed under the License is distributed on an "AS IS" BASIS,
-//  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-//  *   See the License for the specific language governing permissions and
-//  *   limitations under the License.
-//  *
-//  */
-
-// 'use strict';
-
-// // the models to be transformed into the output DSL JSON meta file
-// var yang = [require('./json-nsd.json'), require('./json-vnfd.json')];
-
-// var _ = require('lodash');
-// var inet = require('./ietf-inet-types.yang.json');
-
-// var utils = {
-//     resolvePath(obj, path) {
-//             // supports a.b, a[1] and foo[bar], etc.
-//             // where obj is ['nope', 'yes', {a: {b: 1}, foo: 2}]
-//             // then [1] returns 'yes'; [2].a.b returns 1; [2].a[foo] returns 2;
-//             path = path.split(/[\.\[\]]/).filter(d => d);
-//             return path.reduce((r, p) => {
-//                     if (r) {
-//                             return r[p];
-//                     }
-//             }, obj);
-//     },
-//     assignPathValue(obj, path, value) {
-//             path = path.split(/[\.\[\]]/).filter(d => d);
-//             // enable look-ahead to determine if type is array or object
-//             const pathCopy = path.slice();
-//             // last item in path used to assign value on the resolved object
-//             const name = path.pop();
-//             const resolvedObj = path.reduce((r, p, i) => {
-//                     if (typeof(r[p]) !== 'object') {
-//                             // look-ahead to see if next path item is a number
-//                             const isArray = !isNaN(parseInt(pathCopy[i + 1], 10));
-//                             r[p] = isArray ? [] : {}
-//                     }
-//                     return r[p];
-//             }, obj);
-//             resolvedObj[name] = value;
-//     }
-// };
-
-// var isType = d => /^(leaf|leaf-list|list|container|choice|case|uses)$/.test(d);
-
-// function deriveCardinalityFromProperty(property, typeName) {
-//     if (String(property.mandatory) === 'true') {
-//             return '1';
-//     }
-//     let min = 0, max = Infinity;
-//     if (property.hasOwnProperty('min-elements')) {
-//             min = parseInt(property['min-elements'], 10) || 0;
-//     }
-//     if (property.hasOwnProperty('max-elements')) {
-//             max = parseInt(property['max-elements'], 10) || Infinity;
-//     } else {
-//             if (!/^(list|leaf-list)$/.test(typeName)) {
-//                     max = '1';
-//             }
-//     }
-//     if (min > max) {
-//             return String(min);
-//     }
-//     if (min === max) {
-//             return String(min);
-//     }
-//     return String(min) + '..' + (max === Infinity ? 'N' : max);
-// }
-
-// function cleanWhitespace(text) {
-//     if (typeof text === 'string') {
-//             return text.replace(/\s+/g, ' ');
-//     }
-//     return text;
-// }
-
-// function buildProperties(typeData, typeName) {
-//     var properties = [];
-//     Object.keys(typeData).forEach(name => {
-//             var property = typeData[name];
-//             var listKey = typeName === 'list' ? String(property.key).split(/\s/).filter(k => k && k !== 'undefined') : false;
-//             var meta = {
-//                     name: name,
-//                     type: typeName,
-//                     description: cleanWhitespace(property.description),
-//                     cardinality: deriveCardinalityFromProperty(property, typeName),
-//                     'data-type': property.type,
-//                     properties: Object.keys(property).filter(isType).reduce((r, childType) => {
-//                             return r.concat(buildProperties(property[childType], childType));
-//                     }, [])
-//             };
-//             if (listKey) {
-//                     meta.key = listKey;
-//             }
-//             properties.push(meta);
-//     });
-//     return properties;
-// }
-
-// function lookupUses(uses, yang) {
-//     function doLookup(lookupTypeName) {
-//             var key;
-//             // warn: hardcoded prefix support for mano-types - other prefixes will be ignored
-//             if (/^manotypes:/.test(lookupTypeName)) {
-//                     var moduleName = lookupTypeName.split(':')[1];
-//                     key = ['dependencies.mano-types.module.mano-types.grouping', moduleName].join('.');
-//             } else {
-//                     var name = yang.name.replace(/^rw-/, '');
-//                     key = ['dependencies', name, 'module', name, 'grouping', lookupTypeName].join('.');
-//             }
-//             return utils.resolvePath(yang, key);
-//     }
-//     if (typeof uses === 'object') {
-//             return Object.keys(uses).reduce((result, key) => {
-//                     var found = doLookup(key);
-//                     Object.keys(found).filter(isType).forEach(type => {
-//                             var property = result[type] || (result[type] = {});
-//                             Object.assign(property, found[type]);
-//                     });
-//                     return result;
-//             }, {});
-//     } else if (typeof uses === 'string') {
-//             return doLookup(uses);
-//     }
-//     return {};
-// }
-
-// function lookupTypedef(property, yang) {
-//     var key;
-//     var lookupTypeName = property.type;
-//     // warn: hardcoded prefix support - other prefixes will be ignored
-//     if (/^manotypes:/.test(lookupTypeName)) {
-//             var lookupName = lookupTypeName.split(':')[1];
-//             key = ['dependencies.mano-types.module.mano-types.typedef', lookupName].join('.');
-//     } else if (/^inet:/.test(lookupTypeName)) {
-//             var lookupName = lookupTypeName.split(':')[1];
-//             yang = inet;
-//             key = ['schema.module.ietf-inet-types.typedef', lookupName].join('.');
-//     }
-//     if (key) {
-//             return utils.resolvePath(yang, key);
-//     }
-// }
-
-// function resolveUses(property, yang) {
-//     var childData = property.uses;
-//     var resolved = lookupUses(childData, yang);
-//     //console.log('uses', childData, 'found', resolved);
-//     Object.keys(resolved).forEach(type => {
-//             var parentTypes = property[type] || (property[type] = {});
-//             // copy types into the parent types bucket
-//             Object.assign(parentTypes, resolveReferences(yang, resolved[type]));
-//     });
-//     delete property.uses;
-// }
-
-// function resolveTypedef(property, yang) {
-//     if (/:/.test(property.type)) {
-//             var found = lookupTypedef(property, yang);
-//             if (found) {
-//                     Object.assign(property, found);
-//             }
-//     }
-// }
-
-// function resolveReferences(yang, data) {
-//     var dataClone = _.cloneDeep(data);
-//     function doResolve(typeData) {
-//             Object.keys(typeData).forEach(name => {
-//                     var property = typeData[name];
-//                     resolveTypedef(property, yang);
-//                     Object.keys(property).filter(isType).forEach(childType => {
-//                             if (childType === 'uses') {
-//                                     resolveUses(property, yang);
-//                             } else {
-//                                     doResolve(property[childType]);
-//                             }
-//                     });
-//             });
-//     }
-//     doResolve(dataClone);
-//     return dataClone;
-// }
-
-// function module(yang) {
-//     let module;
-//     var name = yang.name.replace(/^rw-/, '');
-//     if (!name) {
-//             throw 'no name given in json yang';
-//     }
-//     const path = ['container', name + '-catalog'].join('.');
-//     module = utils.resolvePath(yang, path);
-
-//     if (!module) {
-//             module = utils.resolvePath(yang, ['schema', 'module', name, path].join('.'));
-//     }
-//     if (!module) {
-//             module = utils.resolvePath(yang, ['dependencies', name, 'module', name, path].join('.'));
-//     }
-//     if (!module) {
-//             throw 'cannot find the module' + name;
-//     }
-
-//     // module/agument/nsd:nsd-catalog/nsd:nsd/meta
-//     const augLeafPath = ['schema.module', 'rw-' + name, 'augment', '/' + name + ':' + name + '-catalog/' + name + ':' + name, 'leaf'];
-//     const meta = utils.resolvePath(yang, augLeafPath.concat('meta').join('.'));
-
-//     const putLeafPath = ['dependencies', name, 'module', name, path, 'list', name, 'leaf'];
-
-//     if (meta) {
-//             utils.assignPathValue(yang, putLeafPath.concat(['meta']).join('.'), meta);
-//     }
-
-//     // module/agument/nsd:nsd-catalog/nsd:nsd/logo
-//     const logo = utils.resolvePath(yang, augLeafPath.concat('logo').join('.'));
-//     if (logo) {
-//             utils.assignPathValue(yang, putLeafPath.concat(['logo']).join('.'), logo);
-//     }
-//     var data = module.list;
-
-//     return {name: name, data: resolveReferences(yang, data)};
-
-// }
-
-// function reduceModule(result, module) {
-//     result[module.name] = buildProperties(module.data, 'list')[0];
-//     return result;
-// }
-
-// var result = yang.map(module).reduce(reduceModule, {});
-
-// console.log(JSON.stringify(result, null, 5));
+//: /*
+//:  * 
+//:  *   Copyright 2016 RIFT.IO Inc
+//:  *
+//:  *   Licensed under the Apache License, Version 2.0 (the "License");
+//:  *   you may not use this file except in compliance with the License.
+//:  *   You may obtain a copy of the License at
+//:  *
+//:  *       http://www.apache.org/licenses/LICENSE-2.0
+//:  *
+//:  *   Unless required by applicable law or agreed to in writing, software
+//:  *   distributed under the License is distributed on an "AS IS" BASIS,
+//:  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//:  *   See the License for the specific language governing permissions and
+//:  *   limitations under the License.
+//:  *
+//:  */
+//: 
+//: 'use strict';
+//: 
+//: // the models to be transformed into the output DSL JSON meta file
+//: var yang = [require('./json-nsd.json'), require('./json-vnfd.json')];
+//: 
+//: var _ = require('lodash');
+//: var inet = require('./ietf-inet-types.yang.json');
+//: 
+//: var utils = {
+//:    resolvePath(obj, path) {
+//:            // supports a.b, a[1] and foo[bar], etc.
+//:            // where obj is ['nope', 'yes', {a: {b: 1}, foo: 2}]
+//:            // then [1] returns 'yes'; [2].a.b returns 1; [2].a[foo] returns 2;
+//:            path = path.split(/[\.\[\]]/).filter(d => d);
+//:            return path.reduce((r, p) => {
+//:                    if (r) {
+//:                            return r[p];
+//:                    }
+//:            }, obj);
+//:    },
+//:    assignPathValue(obj, path, value) {
+//:            path = path.split(/[\.\[\]]/).filter(d => d);
+//:            // enable look-ahead to determine if type is array or object
+//:            const pathCopy = path.slice();
+//:            // last item in path used to assign value on the resolved object
+//:            const name = path.pop();
+//:            const resolvedObj = path.reduce((r, p, i) => {
+//:                    if (typeof(r[p]) !== 'object') {
+//:                            // look-ahead to see if next path item is a number
+//:                            const isArray = !isNaN(parseInt(pathCopy[i + 1], 10));
+//:                            r[p] = isArray ? [] : {}
+//:                    }
+//:                    return r[p];
+//:            }, obj);
+//:            resolvedObj[name] = value;
+//:    }
+//: };
+//: 
+//: var isType = d => /^(leaf|leaf-list|list|container|choice|case|uses)$/.test(d);
+//: 
+//: function deriveCardinalityFromProperty(property, typeName) {
+//:    if (String(property.mandatory) === 'true') {
+//:            return '1';
+//:    }
+//:    let min = 0, max = Infinity;
+//:    if (property.hasOwnProperty('min-elements')) {
+//:            min = parseInt(property['min-elements'], 10) || 0;
+//:    }
+//:    if (property.hasOwnProperty('max-elements')) {
+//:            max = parseInt(property['max-elements'], 10) || Infinity;
+//:    } else {
+//:            if (!/^(list|leaf-list)$/.test(typeName)) {
+//:                    max = '1';
+//:            }
+//:    }
+//:    if (min > max) {
+//:            return String(min);
+//:    }
+//:    if (min === max) {
+//:            return String(min);
+//:    }
+//:    return String(min) + '..' + (max === Infinity ? 'N' : max);
+//: }
+//: 
+//: function cleanWhitespace(text) {
+//:    if (typeof text === 'string') {
+//:            return text.replace(/\s+/g, ' ');
+//:    }
+//:    return text;
+//: }
+//: 
+//: function buildProperties(typeData, typeName) {
+//:    var properties = [];
+//:    Object.keys(typeData).forEach(name => {
+//:            var property = typeData[name];
+//:            var listKey = typeName === 'list' ? String(property.key).split(/\s/).filter(k => k && k !== 'undefined') : false;
+//:            var meta = {
+//:                    name: name,
+//:                    type: typeName,
+//:                    description: cleanWhitespace(property.description),
+//:                    cardinality: deriveCardinalityFromProperty(property, typeName),
+//:                    'data-type': property.type,
+//:                    properties: Object.keys(property).filter(isType).reduce((r, childType) => {
+//:                            return r.concat(buildProperties(property[childType], childType));
+//:                    }, [])
+//:            };
+//:            if (listKey) {
+//:                    meta.key = listKey;
+//:            }
+//:            properties.push(meta);
+//:    });
+//:    return properties;
+//: }
+//: 
+//: function lookupUses(uses, yang) {
+//:    function doLookup(lookupTypeName) {
+//:            var key;
+//:            // warn: hardcoded prefix support for mano-types - other prefixes will be ignored
+//:            if (/^manotypes:/.test(lookupTypeName)) {
+//:                    var moduleName = lookupTypeName.split(':')[1];
+//:                    key = ['dependencies.mano-types.module.mano-types.grouping', moduleName].join('.');
+//:            } else {
+//:                    var name = yang.name.replace(/^rw-/, '');
+//:                    key = ['dependencies', name, 'module', name, 'grouping', lookupTypeName].join('.');
+//:            }
+//:            return utils.resolvePath(yang, key);
+//:    }
+//:    if (typeof uses === 'object') {
+//:            return Object.keys(uses).reduce((result, key) => {
+//:                    var found = doLookup(key);
+//:                    Object.keys(found).filter(isType).forEach(type => {
+//:                            var property = result[type] || (result[type] = {});
+//:                            Object.assign(property, found[type]);
+//:                    });
+//:                    return result;
+//:            }, {});
+//:    } else if (typeof uses === 'string') {
+//:            return doLookup(uses);
+//:    }
+//:    return {};
+//: }
+//: 
+//: function lookupTypedef(property, yang) {
+//:    var key;
+//:    var lookupTypeName = property.type;
+//:    // warn: hardcoded prefix support - other prefixes will be ignored
+//:    if (/^manotypes:/.test(lookupTypeName)) {
+//:            var lookupName = lookupTypeName.split(':')[1];
+//:            key = ['dependencies.mano-types.module.mano-types.typedef', lookupName].join('.');
+//:    } else if (/^inet:/.test(lookupTypeName)) {
+//:            var lookupName = lookupTypeName.split(':')[1];
+//:            yang = inet;
+//:            key = ['schema.module.ietf-inet-types.typedef', lookupName].join('.');
+//:    }
+//:    if (key) {
+//:            return utils.resolvePath(yang, key);
+//:    }
+//: }
+//: 
+//: function resolveUses(property, yang) {
+//:    var childData = property.uses;
+//:    var resolved = lookupUses(childData, yang);
+//:    //console.log('uses', childData, 'found', resolved);
+//:    Object.keys(resolved).forEach(type => {
+//:            var parentTypes = property[type] || (property[type] = {});
+//:            // copy types into the parent types bucket
+//:            Object.assign(parentTypes, resolveReferences(yang, resolved[type]));
+//:    });
+//:    delete property.uses;
+//: }
+//: 
+//: function resolveTypedef(property, yang) {
+//:    if (/:/.test(property.type)) {
+//:            var found = lookupTypedef(property, yang);
+//:            if (found) {
+//:                    Object.assign(property, found);
+//:            }
+//:    }
+//: }
+//: 
+//: function resolveReferences(yang, data) {
+//:    var dataClone = _.cloneDeep(data);
+//:    function doResolve(typeData) {
+//:            Object.keys(typeData).forEach(name => {
+//:                    var property = typeData[name];
+//:                    resolveTypedef(property, yang);
+//:                    Object.keys(property).filter(isType).forEach(childType => {
+//:                            if (childType === 'uses') {
+//:                                    resolveUses(property, yang);
+//:                            } else {
+//:                                    doResolve(property[childType]);
+//:                            }
+//:                    });
+//:            });
+//:    }
+//:    doResolve(dataClone);
+//:    return dataClone;
+//: }
+//: 
+//: function module(yang) {
+//:    let module;
+//:    var name = yang.name.replace(/^rw-/, '');
+//:    if (!name) {
+//:            throw 'no name given in json yang';
+//:    }
+//:    const path = ['container', name + '-catalog'].join('.');
+//:    module = utils.resolvePath(yang, path);
+//: 
+//:    if (!module) {
+//:            module = utils.resolvePath(yang, ['schema', 'module', name, path].join('.'));
+//:    }
+//:    if (!module) {
+//:            module = utils.resolvePath(yang, ['dependencies', name, 'module', name, path].join('.'));
+//:    }
+//:    if (!module) {
+//:            throw 'cannot find the module' + name;
+//:    }
+//: 
+//:    // module/agument/nsd:nsd-catalog/nsd:nsd/meta
+//:    const augLeafPath = ['schema.module', 'rw-' + name, 'augment', '/' + name + ':' + name + '-catalog/' + name + ':' + name, 'leaf'];
+//:    const meta = utils.resolvePath(yang, augLeafPath.concat('meta').join('.'));
+//: 
+//:    const putLeafPath = ['dependencies', name, 'module', name, path, 'list', name, 'leaf'];
+//: 
+//:    if (meta) {
+//:            utils.assignPathValue(yang, putLeafPath.concat(['meta']).join('.'), meta);
+//:    }
+//: 
+//:    // module/agument/nsd:nsd-catalog/nsd:nsd/logo
+//:    const logo = utils.resolvePath(yang, augLeafPath.concat('logo').join('.'));
+//:    if (logo) {
+//:            utils.assignPathValue(yang, putLeafPath.concat(['logo']).join('.'), logo);
+//:    }
+//:    var data = module.list;
+//: 
+//:    return {name: name, data: resolveReferences(yang, data)};
+//: 
+//: }
+//: 
+//: function reduceModule(result, module) {
+//:    result[module.name] = buildProperties(module.data, 'list')[0];
+//:    return result;
+//: }
+//: 
+//: var result = yang.map(module).reduce(reduceModule, {});
+//: 
+//: console.log(JSON.stringify(result, null, 5));
index 17c16e6..dde6ae0 100755 (executable)
@@ -1074,7 +1074,7 @@ module mano-types
         type string;
       }
 
-      leaf polling_interval_secs {
+      leaf polling-interval-secs {
         description "The HTTP polling interval in seconds";
         type uint8;
         default 2;
index f5e95ee..9b467a7 100644 (file)
@@ -24,9 +24,14 @@ import alt from '../alt';
 class CatalogDataSourceActions {
 
        constructor() {
-               this.generateActions('loadCatalogsSuccess', 'loadCatalogsError', 'deleteCatalogItemSuccess', 'deleteCatalogItemError', 'saveCatalogItemSuccess', 'saveCatalogItemError');
-       }
-
+               this.generateActions(
+                       'loadCatalogsSuccess', 
+                       'loadCatalogsError', 
+                       'deleteCatalogItemSuccess', 
+                       'deleteCatalogItemError',
+                       'saveCatalogItemSuccess', 
+                       'saveCatalogItemError');
+           }
 }
 
 export default alt.createActions(CatalogDataSourceActions);
index 80ec497..f7f2354 100644 (file)
@@ -1,4 +1,3 @@
-
 /*
  *
  *   Copyright 2016 RIFT.IO Inc
@@ -24,10 +23,22 @@ import alt from '../alt';
 class ComposerAppActions {
 
        constructor() {
-               this.generateActions('showError', 'clearError', 'setDragState', 'propertySelected', 'showJsonViewer', 'closeJsonViewer', 'selectModel', 'outlineModel', 'clearSelection', 'enterFullScreenMode', 'exitFullScreenMode',
-                             'showAssets', 'showDescriptor');
+               this.generateActions(
+                       'showError', 
+                       'clearError', 
+                       'setDragState', 
+                       'propertySelected', 
+                       'showJsonViewer', 
+                       'closeJsonViewer', 
+                       'selectModel', 'outlineModel', 
+                       'clearSelection', 
+                       'enterFullScreenMode', 
+                       'exitFullScreenMode',
+                       'showAssets', 
+                       'showDescriptor',
+                       'recordDescriptorError');
        }
 
 }
 
-export default alt.createActions(ComposerAppActions);
+export default alt.createActions(ComposerAppActions);
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/actions/DescriptorEditorActions.js b/skyquake/plugins/composer/src/src/actions/DescriptorEditorActions.js
new file mode 100644 (file)
index 0000000..85a7c19
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 
+ *   Copyright 2017 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+import alt from '../alt';
+
+class DescriptorEditorActions {
+
+       constructor() {
+               this.generateActions(
+                       'setError',
+                       'setValue',
+                       'assignValue',
+                       'addListItem',
+                       'removeListItem',
+                       'showHelpForNothing',
+                       'showHelpForAll',
+                       'showHelpOnFocus',
+                       'setOpenState',
+                       'collapseAllPanels',
+                       'expandAllPanels',
+                       'expandPanel',
+                       'showPanelsWithData'
+               );
+       }
+
+}
+
+export default alt.createActions(DescriptorEditorActions);
\ No newline at end of file
index d721391..6899958 100644 (file)
@@ -46,10 +46,10 @@ class PanelResizeAction {
 
                if (e.detail && e.detail.side) {
                        // a ResizeManager event
-                       this.dispatch(PanelResizeAction.buildResizeManagerInfo(e))
+                       return PanelResizeAction.buildResizeManagerInfo(e);
                } else {
                        // a window event
-                       this.dispatch(PanelResizeAction.buildWindowResizeInfo(e));
+                       return PanelResizeAction.buildWindowResizeInfo(e);
                }
 
        }
index d8bf42d..c05f403 100644 (file)
@@ -21,7 +21,5 @@
  */
 'use strict';
 
-var Alt = require('alt');
-var alt = new Alt();
-
-export default alt;
+import Alt from 'widgets/skyquake_container/skyquakeAltInstance.js';
+export default Alt;
index 76a569f..65b90fd 100644 (file)
@@ -40,6 +40,7 @@ const Button = React.createClass({
                        label: null,
                        title: null,
                        src: null,
+                       disabled: false,
                        onClick: () => {}
                };
        },
@@ -57,8 +58,15 @@ const Button = React.createClass({
                const title = this.props.title;
                const draggable = this.props.draggable;
                const className = ClassNames(this.props.className, 'Button');
+               let style = {
+               }
+               if(this.props.disabled) {
+                       style.pointerEvents = 'none';
+                       style.cursor = 'not-allowed';
+                       style.opacity = 0.25;
+               }
                return (
-                       <div className={className} onClick={this.props.onClick} title={title} draggable={draggable} onDragStart={this.props.onDragStart}>
+                       <div className={className} onClick={this.props.onClick} title={title} draggable={draggable} onDragStart={this.props.onDragStart} style={style}>
                                { src ? <img src={src} /> : null }
                                {label}
                        </div>
index 160db5f..331c1bf 100644 (file)
@@ -34,9 +34,11 @@ import CanvasPanelTray from './CanvasPanelTray'
 import EditForwardingGraphPaths from './EditorForwardingGraph/EditForwardingGraphPaths'
 import SelectionManager from '../libraries/SelectionManager'
 import DescriptorModelIconFactory from '../libraries/model/IconFactory'
-
 import FileManager from './filemanager/FileManager.jsx';
+import { isRBACValid } from 'widgets/skyquake_rbac/skyquakeRBAC';
+import ROLES from 'utils/roleConstants.js';
 
+import ConfigPrimitiveParameters from './ConfigPrimitiveParameters/ConfigPrimitiveParameters'
 import '../styles/CanvasPanel.scss'
 
 const CanvasPanel = React.createClass({
@@ -69,47 +71,62 @@ const CanvasPanel = React.createClass({
                        left: this.props.layout.left
                };
                var req = require.context("../", true, /^\.\/.*\.svg$/);
+               const User = this.props.User || {};
+               const isModifiableByUser = isRBACValid(User, [ROLES.PROJECT.PROJECT_ADMIN, ROLES.PROJECT.CATALOG_ADMIN]);
                const hasItem = this.props.containers.length !== 0;
                const isEditingNSD = DescriptorModelFactory.isNetworkService(this.props.containers[0]);
                const isDescriptorView = (this.props.panelTabShown == 'descriptor');
                const hasNoCatalogs = this.props.hasNoCatalogs;
-               const bodyComponent = hasItem ? <CatalogItemCanvasEditor zoom={this.props.zoom} isShowingMoreInfo={this.props.showMore} containers={this.props.containers}/> : messages.canvasWelcome();
+               const bodyComponent = hasItem ? (
+                       <CatalogItemCanvasEditor
+                               readOnly={!isModifiableByUser}
+                               zoom={this.props.zoom}
+                               isShowingMoreInfo={this.props.showMore}
+                               containers={this.props.containers} />) : messages.canvasWelcome();
                const viewFiles = this.props.panelTabShown == 'assets';
-               const viewButtonTabs =  !hasItem ? null : (
-                  <div className="CanvasPanelTabs">
-                      <div className="CatalogFilter">
-                                               <button className={isDescriptorView ? '-selected' : ''} onClick={ComposerAppActions.showDescriptor}>
-                                                       Descriptor
+               const viewButtonTabs = !hasItem ? null : (
+                       <div className="CanvasPanelTabs">
+                               <div className="CatalogFilter">
+                                       <button className={isDescriptorView ? '-selected' : ''} onClick={ComposerAppActions.showDescriptor}>
+                                               Descriptor
                                                </button>
-                                               {
-                                                       this.props.files ?
-                                                               <button className={!isDescriptorView ? '-selected' : ''}  onClick={ComposerAppActions.showAssets}>
-                                                                       Assets
+                                       {
+                                               this.props.files ?
+                                                       <button className={!isDescriptorView ? '-selected' : ''} onClick={ComposerAppActions.showAssets}>
+                                                               Assets
                                                                </button>
                                                        : null
-                                               }
-                                       </div>
+                                       }
                                </div>
-                       )
+                       </div>
+               )
+               //CanvasPanelTray panel to display
+               let displayedPanel = null;
+               switch (this.props.displayedPanel) {
+                       case 'forwarding': displayedPanel = (<EditForwardingGraphPaths containers={this.props.containers} />); break;
+                       case 'parameter': displayedPanel = (<ConfigPrimitiveParameters containers={this.props.containers} />); break;
+                       default: displayedPanel = (<div><p className="welcome-message">Please select a tab</p></div>); break;
+               }
                return (
-                       <div id="canvasPanelDiv" className="CanvasPanel" style={style} onDragOver={this.onDragOver} onDrop={this.onDrop}>
-                               <div onDoubleClick={this.onDblClickOpenFullScreen}  className="CanvasPanelHeader panel-header" data-resizable="limit_bottom">
+                       <div id="canvasPanelDiv" className="CanvasPanel" style={style} 
+                               onDragOver={isModifiableByUser ? this.onDragOver : null} onDrop={isModifiableByUser ? this.onDrop : null}>
+                               <div onDoubleClick={this.onDblClickOpenFullScreen} className="CanvasPanelHeader panel-header" data-resizable="limit_bottom">
                                        <h1>
                                                {hasItem ? <img src={req('./' + DescriptorModelIconFactory.getUrlForType(this.props.containers[0].type, 'black'))} width="20px" /> : null}
                                                <span className="model-name">{this.props.title}</span>
                                        </h1>
                                </div>
                                {viewButtonTabs}
-                               <div className="CanvasPanelBody panel-body" style={{marginRight: this.props.layout.right, bottom: this.props.layout.bottom}} >
-                                       {hasNoCatalogs ? null : viewFiles ? <FileManager files={this.props.files} type={this.props.type} item={this.props.item} filesState={this.props.filesState} newPathName={this.props.newPathName} /> : bodyComponent}
+                               <div className="CanvasPanelBody panel-body" style={{ marginRight: this.props.layout.right, bottom: this.props.layout.bottom }} >
+                                       {hasNoCatalogs ? null : viewFiles ? <FileManager files={this.props.files} type={this.props.type} item={this.props.item} filesState={this.props.filesState} newPathName={this.props.newPathName} User={User} /> : bodyComponent}
                                </div>
                                {
                                        isDescriptorView ?
-                                               <CanvasZoom zoom={this.props.zoom} style={{bottom: this.props.layout.bottom + 20}}/>
+                                               <CanvasZoom zoom={this.props.zoom} style={{ bottom: this.props.layout.bottom + 20 }} />
                                                : null
                                }
-                               <CanvasPanelTray layout={this.props.layout} show={isEditingNSD && isDescriptorView}>
-                                       <EditForwardingGraphPaths containers={this.props.containers} />
+                               <CanvasPanelTray layout={this.props.layout} displayedPanel={this.props.displayedPanel} show={isEditingNSD && isDescriptorView}>
+                                       {displayedPanel}
                                </CanvasPanelTray>
                        </div>
                );
@@ -135,7 +152,7 @@ const CanvasPanel = React.createClass({
        handleDropCanvasAction(event, data) {
                const action = cc.camel('on-' + data.action);
                if (typeof this[action] === 'function') {
-                       if (this[action]({clientX: event.clientX, clientY: event.clientY})) {
+                       if (this[action]({ clientX: event.clientX, clientY: event.clientY })) {
                                event.preventDefault();
                        }
                } else {
@@ -152,14 +169,14 @@ const CanvasPanel = React.createClass({
                } else if (DescriptorModelFactory.isNetworkService(currentItem)) {
                        // so add the item to the nsd and re-render the canvas
                        switch (data.item.uiState.type) {
-                       case 'vnfd':
-                               this.onAddVnfd(data.item, {clientX: event.clientX, clientY: event.clientY});
-                               break;
-                       case 'pnfd':
-                               this.onAddPnfd(data.item, {clientX: event.clientX, clientY: event.clientY});
-                               break;
-                       default:
-                               console.warn(`Unknown catalog-item type. Expect type "nsd", "vnfd" or "pnfd" but got ${data.item.uiState.type}.`);
+                               case 'vnfd':
+                                       this.onAddVnfd(data.item, { clientX: event.clientX, clientY: event.clientY });
+                                       break;
+                               case 'pnfd':
+                                       this.onAddPnfd(data.item, { clientX: event.clientX, clientY: event.clientY });
+                                       break;
+                               default:
+                                       console.warn(`Unknown catalog-item type. Expect type "nsd", "vnfd" or "pnfd" but got ${data.item.uiState.type}.`);
                        }
                } else {
                        // otherwise the default action is to open the item
index 57f29a0..5fde578 100644 (file)
@@ -16,13 +16,17 @@ export default function (props) {
                right: props.layout.right,
                display: props.show ? false : 'none'
        };
+    const PANEL = {
+        FORWARD: 'forwarding',
+        PARAMETER: 'parameter'
+    }
        const classNames = ClassNames('CanvasPanelTray', {'-with-transitions': !document.body.classList.contains('resizing')});
        function onClickToggleOpenClose(event) {
                if (event.defaultPrevented) return;
                event.preventDefault();
                // don't toggle if the user was resizing
                if (!uiTransient.isResizing) {
-                       CanvasPanelTrayActions.toggleOpenClose();
+                       CanvasPanelTrayActions.toggleOpenClose(event);
                }
                event.target.removeEventListener('mousemove', onMouseMove, true);
        }
@@ -37,10 +41,29 @@ export default function (props) {
        const isOpen = style.height > 25;
        return (
                <div className={classNames} data-resizable="top" data-resizable-handle-offset="4" style={style}>
-                       <h1 data-open-close-icon={isOpen ? 'open' : 'closed'} onMouseDownCapture={onMouseDown} onClick={onClickToggleOpenClose}>Forwarding Graphs</h1>
+            <div  className="CanvasPanelTray-buttons" onClick={onClickToggleOpenClose}>
+                <button
+                    style={{flex: '1 1 auto'}}
+                    className={ClassNames({'-selected': props.displayedPanel === PANEL.FORWARD})}
+                    onMouseDownCapture={onMouseDown}
+                    data-event={PANEL.FORWARD}>
+                    Forwarding Graphs
+                </button>
+
+                <button
+                    style={{flex: '1 1 auto', borderLeft: '1px solid white'}}
+                    className={ClassNames({'-selected': props.displayedPanel === PANEL.PARAMETER})}
+                    onMouseDownCapture={onMouseDown}
+                    data-event={PANEL.PARAMETER}>
+                        Config Parameter Map
+                </button>
+                <div  data-open-close-icon={isOpen ? 'open' : 'closed'}  style={{flex: '0 1', width: '40px', height: '25px', cursor: 'pointer'}} data-event='arrow'>
+
+                </div>
+            </div>
                        <div className="tray-body">
                                {props.children}
                        </div>
                </div>
        );
-}
\ No newline at end of file
+}
index 097596d..77c3497 100644 (file)
@@ -27,31 +27,28 @@ import ComposerAppStore from '../stores/ComposerAppStore'
 import '../styles/CatalogItemCanvasEditor.scss'
 import '../styles/DescriptorGraph.scss'
 
-const CatalogItemCanvasEditor = React.createClass({
-       mixins: [PureRenderMixin],
-       getInitialState() {
-               return {
-                       graph: null
-               };
-       },
-       getDefaultProps() {
-               return {
+class CatalogItemCanvasEditor extends React.Component {
+       constructor(props) {
+               super(props);
+               this.state = {
+                       graph: null,
                        zoom: 100,
                        containers: [],
                        isShowingMoreInfo: false
                };
-       },
-       componentWillMount() {
-       },
+       }
+
        componentDidMount() {
                const element = ReactDOM.findDOMNode(this.refs.descriptorGraph);
                const options = {
-                       zoom: this.props.zoom
+                       zoom: this.props.zoom,
+                       readOnly: this.props.readOnly
                };
                const graph = new DescriptorGraph(element, options);
                graph.containers = this.props.containers;
                this.setState({graph: graph});
-       },
+       }
+
        componentDidUpdate() {
                this.state.graph.containers = this.props.containers;
                const isNSD = this.props.containers[0] && this.props.containers[0].uiState.type === 'nsd';
@@ -61,10 +58,12 @@ const CatalogItemCanvasEditor = React.createClass({
                        this.state.graph.showMoreInfo = true;
                }
                this.state.graph.update();
-       },
+       }
+
        componentWillUnmount() {
                this.state.graph.destroy();
-       },
+       }
+
        render() {
                const graph = this.state.graph;
                if (graph) {
@@ -78,6 +77,6 @@ const CatalogItemCanvasEditor = React.createClass({
                        </div>
                );
        }
-});
+};
 
 export default CatalogItemCanvasEditor;
index 752d678..53efd78 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
 import React from 'react'
 import PureRenderMixin from 'react-addons-pure-render-mixin'
 import EditDescriptorModelProperties from './EditDescriptorModelProperties'
+import '../styles/CatalogItemDetailsEditor.scss'
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
 
 const CatalogItemDetailsEditor = React.createClass({
        mixins: [PureRenderMixin],
@@ -32,6 +37,10 @@ const CatalogItemDetailsEditor = React.createClass({
                        width: 0
                };
        },
+       contextTypes: {
+           router: React.PropTypes.object,
+           userProfile: React.PropTypes.object
+       },
        componentWillMount() {
        },
        componentDidMount() {
@@ -41,17 +50,36 @@ const CatalogItemDetailsEditor = React.createClass({
        componentWillUnmount() {
        },
        render() {
+       const User = this.context.userProfile;
 
-               const container = this.props.container || {model: {}, uiState: {}};
+               const container = this.props.container || { model: {}, uiState: {} };
                if (!(container && container.model && container.uiState)) {
                        return null;
                }
-
+               const height = this.props.height;
+               const style = height > 0 ? { height: height + 'px' } : {};
                return (
-                       <div className="CatalogItemDetailsEditor">
+                       <div className="CatalogItemDetailsEditor" style={style}>
                                <form name="details-descriptor-editor-form">
                                        <div className="properties-group">
-                                               <EditDescriptorModelProperties container={this.props.container} width={this.props.width} />
+                                       {
+                                               isRBACValid(User, [PROJECT_ROLES.PROJECT_ADMIN, PROJECT_ROLES.CATALOG_ADMIN]) ?
+                                               <EditDescriptorModelProperties
+                                                       container={this.props.container}
+                                                       idMaker={this.props.idMaker}
+                                                       showHelp={this.props.showHelp}
+                                                       collapsePanelsByDefault={this.props.collapsePanelsByDefault}
+                                                       openPanelsWithData={this.props.openPanelsWithData}
+                                                       width={this.props.width} />
+                                               :<EditDescriptorModelProperties
+                                                       container={this.props.container}
+                                                       idMaker={this.props.idMaker}
+                                                       showHelp={this.props.showHelp}
+                                                       collapsePanelsByDefault={this.props.collapsePanelsByDefault}
+                                                       openPanelsWithData={this.props.openPanelsWithData}
+                                                       width={this.props.width}
+                                                       readOnly={true} />
+                                        }
                                        </div>
                                </form>
                        </div>
@@ -59,5 +87,4 @@ const CatalogItemDetailsEditor = React.createClass({
 
        }
 });
-
 export default CatalogItemDetailsEditor;
index 78a18e0..1735dbc 100644 (file)
@@ -31,6 +31,29 @@ import SelectionManager from '../libraries/SelectionManager'
 import '../styles/CatalogItems.scss'
 import imgFile from 'file!../images/vendor-riftio.png'
 
+const DEFAULT_NSD_ICON = require('style/img/catalog-nsd-default.svg');
+const DEFAULT_VNFD_ICON = require('style/img/catalog-vnfd-default.svg');
+const DEFAULT_ICON = require('style/img/catalog-default.svg');
+
+function renderVersion (version) {
+       if (version) {
+               return (<span className='version'>{version}</span>);
+       } // else return null by default
+};
+function getImageErrorHandler (type) {
+       return type === 'nsd' ? handleNsdImageError : type === 'vnfd' ? handleVnfdImageError : handleImageError;
+}
+function handleImageError (e, image) {
+       console.log('Bad logo path, using default');
+       e.target.src = image || DEFAULT_ICON;
+};
+function handleNsdImageError (e) {
+       handleImageError(e, DEFAULT_NSD_ICON);
+};
+function handleVnfdImageError (e) {
+       handleImageError(e, DEFAULT_VNFD_ICON);
+};
+
 const CatalogItems = React.createClass({
        mixins: [PureRenderMixin],
        getInitialState() {
@@ -54,17 +77,7 @@ const CatalogItems = React.createClass({
        onChange(state) {
                this.setState(state);
        },
-       renderVersion(version) {
-               if (version) {
-                       return (<span className='version'>{version}</span>);
-               } // else return null by default
-       },
-       handleImageError(e) {
-               console.log('Bad logo path, using default');
-               e.target.src = require('style/img/catalog-default.svg');
-       },
        render() {
-               const self = this;
                const onDragStart = function(event) {
                        const data = {type: 'catalog-item', item: this};
                        event.dataTransfer.effectAllowed = 'copy';
@@ -78,7 +91,18 @@ const CatalogItems = React.createClass({
                        // single clicking an item is handled by ComposerApp::onClick handler
                        //CatalogItemsActions.selectCatalogItem(this);
                };
-               const cleanDataURI = this.cleanDataURI;
+               const cleanDataURI = function (imageString, type, id) {
+                       if (/\bbase64\b/g.test(imageString)) {
+                               return imageString;
+                       } else if (/<\?xml\b/g.test(imageString)) {
+                               const imgStr = imageString.substring(imageString.indexOf('<?xml'));
+                               return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(imgStr);
+                       } else if (/\.(svg|png|gif|jpeg|jpg)$/.test(imageString)) {
+                               return 'assets/logos/' + type + '/' + id + '/' + imageString;
+                               // return require('../images/logos/' + imageString);
+                       }
+                       return type === 'nsd' ? DEFAULT_NSD_ICON : type === 'vnfd' ? DEFAULT_VNFD_ICON : DEFAULT_ICON;
+               }
                const items = this.getCatalogItems().map(function (d) {
                        const isNSD = d.uiState.type === 'nsd';
                        const isVNFD = d.uiState.type === 'vnfd';
@@ -88,8 +112,6 @@ const CatalogItems = React.createClass({
                        const isOpenForEdit = d.uiState.isOpenForEdit;
                        const spanClassNames = ClassNames({'-is-selected': isSelected, '-is-open-for-edit': isOpenForEdit});
                        const sectionClassNames = ClassNames('catalog-item', {'-is-modified': isModified, '-is-deleted': isDeleted});
-                       const instanceCount = d.uiState['instance-ref-count'];
-                       const instanceCountLabel = isNSD && instanceCount ? <span>({instanceCount})</span> : null;
                        let type;
                        if(isNSD) {
                                type = 'nsd';
@@ -104,13 +126,13 @@ const CatalogItems = React.createClass({
                                                        {isModified ? <div className="-is-modified-indicator" title="This descriptor has changes."></div> : null}
                                                        <div className="type-header">{type}</div>
                                                        <dl>
-                                                               <dt className="name">{d.name} {instanceCountLabel}</dt>
+                                                               <dt className="name">{d.name}</dt>
                                                                <dd className="logo">
-                                                               <img className="logo" src={cleanDataURI(d['logo'], type, d.id)} draggable="false"  onError={self.handleImageError} />
+                                                               <img className="logo" src={cleanDataURI(d['logo'], type, d.id)} draggable="false"  onError={getImageErrorHandler(type)} />
                                                                </dd>
                                                                <dd className="short-name" title={d.name}>{d['short-name']}</dd>
                                                                <dd className="description">{d.description}</dd>
-                                                               <dd className="vendor">{d.vendor || d.provider} {self.renderVersion(d.version)}</dd>
+                                                               <dd className="vendor">{d.vendor || d.provider} {renderVersion(d.version)}</dd>
                                                        </dl>
                                                </div>
                                        </div>
@@ -126,21 +148,6 @@ const CatalogItems = React.createClass({
                        </div>
                );
        },
-       cleanDataURI(imageString, type, id) {
-               if (/\bbase64\b/g.test(imageString)) {
-                       return imageString;
-               } else if (/<\?xml\b/g.test(imageString)) {
-                       const imgStr = imageString.substring(imageString.indexOf('<?xml'));
-                       return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(imgStr);
-               } else if (/\.(svg|png|gif|jpeg|jpg)$/.test(imageString)) {
-                       return 'assets/logos/' + type + '/' + id + '/' + imageString;
-                       // return require('../images/logos/' + imageString);
-               }
-               if(type == 'nsd' || type == 'vnfd') {
-                       return require('style/img/catalog-'+type+'-default.svg');
-               }
-               return require('style/img/catalog-default.svg');
-       },
        getCatalogItems() {
                const catalogFilter = (d) => {return d.type === this.props.filterByType};
                return this.state.catalogs.filter(catalogFilter).reduce((result, catalog) => {
index 0314e19..8de0f00 100644 (file)
@@ -39,6 +39,8 @@ import ComposerAppStore from '../stores/ComposerAppStore'
 import CatalogPanelStore from '../stores/CatalogPanelStore'
 import LoadingIndicator from './LoadingIndicator'
 import SelectionManager from '../libraries/SelectionManager'
+import { isRBACValid } from 'widgets/skyquake_rbac/skyquakeRBAC';
+import ROLES from 'utils/roleConstants.js';
 
 import '../styles/CatalogPanel.scss'
 
@@ -80,7 +82,12 @@ const CatalogPanel = React.createClass({
                document.body.removeEventListener('dragend', this.onDragEnd);
                window.removeEventListener('dragend', this.onDragEnd);
        },
+       contextTypes: {
+               userProfile: React.PropTypes.object
+       },
        render() {
+               const User = this.context.userProfile || {};
+               const isModifiableByUser = isRBACValid(User, [ROLES.PROJECT.PROJECT_ADMIN, ROLES.PROJECT.CATALOG_ADMIN]);
 
                const onDropCatalogItem = e => {
                        e.preventDefault();
@@ -112,12 +119,30 @@ const CatalogPanel = React.createClass({
                const isDraggingFiles = uiTransientState.isDragging && uiTransientState.isDraggingFiles;
                const updateDropZone = createDropZone.bind(this, UploadDropZone.ACTIONS.update, '.action-update-catalog-package');
                const onboardDropZone = createDropZone.bind(this, UploadDropZone.ACTIONS.onboard, '.action-onboard-catalog-package');
-               const className = ClassNames('CatalogPanel', {'-is-tray-open': this.state.isTrayOpen});
+               const className = ClassNames('CatalogPanel', { '-is-tray-open': this.state.isTrayOpen });
                const hasNoCatalogs = this.props.hasNoCatalogs;
                const isLoading = this.props.isLoading;
+               const packageManagerPanel = (
+                       <CatalogPanelTray show={this.state.isTrayOpen}>
+                               <DropZonePanel show={isDraggingItem} title="Drop catalog item to export.">
+                                       <DropTarget className="action-export-catalog-items" onDrop={onDropCatalogItem}>
+                                               <span>Export catalog item.</span>
+                                       </DropTarget>
+                               </DropZonePanel>
+                               <DropZonePanel show={isDraggingFiles}>
+                                       <DropTarget className="action-onboard-catalog-package" dropZone={onboardDropZone} onDrop={onDropOnboardPackage}>
+                                               <span>On-board new catalog package.</span>
+                                       </DropTarget>
+                                       <DropTarget className="action-update-catalog-package" dropZone={updateDropZone} onDrop={onDropUpdatePackage}>
+                                               <span>Update existing catalog package.</span>
+                                       </DropTarget>
+                               </DropZonePanel>
+                               <CatalogPackageManager />
+                       </CatalogPanelTray>
+               );
                return (
-                       <div className={className} data-resizable="right" data-resizable-handle-offset="0 6" style={{width: this.props.layout.left}}>
-                               <CatalogPanelToolbar />
+                       <div className={className} data-resizable="right" data-resizable-handle-offset="0 6" style={{ width: this.props.layout.left }}>
+                               <CatalogPanelToolbar rbacDisabled={this.props.rbacDisabled} />
                                <div className="CatalogPanelBody">
                                        {(() => {
                                                if (isLoading) {
@@ -128,7 +153,7 @@ const CatalogPanel = React.createClass({
                                                        )
                                                }
                                                if (hasNoCatalogs) {
-                                                       return messages.catalogWelcome;
+                                                       return messages.catalogWelcome(isModifiableByUser);
                                                }
                                                return (
                                                        <div>
@@ -138,22 +163,7 @@ const CatalogPanel = React.createClass({
                                                );
                                        })()}
                                </div>
-                               <CatalogPanelTray show={this.state.isTrayOpen}>
-                                       <DropZonePanel show={isDraggingItem} title="Drop catalog item to export.">
-                                               <DropTarget className="action-export-catalog-items" onDrop={onDropCatalogItem}>
-                                                       <span>Export catalog item.</span>
-                                               </DropTarget>
-                                       </DropZonePanel>
-                                       <DropZonePanel show={isDraggingFiles}>
-                                               <DropTarget className="action-onboard-catalog-package" dropZone={onboardDropZone} onDrop={onDropOnboardPackage}>
-                                                       <span>On-board new catalog package.</span>
-                                               </DropTarget>
-                                               <DropTarget className="action-update-catalog-package" dropZone={updateDropZone} onDrop={onDropUpdatePackage}>
-                                                       <span>Update existing catalog package.</span>
-                                               </DropTarget>
-                                       </DropZonePanel>
-                                       <CatalogPackageManager />
-                               </CatalogPanelTray>
+                               {isModifiableByUser ? packageManagerPanel : null}
                        </div>
                );
 
index 1501ecd..1b31085 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -32,6 +32,10 @@ import imgOnboard from '../../../node_modules/open-iconic/svg/cloud-upload.svg'
 import imgUpdate from '../../../node_modules/open-iconic/svg/rain.svg'
 import imgDownload from '../../../node_modules/open-iconic/svg/cloud-download.svg'
 import imgDelete from '../../../node_modules/open-iconic/svg/trash.svg'
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
 
 const CatalogHeader = React.createClass({
        mixins: [PureRenderMixin],
@@ -48,28 +52,32 @@ const CatalogHeader = React.createClass({
        },
        componentWillUnmount() {
        },
+       contextTypes: {
+           userProfile: React.PropTypes.object
+       },
        render() {
+               const disabled = !isRBACValid(this.context.userProfile, [PROJECT_ROLES.PROJECT_ADMIN, PROJECT_ROLES.CATALOG_ADMIN]);
                return (
                        <div className="CatalogPanelToolbar">
                                <h1>Descriptor Catalogs</h1>
                                <div className="btn-bar">
                                        <div className="btn-group">
-                                               <Button type="image" title="OnBoard a catalog package" className="action-onboard-catalog-package" onClick={this.onClickOnBoardCatalog} src={imgOnboard} />
-                                               <Button type="image" title="Update a catalog package" className="action-update-catalog-package" onClick={this.onClickUpdateCatalog} src={imgUpdate} />
-                                               <Button type="image" title="Export selected catalog item(s)" className="action-export-catalog-items" onClick={this.onClickExportCatalogItems} src={imgDownload} />
+                                               <Button type="image" title="OnBoard a catalog package" className="action-onboard-catalog-package" onClick={this.onClickOnBoardCatalog} src={imgOnboard} disabled={disabled}/>
+                                               <Button type="image" title="Update a catalog package" className="action-update-catalog-package" onClick={this.onClickUpdateCatalog} src={imgUpdate}  disabled={disabled}/>
+                                               <Button type="image" title="Export selected catalog item(s)" className="action-export-catalog-items" onClick={this.onClickExportCatalogItems} src={imgDownload}  disabled={disabled}/>
                                        </div>
                                        <div className="btn-group">
                                                <div className="menu">
-                                                       <Button type="image" title="Create a new catalog item" className="action-create-catalog-item" onClick={this.onClickCreateCatalogItem.bind(null, 'nsd')} src={imgAdd} />
+                                                       <Button type="image" title="Create a new catalog item" className="action-create-catalog-item" onClick={this.onClickCreateCatalogItem.bind(null, 'nsd')} src={imgAdd}  disabled={disabled}/>
                                                        <div className="sub-menu">
-                                                               <Button type="image" title="Create a new catalog item" className="action-create-catalog-item" onClick={this.onClickCreateCatalogItem.bind(null, 'nsd')} src={imgAdd} label="Add NSD" />
-                                                               <Button type="image" title="Create a new catalog item" className="action-create-catalog-item" onClick={this.onClickCreateCatalogItem.bind(null, 'vnfd')} src={imgAdd} label="Add VNFD" />
+                                                               <Button type="image" title="Create a new catalog item" className="action-create-catalog-item" onClick={this.onClickCreateCatalogItem.bind(null, 'nsd')} src={imgAdd} label="Add NSD"  disabled={disabled}/>
+                                                               <Button type="image" title="Create a new catalog item" className="action-create-catalog-item" onClick={this.onClickCreateCatalogItem.bind(null, 'vnfd')} src={imgAdd} label="Add VNFD"  disabled={disabled}/>
                                                        </div>
                                                </div>
-                                               <Button type="image" title="Copy catalog item" className="action-copy-catalog-item" onClick={this.onClickDuplicateCatalogItem} src={imgCopy} />
+                                               <Button type="image" title="Copy catalog item" className="action-copy-catalog-item" onClick={this.onClickDuplicateCatalogItem} src={imgCopy}  disabled={disabled}/>
                                        </div>
                                        <div className="btn-group">
-                                               <Button type="image" title="Delete catalog item" className="action-delete-catalog-item" onClick = {this.onClickDeleteCatalogItem} src={imgDelete} />
+                                               <Button type="image" title="Delete catalog item" className="action-delete-catalog-item" onClick = {this.onClickDeleteCatalogItem} src={imgDelete}  disabled={disabled}/>
                                        </div>
                                </div>
                        </div>
index 861c38b..d7a40d3 100644 (file)
@@ -48,6 +48,11 @@ import TooltipManager from '../libraries/TooltipManager'
 import CatalogItemsActions from '../actions/CatalogItemsActions'
 import CommonUtils from 'utils/utils.js'
 import FileManagerActions from './filemanager/FileManagerActions';
+import { SkyquakeRBAC, isRBACValid } from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+import _isEmpty from 'lodash/isEmpty';
+import _isEqual from 'lodash/isEqual';
+
 import 'normalize.css'
 import '../styles/AppRoot.scss'
 import 'style/layout.scss'
@@ -60,214 +65,221 @@ const clearLocalStorage = utils.getSearchParams(window.location).hasOwnProperty(
 const preventDefault = e => e.preventDefault();
 const clearDragState = () => ComposerAppActions.setDragState(null);
 
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
+const CATALOG_POLLING_INTERVAL = 2000;
+
 
 const ComposerApp = React.createClass({
-       mixins: [PureRenderMixin],
-       getInitialState() {
-               return ComposerAppStore.getState();
-       },
-       getDefaultProps() {
-               return {};
-       },
-       componentWillMount() {
-               if (clearLocalStorage) {
-                       window.localStorage.clear();
-               }
-        if(this.item) {
+    getInitialState() {
+        return ComposerAppStore.getState();
+    },
+    getDefaultProps() {
+        return {};
+    },
+    contextTypes: {
+        router: React.PropTypes.object,
+        userProfile: React.PropTypes.object
+    },
+    componentWillUpdate(nextProps, nextState, nextContext) {
+        if (!_isEmpty(nextContext.userProfile)) {
+            CatalogDataStore.setUserProfile(nextContext.userProfile);
+        }
+    },
+    shouldComponentUpdate: function (nextProps, nextState, nextContext) {
+        if (!this.userProfile && !_isEmpty(nextContext.userProfile)) {
+            this.userProfile = nextContext.userProfile;
+            CatalogDataStore.setUserProfile(nextContext.userProfile);
+            return true;
+        }
+        return !_isEqual(this.props, nextProps) ||
+            !_isEqual(this.state, nextState);
+    },
+    componentWillMount() {
+        if (clearLocalStorage) {
+            window.localStorage.clear();
+        }
+        if (this.item) {
             FileManagerActions.openFileManagerSockets();
         }
-               this.state.isLoading = CatalogDataStore.getState().isLoading;
-               ComposerAppStore.listen(this.onChange);
-               CatalogDataStore.listen(this.onCatalogDataChanged);
-               window.addEventListener('resize', this.resize);
-               window.onbeforeunload = this.onBeforeUnload;
-               // prevent browser from downloading any drop outside of our specific drop zones
-               window.addEventListener('dragover', preventDefault);
-               window.addEventListener('drop', preventDefault);
-               // ensure drags initiated in the app clear the state on drop
-               window.addEventListener('drop', clearDragState);
-               DeletionManager.addEventListeners();
-       },
-       componentWillUnmount() {
-               window.removeEventListener('resize', this.resize);
-               window.removeEventListener('dragover', preventDefault);
-               window.removeEventListener('drop', preventDefault);
-               window.removeEventListener('drop', clearDragState);
+        this.state.isLoading = CatalogDataStore.getState().isLoading;
+        ComposerAppStore.listen(this.onChange);
+        CatalogDataStore.listen(this.onCatalogDataChanged);
+        window.addEventListener('resize', this.resize);
+        // prevent browser from downloading any drop outside of our specific drop zones
+        window.addEventListener('dragover', preventDefault);
+        window.addEventListener('drop', preventDefault);
+        // ensure drags initiated in the app clear the state on drop
+        window.addEventListener('drop', clearDragState);
+        DeletionManager.addEventListeners();
+    },
+    componentWillUnmount() {
+        window.removeEventListener('resize', this.resize);
+        window.removeEventListener('dragover', preventDefault);
+        window.removeEventListener('drop', preventDefault);
+        window.removeEventListener('drop', clearDragState);
         FileManagerActions.closeFileManagerSockets();
-               // resizeManager automatically registered its event handlers
-               resizeManager.removeAllEventListeners();
-               ComposerAppStore.unlisten(this.onChange);
-               CatalogDataStore.unlisten(this.onCatalogDataChanged);
-               DeletionManager.removeEventListeners();
-               TooltipManager.removeEventListeners();
-       },
-       componentDidMount() {
-               resizeManager.addAllEventListeners();
-               const snapshot = window.localStorage.getItem('composer');
-               if (snapshot) {
-                       alt.bootstrap(snapshot);
-               }
-               document.body.addEventListener('keydown', (event) => {
-                       // prevent details editor form from blowing up the app
-                       const ENTER_KEY = 13;
-                       if (event.which === ENTER_KEY) {
-                               event.preventDefault();
-                               return false;
-                       }
-               });
-               const appRootElement = ReactDOM.findDOMNode(this.refs.appRoot);
-               TooltipManager.addEventListeners(appRootElement);
-               SelectionManager.onClearSelection = () => {
-                       if (this.state.item) {
-                               CatalogItemsActions.catalogItemMetaDataChanged.defer(this.state.item);
-                       }
-               };
-       },
-       componentDidUpdate() {
-               if (this.state.fullScreenMode) {
-                       document.body.classList.add('-is-full-screen');
-               } else {
-                       document.body.classList.remove('-is-full-screen');
-               }
-               SelectionManager.refreshOutline();
-       },
-       resize(e) {
-               PanelResizeAction.resize(e);
-       },
-       getModel() {
-               let html;
-               let self = this;
-               DescriptorModelMetaFactory.init().then(function(){
-
-                       self.setState({
-                               hasModel: true
-                       })
-               });
-       },
-       render() {
-               let html = null;
-               let self = this;
-               if(this.state.hasModel) {
+        // resizeManager automatically registered its event handlers
+        resizeManager.removeAllEventListeners();
+        ComposerAppStore.unlisten(this.onChange);
+        CatalogDataStore.unlisten(this.onCatalogDataChanged);
+        DeletionManager.removeEventListeners();
+        TooltipManager.removeEventListeners();
+        if (this.catalogMonitorId) {
+            clearTimeout(this.catalogMonitorId);
+        }
+    },
+    componentDidMount() {
+        resizeManager.addAllEventListeners();
+        const snapshot = window.localStorage.getItem('composer');
+        if (snapshot) {
+            alt.bootstrap(snapshot);
+        }
+        document.body.addEventListener('keydown', (event) => {
+            // prevent details editor form from blowing up the app
+            const ENTER_KEY = 13;
+            if (event.which === ENTER_KEY) {
+                event.preventDefault();
+                return false;
+            }
+        });
+        const loadCatalogs = () => {
+            CatalogDataStore.loadCatalogs();
+            if (CATALOG_POLLING_INTERVAL) {
+                this.catalogMonitorId = setTimeout(loadCatalogs, CATALOG_POLLING_INTERVAL);
+            }
+        };
+        loadCatalogs();
+        DescriptorModelMetaFactory.init().then(() => this.setState({ hasModel: true }));
+    },
+    componentDidUpdate() {
+        if (this.state.fullScreenMode) {
+            document.body.classList.add('-is-full-screen');
+        } else {
+            document.body.classList.remove('-is-full-screen');
+        }
+        SelectionManager.refreshOutline();
+    },
+    resize(e) {
+        PanelResizeAction.resize(e);
+    },
+    render() {
+        let html = null;
+        let self = this;
+        const User = this.userProfile || {};
+        const rbacDisabled = !isRBACValid(User, [PROJECT_ROLES.PROJECT_ADMIN, PROJECT_ROLES.CATALOG_ADMIN]);
+        if (this.state.hasModel) {
 
-                       function onClickUpdateSelection(event) {
-                               if (event.defaultPrevented) {
-                                       return
-                               }
-                               const element = SelectionManager.getClosestElementWithUID(event.target);
-                               if (element) {
-                                       SelectionManager.select(element);
-                                       SelectionManager.refreshOutline();
-                                       event.preventDefault();
-                               } else {
-                                       SelectionManager.clearSelectionAndRemoveOutline();
-                               }
-                       }
+            function onClickUpdateSelection(event) {
+                if (event.defaultPrevented) {
+                    return
+                }
+                const element = SelectionManager.getClosestElementWithUID(event.target);
+                if (element) {
+                    SelectionManager.select(element);
+                    SelectionManager.refreshOutline();
+                    event.preventDefault();
+                } else {
+                    if (event.target.offsetParent && !event.target.offsetParent.classList.contains("tray-body")) {
+                        SelectionManager.clearSelectionAndRemoveOutline();
+                    }
+                }
+            }
 
+            let cpNumber = 0;
+            let AppHeader = (<div className="AppHeader">
+                <RiftHeader />
+            </div>);
+            // AppHeader = null;
+            const classNames = ClassNames('ComposerApp');
+            const isNew = self.state.item && self.state.item.uiState.isNew;
+            const hasItem = self.state.item && self.state.item.uiState;
+            const isModified = self.state.item && self.state.item.uiState.modified;
+            const isEditingNSD = self.state.item && self.state.item.uiState && /nsd/.test(self.state.item.uiState.type);
+            const isEditingVNFD = self.state.item && self.state.item.uiState && /vnfd/.test(self.state.item.uiState.type);
+            const containers = [self.state.item].reduce(DescriptorModelFactory.buildCatalogItemFactory(CatalogDataStore.getState().catalogs), []);
 
-                       let AppHeader = (<div className="AppHeader">
-                                                               <RiftHeader />
-                                                       </div>);
-                       // AppHeader = null;
-                       const classNames = ClassNames('ComposerApp');
-                       const isNew = self.state.item && self.state.item.uiState.isNew;
-                       const hasItem = self.state.item && self.state.item.uiState;
-                       const isModified = self.state.item && self.state.item.uiState.modified;
-                       const isEditingNSD = self.state.item && self.state.item.uiState && /nsd/.test(self.state.item.uiState.type);
-                       const isEditingVNFD = self.state.item && self.state.item.uiState && /vnfd/.test(self.state.item.uiState.type);
-            const containers = self.state.containers;
-                       const canvasTitle = containers.length ? containers[0].model.name : '';
-                       const hasNoCatalogs = CatalogDataStore.getState().catalogs.length === 0;
-                       const isLoading = self.state.isLoading;
+            containers.filter(d => DescriptorModelFactory.isConnectionPoint(d)).forEach(d => {
+                d.cpNumber = ++cpNumber;
+                containers.filter(d => DescriptorModelFactory.isVnfdConnectionPointRef(d)).filter(ref => ref.key === d.key).forEach(ref => ref.cpNumber = d.cpNumber);
+            });
+            const canvasTitle = containers.length ? containers[0].model.name : '';
+            const hasNoCatalogs = CatalogDataStore.getState().catalogs.length === 0;
+            const isLoading = self.state.isLoading;
 
             //Bridge element for Crouton fix. Should eventually put Composer on same flux context
             const Bridge = this.state.ComponentBridgeElement;
 
-                       html = (
-                               <div ref="appRoot" id="RIFT_wareLaunchpadComposerAppRoot" className="AppRoot" onClick={onClickUpdateSelection}>
+            html = (
+                <div ref={element => TooltipManager.addEventListeners(element)} id="RIFT_wareLaunchpadComposerAppRoot" className="AppRoot" onClick={onClickUpdateSelection}>
                     <Bridge />
-                                       <i className="corner-accent top left" />
-                                       <i className="corner-accent top right" />
-                                       <i className="corner-accent bottom left" />
-                                       <i className="corner-accent bottom right" />
-                                       {AppHeader}
-                                       <div className="AppBody">
-                                               <div className={classNames}>
-                                                       <CatalogPanel layout={self.state.layout}
-                                                                                 isLoading={isLoading}
-                                                                                 hasNoCatalogs={hasNoCatalogs}
-                                                                                 filterByType={self.state.filterCatalogByTypeValue} />
-                                                       <CanvasPanel layout={self.state.layout}
-                                                                                hasNoCatalogs={hasNoCatalogs}
-                                                                                showMore={self.state.showMore}
-                                                                                containers={containers}
-                                                                                title={canvasTitle}
-                                                                                zoom={self.state.zoom}
-                                                                                panelTabShown={self.state.panelTabShown}
-                                                                                files={self.state.files}
-                                                                                filesState={self.state.filesState}
-                                         newPathName={self.state.newPathName}
-                                                                                item={self.state.item}
-                                                                                type={self.state.filterCatalogByTypeValue}
-                                                                                 />
-                                                       {
-                                                               (self.state.panelTabShown == 'descriptor') ?
-                                                               <DetailsPanel layout={self.state.layout}
-                                                                                 hasNoCatalogs={hasNoCatalogs}
-                                                                                 showMore={self.state.showMore}
-                                                                                 containers={containers}
-                                                                                 showJSONViewer={self.state.showJSONViewer} />
-                                                         : null
-                                                       }
-
-                                                       <ComposerAppToolbar layout={self.state.layout}
-                                                                                               showMore={self.state.showMore}
-                                                                                               isEditingNSD={isEditingNSD}
-                                                                                               isEditingVNFD={isEditingVNFD}
-                                                                                               isModified={isModified}
-                                                                                               isNew={isNew}
-                                                                                               disabled={!hasItem}
-                                                                                               onClick={event => event.stopPropagation()}
-                                                                                               panelTabShown={self.state.panelTabShown}/>
-                                               </div>
-                                       </div>
-                                       <ModalOverlay />
-                               </div>
-                       );
-               } else {
-                       this.getModel();
-               }
-               return html;
-       },
-       onChange(state) {
-               this.setState(state);
-       },
-       onCatalogDataChanged(catalogDataState) {
-               const catalogs = catalogDataState.catalogs;
-               const unsavedChanges = catalogs.reduce((result, catalog) => {
-                       if (result) {
-                               return result;
-                       }
-                       return catalog.descriptors.reduce((result, descriptor) => {
-                               if (result) {
-                                       return result;
-                               }
-                               return descriptor.uiState.modified;
-                       }, false);
-               }, false);
-               this.setState({
-                       unsavedChanges: unsavedChanges,
-                       isLoading: catalogDataState.isLoading
-               });
-       },
-       onBeforeUnload() {
-               // https://trello.com/c/c8v321Xx/160-prompt-user-to-save-changes
-               //const snapshot = alt.takeSnapshot();
-               //window.localStorage.setItem('composer', snapshot);
-               if (this.state.unsavedChanges) {
-                       return 'You have unsaved changes. If you do not onboard (or update) your changes they will be lost.';
-               }
-       }
+                    <i className="corner-accent top left" />
+                    <i className="corner-accent top right" />
+                    <i className="corner-accent bottom left" />
+                    <i className="corner-accent bottom right" />
+                    {AppHeader}
+                    <div className="AppBody">
+                        <div className={classNames}>
+                            <CatalogPanel layout={self.state.layout}
+                                isLoading={isLoading}
+                                hasNoCatalogs={hasNoCatalogs}
+                                filterByType={self.state.filterCatalogByTypeValue}
+                                rbacDisabled={rbacDisabled} />
+                            <CanvasPanel layout={self.state.layout}
+                                hasNoCatalogs={hasNoCatalogs}
+                                showMore={self.state.showMore}
+                                containers={containers}
+                                title={canvasTitle}
+                                zoom={self.state.zoom}
+                                panelTabShown={self.state.panelTabShown}
+                                files={self.state.files}
+                                filesState={self.state.filesState}
+                                newPathName={self.state.newPathName}
+                                item={self.state.item}
+                                type={self.state.filterCatalogByTypeValue}
+                                rbacDisabled={rbacDisabled}
+                                User={User}
+                            />
+                            {
+                                (self.state.panelTabShown == 'descriptor') ?
+                                    <DetailsPanel layout={self.state.layout}
+                                        hasNoCatalogs={hasNoCatalogs}
+                                        showMore={self.state.showMore}
+                                        containers={containers}
+                                        showHelp={self.state.showHelp}
+                                        collapsePanelsByDefault={self.state.collapsePanelsByDefault}
+                                        openPanelsWithData={self.state.openPanelsWithData}
+                                        showJSONViewer={self.state.showJSONViewer} />
+                                    : null
+                            }
 
+                            <ComposerAppToolbar layout={self.state.layout}
+                                showMore={self.state.showMore}
+                                isEditingNSD={isEditingNSD}
+                                isEditingVNFD={isEditingVNFD}
+                                isModified={isModified}
+                                isNew={isNew}
+                                disabled={!hasItem || rbacDisabled}
+                                onClick={event => event.stopPropagation()}
+                                panelTabShown={self.state.panelTabShown} />
+                        </div>
+                    </div>
+                    <ModalOverlay />
+                </div>
+            );
+        }
+        return html;
+    },
+    onChange(state) {
+        this.setState(state);
+    },
+    onCatalogDataChanged(catalogDataState) {
+        this.setState({
+            isLoading: catalogDataState.isLoading
+        });
+    }
 });
 
+
 export default ComposerApp;
index 458e774..0a2c860 100644 (file)
@@ -136,11 +136,8 @@ const ComposerAppToolbar = React.createClass({
                const style = {left: this.props.layout.left};
                const saveClasses = ClassNames('ComposerAppSave', {'primary-action': this.props.isModified || this.props.isNew});
                const cancelClasses = ClassNames('ComposerAppCancel', {'secondary-action': this.props.isModified});
-               if (this.props.disabled) {
-                       return (
-                               <div className="ComposerAppToolbar" style={style}></div>
-                       );
-               }
+               let isDisabled = this.props.disabled;
+               // console.log('rbacDisabled', isDisabled )
                const hasSelection = SelectionManager.getSelections().length > 0;
                if(this.props.panelTabShown != 'descriptor') {
                        style.pointerEvents = 'none';
@@ -156,35 +153,38 @@ const ComposerAppToolbar = React.createClass({
                                        if (this.props.isEditingNSD || this.props.isEditingVNFD) {
                                                return (
                                                        <div className="FileActions">
-                                                               <Button className={saveClasses} onClick={this.onClickSave} label={messages.getSaveActionLabel(this.props.isNew)} src={imgSave} />
-                                                               <Button className={cancelClasses} onClick={this.onClickCancel} label="Cancel" src={imgCancel} />
-                                                               <Button className="ComposerAppToggleJSONViewerAction" onClick={this.toggleJSONViewer} label="YAML Viewer" src={imgJSONViewer} />
+                                                               <Button className={saveClasses} onClick={this.onClickSave} label={messages.getSaveActionLabel(this.props.isNew)} src={imgSave} disabled={isDisabled} />
+                                                               <Button className={cancelClasses} onClick={this.onClickCancel} label="Cancel" src={imgCancel} disabled={isDisabled} />
+                                                               <Button className="ComposerAppToggleJSONViewerAction" onClick={this.toggleJSONViewer} label="YAML Viewer" src={imgJSONViewer} disabled={isDisabled} />
                                                        </div>
                                                );
                                        }
                                })()}
                                <div className="LayoutActions">
-                                       <Button className="action-auto-layout" onClick={this.onClickAutoLayout} label="Auto Layout" src={imgLayout} />
+                                       <Button className="action-auto-layout" onClick={this.onClickAutoLayout} label="Auto Layout" src={imgLayout} disabled={isDisabled} />
                                        {this.props.isEditingNSD ||
                                                this.props.isEditingVNFD ? <Button className="action-add-vld"
+                                                                                                                  disabled={isDisabled}
                                                                                                                   draggable="true"
                                                                                                                   label={this.props.isEditingNSD ? 'Add VLD' : 'Add IVLD'}
                                                                                                                   src={imgVLD}
                                                                                                                   onDragStart={this.onDragStartAddVld}
                                                                                                                   onClick={this.onClickAddVld} /> : null}
                                        {this.props.isEditingNSD ? <Button className="action-add-vnffg"
+                                                                                                          disabled={isDisabled}
                                                                                                           draggable="true"
                                                                                                           label="Add VNFFG"
                                                                                                           src={imgFG}
                                                                                                           onDragStart={this.onDragStartAddVnffg}
                                                                                                           onClick={this.onClickAddVnffg} /> : null}
                                        {this.props.isEditingVNFD ? <Button className="action-add-vdu"
+                                                                                                               disabled={isDisabled}
                                                                                                                draggable="true"
                                                                                                                label="Add VDU"
                                                                                                                src={imgVDU}
                                                                                                                onDragStart={this.onDragStartAddVdu}
                                                                                                                onClick={this.onClickAddVdu} /> : null}
-                                       <Button type="image" title="Delete selected items" className="action-delete-selected-items" disabled={!hasSelection} onClick = {this.onClickDeleteSelected} src={imgDelete} label="Delete" />
+                                       <Button type="image" title="Delete selected items" className="action-delete-selected-items" disabled={!hasSelection || isDisabled} onClick = {this.onClickDeleteSelected} src={imgDelete} label="Delete" />
                                </div>
                        </div>
                );
diff --git a/skyquake/plugins/composer/src/src/components/ConfigPrimitiveParameters/ConfigPrimitiveParameters.js b/skyquake/plugins/composer/src/src/components/ConfigPrimitiveParameters/ConfigPrimitiveParameters.js
new file mode 100644 (file)
index 0000000..8feee34
--- /dev/null
@@ -0,0 +1,354 @@
+
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+
+'use strict';
+
+import _ from 'lodash'
+import d3 from 'd3'
+import React from 'react'
+import Range from '../Range'
+import Button from '../Button'
+import ClassNames from 'classnames'
+import changeCase from 'change-case'
+import LayoutRow from '../LayoutRow'
+import SelectionManager from '../../libraries/SelectionManager'
+import PureRenderMixin from 'react-addons-pure-render-mixin'
+import CatalogItemsActions from '../../actions/CatalogItemsActions'
+import CanvasEditorActions from '../../actions/CanvasEditorActions'
+import DescriptorModelFactory from '../../libraries/model/DescriptorModelFactory'
+import ComposerAppActions from '../../actions/ComposerAppActions'
+import DescriptorModelMetaFactory from '../../libraries/model/DescriptorModelMetaFactory'
+import ComposerAppStore from '../../stores/ComposerAppStore'
+import DeletionManager from '../../libraries/DeletionManager'
+import ContentEditableDiv from '../ContentEditableDiv'
+import TooltipManager from '../../libraries/TooltipManager'
+import HighlightRecordServicePaths from '../../libraries/graph/HighlightRecordServicePaths'
+
+import '../../styles/EditForwardingGraphPaths.scss'
+
+import imgNSD from '../../images/default-catalog-icon.svg'
+import imgFG from '../../../../node_modules/open-iconic/svg/infinity.svg'
+import imgRemove from '../../../../node_modules/open-iconic/svg/trash.svg'
+import imgAdd from '../../../../node_modules/open-iconic/svg/plus.svg'
+import imgConnection from '../../../../node_modules/open-iconic/svg/random.svg'
+import imgClassifier from '../../../../node_modules/open-iconic/svg/spreadsheet.svg'
+import imgReorder from '../../../../node_modules/open-iconic/svg/menu.svg'
+import CatalogDataStore from '../../stores/CatalogDataStore'
+import utils from '../../libraries/utils'
+import getEventPath from '../../libraries/getEventPath'
+import guid from '../../libraries/guid'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+import '../../styles/EditConfigParameterMap.scss';
+
+function configParameterMapMap(ap, i) {
+
+    const context = this;
+    context.vnfapMap = ap;
+    return (
+        <div key={i}>
+        <div>{ap.id}</div>
+        <div>{ap.capability['member-vnf-index']}</div>
+        <div>{ap.capability['capability-ref']}</div>
+
+        </div>
+    )
+
+}
+
+function mapNSD(nsd, i) {
+
+    const context = this;
+    context.nsd = nsd;
+
+    function onClickAddConfigParameterMap(nsd, event) {
+        event.preventDefault();
+        nsd.createConfigParameterMap();
+        CatalogItemsActions.catalogItemDescriptorChanged(nsd.getRoot());
+    }
+
+    const forwardingGraphs = nsd.configParameterMap.map(configParameterMap.bind(context));
+    if (forwardingGraphs.length === 0) {
+        forwardingGraphs.push(
+            <div key="1" className="welcome-message">
+                No Forwarding Graphs to model.
+            </div>
+        );
+    }
+
+    return (
+        <div key={i} className={nsd.className}>
+            {forwardingGraphs}
+            <div className="footer-actions">
+                <div className="row-action-column">
+                    <Button className="create-new-forwarding-graph" src={imgAdd} width="20px" onClick={onClickAddConfigParameterMap.bind(null, nsd)} label="Add new Access Point" />
+                </div>
+            </div>
+        </div>
+    );
+
+}
+
+
+function startEditing() {
+        event.stopPropagation();
+        DeletionManager.removeEventListeners();
+    }
+
+function endEditing() {
+    DeletionManager.addEventListeners();
+}
+
+
+const ConfigPrimitiveParameters = React.createClass({
+    mixins: [PureRenderMixin],
+    getInitialState: function () {
+        return ComposerAppStore.getState();
+    },
+    getDefaultProps: function () {
+        return {
+            containers: []
+        };
+    },
+    componentWillMount: function () {
+    },
+    componentDidMount: function () {
+    },
+    componentDidUpdate: function () {
+    },
+    componentWillUnmount: function () {
+    },
+    render() {
+        const self = this;
+        const containers = this.props.containers;
+        let NSContainer = containers.filter(function(c) {
+           return c.className == "NetworkService"
+        })[0]
+        const context = {
+            component: this,
+            containers: containers
+        };
+
+        const networkService = containers.filter(d => d.type === 'nsd');
+        if (networkService.length === 0) {
+            return <p className="welcome-message">No <img src={imgNSD} width="20px" /> NSD open in the canvas. Try opening an NSD.</p>;
+        }
+        let MapData = constructRequestSourceData(containers);
+        let mapCounter = 1;
+
+
+
+        return (
+                <div className="ConfigParameterMap">
+
+                        <div className="config-parameter-map">
+                            <div className="config-parameter-titles">
+                                <div className="config-parameter">
+                                    Primitive Parameter Request
+                                </div>
+                                <div className="config-parameter">
+                                    Data Source
+                                </div>
+                            </div>
+                            {
+                                MapData.Requests.map(function(r, i) {
+                                    let currentValue = {};
+                                    let SourceOptions = [<option value={JSON.stringify({
+                                            requestValue: r.name,
+                                            requestIndex: r.vnfdIndex
+                                        })} key="reset">No Source Selected</option>]
+                                    MapData.Sources.map(function(s, j) {
+                                        let value = {
+                                            value: s.name,
+                                            index: s.vnfdIndex,
+                                            requestValue: r.name,
+                                            requestIndex: r.vnfdIndex
+                                        }
+                                        if (r.vnfdIndex !== s.vnfdIndex) {
+                                            SourceOptions.push(<option value={JSON.stringify(value)} key={`${j}-${i}`} >{`${s.vnfdName} (${s.vnfdIndex}): ${s.name}`}</option>)
+                                        }
+                                    })
+                                    //Finds current value
+                                    NSContainer.model['config-parameter-map'] && NSContainer.model['config-parameter-map'].map((c)=>{
+                                        if(
+                                            c['config-parameter-request'] &&
+                                            (c['config-parameter-request']['config-parameter-request-ref'] == r.name)
+                                            && (c['config-parameter-request']['member-vnf-index-ref'] == r.vnfdIndex)
+                                           ) {
+                                            currentValue = {
+                                                value: c['config-parameter-source']['config-parameter-source-ref'],
+                                                index: c['config-parameter-source']['member-vnf-index-ref'],
+                                                requestValue: r.name,
+                                                requestIndex: r.vnfdIndex
+                                            };
+                                        }
+                                    })
+                                    currentValue.hasOwnProperty('value') ? mapCounter++ : mapCounter--;
+                                    let currentMapIndex = (mapCounter > 0) ? (mapCounter) - 1: 0;
+                                    return (
+                                            <div key={i} className="EditDescriptorModelProperties -is-tree-view config-parameter config-parameter-group">
+                                                <div  className="config-parameter-request" >{`${r.vnfdName} (${r.vnfdIndex}): ${r.name}`}</div>
+                                                <div className="config-parameter-source">
+                                                    <select
+                                                        onChange={onFormFieldValueChanged.bind(NSContainer, i)}
+                                                        onBlur={endEditing}
+                                                        onMouseDown={startEditing}
+                                                        onMouseOver={startEditing}
+                                                        value={JSON.stringify(currentValue)}
+                                                         >
+                                                        }
+                                                        {SourceOptions}
+                                                    </select>
+                                                </div>
+                                            </div>
+                                    )
+                                })
+                            }
+                        </div>
+                </div>
+        )
+    }
+});
+
+    function onFormFieldValueChanged(index, event) {
+        if (DescriptorModelFactory.isContainer(this)) {
+            event.preventDefault();
+            const name = event.target.name;
+            const value = JSON.parse(event.target.value);
+
+            let ConfigMap = utils.resolvePath(this.model, 'config-parameter-map');
+            let ConfigMapIndex = false;
+            let id = guid().substring(0, 8);
+            //Check current map, if request is present, assign map index.
+            ConfigMap.map(function(c, i) {
+                let req = c['config-parameter-request'];
+                if((req['config-parameter-request-ref'] == value.requestValue) &&
+                   (req['member-vnf-index-ref'] == value.requestIndex)) {
+                    ConfigMapIndex = i;
+                    id = c.id;
+                }
+            });
+            if(!ConfigMapIndex && _.isBoolean(ConfigMapIndex)) {
+                ConfigMapIndex = ConfigMap.length;
+            }
+            if(value.value) {
+                utils.assignPathValue(this.model, 'config-parameter-map.' + ConfigMapIndex + '.config-parameter-source.config-parameter-source-ref', value.value);
+                utils.assignPathValue(this.model, 'config-parameter-map.' + ConfigMapIndex + '.config-parameter-source.member-vnf-index-ref', value.index);
+                utils.assignPathValue(this.model, 'config-parameter-map.' + ConfigMapIndex + '.config-parameter-request.config-parameter-request-ref', value.requestValue);
+                utils.assignPathValue(this.model, 'config-parameter-map.' + ConfigMapIndex + '.config-parameter-request.member-vnf-index-ref', value.requestIndex);
+                utils.assignPathValue(this.model, 'config-parameter-map.' + ConfigMapIndex + '.id', id);
+                CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+            } else {
+                utils.removePathValue(this.model, 'config-parameter-map.' + ConfigMapIndex)
+                CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+
+            }
+        }
+    }
+
+
+//Values from
+//
+
+//To update
+//Container:NSD
+//path
+//["config-parameter", "config-parameter-source"]
+//{config-parameter-source-ref: "service_port", member-vnf-index-ref: 2}
+
+function constructRequestSourceData(containers) {
+    let cds = CatalogDataStore;
+    let catalogs = cds.getTransientCatalogs();
+    let Requests = [];
+    let Sources = [];
+    let vnfdData = {
+        index:[],
+        vnfdIDs:[],
+        indexRefs: {},
+        vnfdRefs:{}
+    };
+
+    //Init VNFD map
+    //{
+    //
+    //  index:[1], //member-vnfd-index-ref
+    //  vnfdIDs:[],
+    //  indexRefs: {
+    //      1: vnfdID
+    //  },
+    //  vnfdRefs: {
+    //      {1.id} : {...}
+    //  }
+    //}
+
+    containers.map(function(c, i) {
+        if(c.className == 'ConstituentVnfd') {
+            vnfdData.index.push(c.vnfdIndex);
+            vnfdData.vnfdIDs.push(c.vnfdId);
+            vnfdData.indexRefs[c.vnfdIndex] = c.vnfdId;
+            vnfdData.vnfdRefs[c.vnfdId] = {
+                id: c.vnfdId,
+                name: c.name,
+                'short-name': c['short-name']
+            };
+        }
+    });
+
+    //Decorate VNFDMap with descriptor data;
+    catalogs[1].descriptors
+        .filter((v) => vnfdData.vnfdIDs.indexOf(v.id) > -1)
+        .map(constructVnfdMap.bind(this, vnfdData));
+
+
+    vnfdData.index.map(function(vnfdIndex) {
+        let vnfdId = vnfdData.indexRefs[vnfdIndex];
+        let vnfd = vnfdData.vnfdRefs[vnfdId];
+        let vnfdShortName = vnfd['short-name'];
+        vnfd.requests && vnfd.requests.map(function(request) {
+            Requests.push(_.merge({
+                            id: vnfdId,
+                            vnfdIndex: vnfdIndex,
+                            vnfdName: vnfdShortName,
+                        }, request))
+        });
+        vnfd.sources && vnfd.sources.map(function(source) {
+            Sources.push(_.merge({
+                            id: vnfdId,
+                            vnfdIndex: vnfdIndex,
+                            vnfdName: vnfdShortName,
+                        }, source));
+        });
+    })
+
+    return {Requests, Sources};
+
+    function constructVnfdMap(vnfdData, vnfd) {
+        let data = {
+            requests: vnfd['config-parameter'] && vnfd['config-parameter']['config-parameter-request'],
+            sources: vnfd['config-parameter'] && vnfd['config-parameter']['config-parameter-source']
+        };
+        vnfdData.vnfdRefs[vnfd.id] =  _.merge(vnfdData.vnfdRefs[vnfd.id], data);
+    }
+
+}
+
+
+
+export default ConfigPrimitiveParameters;
index 2e12052..f87f726 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
 'use strict';
 
 import _cloneDeep from 'lodash/cloneDeep'
+import _isArray from 'lodash/isArray'
+import _isObject from 'lodash/isObject'
+import _keys from 'lodash/keys'
 import React from 'react';
 import PureRenderMixin from 'react-addons-pure-render-mixin'
 import messages from './messages'
 import serializers from '../libraries/model/DescriptorModelSerializer'
 import JSONViewer from 'widgets/JSONViewer/JSONViewer';
 import PopupWindow from './PopupWindow'
+import DetailsPanelToolbar from './DetailsPanelToolbar'
+import NavigateDescriptorModel from './NavigateDescriptorModel'
+import NavigateDescriptorErrors from './NavigateDescriptorErrors'
 import CatalogItemDetailsEditor from './CatalogItemDetailsEditor'
 import SelectionManager from '../libraries/SelectionManager'
 
 import '../styles/DetailsPanel.scss'
 
+function checkForErrors(errors) {
+       return _keys(errors).reduce((inError, k) => {
+               function traverseObject(obj, key) {
+                       const node = obj[key];
+                       if (_isArray(node)) {
+                               return node.reduce((inError, v, i) => {
+                                       if (!inError && v) {
+                                               return _keys(v).reduce((inError, k) => {
+                                                       return inError || traverseObject(v, k);
+                                               }, false);
+                                       }
+                                       return inError;
+                               }, false);
+                       } else if (_isObject(node)) {
+                               return _keys(node).reduce((inError, k) => {
+                                       return inError || traverseObject(node, k);
+                               }, false);
+                       } else {
+                               return !! node;
+                       }
+               }
+               return inError || traverseObject(errors, k);
+       }, false);
+}
+
 const DetailsPanel = React.createClass({
        mixins: [PureRenderMixin, SelectionManager.reactPauseResumeMixin],
        getInitialState() {
@@ -42,6 +73,10 @@ const DetailsPanel = React.createClass({
                };
        },
        componentWillMount() {
+               setTimeout(() => {
+                       const height = this.panel && this.panel.offsetHeight;
+                       this.setState({ height });
+               }, 100);
        },
        componentDidMount() {
        },
@@ -50,24 +85,76 @@ const DetailsPanel = React.createClass({
        },
        componentWillUnmount() {
        },
+       contextTypes: {
+           router: React.PropTypes.object,
+           userProfile: React.PropTypes.object
+       },
+       componentWillUpdate(nextProps) {
+               if ((nextProps.layout != this.props.layout)
+                       && (nextProps.layout.height != this.props.layout.height)) {
+                       this.componentWillMount();
+               }
+       },
+
        render() {
                let json = '{}';
-               let bodyComponent =  messages.detailsWelcome();
+               let bodyContent = this.props.hasNoCatalogs ? null : messages.detailsWelcome();
                const selected = this.props.containers.filter(d => SelectionManager.isSelected(d));
                const selectedContainer = selected[0];
+               let workingHeight = this.state.height || 1;
+
+               function makeId(container, path) {
+                       let idParts = [_isArray(path) ? path.join(':') : path];
+                       idParts.push(container.uid);
+                       while (container.parent) {
+                               container = container.parent;
+                               idParts.push(container.uid);
+                       }
+                       return idParts.reverse().join(':');
+               }
+
                if (selectedContainer) {
-                       bodyComponent = <CatalogItemDetailsEditor container={selectedContainer} width={this.props.layout.right} />;
+                       bodyContent = [];
+                       bodyContent.push(
+                               <DetailsPanelToolbar
+                                       key='toolbar'
+                                       container={selectedContainer}
+                                       showHelp={this.props.showHelp.forAll}
+                                       width={this.props.layout.right} />
+                       );
+                       workingHeight -= 32 + 35;
+                       bodyContent.push(
+                               <NavigateDescriptorModel key='navigate' container={selectedContainer} idMaker={makeId}
+                                       style={{ margin: '8px 8px 15px' }} />
+                       )
+                       workingHeight -= 8 + 15 + 37;
+                       if (checkForErrors(selectedContainer.uiState.error)) {
+                               bodyContent.push(
+                                       <NavigateDescriptorErrors key='errors' container={selectedContainer} idMaker={makeId}
+                                               style={{ margin: '8px 8px 15px' }} />
+                               )
+                               workingHeight -= 8 + 15 + 37;
+                       }
+                       bodyContent.push(
+                               <div key='editor' className="DetailsPanelBody">
+                                       <CatalogItemDetailsEditor
+                                               container={selectedContainer}
+                                               idMaker={makeId}
+                                               showHelp={this.props.showHelp}
+                                               collapsePanelsByDefault={this.props.collapsePanelsByDefault}
+                                               openPanelsWithData={this.props.openPanelsWithData}
+                                               width={this.props.layout.right}
+                                               height={workingHeight} />
+                               </div>
+                       );
                        const edit = _cloneDeep(selectedContainer.model);
                        json = serializers.serialize(edit) || edit;
                }
                const jsonViewerTitle = selectedContainer ? selectedContainer.model.name : 'nothing selected';
-               const hasNoCatalogs = this.props.hasNoCatalogs;
                return (
-                       <div className="DetailsPanel" data-resizable="left" data-resizable-handle-offset="0 5" style={{width: this.props.layout.right}} onClick={event => event.preventDefault()}>
-                               <div className="DetailsPanelBody">
-                                       {hasNoCatalogs ? null : bodyComponent}
-                               </div>
-                               <PopupWindow show={this.props.showJSONViewer} title={jsonViewerTitle}><JSONViewer json={json}/></PopupWindow>
+                       <div ref={el => this.panel = el} className="DetailsPanel" data-resizable="left" data-resizable-handle-offset="0 5" style={{ width: this.props.layout.right }} onClick={event => event.preventDefault()}>
+                               {bodyContent}
+                               <PopupWindow show={this.props.showJSONViewer} title={jsonViewerTitle}><JSONViewer json={json} /></PopupWindow>
                        </div>
                );
        }
diff --git a/skyquake/plugins/composer/src/src/components/DetailsPanelToolbar.jsx b/skyquake/plugins/composer/src/src/components/DetailsPanelToolbar.jsx
new file mode 100644 (file)
index 0000000..4ffc0ef
--- /dev/null
@@ -0,0 +1,63 @@
+
+/*
+ * 
+ *   Copyright 2017 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 Button from './Button'
+import DescriptorEditorActions from '../actions/DescriptorEditorActions'
+
+import '../styles/DetailsPanelToolbar.scss'
+
+import imgQuestionMark from '../../../node_modules/open-iconic/svg/question-mark.svg'
+import imgExpandAll from '../../../node_modules/open-iconic/svg/caret-right.svg'
+import imgCollapseAll from '../../../node_modules/open-iconic/svg/caret-bottom.svg'
+import imgCollapseSome from '../../../node_modules/open-iconic/svg/elevator.svg'
+
+export default function (props) {
+       const { container, showHelp } = props;
+       function onClickAction(action, event) {
+               action({ descriptor: container });
+       }
+       return (
+               <div className="DetailsPanelToolbar">
+                       <div className="btn-bar">
+                               <div className="btn-group" style={{ display: 'inline-block' }}>
+                                       <Button type="image" title="Expand all" className="action-onboard-catalog-package"
+                                               onClick={onClickAction.bind(null, DescriptorEditorActions.expandAllPanels)} src={imgExpandAll} />
+                                       <Button type="image" title="Collapse all" className="action-update-catalog-package"
+                                               onClick={onClickAction.bind(null, DescriptorEditorActions.collapseAllPanels)} src={imgCollapseAll} />
+                                       <Button type="image" title="Collapse only data-less panels" className="action-update-catalog-package"
+                                               onClick={onClickAction.bind(null, DescriptorEditorActions.showPanelsWithData)} src={imgCollapseSome} />
+                               </div>
+                               <div className="btn-group" style={{ display: 'inline-block' }}>
+                                       <Button 
+                                               type="image" 
+                                               label={showHelp ? "Hide" : "Show"}
+                                               title={showHelp ? "Hide descriptions" : "Show descriptions"} 
+                                               className="action-onboard-catalog-package"
+                                               src={imgQuestionMark} 
+                                               onClick={onClickAction.bind(null, 
+                                                       showHelp ? DescriptorEditorActions.showHelpForNothing 
+                                                       : DescriptorEditorActions.showHelpForAll)} 
+                                               />
+                               </div>
+                       </div>
+               </div>
+       );
+}
+
index ad2bf3d..19e699d 100644 (file)
  * This class generates the form fields used to edit the CONFD JSON model.
  */
 
-import _includes from 'lodash/includes'
-import _isArray from 'lodash/isArray'
-import _cloneDeep from 'lodash/cloneDeep'
-import _debounce from 'lodash/debounce';
 import _uniqueId from 'lodash/uniqueId';
 import _set from 'lodash/set';
 import _get from 'lodash/get';
 import _has from 'lodash/has';
+import _keys from 'lodash/keys';
+import _isObject from 'lodash/isObject';
+import _isArray from 'lodash/isArray';
 import utils from '../libraries/utils'
 import React from 'react'
-import ClassNames from 'classnames'
 import changeCase from 'change-case'
 import toggle from '../libraries/ToggleElementHandler'
-import Button from './Button'
 import Property from '../libraries/model/DescriptorModelMetaProperty'
+import SelectionManager from '../libraries/SelectionManager'
 import ComposerAppActions from '../actions/ComposerAppActions'
 import CatalogItemsActions from '../actions/CatalogItemsActions'
-import DESCRIPTOR_MODEL_FIELDS from '../libraries/model/DescriptorModelFields'
+import DescriptorEditorActions from '../actions/DescriptorEditorActions'
 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
-import SelectionManager from '../libraries/SelectionManager'
-import DeletionManager from '../libraries/DeletionManager'
-import DescriptorModelIconFactory from '../libraries/model/IconFactory'
-import getEventPath from '../libraries/getEventPath'
-import CatalogDataStore from '../stores/CatalogDataStore'
 
-import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
-import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
+import ModelBreadcrumb from './model/ModelBreadcrumb'
+import ListItemAsLink from './model/ListItemAsLink'
+import LeafField from './model/LeafField'
+import { List, ListItem } from './model/List'
+import ContainerWrapper from './model/Container'
+import Choice from './model/Choice'
 
 import '../styles/EditDescriptorModelProperties.scss'
 
-const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
 
 function resolveReactKey(value) {
-       const keyPath =  ['uiState', 'fieldKey'];
+       const keyPath = ['uiState', 'fieldKey'];
        if (!_has(value, keyPath)) {
                _set(value, keyPath, _uniqueId());
        }
        return _get(value, keyPath);
 }
 
-function getDescriptorMetaBasicForType(type) {
-       const basicPropertiesFilter = d => _includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
-       return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
+function getTipForProperty(property) {
+       return property.name === 'constituent-vnfd' ? "Drag a VNFD from the Catalog to add more." : null
 }
 
-function getDescriptorMetaAdvancedForType(type) {
-       const advPropertiesFilter = d => !_includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
-       return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
+function selectModel(container, model, property) {
+       ComposerAppActions.selectModel(container.findChildByUid(model));
 }
 
-function getTitle(model = {}) {
-       if (typeof model['short-name'] === 'string' && model['short-name']) {
-               return model['short-name'];
-       }
-       if (typeof model.name === 'string' && model.name) {
-               return model.name;
-       }
-       if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
-               return model.uiState.displayName
-       }
-       if (typeof model.id === 'string') {
-               return model.id;
-       }
+function removeListEntry(container, property, path) {
+       DescriptorEditorActions.removeListItem({ descriptor: container, property, path });
 }
 
-export default function EditDescriptorModelProperties(props) {
-
-       const container = props.container;
-
-       if (!(DescriptorModelFactory.isContainer(container))) {
-               return
-       }
-
-       function startEditing() {
-               DeletionManager.removeEventListeners();
-       }
-
-       function endEditing() {
-               DeletionManager.addEventListeners();
-       }
-
-       function onClickSelectItem(property, path, value, event) {
-               event.preventDefault();
-               const root = this.getRoot();
-               if (SelectionManager.select(value)) {
-                       CatalogItemsActions.catalogItemMetaDataChanged(root.model);
-               }
-       }
-
-       function onFocusPropertyFormInputElement(property, path, value, event) {
-
-               event.preventDefault();
-               startEditing();
-
-               function removeIsFocusedClass(event) {
-                       event.target.removeEventListener('blur', removeIsFocusedClass);
-                       Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
-               }
-
-               removeIsFocusedClass(event);
+function createAndAddItemToPath(container, property, path) {
+       DescriptorEditorActions.addListItem({ descriptor: container, property, path });
+}
 
-               const propertyWrapper = getEventPath(event).reduce((parent, element) => {
-                       if (parent) {
-                               return parent;
-                       }
-                       if (!element.classList) {
-                               return false;
-                       }
-                       if (element.classList.contains('property')) {
-                               return element;
-                       }
-               }, false);
+function notifyPropertyFocused(container, path) {
+       container.getRoot().uiState.focusedPropertyPath = path.join('.');
+       console.debug('property selected', path.join('.'));
+       ComposerAppActions.propertySelected([path.join('.')]);
+}
 
-               if (propertyWrapper) {
-                       propertyWrapper.classList.add('-is-focused');
-                       event.target.addEventListener('blur', removeIsFocusedClass);
-               }
+function setPropertyOpenState(container, path, property, isOpen) {
+       DescriptorEditorActions.setOpenState({ descriptor: container, property, path, isOpen });
+}
 
-       }
+function isDataProperty(property) {
+       return property.type === 'leaf' || property.type === 'leaf_list' || property.type === 'choice';
+}
 
-       function buildAddPropertyAction(container, property, path) {
-               function onClickAddProperty(property, path, event) {
-                       event.preventDefault();
-                       //SelectionManager.resume();
-                       const create = Property.getContainerCreateMethod(property, this);
-                       if (create) {
-                               const model = null;
-                               create(model, path, property);
-                       } else {
-                               const name = path.join('.');
-                               // get a unique name for the new list item based on the current list content
-                               // some lists, based on the key, may not get a uniqueName generated here
-                               const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(container.model[property.name], property);
-                               const value = Property.createModelInstance(property, uniqueName);
-                               utils.assignPathValue(this.model, name, value);
-                       }
-                       CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+function checkIfValueEmpty(value) {
+       if (value === null || typeof value === 'undefined') {
+               return true;
+       } else if (_isArray(value) && !value.length) {
+               return true;
+       } else if (_isObject(value)) {
+               const keys = _keys(value);
+               if (keys.length < 2) {
+                       return !keys.length || (keys[0] === 'uiState')
                }
-               return (
-                               <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
-               );
        }
+       return false;
+}
 
-       function buildRemovePropertyAction(container, property, path) {
-               function onClickRemoveProperty(property, path, event) {
-                       event.preventDefault();
-                       const name = path.join('.');
-                       const removeMethod = Property.getContainerMethod(property, this, 'remove');
-                       if (removeMethod) {
-                               removeMethod(utils.resolvePath(this.model, name));
-                       } else {
-                               utils.removePathValue(this.model, name);
-                       }
-                       CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+export default function EditDescriptorModelProperties(props) {
+       const { container, idMaker, showHelp, collapsePanelsByDefault, openPanelsWithData } = props;
+       const readOnly = props.readOnly || container.isReadOnly;
+       const showElementHelp = showHelp.forAll;
+       const uiState = container.uiState;
+
+       function getPanelOpenedCondition(value, path) {
+               const showOpened = container.getUiState('opened', path);
+               if (typeof showOpened === 'undefined') {
+                       return (openPanelsWithData && !checkIfValueEmpty(value)) ? true : !collapsePanelsByDefault;
                }
-               return (
-                       <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
-               );
+               return showOpened;
        }
 
-       function buildField(container, property, path, value, fieldKey) {
-               let cds = CatalogDataStore;
-               let catalogs = cds.getTransientCatalogs();
-
+       function buildField(property, path, value, fieldKey) {
                const pathToProperty = path.join('.');
-               const isEditable = true;
-               const isGuid = Property.isGuid(property);
-               const isBoolean = Property.isBoolean(property);
-               const isEnumeration = Property.isEnumeration(property);
-               const isLeafRef = Property.isLeafRef(property);
-               const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
-               const placeholder = changeCase.title(property.name);
-               const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
                const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : (isNaN(value) ? undefined : value);
 
                // process the named field value change
                function processFieldValueChange(name, value) {
                        console.debug('processed change for -- ' + name + ' -- with value -- ' + value);
-                       // this = the container being edited
-                       if (DescriptorModelFactory.isContainer(this)) {
-                               utils.assignPathValue(this.model, name, value);
-                               CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
-                       }
-               }
-
-               // change handler used for onChange event
-               const changeHandler = (handleValueChange, event) => {
-                       event.preventDefault();
-                       console.debug(event.target.value);
-                       handleValueChange(event.target.value);
-               };
-               // create an onChange event handler for a text field for the specified field path (debounced to accumulate chars)
-               const onTextChange = changeHandler.bind(null, _debounce(
-                       processFieldValueChange.bind(container, pathToProperty), 2000, {maxWait: 5000})); // max wait for short-name
-               // create an onChange event handler for a select field for the specified field path
-               const onSelectChange = changeHandler.bind(null, processFieldValueChange.bind(container, pathToProperty));
-               
-               if (isEnumeration) {
-                       const enumeration = Property.getEnumeration(property, value);
-                       const options = enumeration.map((d, i) => {
-                               // note yangforge generates values for enums but the system does not use them
-                               // so we categorically ignore them
-                               // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
-                               //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
-                               return <option key={':' + i} value={d.name}>{d.name}</option>;
-                       });
-                       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)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
-                       }
-                       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}>
-                                               {options}
-                               </select>
-                       );
-               }
-
-               if (isLeafRef) {
-                       let fullPathString = container.key + ':' + path.join(':');
-                       let containerRef = container;
-                       while (containerRef.parent) {
-                               fullPathString = containerRef.parent.key + ':' + fullPathString;
-                               containerRef = containerRef.parent;
-                       }
-                       const leafRefPathValues = Property.getLeafRef(property, path, value, fullPathString, catalogs, container);
-
-                       const options = leafRefPathValues && leafRefPathValues.map((d, i) => {
-                               return <option key={':' + i} value={d.value}>{d.value}</option>;
-                       });
-                       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)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
+                       if (DescriptorModelFactory.isContainer(container)) {
+                               DescriptorEditorActions.setValue({ descriptor: container, path, value });
                        }
-                       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}>
-                                               {options}
-                               </select>
-                       );
-               }
-
-               if (isBoolean) {
-                       const options = [
-                               <option key={'true'} value="TRUE">TRUE</option>,
-                               <option key={'false'} value="FALSE">FALSE</option>
-                       ]
-
-                       // if (!isValueSet) {
-                               const noValueDisplayText = changeCase.title(property.name);
-                               options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}></option>);
-                       // }
-                       let val = value;
-                       if(typeof(val) == 'number') {
-                               val = value ? "TRUE" : "FALSE"
-                       }
-                       const isValueSet = (val != '' && val)
-                       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}>
-                                               {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}>
-                                               {options}
-                               </select>
-                       );
                }
 
-               if (property['preserve-line-breaks']) {
-                       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} />
-                       );
+               function onErrorHandler(message) {
+                       DescriptorEditorActions.setError({ descriptor: container, path, message });
                }
 
+               // create an onChange event handler for a select field for the specified field path
+               const onChangeHandler = processFieldValueChange.bind(null, pathToProperty);
                return (
-                       <input 
+                       <LeafField
                                key={fieldKey}
+                               container={container}
+                               property={property}
+                               path={path}
+                               value={value}
                                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}
+                               showHelp={showElementHelp}
+                               onChange={onChangeHandler}
+                               onError={onErrorHandler}
+                               readOnly={readOnly}
+                               errorMessage={_get(container.uiState, ['error'].concat(path))}
                        />
                );
-
        }
 
        /**
@@ -395,451 +158,288 @@ export default function EditDescriptorModelProperties(props) {
         * @param {[property]} properties 
         * @param {string} pathToProperties path within the container to the properties
         * @param {Object} data source for each property
-        * @param {any} props object containing main data panel information, e.g. panel width {width: 375}
-        * which may be useful/necessary to a components rendering.
         * @returns an array of react components
         */
-       function buildComponentsForProperties(container, properties, pathToProperties, data, props) {
+       function buildComponentsForProperties(properties, pathToProperties, data) {
                return properties.map((property) => {
                        let value;
                        let propertyPath = pathToProperties.slice();
+                       if (property.type != 'choice') {
+                               propertyPath.push(property.name);
+                       }
                        if (data && typeof data === 'object') {
-                               value = data[property.name];
+                               value = _get(data, property.name);
                        }
-                       if(property.type != 'choice'){
-                               propertyPath.push(property.name);
+                       let result = null;
+                       try {
+                               result = buildPropertyComponent(property, propertyPath, value);
+                       } catch (e) {
+                               console.error(e);
                        }
-                       return build(container, property, propertyPath, value, props);
+                       return result;
                });
        }
 
-       function buildElement(container, property, valuePath, value) {
-               return buildComponentsForProperties(container, property.properties, valuePath, value);
-       }
+       function buildChoice(property, path, value, uniqueId) {
+               const uiStatePath = path.concat(['uiState']);
+               const choiceStatePath = ['choice', property.name];
+               const fullChoiceStatePath = uiStatePath.concat(choiceStatePath);
 
-       function buildChoice(container, property, path, value, key) {
-
-               function processChoiceChange(name, value) {
-                       if (DescriptorModelFactory.isContainer(this)) {
-
-                               /*
-                                       Transient State is stored for convenience in the uiState field.
-                                       The choice yang type uses case elements to describe the "options".
-                                       A choice can only ever have one option selected which allows
-                                       the system to determine which type is selected by the name of
-                                       the element contained within the field.
-                                */
-                               /*
-                                       const stateExample = {
-                                               uiState: {
-                                                       choice: {
-                                                               'conf-config': {
-                                                                       selected: 'rest',
-                                                                       'case': {
-                                                                               rest: {},
-                                                                               netconf: {},
-                                                                               script: {}
-                                                                       }
-                                                               }
-                                                       }
-                                               }
-                                       };
-                               */
-                               const statePath = ['uiState.choice'].concat(name);
-                               const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
-                               const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
-                               // write state back to the model so the new state objects are captured
-                               utils.assignPathValue(this.model, statePath.join('.'), stateObject);
-
-                               // write the current choice value into the state
-                               let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
-                               let isTopCase = false;
-                               if (!choiceObject) {
-                                       isTopCase = true;
-                                       choiceObject = utils.resolvePath(this.model, [selected].join('.'));
+               function determineSelectedChoice(model) {
+                       let choiceState = utils.resolvePath(container.model, fullChoiceStatePath.join('.'));
+                       if (choiceState) {
+                               return property.properties.find(c => c.name === choiceState.selected);
+                       }
+                       const selectedCase = property.properties.find(c =>
+                               c.properties && c.properties.find(p => _has(model, path.concat([p.name])))
+                       );
+                       // lets remember this
+                       let stateObject = utils.resolvePath(container.model, uiStatePath.join('.'));
+                       stateObject = _set(stateObject || {}, choiceStatePath, { selected: selectedCase ? selectedCase.name : "" });
+                       utils.assignPathValue(container.model, uiStatePath.join('.'), stateObject);
+                       return selectedCase;
+               }
+
+               function pullOutCaseModel(caseName) {
+                       const model = container.model;
+                       const properties = property.properties.find(c => c.name === caseName).properties;
+                       return properties.reduce((o, p) => {
+                               const valuePath = path.concat([p.name]).join('.');
+                               const value = utils.resolvePath(model, valuePath);
+                               if (value) {
+                                       o[p.name] = value;
                                }
-                               utils.assignPathValue(stateObject, [selected].join('.'), _cloneDeep(choiceObject));
+                               return o;
+                       }, {});
+               }
 
-                               if(selected) {
-                                       if(this.model.uiState.choice.hasOwnProperty(name)) {
-                                               delete this.model[selected];
-                                               utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
+               function processChoiceChange(value) {
+                       if (DescriptorModelFactory.isContainer(container)) {
+                               let uiState = utils.resolvePath(container.model, uiStatePath.join('.'));
+                               // const stateObject = utils.resolvePath(container.model, fullChoiceStatePath.join('.')) || {};
+                               let choiceState = _get(uiState, choiceStatePath);
+                               const previouslySelectedChoice = choiceState.selected;
+                               if (previouslySelectedChoice === value) {
+                                       return;
+                               }
+                               if (previouslySelectedChoice) {
+                                       choiceState[previouslySelectedChoice] = pullOutCaseModel(previouslySelectedChoice);
+                               }
+                               const modelUpdate = _keys(choiceState[previouslySelectedChoice]).reduce((o, k) => _set(o, k, null), {})
+                               choiceState.selected = value;
+                               _set(uiState, choiceStatePath, choiceState);
+                               _set(modelUpdate, 'uiState', uiState);
+                               if (choiceState.selected) {
+                                       const previous = choiceState[choiceState.selected];
+                                       if (previous) {
+                                               Object.assign(modelUpdate, previous);
                                        } else {
-                                               // remove the current choice value from the model
-                                               utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
+                                               const newChoice = property.properties.find(p => p.name === choiceState.selected);
+                                               if (newChoice.properties.length === 1) {
+                                                       const property = newChoice.properties[0];
+                                                       if (property.type === 'leaf' && property['data-type'] === 'empty') {
+                                                               let obj = {};
+                                                               obj[property.name] = [null];
+                                                               Object.assign(modelUpdate, obj);
+                                                       }
+                                               }
                                        }
                                }
-
-                               // get any state for the new selected choice
-                               const newChoiceObject = utils.resolvePath(stateObject, [value].join('.')) || {};
-
-                               // assign new choice value to the model
-                               if (isTopCase) {
-                                       utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
-                               } else {
-                                       utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
-                               }
-
-                               // update the selected name
-                               utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
-
-                               CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+                               DescriptorEditorActions.assignValue({ descriptor: container, path, source: modelUpdate });
                        }
                }
 
-               const pathToChoice = path.join('.');
-               const caseByNameMap = {};
-
-               const choiceChangeHandler = processChoiceChange.bind(container, pathToChoice);
-               const onChange = ((handleChoiceChange, event) => {
-                       event.preventDefault();
-                       handleChoiceChange(event.target.value);
-               }).bind(null, choiceChangeHandler);
-
-
-               const cases = property.properties.map(d => {
-                       if (d.type === 'case') {
-                               //Previous it was assumed that a case choice would have only one property. Now we pass on either the only item or the
-                               caseByNameMap[d.name] = d.properties && (d.properties.length == 1 ? d.properties[0] : d.properties);
-                               return {
-                                       optionName: d.name,
-                                       optionTitle: d.description,
-                                       //represents case name and case element name
-                                       optionValue: [d.name, d.properties[0].name].join('.')
-                               };
-                       }
-                       caseByNameMap[d.name] = d;
-                       return {optionName: d.name};
-               });
+               const selectedCase = determineSelectedChoice(container.model);
+               const children = selectedCase ?
+                       <ContainerWrapper property={selectedCase} readOnly={readOnly} showHelp={showElementHelp} showOpened={true}>
+                               {buildComponentsForProperties(selectedCase.properties, path, path.length ? _get(container.model, path) : container.model)}
+                       </ContainerWrapper>
+                       : null;
 
-               const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
-                       return (
-                               <option key={i} value={d.optionValue} title={d.optionTitle}>
-                                       {d.optionName}
-                                       {i ? null : changeCase.title(property.name)}
-                               </option>
-                       );
-               });
+               return (
+                       <Choice key={uniqueId} id={uniqueId} onChange={processChoiceChange} readOnly={readOnly} showHelp={showElementHelp}
+                               property={property} value={selectedCase ? selectedCase.name : null}
+                       >
+                               {children}
+                       </Choice>
+               );
 
-               let selectedOptionPath = ['uiState.choice', pathToChoice, 'selected'].join('.');
-               //Currently selected choice/case statement on UI model
-               let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
-               //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
-               if(!selectedOptionValue) {
-                       //get field properties for choice on container model
-                       let fieldProperties = utils.resolvePath(container.model, pathToChoice);
-                       if(fieldProperties) {
-                               //Check each case statement in model and see if it is present in container model.
-                               cases.map(function(c){
-                                       if(c.optionValue && fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
-                                               utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), c.optionValue);
-                                       }
-                               });
-                               selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
-                       } else {
-                               property.properties.map(function(p) {
-                                       let pname = p.properties[0] && p.properties[0].name;
-                                       if(container.model.hasOwnProperty(pname)) {
-                                               utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
-                                       }
-                               })
-                               selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
-                       }
-               }
-               //If selectedOptionValue is present, take first item in string which represents the case name.
-               const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
-               const isLeaf = Property.isLeaf(valueProperty);
-               const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
-               const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
-               //Some magic that prevents errors for arising
-               let valueResponse = null;
-               if (valueProperty.properties && valueProperty.properties.length) { 
-                       valueResponse = valueProperty.properties.map(valuePropertyFn);
-               } else if (!isMissingDescriptorMeta) {
-                       let value = utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name];
-                       valueResponse = build(container, valueProperty, path.concat(valueProperty.name), value)
-               } else {
-                       valueResponse = valueProperty.map && valueProperty.map(valuePropertyFn);
+       }
+
+       function buildLeafList(property, path, value, uniqueId) {
+               if (!Array.isArray(value)) {
+                       value = [value];
                }
-               function valuePropertyFn(d, i) {
-                       const childPath = path.concat(valueProperty.name, d.name);
-                       const childValue = utils.resolvePath(container.model, childPath.join('.'));
+               const children = value && value.map((v, i) => {
+                       let itemPath = path.concat([i]);
+                       const field = buildField(property, itemPath, v, uniqueId + i);
                        return (
-                               <div key={childPath.concat('info', i).join(':')}>
-                                       {build(container, d, childPath, childValue, props)}
-                               </div>
-                       );
-               }
-               // end magic
-               const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
-
+                               <ListItem key={':' + i} index={i} property={property} readOnly={readOnly} showHelp={showElementHelp}
+                                       showOpened={true} removeItemHandler={removeListEntry.bind(null, container, property, itemPath)} >
+                                       {field}
+                               </ListItem>
+                       )
+               });
                return (
-                       <div key={key} className="choice">
-                               <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}
-                               >
-                                       {options}
-                               </select>
-                               {valueResponse}
-                       </div>
+                       <List key={uniqueId} id={uniqueId} property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
+                               showOpened={true} addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}>
+                               {children}
+                       </List>
                );
-
        }
 
-       function buildSimpleListItem(container, property, path, value, uniqueId, index) {
-               // todo need to abstract this better
-               const title = getTitle(value);
-               var req = require.context("../", true, /\.svg/);
+       function buildList(property, path, value, uniqueId) {
+               if (value && !Array.isArray(value)) {
+                       value = [value];
+               }
+               function getListItemSummary(index, value) {
+                       const keys = property.key.map((key) => value[key]);
+                       const summary = keys.join(' ');
+                       return summary.length > 1 ? summary : '' + (index + 1);
+               }
+               const children = value && value.map((itemValue, i) => {
+                       const itemPath = path.concat([i]);
+                       const key = resolveReactKey(itemValue);
+                       const children = buildComponentsForProperties(property.properties, itemPath, itemValue);
+                       const showOpened = getPanelOpenedCondition(value, itemPath);
+                       return (
+                               <ListItem key={key} property={property} readOnly={readOnly} showHelp={showElementHelp}
+                                       summary={getListItemSummary(i, itemValue)} info={'' + (i + 1)}
+                                       removeItemHandler={removeListEntry.bind(null, container, property, itemPath)}
+                                       showOpened={showOpened} onChangeOpenState={setPropertyOpenState.bind(null, container, itemPath, property, !showOpened)}>
+                                       {children}
+                               </ListItem>
+                       )
+               });
+               const showOpened = getPanelOpenedCondition(value, path);
                return (
-                       <div key={uniqueId} >
-                               <a href="#select-list-item" className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
-                                       <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
-                                       <span>{title}</span>
-                               </a>
-                               {buildRemovePropertyAction(container, property, path)}
-                       </div>
+                       <List key={uniqueId} id={uniqueId} property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
+                               addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}
+                               showOpened={showOpened} onChangeOpenState={setPropertyOpenState.bind(null, container, path, property, !showOpened)}>
+                               {children}
+                       </List>
                );
        }
 
-       function buildRemoveListItem(container, property, valuePath, index) {
-               const className = ClassNames(property.name + '-remove actions');
+       function buildSimpleList(property, path, value, uniqueId) {
+               if (value && !Array.isArray(value)) {
+                       value = [value];
+               }
+               const children = value && value.map((v, i) => {
+                       let itemPath = path.concat([i]);
+                       return (
+                               <ListItemAsLink key={':' + i} property={property} value={v}
+                                       removeItemHandler={removeListEntry.bind(null, container, property, itemPath)}
+                                       selectLinkHandler={selectModel.bind(null, container, v, property)} />
+                       )
+               });
+               const tip = getTipForProperty(property);
+               const showOpened = getPanelOpenedCondition(value, path);
+               const changeOpenState = setPropertyOpenState.bind(null, container, path, property, !showOpened);
                return (
-                       <div className={className}>
-                               <h3>
-                                       <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
-                                       <span className="info">{index + 1}</span>
-                                       {buildRemovePropertyAction(container, property, valuePath)}
-                               </h3>
-                       </div>
+                       <List name={uniqueId} id={uniqueId} key={uniqueId} tip={tip}
+                               property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
+                               addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}
+                               showOpened={showOpened} onChangeOpenState={changeOpenState}>
+                               {children}
+                       </List>
                );
        }
 
-       function buildLeafListItem(container, property, valuePath, value, uniqueId, index) {
-               // look at the type to determine how to parse the value
+       function buildContainer(property, path, value, uniqueId) {
+               const children = buildComponentsForProperties(property.properties, path, value);
+               const showOpened = getPanelOpenedCondition(value, path);
+               const changeOpenState = setPropertyOpenState.bind(null, container, path, property, !showOpened);
                return (
-                       <div key={uniqueId}>
-                               {buildRemoveListItem(container, property, valuePath, index)}
-                               {buildField(container, property, valuePath, value, uniqueId)}
-                       </div>
-
+                       <ContainerWrapper key={uniqueId} id={uniqueId} property={property} readOnly={readOnly}
+                               showHelp={showElementHelp} summary={checkIfValueEmpty(value) ? null : '*'}
+                               showOpened={showOpened} onChangeOpenState={changeOpenState}>
+                               {children}
+                       </ContainerWrapper>
                );
        }
 
-       function build(container, property, path, value, props = {}) {
+       function buildPropertyComponent(property, path, value) {
 
                const fields = [];
-               const isLeaf = Property.isLeaf(property);
                const isArray = Property.isArray(property);
                const isObject = Property.isObject(property);
-               const isLeafList = Property.isLeafList(property);
-               const isRequired = Property.isRequired(property);
                const title = changeCase.titleCase(property.name);
-               const columnCount = property.properties.length || 1;
-               const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
-               const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
 
                // create a unique Id for use as react component keys and html element ids
                // use uid (from ui info) instead of id property (which is not stable)
-               let uniqueId = container.uid;
-               let containerRef = container;
-               while (containerRef.parent) {
-                       uniqueId = containerRef.parent.uid + ':' + uniqueId;
-                       containerRef = containerRef.parent;
-               }
-               uniqueId += ':' + path.join(':')
+               let uniqueId = idMaker(container, path);
 
                if (!property.properties && isObject) {
+                       console.debug('no properties', property);
                        const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
                        property.properties = uiState.properties;
                }
 
-               const hasProperties = _isArray(property.properties) && property.properties.length;
-               const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
-
-               // ensure value is not undefined for non-leaf property types
-               if (isObject) {
-                       if (typeof value !== 'object') {
-                               value = isArray ? [] : {};
-                       }
-               }
-               const valueAsArray = _isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
-
-               const isMetaField = property.name === 'meta';
-               const isCVNFD = property.name === 'constituent-vnfd';
-               const isSimpleListView = Property.isSimpleList(property);
-
-               valueAsArray.forEach((value, index) => {
-
-                       let field;
-                       const valuePath = path.slice();
-                       // create a unique field Id for use as react component keys and html element ids
-                       // notes: 
-                       //   keys only need to be unique on components in the same array
-                       //   html element ids should be unique with the document (or form)
-                       let fieldId = uniqueId;
-
-                       if (isArray) {
-                               valuePath.push(index);
-                               fieldId = isLeafList ? fieldId + index + value : resolveReactKey(value);
-                       }
-
-                       if (isMetaField) {
-                               if (typeof value === 'object') {
-                                       value = JSON.stringify(value, undefined, 12);
-                               } else if (typeof value !== 'string') {
-                                       value = '{}';
-                               }
-                       }
-
-                       if (isMissingDescriptorMeta) {
-                               field = <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>;
-                       } else if (property.type === 'choice') {
-                               field = buildChoice(container, property, valuePath, value, fieldId);
-                       } else if (isSimpleListView) {
-                               field = buildSimpleListItem(container, property, valuePath, value, fieldId, index);
-                       } else if (isLeafList) {
-                               field = buildLeafListItem(container, property, valuePath, value, fieldId, index);
-                       } else if (hasProperties) {
-                               field = buildElement(container, property, valuePath, value, fieldId)
-                       } else {
-                               field = buildField(container, property, valuePath, value, fieldId);
-                       }
-
-                       function onClickLeaf(property, path, value, event) {
-                               if (event.isDefaultPrevented()) {
-                                       return;
-                               }
-                               event.preventDefault();
-                               event.stopPropagation();
-                               this.getRoot().uiState.focusedPropertyPath = path.join('.');
-                               console.debug('property selected', path.join('.'));
-                               ComposerAppActions.propertySelected([path.join('.')]);
-                       }
-
-                       const clickHandler = isLeaf ? onClickLeaf : () => {};
-                       const isContainerList = isArray && !(isSimpleListView || isLeafList);
-
-                       fields.push(
-                               <div key={fieldId}
-                                        className={ClassNames('property-content', {'simple-list': isSimpleListView})}
-                                        onClick={clickHandler.bind(container, property, valuePath, value)}>
-                                       {isContainerList ? buildRemoveListItem(container, property, valuePath, index) : null}
-                                       {field}
-                               </div>
+               if (property.type === 'leaf') {
+                       return buildField(property, path, value, uniqueId);
+               } else if (property.type === 'leaf_list') {
+                       return buildLeafList(property, path, value, uniqueId);
+               } else if (property.type === 'list') {
+                       return Property.isSimpleList(property) ?
+                               buildSimpleList(property, path, value, uniqueId)
+                               :
+                               buildList(property, path, value, uniqueId);
+               } else if (property.type === 'container') {
+                       return buildContainer(property, path, value, uniqueId);
+               } else if (property.type === 'choice') {
+                       return buildChoice(property, path, value, uniqueId);
+               } else {
+                       return (
+                               <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>
                        );
-
-               });
-
-               classNames['-is-leaf'] = isLeaf;
-               classNames['-is-array'] = isArray;
-               classNames['cols-' + columnCount] = isColumnar;
-
-               if (property.type === 'choice') {
-                       value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
-                       if(!value) {
-                               property.properties.map(function(p) {
-                                       let pname = p.properties[0] && p.properties[0].name;
-                                       if(container.model.hasOwnProperty(pname)) {
-                                               value = container.model[pname];
-                                       }
-                               })
-                       }
                }
+       }
 
-               let displayValue = typeof value === 'object' ? '' : value;
-               const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
-
-               const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
-
-               return (
-                       <div key={uniqueId} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
-                               <h3 className="property-label">
-                                       <label htmlFor={uniqueId}>
-                                               <span className={property.type + '-name name'}>{title}</span>
-                                               <small>
-                                                       <span className={property.type + '-info info'}>{displayValueInfo}</span>
-                                                       <span className={property.type + '-value value'}>{displayValue}</span>
-                                               </small>
-                                               {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
-                                       </label>
-                               </h3>
-                               <span className={property.type + '-description description'}>{property.description}</span>
-                               <val className="property-value">
-                                       {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
-                                       {fields}
-                               </val>
-                       </div>
-               );
 
+       if (!(DescriptorModelFactory.isContainer(container))) {
+               return
        }
 
        const containerType = container.uiState['qualified-type'] || container.uiState.type;
-       const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
-
-       function buildBasicGroup() {
-               if (basicProperties.length === 0) {
-                       return null;
-               }
-               return (
-                       <div className="basic-properties-group">
-                               <h2>Basic</h2>
-                               <div>
-                                       {buildComponentsForProperties(container, basicProperties, [], container.model)}
-                               </div>
-                       </div>
-               );
+       let properties = DescriptorModelMetaFactory.getModelMetaForType(containerType).properties;
+       const breadcrumb = [];
+       if (container.parent) {
+               breadcrumb.push(container.parent);
        }
-
-       function buildAdvancedGroup() {
-               const properties = getDescriptorMetaAdvancedForType(containerType).properties;
-               if (properties.length === 0) {
-                       return null;
+       breadcrumb.push(container);
+       // bubble all data properties to top of list
+       let twoLists = properties.reduce((o, property) => {
+               const value = _get(container.model, [property.name]);
+               if (isDataProperty(property)) {
+                       o.listOne.push(property);
+               } else {
+                       o.listTwo.push(property);
                }
-               const hasBasicFields = basicProperties.length > 0;
-               const closeGroup = basicProperties.length > 0;
-               return (
-                       <div className="advanced-properties-group">
-                               <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
-                                       <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
-                                       <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
-                               </h1>
-                               <div className="toggleable">
-                                       {buildComponentsForProperties(container, properties, [], container.model, {toggle: true, width: props.width})}
-                               </div>
-                               <div className="toggle-bottom-spacer" style={{visibility: 'hidden', 'height': '50%', position: 'absolute'}}>We need this so when the user closes the panel it won't shift away and scare the bj out of them!</div>
-                       </div>
-               );
-       }
-
-       function buildMoreLess(d, i) {
-               return (
-                       <span key={'bread-crumb-part-' + i}>
-                               <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
-                               <i> / </i>
-                       </span>
-               );
+               return o;
+       }, { listOne: [], listTwo: [] });
+       properties = twoLists.listOne.concat(twoLists.listTwo);
+       const children = buildComponentsForProperties(properties, [], container.model);
+
+       function onClick(event) {
+               console.debug(event.target);
+               if (event.isDefaultPrevented()) {
+                       return;
+               }
+               event.preventDefault();
+               event.stopPropagation();
+               // notifyFocusedHandler();
        }
 
-       const path = [];
-       if (container.parent) {
-               path.push(container.parent);
+       function onWrapperFocus(event) {
+               console.debug(event.target);
+               //notifyFocusedHandler();
        }
-       path.push(container);
 
        return (
-               <div className="EditDescriptorModelProperties -is-tree-view">
-                       <h1>{path.map(buildMoreLess)}</h1>
-                       {buildBasicGroup()}
-                       {buildAdvancedGroup()}
+               <div className="EditDescriptorModelProperties -is-tree-view" onClick={onClick} onFocus={onWrapperFocus}>
+                       <ModelBreadcrumb path={breadcrumb} />
+                       {children}
                </div>
        );
 };
index 0f39d30..e528972 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -45,6 +45,11 @@ import mapRecordServicePath from './mapRecordServicePath'
 import onCutDelegateToRemove from './onCutDelegateToRemove'
 import onClickSelectAndShowInDetailsPanel from './onClickSelectAndShowInDetailsPanel'
 
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
 import '../../styles/EditForwardingGraphPaths.scss'
 
 import imgNSD from '../../images/default-catalog-icon.svg'
@@ -119,7 +124,11 @@ function mapFG(fg, i) {
                <div key={i} className={fg.className} data-uid={fg.uid} data-offset-width="true" onClick={onClickSelectAndShowInDetailsPanel.bind(null, fg)} onCut={onCutDelegateToRemove.bind(null, fg)}>
                        <div key="outline-indicator" data-outline-indicator="true"></div>
                        <div className="header-actions">
-                               <Button className="remove-forwarding-graph" title="Remove" onClick={onClickRemoveForwardingGraph.bind(null, fg)} src={imgRemove}/>
+                               {
+                                       this.isRBACValid ?
+                                               <Button className="remove-forwarding-graph" title="Remove" onClick={onClickRemoveForwardingGraph.bind(null, fg)} src={imgRemove}/>
+                                               : null
+                               }
                        </div>
                        <LayoutRow primaryActionColumn={toggleSelectAllPaths} secondaryActionColumn={<img className="fg-icon" src={imgFG} width="20px" />}>
                                <small>{fg.title}</small>
@@ -133,7 +142,11 @@ function mapFG(fg, i) {
                                {fg.classifier.map(mapClassifier.bind(null, context))}
                                <div className="footer-actions">
                                        <div className="row-action-column">
-                                               <Button className="create-new-classifier" src={imgAdd} width="20px" onClick={onClickAddClassifier.bind(null, context, fg)} label="Add Classifier" />
+                                       {
+                                               this.isRBACValid ?
+                                                       <Button className="create-new-classifier" src={imgAdd} width="20px" onClick={onClickAddClassifier.bind(null, context, fg)} label="Add Classifier" />
+                                                       : null
+                                       }
                                        </div>
                                </div>
                        </div>
@@ -167,7 +180,11 @@ function mapNSD(nsd, i) {
                        {forwardingGraphs}
                        <div className="footer-actions">
                                <div className="row-action-column">
-                                       <Button className="create-new-forwarding-graph" src={imgAdd} width="20px" onClick={onClickAddForwardingGraph.bind(null, nsd)} label="Add new Forwarding Graph" />
+                               {
+                                       this.isRBACValid ?
+                                               <Button className="create-new-forwarding-graph" src={imgAdd} width="20px" onClick={onClickAddForwardingGraph.bind(null, nsd)} label="Add new Forwarding Graph" />
+                                               : null
+                               }
                                </div>
                        </div>
                </div>
@@ -193,14 +210,16 @@ const EditForwardingGraphPaths = React.createClass({
        },
        componentWillUnmount: function () {
        },
+       contextTypes: {
+           userProfile: React.PropTypes.object
+       },
        render() {
-
                const containers = this.props.containers;
                const context = {
                        component: this,
-                       containers: containers
+                       containers: containers,
+                       isRBACValid: isRBACValid(this.context.userProfile, [PROJECT_ROLES.PROJECT_ADMIN, PROJECT_ROLES.CATALOG_ADMIN])
                };
-
                const networkService = containers.filter(d => d.type === 'nsd');
                if (networkService.length === 0) {
                        return <p className="welcome-message">No <img src={imgNSD} width="20px" /> NSD open in the canvas. Try opening an NSD.</p>;
diff --git a/skyquake/plugins/composer/src/src/components/NavigateDescriptorErrors.jsx b/skyquake/plugins/composer/src/src/components/NavigateDescriptorErrors.jsx
new file mode 100644 (file)
index 0000000..e3b6b74
--- /dev/null
@@ -0,0 +1,73 @@
+
+/*
+ * 
+ *   Copyright 2017 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 _keys from 'lodash/keys'
+import _get from 'lodash/get'
+import _isObject from 'lodash/isObject'
+import _isArray from 'lodash/isArray'
+import Button from './Button'
+import PropertyCrumb from './model/PropertyCrumb'
+import PropertyNavigate from './model/PropertyNavigate'
+import DescriptorEditorActions from '../actions/DescriptorEditorActions'
+import Select from 'react-select';
+import 'react-select/dist/react-select.css'
+import '../styles/DetailsPanelErrors.scss'
+
+export default function (props) {
+       const { container, idMaker, style } = props;
+       const errors = container.uiState.error;
+       const errorOptions = _keys(errors).reduce((outer, k) => {
+               function pieceOfPath(obj, key) {
+                       const node = obj[key];
+                       if (_isArray(node)) {
+                               const items = node.reduce((inner, v, i) => {
+                                       if (v) {
+                                               const paths = _keys(v).reduce((a, k) => {
+                                                       const paths = pieceOfPath(v, k);
+                                                       return a.concat(paths);
+                                               }, []);
+                                               // inner = paths.map(p => [i].concat(p));
+                                               inner = paths.map(p => [i].concat(p));
+                                       }
+                                       return inner;
+                               }, []);
+                               return items.map(i => [key].concat(i));
+                       } else if (_isObject(node)) {
+                               return _keys(node).reduce((inner, k) => {
+                                       const paths = pieceOfPath(node, k);
+                                       return inner.concat(paths);
+                               }, []);
+                       } else {
+                               return [key];
+                       }
+               }
+               const paths = pieceOfPath(errors, k);
+               return outer.concat(paths);
+       }, []).reduce((a, path) => {
+               // only add if there is an error message
+               _get(errors, path) && a.push({ value: path, label: _isArray(path) ? path.join(' . ') : path})
+               return a;
+       }, []);
+       return errorOptions.length ?
+               (<PropertyNavigate container={container} idMaker={idMaker} options={errorOptions} 
+                       style={style} placeholder="Select error to correct" />)
+               : <div />;
+}
+
diff --git a/skyquake/plugins/composer/src/src/components/NavigateDescriptorModel.jsx b/skyquake/plugins/composer/src/src/components/NavigateDescriptorModel.jsx
new file mode 100644 (file)
index 0000000..244c4e4
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 1/18/16.
+ *
+ * This class generates the form fields used to edit the CONFD JSON model.
+ */
+
+import _uniqueId from 'lodash/uniqueId';
+import _set from 'lodash/set';
+import _get from 'lodash/get';
+import _has from 'lodash/has';
+import _keys from 'lodash/keys';
+import _isObject from 'lodash/isObject';
+import _isArray from 'lodash/isArray';
+import _isNumber from 'lodash/isNumber';
+import utils from '../libraries/utils'
+import React from 'react'
+import changeCase from 'change-case'
+import toggle from '../libraries/ToggleElementHandler'
+import Property from '../libraries/model/DescriptorModelMetaProperty'
+import SelectionManager from '../libraries/SelectionManager'
+import ComposerAppActions from '../actions/ComposerAppActions'
+import CatalogItemsActions from '../actions/CatalogItemsActions'
+import DescriptorEditorActions from '../actions/DescriptorEditorActions'
+import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
+import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
+import PropertyNavigate from './model/PropertyNavigate'
+
+
+import '../styles/EditDescriptorModelProperties.scss'
+
+function selectModel(container, model) {
+       const root = container.getRoot();
+       if (SelectionManager.select(model)) {
+               CatalogItemsActions.catalogItemMetaDataChanged(root.model);
+       }
+}
+
+function isDataProperty(property) {
+       return property.type === 'leaf' || property.type === 'leaf_list' || property.type === 'choice';
+}
+
+function checkIfValueEmpty(value) {
+       if (value === null || typeof value === 'undefined') {
+               return true;
+       } else if (_isArray(value) && !value.length) {
+               return true;
+       } else if (_isObject(value)) {
+               const keys = _keys(value);
+               if (keys.length < 2) {
+                       return !keys.length || (keys[0] === 'uiState')
+               }
+       }
+       return false;
+}
+
+function makeOption(path, value) {
+       let labelPath = path.map(node => _isNumber(node) ? node + 1: node);
+       return {
+               value: path,
+               label: labelPath.join(' . ') + (value ? ' : ' + value : '')
+       }
+}
+
+export default function NavigateDescriptorModel(props) {
+       const { container, idMaker, style } = props;
+       const uiState = container.uiState;
+
+       function buildField(property, path, value) {
+               return [makeOption(path, value)];
+       }
+
+       function buildLeafList(property, path, value) {
+               const searchValue = Array.isArray(value) ? value.join(' ') : value;
+               return [makeOption(path, searchValue)];
+       }
+
+       function buildChoice(property, path, value) {
+               const uiStatePath = path.concat(['uiState']);
+               const choiceStatePath = ['choice', property.name];
+               const fullChoiceStatePath = uiStatePath.concat(choiceStatePath);
+
+               function determineSelectedChoice(model) {
+                       let choiceState = utils.resolvePath(container.model, fullChoiceStatePath.join('.'));
+                       if (choiceState) {
+                               return property.properties.find(c => c.name === choiceState.selected);
+                       }
+                       const selectedCase = property.properties.find(c =>
+                               c.properties && c.properties.find(p => _has(model, path.concat([p.name])))
+                       );
+                       return selectedCase;
+               }
+
+               const selectedCase = determineSelectedChoice(container.model);
+               return [makeOption(path)].concat(selectedCase ?
+                       buildComponentsForProperties(
+                               selectedCase.properties, path, path.length ? _get(container.model, path) : container.model) :
+                       []);
+       }
+
+       function buildList(property, path, value, uniqueId) {
+               if (value && !Array.isArray(value)) {
+                       value = [value];
+               }
+
+               function getListItemSummary(index, value) {
+                       const keys = property.key.map((key) => value[key]);
+                       const summary = keys.join(' ');
+                       return summary.length > 1 ? summary : '' + (index + 1);
+               }
+               const children = value ? value.reduce((a, itemValue, i) => {
+                       const itemPath = path.concat([i]);
+                       return a.concat(buildComponentsForProperties(property.properties, itemPath, itemValue));
+               }, [makeOption(path)])
+                       : [makeOption(path)];
+               return children;
+       }
+
+       function buildSimpleList(property, path, value, uniqueId) {
+               return [makeOption(path)];
+       }
+
+       function buildContainer(property, path, value, uniqueId, readOnly) {
+               return buildComponentsForProperties(property.properties, path, value);
+       }
+
+       /**
+        * buiid and return an array of components representing an editor for each property.
+        * 
+        * @param {any} container the master document being edited
+        * @param {[property]} properties 
+        * @param {string} pathToProperties path within the container to the properties
+        * @param {Object} data source for each property
+        * which may be useful/necessary to a components rendering.
+        * @returns an array of react components
+        */
+       function buildComponentsForProperties(properties, pathToProperties, data) {
+               return properties.reduce((a, property) => {
+                       let value;
+                       let propertyPath = pathToProperties.slice();
+                       if (property.type != 'choice') {
+                               propertyPath.push(property.name);
+                       }
+                       if (data && typeof data === 'object') {
+                               value = _get(data, property.name);
+                       }
+                       let result = [];
+                       try {
+                               result = buildPropertyComponent(property, propertyPath, value);
+                       } catch (e) {
+                               console.error(e);
+                       }
+                       return a.concat(result);
+               }, []);
+       }
+
+       function buildPropertyComponent(property, path, value) {
+
+               const fields = [];
+               const isObject = Property.isObject(property);
+               const title = changeCase.titleCase(property.name);
+
+               // create a unique Id for use as react component keys and html element ids
+               // use uid (from ui info) instead of id property (which is not stable)
+               let uniqueId = container.uid;
+               let containerRef = container;
+               while (containerRef.parent) {
+                       uniqueId = containerRef.parent.uid + ':' + uniqueId;
+                       containerRef = containerRef.parent;
+               }
+               uniqueId += ':' + path.join(':')
+
+               if (!property.properties && isObject) {
+                       console.debug('no properties', property);
+                       const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
+                       property.properties = uiState.properties;
+               }
+
+               if (property.type === 'leaf') {
+                       return buildField(property, path, value, uniqueId);
+               } else if (property.type === 'leaf_list') {
+                       return buildLeafList(property, path, value, uniqueId);
+               } else if (property.type === 'list') {
+                       return Property.isSimpleList(property) ?
+                               buildSimpleList(property, path, value, uniqueId) :
+                               buildList(property, path, value, uniqueId);
+               } else if (property.type === 'container') {
+                       return buildContainer(property, path, value, uniqueId);
+               } else if (property.type === 'choice') {
+                       return buildChoice(property, path, value, uniqueId);
+               } else {
+                       return ([]);
+               }
+       }
+
+
+       if (!(DescriptorModelFactory.isContainer(container))) {
+               return null;
+       }
+
+       const containerType = container.uiState['qualified-type'] || container.uiState.type;
+       let properties = DescriptorModelMetaFactory.getModelMetaForType(containerType).properties;
+       // bubble all data properties to top of list
+       let twoLists = properties.reduce((o, property) => {
+               const value = _get(container.model, [property.name]);
+               if (isDataProperty(property)) {
+                       o.listOne.push(property);
+               } else {
+                       o.listTwo.push(property);
+               }
+               return o;
+       }, {
+                       listOne: [],
+                       listTwo: []
+               });
+       properties = twoLists.listOne.concat(twoLists.listTwo);
+       const options = buildComponentsForProperties(properties, [], container.model);
+       return options.length ?
+               (<PropertyNavigate container={container} idMaker={idMaker} options={options} 
+                       style={style} placeholder="Select to navigate" />)
+               : <div />;
+};
\ No newline at end of file
index 16dc5fd..27852c9 100644 (file)
@@ -48,12 +48,7 @@ const RiftHeader = React.createClass({
        },
        componentDidMount() {
                RiftHeaderStore.listen(this.onChange);
-               const loadCatalogs = function () {
-                       RiftHeaderStore.loadCatalogs();
-                       uiTransientState.timeoutId = setTimeout(loadCatalogs, 2000);
-               };
                RiftHeaderStore.requestLaunchpadConfig();
-               loadCatalogs();
        },
        componentDidUpdate() {
        },
@@ -68,9 +63,6 @@ const RiftHeader = React.createClass({
                this.setState(state);
        },
        onClickOpenDashboard() {
-               if (uiTransientState.timeoutId) {
-                       clearTimeout(uiTransientState.timeoutId);
-               }
                RiftHeaderStore.unlisten(this.onChange);
                window.location.href = '//' + window.location.hostname + ':8000/index.html?api_server=' + utils.getSearchParams(window.location).api_server + '#/launchpad/' + utils.getSearchParams(window.location).mgmt_domain_name;
        },
index 031c996..d5eea51 100644 (file)
@@ -43,14 +43,17 @@ const ASSET_TYPE = {
         { id: 'ICONS', folder: 'icons', title: "Icons", allowFolders: false },
         { id: 'SCRIPTS', folder: 'scripts', title: "scripts", allowFolders: true },
         { id: 'NS_CONFIG', folder: 'ns_config', title: "NS Config", allowFolders: false },
-        { id: 'VNF_CONFIG', folder: 'vnf_config', title: "VNF Config", allowFolders: false }
+        { id: 'VNF_CONFIG', folder: 'vnf_config', title: "VNF Config", allowFolders: false },
+        { id: 'DOC', folder: 'doc', title: "Doc", allowFolders: false },
+        { id: 'TEST', folder: 'test', title: "Test", allowFolders: false }
     ],
     'vnfd': [
         { id: 'ICONS', folder: 'icons', title: "Icons", allowFolders: false },
         { id: 'CHARMS', folder: 'charms', title: "charms", allowFolders: true },
         { id: 'SCRIPTS', folder: 'scripts', title: "scripts", allowFolders: true },
-        { id: 'IMAGES', folder: 'images', title: "images", allowFolders: false },
         { id: 'CLOUD_INIT', folder: 'cloud_init', title: "cloud_init", allowFolders: false },
+        { id: 'DOC', folder: 'doc', title: "Doc", allowFolders: false },
+        { id: 'TEST', folder: 'test', title: "Test", allowFolders: false },
         { id: 'README', folder: '.', title: "readme", allowFolders: false }
     ]
 }
@@ -73,10 +76,16 @@ function normalizeAssets(packageType, assetInfo, filesStatus) {
             folders.reverse();
             assets[assetGroup.id] = folders.map(fullName => {
                 let path = fullName.slice(typeFolder.length + 1);
-                let files = assetInfo.data[fullName].map(info => ({
-                    name: info.name.startsWith(fullName) ? info.name.slice(fullName.length + 1) : info.name,
-                    status: filesStatus[info.name]
-                }));
+                let files = assetInfo.data[fullName].reduce((assets, info) =>
+                    {
+                        let name = info.name.startsWith(fullName) ? info.name.slice(fullName.length + 1) : info.name;
+                        let status = filesStatus[info.name];
+                        if (fullName !== '.' || !(name.endsWith('.yaml') || name.startsWith('checksum'))) {
+                            assets.push({ name, status });
+                        }
+                        return assets;
+                    }
+                , []);
                 return { path, files };
             });
         }
@@ -105,8 +114,13 @@ class FileManager extends React.Component {
     }
     render() {
         let { files, filesState, type, item, actions } = this.props;
+        if (!item) {
+            return null;
+        }
         let assets = normalizeAssets(type, files, filesState);
         let children = [];
+        const User = this.props.User || {};
+        const ProjectID =  User.projectId;
         let assetTypes = ASSET_TYPE[type];
         assetTypes.forEach(assetGroup => {
             const typeFolder = assetGroup.folder;
@@ -129,6 +143,7 @@ class FileManager extends React.Component {
                     allowsFolders={assetGroup.allowFolders}
                     folders={subFolders}
                     showNotification={actions.showNotification}
+                    ProjectID={ProjectID}
                 />
             )
         }, this);
@@ -201,7 +216,7 @@ class AssetGroup extends React.Component {
             <Panel title={title} itemClassName="nested" no-corners>
                 {folderCreateComponent}
                 <div className="folder">
-                    <FileAssetList files={files} path={path} packageId={packageId} packageType={packageType} assetGroup={assetGroup} />
+                    <FileAssetList files={files} path={path} packageId={packageId} packageType={packageType} assetGroup={assetGroup} ProjectID={this.props.ProjectID} />
                     <Panel className="addFileSection" no-corners>
                         <ItemUpload packageType={packageType} packageId={packageId} path={path} assetGroup={assetGroup} />
                         <div style={{ marginLeft: '0.5rem' }}>
@@ -227,7 +242,7 @@ function FileAssetList(props) {
     if (files) {
         children = files.map(function (file, i) {
             if (!file.hasOwnProperty('contents')) {
-                return <FileAsset key={file.name} file={file} path={path} id={packageId} type={packageType} assetGroup={assetGroup} />
+                return <FileAsset ProjectID={props.ProjectID} key={file.name} file={file} path={path} id={packageId} type={packageType} assetGroup={assetGroup} />
             }
         })
     }
@@ -240,7 +255,7 @@ function FileAssetList(props) {
 }
 
 function FileAsset(props) {
-    let { file, path, type, assetGroup, id } = props;
+    let { file, path, type, assetGroup, id, ProjectID } = props;
     const name = file.name;
     const downloadHost = API_SERVER.match('localhost') || API_SERVER.match('127.0.0.1') ? `${window.location.protocol}//${window.location.hostname}` : API_SERVER;
     //{`${window.location.protocol}//${API_SERVER}:4567/api/package${type}/${id}/${path}/${name}`}
@@ -253,7 +268,7 @@ function FileAsset(props) {
                         {file.status && (file.status == 'IN_PROGRESS' || file.status == 'DOWNLOADING') ? <LoadingIndicator size={2} /> : file.status}
                     </div>
                     <div className="file-name">
-                        <a target="_blank" href={`${downloadHost}:4567/api/package/${type}/${id}/${assetGroup.folder}${path}/${name}`}>{name}</a>
+                        <a target="_blank" href={`${downloadHost}:8008/mano/api/package/${type}/${ProjectID}/${id}/${assetGroup.folder}${path}/${name}`}>{name}</a>
                     </div>
                 </div>
                 <div className="file-action"
index 616eaad..5e5a4e4 100644 (file)
@@ -153,14 +153,15 @@ const FileManagerSource = {
     openDownloadMonitoringSocket: function() {
         return {
             remote: function(state, packageID) {
+                let encodedId = encodeURIComponent(packageID);
                 return new Promise(function(resolve, reject) {
                     //api/operational/download-jobs/job/
                    $.ajax({
-                    url: '/socket-polling?api_server=' + API_SERVER,
+                    url: '/socket-polling',
                     type: 'POST',
                     beforeSend: Utils.addAuthorizationStub,
                     data: {
-                      url: 'composer/api/file-manager/jobs/' + packageID + '?api_server=' + API_SERVER,
+                      url: 'composer/api/file-manager/jobs/' + encodedId + '?api_server=' + API_SERVER,
                     },
                     success: function(data, textStatus, jqXHR) {
                         Utils.checkAndResolveSocketRequest(data, resolve, reject);
@@ -178,14 +179,15 @@ const FileManagerSource = {
     openFileMonitoringSocket: function() {
         return {
             remote: function(state, id, type) {
+                let encodedId = encodeURIComponent(id);
                 return new Promise(function(resolve, reject) {
                     //api/operational/download-jobs/job/
                    $.ajax({
-                    url: '/socket-polling?api_server=' + API_SERVER,
+                    url: '/socket-polling',
                     type: 'POST',
                     beforeSend: Utils.addAuthorizationStub,
                     data: {
-                      url: 'composer/api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + id
+                      url: 'composer/api/file-manager?api_server=' + utils.getSearchParams(window.location).api_server +'&package_type=' + type + '&package_id=' + encodedId
                     },
                     success: function(data, textStatus, jqXHR) {
                         Utils.checkAndResolveSocketRequest(data, resolve, reject);
index 0128aa4..c66641a 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,11 +29,11 @@ const message = {
        detailsWelcome() {
                return <p className="welcome-message">Select an object to view details.</p>;
        },
-       canvasWelcome() {
+       canvasWelcome(isValid) {
                return (
                        <span>
                                <p className="welcome-message">Double-click a Descriptor to open.</p>
-                               <p className="welcome-message">Or drag a Descriptor to add to Canvas.</p>
+                               {isValid ? <p className="welcome-message">Or drag a Descriptor to add to Canvas.</p> : null}
                        </span>
                );
        },
@@ -43,8 +43,8 @@ const message = {
        get showLessTitle() {
                return 'Show Less';
        },
-       get catalogWelcome() {
-               return <p className="welcome-message">To onboard a descriptor, drag the package to the catalog or click the Onboard button (<img style={{width: '20px'}} src={imgOnboard} />) to select the package.</p>;
+       catalogWelcome(isValid) {
+               return isValid ? <p className="welcome-message">To onboard a descriptor, drag the package to the catalog or click the Onboard button (<img style={{width: '20px'}} src={imgOnboard} />) to select the package.</p> : <p className="welcome-message"> No descriptors have been onboarded</p> ;
        },
        getSaveActionLabel(isNew) {
                return isNew ? 'Onboard' : 'Update';
diff --git a/skyquake/plugins/composer/src/src/components/model/Choice.jsx b/skyquake/plugins/composer/src/src/components/model/Choice.jsx
new file mode 100644 (file)
index 0000000..6d732fe
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ *
+ *   Copyright 2017 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 changeCase from 'change-case'
+import Button from '../Button'
+import ClassNames from 'classnames'
+import CatalogItemsActions from '../../actions/CatalogItemsActions'
+import DescriptorModelMetaFactory from '../../libraries/model/DescriptorModelMetaFactory'
+import Property from '../../libraries/model/DescriptorModelMetaProperty'
+import utils from '../../libraries/utils'
+
+import PropertyPanel from './PropertyPanel'
+import Select from './Select'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+import imgAdd from '../../../../node_modules/open-iconic/svg/plus.svg'
+
+export default function (props) {
+       const { id, property, value, readOnly, onChange, showHelp, children } = props;
+
+       const title = changeCase.titleCase(property.name);
+       const helpText = showHelp ? property.description : null;
+       const cases = property.properties.map(d => ({ name: d.name, value: d.name }));
+
+       return (
+               <div className='-is-leaf'>
+                       <PropertyPanel title={title} helpText={helpText}>
+                               <div className="choice">
+                                       <Select
+                                               id={id}
+                                               value={value}
+                                               options={cases}
+                                               title={title}
+                                               placeholder={property.name}
+                                               onChange={onChange}
+                                               readOnly={readOnly} />
+                                       {children}
+                               </div>
+                       </PropertyPanel >
+               </div >
+       );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/Container.jsx b/skyquake/plugins/composer/src/src/components/model/Container.jsx
new file mode 100644 (file)
index 0000000..aa24081
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ *
+ *   Copyright 2017 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 PropertyPanel from './PropertyPanel'
+import changeCase from 'change-case'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+
+export default function (props) {
+       const { id, property, summary, showHelp, showOpened, onChangeOpenState, children } = props;
+       const title = changeCase.titleCase(property.name);
+       const helpText = showHelp ? property.description : null;
+       const info = showOpened ? null : summary;
+       return (
+               <div id={id} className='-is-container'>
+                       <PropertyPanel title={title} info={info} helpText={helpText}
+                               showOpened={showOpened} onChangeOpenState={onChangeOpenState}>
+                               <div className={'property-content'} >
+                                       {children}
+                               </div>
+                       </PropertyPanel>
+               </div>
+       );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/EditDescriptorUtils.js b/skyquake/plugins/composer/src/src/components/model/EditDescriptorUtils.js
new file mode 100644 (file)
index 0000000..aec8f3d
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ *
+ *   Copyright 2016-2017 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 getEventPath from '../../libraries/getEventPath'
+import DeletionManager from '../../libraries/DeletionManager'
+
+function startEditing() {
+       DeletionManager.removeEventListeners();
+}
+
+function endEditing() {
+       DeletionManager.addEventListeners();
+}
+
+function onFocusPropertyFormInputElement(event) {
+
+       console.debug('property focus ', event.target.id);
+       event.preventDefault();
+       startEditing();
+
+       function removeIsFocusedClass(event) {
+               event.target.removeEventListener('blur', removeIsFocusedClass);
+               Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
+       }
+
+       removeIsFocusedClass(event);
+
+       const propertyWrapper = getEventPath(event).reduce((parent, element) => {
+               if (parent) {
+                       return parent;
+               }
+               if (!element.classList) {
+                       return false;
+               }
+               if (element.classList.contains('property')) {
+                       return element;
+               }
+       }, false);
+
+       if (propertyWrapper) {
+               propertyWrapper.classList.add('-is-focused');
+               event.target.addEventListener('blur', removeIsFocusedClass);
+       }
+}
+
+function getTitle(model = {}) {
+       if (typeof model['short-name'] === 'string' && model['short-name']) {
+               return model['short-name'];
+       }
+       if (typeof model.name === 'string' && model.name) {
+               return model.name;
+       }
+       if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
+               return model.uiState.displayName
+       }
+       if (typeof model.id === 'string') {
+               return model.id;
+       }
+}
+
+export {
+       startEditing,
+       endEditing,
+       onFocusPropertyFormInputElement,
+       getTitle
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/components/model/LeafEditor.jsx b/skyquake/plugins/composer/src/src/components/model/LeafEditor.jsx
new file mode 100644 (file)
index 0000000..1a00d28
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ *
+ *   Copyright 2016-2017 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 _debounce from 'lodash/debounce';
+import React from 'react'
+import ClassNames from 'classnames'
+import changeCase from 'change-case'
+import Property from '../../libraries/model/DescriptorModelMetaProperty'
+import CatalogDataStore from '../../stores/CatalogDataStore'
+import _isInt from 'validator/lib/isInt'
+import _toInt from 'validator/lib/toInt'
+import _isFloat from 'validator/lib/isFloat'
+import _toFloat from 'validator/lib/toFloat'
+import _trim from 'validator/lib/trim'
+import _isIP from 'validator/lib/isIP'
+
+import { startEditing, endEditing, onFocusPropertyFormInputElement } from './EditDescriptorUtils'
+import Select from './Select'
+
+function validateRequired(isRequired, value) {
+       value = value.trim();
+       return isRequired && !value ? { success: false, message: "A value is required." } : { success: true, value: null };
+}
+
+function editorExitHandler(isValueRequired, onExit, onError, event) {
+       const value = event.target.value;
+       const result = validateRequired(isValueRequired, value);
+       onExit && onExit(result);
+       endEditing();
+}
+
+function Enumeration(props) {
+       const { id, property, title, readOnly, onChange, onError, onExit } = props;
+       let value = props.value;
+       const enumeration = Property.getEnumeration(property, value);
+       const hasDefaultValue = !!property['default-value'];
+       const required = property.mandatory || hasDefaultValue;
+       if (!value && hasDefaultValue) {
+               value = property['default-value'];
+       }
+       return (
+               <Select
+                       id={id}
+                       value={value}
+                       options={enumeration}
+                       title={title}
+                       placeholder={property.name}
+                       onChange={onChange}
+                       onExit={editorExitHandler.bind(null, required, onExit, onError)}
+                       required={required} r
+                       readOnly={readOnly} />
+       );
+}
+
+function Reference(props) {
+       const { id, property, title, path, container, readOnly, onChange, onError, onExit } = props;
+       let value = props.value;
+       const catalogs = props.catalogs || CatalogDataStore.getTransientCatalogs();
+       let fullPathString = container.key + ':' + path.join(':');
+       let containerRef = container;
+       while (containerRef.parent) {
+               fullPathString = containerRef.parent.key + ':' + fullPathString;
+               containerRef = containerRef.parent;
+       }
+       const leafRefPathValues = Property.getLeafRef(property, path, value, fullPathString, catalogs, container);
+       const required = property.mandatory;
+       if (value && !leafRefPathValues.find(option => option.isSelected)) {
+               value = null;
+       }
+
+       return (
+               <Select
+                       id={id}
+                       value={value}
+                       options={leafRefPathValues}
+                       title={title}
+                       placeholder={property.name}
+                       onChange={onChange}
+                       onExit={editorExitHandler.bind(null, required, onExit, onError)}
+                       required={required}
+                       readOnly={readOnly} />
+       );
+}
+
+function Boolean(props) {
+       const { id, property, title, readOnly, onChange, onError, onExit } = props;
+       let value = props.value;
+       const hasDefaultValue = !!property['default-value'];
+       const required = property.mandatory || hasDefaultValue;
+       const typeOfValue = typeof value;
+       if (typeOfValue === 'number' || typeOfValue === 'boolean') {
+               value = value ? 'TRUE' : 'FALSE';
+       } else if (value) {
+               value = value.toUpperCase();
+       } else {
+               if (hasDefaultValue) {
+                       value = ('' + property['default-value']).toUpperCase();
+               }
+       }
+       const options = [
+               { name: "TRUE", value: 'TRUE' },
+               { name: "FALSE", value: 'FALSE' }
+       ]
+       return (
+               <Select
+                       id={id}
+                       value={value}
+                       options={options}
+                       title={title}
+                       placeholder={property.name}
+                       onChange={onChange}
+                       onExit={editorExitHandler.bind(null, required, onExit, onError)}
+                       required={required}
+                       readOnly={readOnly} />
+       );
+}
+
+function Empty(props) {
+       // 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
+       const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
+       const { id, property, value, title, readOnly, onChange } = props;
+       let isEmptyLeafPresent = !!value;
+       let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
+       const options = [
+               { name: "Enabled", value: EMPTY_LEAF_PRESENT }
+       ]
+
+       return (
+               <Select
+                       id={id}
+                       value={present}
+                       placeholder={"Not Enabled"}
+                       options={options}
+                       title={title}
+                       onChange={onChange}
+                       readOnly={readOnly} />
+       );
+}
+
+function getValidator(property) {
+       function validateInteger(constraints, value) {
+               return _isInt(value, constraints) ? { success: true, value: _toInt(value) } :
+                       { success: false, value, message: "The value is not an integer or does not meet the property constraints." };
+       }
+       function validateDecimal(constraints, value) {
+               return _isFloat(value, constraints) ? { success: true, value: _toFloat(value) } :
+                       { success: false, value, message: "The value is not a decimal number or does not meet the property constraints." };
+       }
+       function validateProperty(validator, errorMessage, value) {
+               return validator(value) ? { success: true, value } :
+                       { success: false, value, message: errorMessage };
+       }
+       const name = property.name;
+       if (name === 'ip-address' || name.endsWith('-ip-address')) {
+               return validateProperty.bind(null, _isIP, "The value is not a valid ip address.")
+       }
+       switch (property['data-type']) {
+               case 'int8':
+                       return validateInteger.bind(null, { min: -128, max: 127 });
+               case 'int16':
+                       return validateInteger.bind(null, { min: -32768, max: 32767 });
+               case 'int32':
+                       return validateInteger.bind(null, { min: -2147483648, max: 2147483647 });
+               case 'int64':
+                       return validateInteger.bind(null, null);
+               case 'uint8':
+                       return validateInteger.bind(null, { min: 0, max: 255 });
+               case 'uint16':
+                       return validateInteger.bind(null, { min: 0, max: 65535 });
+               case 'uint32':
+                       return validateInteger.bind(null, { min: 0, max: 4294967295 });
+               case 'uint64':
+                       return validateInteger.bind(null, { min: 0 });
+               case 'decimal64':
+                       return validateDecimal.bind(null, null)
+               case 'string':
+               default:
+                       return function (value) { return { success: true, value } };
+       }
+}
+
+function messageTemplate(strings, ...keys) {
+       return (function (...vars) {
+               let helpInfo = vars.reduce((o, info) => Object.assign(o, info), {});
+               return keys.reduce((s, key, i) => {
+                       return s + helpInfo[key] + strings[i + 1];
+               }, strings[0]);
+       });
+}
+
+const errorMessage = messageTemplate`"${'value'}" is ${'error'}. ${'message'}`;
+
+class Input extends React.Component {
+       constructor(props) {
+               super(props);
+               let originalValue = props.value ? props.value : null; // normalize empty value
+               this.state = { originalValue };
+       }
+
+       componentWillReceiveProps(nextProps) {
+               const { value } = nextProps
+               if (value !== this.state.originalValue) {
+                       let originalValue = value ? value : null; // normalize empty value
+                       this.setState({ originalValue })
+               }
+       }
+
+       render() {
+               const { id, property, value, title, readOnly, onChange, onError, onExit } = this.props;
+               const { originalValue } = this.state;
+               const isGuid = Property.isGuid(property);
+               const className = ClassNames(property.name + '-input', { '-is-guid': isGuid, '-is-required': required });
+               const placeholder = property.name;
+               const required = property.mandatory;
+
+               const validator = getValidator(property);
+               function handleValueChanged(newValue) {
+                       newValue = newValue.trim();
+                       const result = !newValue ? validateRequired(required, newValue) : validator(newValue);
+                       result.success ? (originalValue !== result.value) && onChange(result.value) : onError(result.message);
+               }
+               const changeHandler = _debounce(handleValueChanged, 2000);
+               function onInputChange(e) {
+                       e.preventDefault();
+                       changeHandler(_trim(e.target.value));
+               }
+               function onBlur(e) {
+                       changeHandler.cancel();
+                       const value = _trim(e.target.value);
+                       const result = !value ? validateRequired(required, value) : validator(value);
+                       if (result.success) {
+                               // just in case we missed it by cancelling the debouncer
+                               (originalValue !== result.value) && onChange(result.value);
+                       }
+                       onExit(result);
+               }
+               if (property['preserve-line-breaks']) {
+                       return (
+                               <textarea
+                                       cols="5"
+                                       id={id}
+                                       defaultValue={value}
+                                       placeholder={placeholder}
+                                       className={className}
+                                       onChange={readOnly ? null : onInputChange}
+                                       onFocus={onFocusPropertyFormInputElement}
+                                       onBlur={readOnly ? null : editorExitHandler.bind(null, required, onExit, onError)}
+                                       onMouseDown={startEditing}
+                                       onMouseOver={startEditing}
+                                       onMouseOut={endEditing}
+                                       onMouseLeave={endEditing}
+                                       required={required} readOnly={readOnly} />
+                       );
+               }
+               return (
+                       <input
+                               id={id}
+                               type="text"
+                               defaultValue={value}
+                               className={className}
+                               placeholder={placeholder}
+                               onChange={readOnly ? null : onInputChange}
+                               onFocus={onFocusPropertyFormInputElement}
+                               onBlur={readOnly ? null : onBlur}
+                               onMouseDown={startEditing}
+                               onMouseOver={startEditing}
+                               onMouseOut={endEditing}
+                               onMouseLeave={endEditing}
+                               required={required} readOnly={readOnly}
+                       />
+               );
+       }
+}
+
+export {
+       Input,
+       Empty,
+       Boolean,
+       Reference,
+       Enumeration
+};
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/components/model/LeafField.jsx b/skyquake/plugins/composer/src/src/components/model/LeafField.jsx
new file mode 100644 (file)
index 0000000..15d93e2
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ *
+ *   Copyright 2017 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 Button from '../Button'
+import ClassNames from 'classnames'
+import changeCase from 'change-case'
+import Property from '../../libraries/model/DescriptorModelMetaProperty'
+import { Input, Empty, Boolean, Reference, Enumeration } from './LeafEditor'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+
+function buildEditor(container, property, path, value, readOnly, id, onChange, onError, onExit) {
+       const title = path.join('.');
+
+       let editor = null;
+       if (Property.isEnumeration(property)) {
+               editor = <Enumeration
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (Property.isLeafRef(property)) {
+               editor = <Reference
+                       container={container}
+                       property={property}
+                       path={path}
+                       value={value}
+                       id={id}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (Property.isBoolean(property)) {
+               editor = <Boolean
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (Property.isLeafEmpty(property)) {
+               editor = <Empty
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       } else if (Property.name === 'meta') {
+               if (typeof value === 'object') {
+                       value = JSON.stringify(value, undefined, 12);
+               } else if (typeof value !== 'string') {
+                       value = '{}';
+               }
+       } else {
+               editor = <Input
+                       property={property}
+                       id={id}
+                       value={value}
+                       title={title}
+                       onChange={onChange} onError={onError} onExit={onExit}
+                       readOnly={readOnly}
+               />
+       }
+       return editor;
+}
+
+export default class LeafField extends React.Component {
+       constructor(props) {
+               super(props);
+               this.state = { showHelp: props.showHelp, isInError: props.errorMessage };
+       }
+       componentWillReceiveProps(nextProps) {
+               const { showHelp, errorMessage } = nextProps
+               if (showHelp !== this.state.showHelp && !this.state.isInError) {
+                       this.setState({ showHelp })
+               }
+               if (errorMessage !== this.state.errorMessage) {
+                       this.setState({ showHelp: !!errorMessage || showHelp, isInError: errorMessage })
+               }
+       }
+       render() {
+               const { container, property, path, value, id, readOnly, onChange, onError } = this.props;
+               let title = changeCase.titleCase(property.name);
+               const showHelp = this.state.showHelp;
+               const description = property.description;
+               const helpText = this.state.isInError ? this.state.isInError + " " + description : description;
+               if (property.mandatory) {
+                       title = "* " + title;
+               }
+               const errorHandler = (message) => {
+                       this.setState({ showHelp: true, isInError: message });
+               }
+               const changeHandler = (value) => {
+                       this.setState({ showHelp: this.props.showHelp, isInError: false });
+                       onChange && onChange(value);
+               }
+               const exitHandler = (exitResult) => {
+                       if (!exitResult.success) {
+                               // errorHandler(exitResult.message);
+                               onError && onError(exitResult.message);
+                       }
+               }
+
+               const editor = buildEditor(
+                       container, property, path, value, readOnly, id,
+                       changeHandler, errorHandler, exitHandler);
+               const helpStyle = {
+                       display: showHelp ? 'inline-block' : 'none',
+                       paddingTop: '2px',
+                       color: this.state.isInError ? 'red' : 'inherit'
+               };
+               return (
+                       <div className={ClassNames('leaf-property -is-leaf property')}>
+                               <h3 className="property-label">
+                                       <label htmlFor={id}>
+                                               <span className={'leaf-name name info'}>{title}</span>
+                                       </label>
+                               </h3>
+                               <val className="property-value">
+                                       <div className={ClassNames('property-content')}>
+                                               {editor}
+                                       </div>
+                               </val>
+                               <span className={'leaf-description description'}
+                                       style={helpStyle}>{helpText}</span>
+                       </div>
+               );
+       }
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/components/model/List.jsx b/skyquake/plugins/composer/src/src/components/model/List.jsx
new file mode 100644 (file)
index 0000000..a4b1204
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ *
+ *   Copyright 2017 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 changeCase from 'change-case'
+import Button from '../Button'
+import ClassNames from 'classnames'
+import CatalogItemsActions from '../../actions/CatalogItemsActions'
+import DescriptorModelMetaFactory from '../../libraries/model/DescriptorModelMetaFactory'
+import Property from '../../libraries/model/DescriptorModelMetaProperty'
+import utils from '../../libraries/utils'
+
+import PropertyPanel from './PropertyPanel'
+import PanelHeader from './PanelHeader'
+import RemoveItemButton from './RemoveItemButton'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+import imgAdd from '../../../../node_modules/open-iconic/svg/plus.svg'
+import imgRemove from '../../../../node_modules/open-iconic/svg/trash.svg'
+
+function ListItemHeader(props) {
+       const { 
+               property, showOpened, onChangeOpenState, 
+               info, summary, readOnly, removeItemHandler } = props;
+       const className = ClassNames(property.name + '-property');
+       const title = changeCase.title(property.name);
+       const name = showOpened ? title : summary ? '' : title;
+       const newInfo = showOpened ? info : summary || info;
+       return (
+               <div className={className}>
+                       <PanelHeader name={name} info={newInfo}
+                               showOpened={showOpened} onChangeOpenState={onChangeOpenState}
+                               action={readOnly ? null : { invoke: removeItemHandler, icon: imgRemove, title: "Remove" }} />
+               </div>
+       );
+}
+function ListItem(props) {
+       const { 
+               property, info, summary, readOnly, 
+               showOpened, onChangeOpenState, children, removeItemHandler } = props;
+       return (
+               <div className={'property-content'}>
+                       <ListItemHeader property={property} info={info} summary={summary} 
+                               showOpened={showOpened} onChangeOpenState={onChangeOpenState}
+                               readOnly={readOnly} removeItemHandler={removeItemHandler} />
+                       {showOpened ? children : null}
+               </div>
+       );
+}
+
+
+class List extends React.Component {
+       constructor(props) {
+               super(props);
+       }
+       render() {
+               const {
+                       property, value, readOnly, showHelp, tip, showOpened,
+                       onChangeOpenState, children, addItemHandler, id } = this.props;
+               const title = changeCase.titleCase(property.name);
+               const info = (children ? children.length : '0') + ' items';
+               const description = property.description;
+               const nodeType = property.type;
+               const tipText = tip && !readOnly ?
+                       <span key="tip" className={nodeType + '-tip tip'}>{tip}</span>
+                       : null;
+
+               let classNames = ['-is-array'];
+               if (property.type === 'leaf_list') {
+                       classNames.push('-is-leaf');
+               }
+
+               return (
+                       <div id={id} className={ClassNames(classNames)}>
+                               <PropertyPanel title={title} info={info} helpText={showHelp ? description : null}
+                                       showOpened={showOpened} onChangeOpenState={onChangeOpenState}
+                                       action={readOnly ? null : { invoke: addItemHandler, icon: imgAdd, title: "Add" }}>
+                                       {tipText ? [tipText].concat(children) : children}
+                               </PropertyPanel>
+                       </div >
+               );
+       }
+}
+
+List.defaultProps = {
+}
+
+export {
+       List,
+       ListItem
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/components/model/ListItemAsLink.jsx b/skyquake/plugins/composer/src/src/components/model/ListItemAsLink.jsx
new file mode 100644 (file)
index 0000000..b629c02
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ *
+ *   Copyright 2016-2017 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 Property from '../../libraries/model/DescriptorModelMetaProperty'
+import { getTitle } from './EditDescriptorUtils'
+import DescriptorModelIconFactory from '../../libraries/model/IconFactory'
+import RemoveItemButton from './RemoveItemButton'
+
+export default function ListItemAsLink(props) {
+    const { property, value, removeItemHandler, selectLinkHandler } = props;
+    // todo need to abstract this better
+    const title = getTitle(value);
+    var req = require.context("../../", true, /\.svg/);
+
+    function onClickSelectItem(event) {
+        event.preventDefault();
+        selectLinkHandler();
+    }
+
+    return (
+        <div className='property-content simple-list'>
+            <a href="#select-list-item" className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem}>
+                <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
+                <span>{title}</span>
+            </a>
+            <RemoveItemButton removeItemHandler={removeItemHandler} />
+        </div>
+    );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/ModelBreadcrumb.jsx b/skyquake/plugins/composer/src/src/components/model/ModelBreadcrumb.jsx
new file mode 100644 (file)
index 0000000..b4579cf
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ *
+ *   Copyright 2016-2017 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 ComposerAppActions from '../../actions/ComposerAppActions'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+
+export default function ModelBreadcrumb(props) {
+    const { path } = props;
+    const linkCount = path.length - 1;
+    function onClickSelectItem(node, event) {
+        event.preventDefault();
+       ComposerAppActions.selectModel(node);
+    }
+    const crumbs = path.map((node, i) =>
+        i < linkCount ?
+            <span key={node.title}>
+                <a href="#select-item" onClick={onClickSelectItem.bind(null, node)}>{node.title}</a>
+                <i> / </i>
+            </span>
+            :
+            <span key={node.title}>
+                {node.title}
+            </span>
+    );
+    return <h1>{crumbs}</h1>
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/PanelHeader.jsx b/skyquake/plugins/composer/src/src/components/model/PanelHeader.jsx
new file mode 100644 (file)
index 0000000..72edfbe
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ *
+ *   Copyright 2017 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 changeCase from 'change-case'
+import Button from '../Button'
+import { CaretRightIcon } from 'react-open-iconic-svg';
+import { CaretBottomIcon } from 'react-open-iconic-svg';
+
+import '../../styles/EditDescriptorModelProperties.scss'
+
+export default function PanelHeader(props) {
+       const {
+               name, info, action, helpText,
+               showOpened, onChangeOpenState } = props;
+
+       function onClickOpenClose(e) {
+               onChangeOpenState();
+       }
+       function onClickAction(e) {
+               e.preventDefault();
+               action.invoke();
+       }
+
+       const isExpandCollapseButtonNeeded = !!onChangeOpenState;
+       const isOpened = isExpandCollapseButtonNeeded ? showOpened : true;
+       const actionButton = isOpened && action ?
+               <Button className="inline-hint" onClick={onClickAction} label={action.title} src={action.icon} />
+               : null;
+       const expandoButtonStyle = { fill: '#586e75', cursor: 'pointer' };
+       const expandoButton = !isExpandCollapseButtonNeeded ? null : isOpened ?
+               <CaretBottomIcon style={expandoButtonStyle} onClick={onClickOpenClose} />
+               : <CaretRightIcon style={expandoButtonStyle} onClick={onClickOpenClose} />;
+       const help = isOpened && helpText ? <span className={'description'} >{helpText}</span> : null;
+       return (
+               <div>
+                       <h3 className='property-label'>
+                               {expandoButton}
+                               <span className={'name'} onClick={onClickOpenClose} style={{cursor: 'pointer'}} >{name}</span>
+                               <small>
+                                       <span className={'info'}>{info}</span>
+                               </small>
+                               {actionButton}
+                       </h3>
+                       <div>{help}</div>
+               </div>
+       )
+}
+
diff --git a/skyquake/plugins/composer/src/src/components/model/PropertyCrumb.jsx b/skyquake/plugins/composer/src/src/components/model/PropertyCrumb.jsx
new file mode 100644 (file)
index 0000000..7e6cdad
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ *
+ *   Copyright 2016-2017 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 _keys from 'lodash/keys'
+import _isObject from 'lodash/isObject'
+import DescriptorEditorActions from '../../actions/DescriptorEditorActions'
+import SelectionManager from '../../libraries/SelectionManager'
+
+import '../../styles/EditDescriptorModelProperties.scss'
+
+function makeId(container, path) {
+    let idParts = [path];
+    idParts.push(container.uid);
+    while (container.parent) {
+        container = container.parent;
+        idParts.push(container.uid);
+    }
+    return idParts.reverse().join(':');
+}
+
+export default function PropertyCrumb(props) {
+    const { container, errors } = props;
+    const errorPaths = _keys(errors).reduce((a, k) => {
+        function pieceOfPath(obj, key) {
+            if (_isObject(obj[key])) {
+                const node = obj[key];
+                return _keys(node).reduce((a,k) => {
+                    const paths = pieceOfPath(node, k).map(e => key + '.' + e)
+                    return a.concat(paths);
+                }, []);
+            } else {
+                return obj[key] ? [key] : [];
+            }
+        }
+        const paths = pieceOfPath(errors, k);
+        return a.concat(paths);
+    }, []);
+
+    function onClickSelectItem(path, event) {
+        event.preventDefault();
+        // DescriptorEditorActions.setFocus({descriptor: container, path})
+        const element = document.getElementById(makeId(container, path));
+        element && element.scrollIntoView() && setTimeout(() => element.focus(), 1);
+
+        // const root = node.getRoot();
+        // if (SelectionManager.select(node)) {
+        //     DescriptorEditorActions.catalogItemMetaDataChanged(root.model);
+        // }
+    }
+    const crumbs = errorPaths.map((path, i) => 
+        <span key={path}>
+            <a href="#select-item" onClick={onClickSelectItem.bind(null, path)}>{path}</a>
+        </span>
+    );
+    return (
+        <div style={{ margin: '3px 6px' }} >
+            <h3>{crumbs}</h3>
+        </div>
+    );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/PropertyNavigate.jsx b/skyquake/plugins/composer/src/src/components/model/PropertyNavigate.jsx
new file mode 100644 (file)
index 0000000..4a41e99
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ *
+ *   Copyright 2016-2017 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 _keys from 'lodash/keys'
+import _isObject from 'lodash/isObject'
+
+import Select from 'react-select';
+import 'react-select/dist/react-select.css'
+
+import DescriptorEditorActions from '../../actions/DescriptorEditorActions'
+import CatalogItemsActions from '../../actions/CatalogItemsActions'
+import Property from '../../libraries/model/DescriptorModelMetaProperty'
+import SelectionManager from '../../libraries/SelectionManager'
+
+export default function PropertyNavigate(props) {
+    const { container, idMaker, options, placeholder, style } = props;
+
+    function gotoProperty(descriptor, path) {
+        DescriptorEditorActions.expandPanel({ descriptor, path });
+        function bringIntoViewAndFocusOnProperty() {
+            const element = document.getElementById(idMaker(container, path));
+            if (element) {
+                element.scrollIntoView();
+                setTimeout(function () {
+                    element.focus()
+                }, 100);
+            }
+        }
+        setTimeout(bringIntoViewAndFocusOnProperty, 100);
+    }
+    function onClickSelectItem(item) {
+        // we don't support traversing into an 'external' model (e.g. vlds)
+        // if we did then we would need to know when and then invoke something like
+        // CatalogItemsActions.catalogItemMetaDataChanged(root.model);
+        // and then after a delay fire the gotoProperty step
+        gotoProperty(container, item.value);
+    }
+    return (
+        <div style={style} >
+            <Select
+                placeholder={placeholder}
+                options={options}
+                onChange={onClickSelectItem}
+                scrollMenuIntoView={true}
+            />
+        </div>
+    );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/PropertyPanel.jsx b/skyquake/plugins/composer/src/src/components/model/PropertyPanel.jsx
new file mode 100644 (file)
index 0000000..25f13ef
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ *
+ *   Copyright 2017 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 changeCase from 'change-case'
+import Button from '../Button'
+import { TextIcon } from 'react-open-iconic-svg';
+import { CaretRightIcon } from 'react-open-iconic-svg';
+import { CaretBottomIcon } from 'react-open-iconic-svg';
+import PanelHeader from './PanelHeader'
+
+
+import '../../styles/EditDescriptorModelProperties.scss'
+
+export default function PropertyPanel(props) {
+       const {
+               title, children, info, action, helpText,
+               readOnly, showOpened, onChangeOpenState } = props;
+       const isExpandCollapseButtonNeeded = !!onChangeOpenState;
+       const isOpened = isExpandCollapseButtonNeeded ? showOpened : true;
+
+       const val = (isOpened && children && (Array.isArray(children) ? children.length : true)) ?
+               <val className="property-value">
+                       {children}
+               </val>
+               : null;
+       return (
+               <div className={'property'}>
+                       <PanelHeader name={title} info={info} helpText={helpText}
+                               showOpened={showOpened} onChangeOpenState={onChangeOpenState}
+                               action={readOnly ? null : action} />
+                       {val}
+               </div>
+       );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/RemoveItemButton.jsx b/skyquake/plugins/composer/src/src/components/model/RemoveItemButton.jsx
new file mode 100644 (file)
index 0000000..7164d74
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ *
+ *   Copyright 2016-2017 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 Property from '../../libraries/model/DescriptorModelMetaProperty'
+import utils from '../../libraries/utils'
+import Button from '../Button'
+import CatalogItemsActions from '../../actions/CatalogItemsActions'
+
+import imgRemove from '../../../../node_modules/open-iconic/svg/trash.svg'
+
+export default function RemoveItemButton(props) {
+    const { removeItemHandler } = props;
+    function onClickRemoveProperty(event) {
+        event.preventDefault();
+        removeItemHandler();
+    }
+    return (
+        <Button 
+            className="remove-property-action inline-hint" 
+            title="Remove" 
+            onClick={onClickRemoveProperty} 
+            label="Remove" 
+            src={imgRemove} 
+        />
+    );
+}
diff --git a/skyquake/plugins/composer/src/src/components/model/Select.jsx b/skyquake/plugins/composer/src/src/components/model/Select.jsx
new file mode 100644 (file)
index 0000000..2522000
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ *
+ *   Copyright 2016-2017 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 1/18/16.
+ *
+ * This class generates the form fields used to edit the CONFD JSON model.
+ */
+
+import React from 'react'
+import ClassNames from 'classnames'
+import changeCase from 'change-case'
+import Property from '../../libraries/model/DescriptorModelMetaProperty'
+import CatalogDataStore from '../../stores/CatalogDataStore'
+
+import { startEditing, endEditing, onFocusPropertyFormInputElement } from './EditDescriptorUtils'
+import '../../styles/EditDescriptorModelProperties.scss'
+
+export default function Select(props) {
+       const { id, label, value, options, title, required, readOnly, onChange } = props;
+       const selectOptions = options.map((o, i) => <option key={':' + i} value={o.value}>{o.name}</option>);
+       if (!value || !required) {
+               let placeholder = props.placeholder || " ";
+               placeholder = changeCase.title(placeholder);
+               const noValueDisplayText = placeholder;
+               selectOptions.unshift(<option key={'(value-not-set)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
+       }
+       function onSelectChange(e) {
+               e.preventDefault();
+               onChange(e.target.value);
+       }
+       return (
+               <select
+                       id={id}
+                       className={ClassNames({ '-value-not-set': !value, '-is-required': required })}
+                       defaultValue={value}
+                       title={title}
+                       onChange={onSelectChange}
+                       onFocus={onFocusPropertyFormInputElement}
+                       onBlur={endEditing}
+                       onMouseDown={startEditing}
+                       onMouseOver={startEditing}
+                       required={required} 
+                       disabled={readOnly}>
+                       {selectOptions}
+               </select>
+       );
+}
index 680a02f..c491141 100644 (file)
@@ -45,7 +45,7 @@ function initializeDropZone(element = '#dropzone', button = false, action = ACTI
        let dev_download_server = Utils.getSearchParams(window.location).dev_download_server;
        DropZone.autoDiscover = false;
        return new DropZone(element, {
-               paramName: 'package',
+               paramName: 'file',
                url() {
                        let {packageType, packageId, assetGroup, path} = getUploadProps();
                        if (action === ACTIONS.update) {
diff --git a/skyquake/plugins/composer/src/src/libraries/PackageManagerApi.js b/skyquake/plugins/composer/src/src/libraries/PackageManagerApi.js
deleted file mode 100644 (file)
index 69fa4d6..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- *
- *   Copyright 2016 RIFT.IO Inc
- *
- *   Licensed under the Apache License, Version 2.0 (the "License");
- *   you may not use this file except in compliance with the License.
- *   You may obtain a copy of the License at
- *
- *       http://www.apache.org/licenses/LICENSE-2.0
- *
- *   Unless required by applicable law or agreed to in writing, software
- *   distributed under the License is distributed on an "AS IS" BASIS,
- *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *   See the License for the specific language governing permissions and
- *   limitations under the License.
- *
- */
-
-import guid from '../libraries/guid'
-import DropZone from 'dropzone'
-import Utils from '../libraries/utils'
-import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions'
-import ReactDOM from 'react-dom'
-import $ from 'jquery'
-
-const API_SERVER = Utils.getSearchParams(window.location).api_server;
-
-
-
-
-export default class PackageManager {
-    constructor(element, button, action) {
-        this.stagingArea = {
-            packages: {
-                ids: []
-            }
-        }
-        this.stagingAreaMonitor = null;
-    }
-    createStagingArea(type, name) {
-        return $.ajax({
-            url: Utils.getSearchParams(window.location).api_server + ':8008/api/operations/create-staging-area',
-            type: 'POST',
-            data: {
-                "input" : {
-                    // Package type not important for package upload.
-                    "package-type": type || "NSD",
-                    "name": name || "Package Staging Area"
-                }
-            },
-            error: function() {
-                console.log('Something went wrong creating the staging area: ', arguments)
-            }
-        }).then(function(data) {
-            /*
-            {
-              "output": {
-                "endpoint": "api/upload/85f8e2dc-638b-46e7-89cb-ee8de322066f",
-                "port": "4568"
-              }
-            }
-             */
-            const id = data.output.endpoint.split('/')[2];
-            const port = data.output.port;
-            this.stagingArea.packages.ids.push(id);
-            this.stagingArea.packages[id] = {
-                port: port
-            };
-            return data
-        })
-    }
-    monitoringStagingAreaSocket() {
-        let self = this;
-        if(self.stagingAreaMonitor) {
-            return self.stagingAreaMonitor;
-        }
-        new Promise(function(resolve, reject) {
-            $.ajax({
-                url: '/socket-polling',
-                type: 'POST',
-                beforeSend: Utils.addAuthorizationStub,
-                data: {
-                  url: 'launchpad/api/nsr?api_server=' + API_SERVER
-                },
-                success: function(data, textStatus, jqXHR) {
-                  Utils.checkAndResolveSocketRequest(data, resolve, reject, self.monitoringStagingAreaSocketHandler);
-                }
-            })
-        })
-
-        return undefined;
-    }
-    monitoringStagingAreaSocketHandler(connection) {
-        let self = this;
-        let ws = window.multiplexer.channel(connection);
-        if (!connection) return;
-        self.stagingAreaMonitor = connection;
-        ws.onmessage = function(socket) {
-            try {
-                Utils.checkAuthentication(data.statusCode, function() {
-                    ws.close();
-                });
-
-            } catch(e) {
-                console.log('An exception occurred in monitoringStagingAreaSocketHandler', e)
-            }
-        }
-    }
-
-}
-
-
-
index 3c792e7..71bbaf1 100644 (file)
@@ -12,10 +12,18 @@ import '../styles/TooltipManager.scss'
 class TooltipManager {
 
        static addEventListeners(element = document.body) {
-               TooltipManager.element = element;
+               if (element === TooltipManager.element) {
+                       return;
+               }
+               // remove listeners for current element
                TooltipManager.removeEventListeners();
-               TooltipManager.element.addEventListener('mousedown', TooltipManager.onScrollRemoveTooltip, true);
-               TooltipManager.element.addEventListener('scroll', TooltipManager.onScrollRemoveTooltip, true);
+               TooltipManager.element = element;
+               if (element) { 
+                       // make sure new element is clean
+                       TooltipManager.removeEventListeners();
+                       TooltipManager.element.addEventListener('mousedown', TooltipManager.onScrollRemoveTooltip, true);
+                       TooltipManager.element.addEventListener('scroll', TooltipManager.onScrollRemoveTooltip, true);
+               }
        }
 
        static removeEventListeners() {
index 8f7344e..3e3cd39 100644 (file)
@@ -45,8 +45,16 @@ export default class UID {
                return !!UID.from(obj);
        }
 
-       static assignUniqueId(obj) {
-               return obj[UID.propertyName] = UID.create();
+       static assignUniqueId(obj, uid = null) {
+               if (!obj || /undefined|null/.test(obj)) {
+                       return;
+               }
+               uid = uid || UID.create();
+               if (obj.uiState) {
+                       obj.uiState[UID.propertyName] = uid;
+               } else {
+                       obj[UID.propertyName] = uid;
+               }
        }
 
 }
index 69ae493..519b246 100644 (file)
@@ -147,10 +147,11 @@ export default class DescriptorGraph {
                selection.addContainers(containers);
                selection.render();
 
-               const edgeBuilder = new DescriptorGraphEdgeBuilder(graph);
-               edgeBuilder.addContainers(containers);
-               edgeBuilder.render();
-
+               if (!this.props.readOnly) {
+                       const edgeBuilder = new DescriptorGraphEdgeBuilder(graph);
+                       edgeBuilder.addContainers(containers);
+                       edgeBuilder.render();
+               }
                const grid = new DescriptorGraphGrid(graph, {size: defaults.snapTo, padding: defaults.padding});
                grid.render();
 
index 571fcdf..2a7c009 100644 (file)
@@ -253,7 +253,7 @@ export default function RelationsAndNetworksLayout() {
                                        // warn assigning same instance (e.g. pass by reference) so that changes will reflect thru
                                        cpRef.position = source.position;
                                        connectionPointRefList.push(cpRef);
-                               } catch(e) {
+                               } catch (e) {
                                        return;
                                }
                        });
@@ -408,14 +408,16 @@ export default function RelationsAndNetworksLayout() {
                test.render();
        }
 
-       function drawRelationPointsAndPaths (graph, relationEdges) {
+       function drawRelationPointsAndPaths(graph, relationEdges) {
 
                const paths = graph.paths.selectAll('.relation').data(relationEdges, DescriptorModelFactory.containerIdentity);
 
                paths.enter().append('path')
                        .attr({
                                'class': d => {
-                                       return ClassNames('relation', d.type, {'-is-selected': d.uiState && SelectionManager.isSelected(d) /*d.uiState && d.uiState.selected*/});
+                                       return ClassNames('relation', d.type, {
+                                               '-is-selected': d.uiState && SelectionManager.isSelected(d) /*d.uiState && d.uiState.selected*/
+                                       });
                                },
                                stroke: 'red',
                                fill: 'transparent',
@@ -505,7 +507,9 @@ export default function RelationsAndNetworksLayout() {
                        // todo extract drag behavior into class DescriptorGraphDrag
 
                        const drag = this.drag = d3.behavior.drag()
-                               .origin(function(d) { return d; })
+                               .origin(function (d) {
+                                       return d;
+                               })
                                .on('drag.graph', function (d) {
                                        uiTransientState.isDragging = true;
                                        const mouse = d3.mouse(graph.g.node());
@@ -549,7 +553,9 @@ export default function RelationsAndNetworksLayout() {
                                        container.props.descriptorWidth = layoutInfo.width;
                                        container.props.descriptorHeight = layoutInfo.height;
                                }
-                               container.dragHandler = drag;
+                               if (!props.readOnly) {
+                                       container.dragHandler = drag;
+                               }
                                container.addContainers(containerList);
                                return container;
                        });
index eb70e2a..3db2a0d 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,6 +21,8 @@
  */
 
 import _isArray from 'lodash/isArray'
+import _set from 'lodash/set'
+import _get from 'lodash/get'
 import guid from '../guid'
 import Position from '../graph/Position'
 import IconFactory from './IconFactory'
@@ -34,7 +36,7 @@ import DescriptorModelMetaFactory from './DescriptorModelMetaFactory'
  */
 export default class DescriptorModel {
 
-       constructor(model = {uiState: {}}, parent = null) {
+       constructor(model = {uiState: {}}, parent = null, readonly = false) {
                // when our instance has no more strong references
                // then our properties will get garbage collected.
                this._props_ = new WeakMap();
@@ -49,6 +51,7 @@ export default class DescriptorModel {
                if (parent instanceof DescriptorModel) {
                        parent.addChild(this);
                }
+               this.isReadOnly = readonly;
        }
 
        get fieldNames() {
@@ -309,4 +312,12 @@ export default class DescriptorModel {
                return length !== this[propertyName].length;
        }
 
+       setUiState(setting, path, value){
+               _set(this.uiState, [setting].concat(path), value);
+       }
+
+       getUiState(setting, path, defaultValue){
+               return _get(this.uiState, [setting].concat(path), defaultValue);
+       }
+
 }
index 6de5d21..22069bf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -52,7 +52,7 @@ import VirtualNetworkFunctionReadOnlyWrapper from './descriptors/VirtualNetworkF
 import InternalConnectionPointRef from './descriptors/InternalConnectionPointRef'
 import VirtualNetworkFunctionConnectionPoint from './descriptors/VirtualNetworkFunctionConnectionPoint'
 import VirtualDeploymentUnitInternalConnectionPoint from './descriptors/VirtualDeploymentUnitInternalConnectionPoint'
-
+import VirtualNetworkFunctionAccessPointMap from './descriptors/VirtualNetworkFunctionAccessPointMap'
 function findChildDescriptorModelAndUpdateModel(model, parent) {
        if (parent instanceof DescriptorModel) {
                const child = parent.findChildByUid(model);
@@ -121,6 +121,9 @@ class DescriptorModelFactory {
                        fg.rsp.forEach(rsp => mapRSP(rsp, containerList));
                        fg.classifier.forEach(classifier => mapClassifier(classifier, containerList));
                }
+        function mapConfigParameterMap(ap, containerList) {
+            containerList.push(ap);
+        }
 
                function mapVDU(vdu, containerList) {
                        containerList.push(vdu);
@@ -144,6 +147,7 @@ class DescriptorModelFactory {
                        nsd.constituentVnfd.forEach(cvnfd => mapCVNFD(cvnfd, containerList));
                        nsd.vld.forEach(vld => mapVLD(vld, containerList));
                        nsd.vnffgd.forEach(fg => mapFG(fg, containerList));
+            nsd.configParameterMap.forEach(ap => mapConfigParameterMap(ap, containerList));
                }
 
                function mapVNFD(vnfd, containerList) {
@@ -211,22 +215,25 @@ class DescriptorModelFactory {
                return findChildDescriptorModelAndUpdateModel(model, parent) || new VirtualLink(model, parent);
        }
 
-       static newInternalVirtualLink(model, parent) {
-               return findChildDescriptorModelAndUpdateModel(model, parent) || new InternalVirtualLink(model, parent);
+       static newInternalVirtualLink(model, parent, readonly) {
+               return findChildDescriptorModelAndUpdateModel(model, parent) || new InternalVirtualLink(model, parent, readonly);
        }
 
        static newPhysicalNetworkFunction(model, parent) {
                return findChildDescriptorModelAndUpdateModel(model, parent) || new PhysicalNetworkFunction(model, parent);
        }
 
-       static newConstituentVnfdConnectionPoint(model, parent) {
-               return findChildDescriptorModelAndUpdateModel(model, parent) || new ConstituentVnfdConnectionPoint(model, parent);
+       static newConstituentVnfdConnectionPoint(model, parent, readonly) {
+               return findChildDescriptorModelAndUpdateModel(model, parent) || new ConstituentVnfdConnectionPoint(model, parent, readonly);
        }
 
        static newVnfdConnectionPointRef(model, parent) {
                return findChildDescriptorModelAndUpdateModel(model, parent) || new VnfdConnectionPointRef(model, parent);
        }
 
+    static newVirtualNetworkFunctionAccessPointMap(model, parent) {
+        return findChildDescriptorModelAndUpdateModel(model, parent) || new VirtualNetworkFunctionAccessPointMap(model, parent);
+    }
        static newForwardingGraph(model, parent) {
                return findChildDescriptorModelAndUpdateModel(model, parent) || new ForwardingGraph(model, parent);
        }
@@ -323,6 +330,9 @@ class DescriptorModelFactory {
                return obj instanceof VirtualNetworkFunction;
        }
 
+    static isVirtualNetworkFunctionAccessPointMap(obj) {
+        return obj instanceof VirtualNetworkFunctionAccessPointMap;
+    }
        static isForwardingGraph(obj) {
                return obj instanceof ForwardingGraph;
        }
@@ -343,6 +353,9 @@ class DescriptorModelFactory {
                return NetworkService;
        }
 
+    static get VirtualNetworkFunctionAccessPointMap () {
+        return VirtualNetworkFunctionAccessPointMap;
+    }
        static get ForwardingGraph () {
                return ForwardingGraph;
        }
index 8b86b0c..d8d9e58 100644 (file)
@@ -28,6 +28,7 @@ export default {
        vld: common.concat([]),
        vnfd: common.concat(['vdu', 'internal-vld']),
        'vnfd.vdu': common.concat(['image', 'image-checksum', 'external-interface', 'vm-flavor', 'cloud-init', 'filename']),
+    'nsd.config-parameter-map': common.concat([]),
        // white-list valid fields to send in the meta field
        meta: ['containerPositionMap']
 };
index 1ba8912..f7e8562 100644 (file)
@@ -15,6 +15,7 @@ const assign = Object.assign;
 
 const exportInnerTypesMap = {
        'constituent-vnfd': 'nsd.constituent-vnfd',
+    'config-parameter-map': 'nsd.config-parameter-map',
        'vdu': 'vnfd.vdu'
 };
 
@@ -121,7 +122,7 @@ function serialize_choice(data) {
        let keys = Object.keys(data);
        if (keys) {
                const chosen = this.properties.find(
-                       c => c.type === 'case' && c.properties && c.properties.some(p => keys.indexOf(p.name) > -1));
+                       c => c.type === 'case' && c.properties && c.properties.some(p => keys.indexOf(p.name) > -1 && data[p.name]));
                return chosen && serializeAll(chosen.properties, data);
        }
        return null;
@@ -188,6 +189,10 @@ export default {
                                                        parentMap[':meta'] = parentObj;
                                                        const properties = parentObj && parentObj.properties ? parentObj.properties : [];
                                                        properties.forEach(p => {
+                                                               const colonIndex = p.name.indexOf(':');
+                                                               if (colonIndex > 0) {
+                                                                               p.name = p.name.slice(colonIndex+1);
+                                                               }
                                                                parentMap[p.name] = mapProperties({}, assign(p, {
                                                                        ':qualified-type': parentObj[':qualified-type'] + '.' + p.name
                                                                }));
index e064457..968bc24 100644 (file)
@@ -78,8 +78,8 @@ export default {
                return /string|int/.test(property['data-type']) || Property.isEnumeration(property) || Property.isGuid(property);
        },
        defaultValue(property = {}) {
-               if (property.defaultValue) {
-                       return property.defaultValue;
+               if (property['default-value']) {
+                       return property['default-value'];
                }
                if (this.isObject(property)) {
                        return {};
@@ -200,6 +200,9 @@ export default {
                        if (property.type === 'leaf') {
                                return defaultValue(property);
                        }
+                       if (property.type === 'leaf_list' ) {
+                               return "";
+                       }
                        if (/list/.test(property.type)) {
                                property.type = 'container';
                        }
index b496041..5a913a0 100644 (file)
@@ -15,9 +15,6 @@
  *   limitations under the License.
  *
  */
-/**
- * Created by onvelocity on 10/20/15.
- */
 
 import DescriptorModelMetaFactory from './DescriptorModelMetaFactory'
 
@@ -43,4 +40,4 @@ const DescriptorModelSerializer = {
                return result;
        }
 }
-export default DescriptorModelSerializer;
\ No newline at end of file
+export default DescriptorModelSerializer;
index 7d07571..409c221 100644 (file)
@@ -45,8 +45,8 @@ export default class ConnectionPoint extends DescriptorModel {
                return 'vnfd.' + ConnectionPoint.type;
        }
 
-       constructor(model, parent) {
-               super(model, parent);
+       constructor(model, parent, readonly) {
+               super(model, parent, readonly);
                this.type = ConnectionPoint.type;
                this.uiState['qualified-type'] = ConnectionPoint.qualifiedType;
                this.className = 'VirtualNetworkFunctionConnectionPoint';
index 5596ffd..adaedc6 100644 (file)
@@ -25,8 +25,8 @@ import ConnectionPoint from './ConnectionPoint'
 
 export default class ConstituentVnfdConnectionPoint extends ConnectionPoint {
 
-       constructor(model, parent) {
-               super(model, parent);
+       constructor(model, parent, readonly) {
+               super(model, parent, readonly);
                this.uid = parent.uid + this.key;
        }
 
index b3e4fc6..3cf79db 100644 (file)
@@ -36,8 +36,8 @@ export default class InternalVirtualLink extends DescriptorModel {
                return 'vnfd.' + InternalVirtualLink.type;
        }
 
-       constructor(model, parent) {
-               super(model, parent);
+       constructor(model, parent, readonly) {
+               super(model, parent, readonly);
                this.type = InternalVirtualLink.type;
                this.uiState['qualified-type'] = InternalVirtualLink.qualifiedType;
                this.className = InternalVirtualLink.className;
index b10fceb..9a27b45 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -24,6 +24,7 @@
 import ColorGroups from '../../ColorGroups'
 import DescriptorModel from '../DescriptorModel'
 import ForwardingGraph from './ForwardingGraph'
+import VirtualNetworkFunctionAccessPointMap from './VirtualNetworkFunctionAccessPointMap'
 import VirtualLink from './VirtualLink'
 import ConstituentVnfd from './ConstituentVnfd'
 import PhysicalNetworkFunction from './PhysicalNetworkFunction'
@@ -129,6 +130,45 @@ export default class NetworkService extends DescriptorModel {
        }
 
 
+// <<<<<<< Updated upstream
+//     get configParameterMap() {
+//         if (!this.model['config-parameter-map']) {
+//             this.model['config-parameter-map'] = [];
+//         }
+//         return this.model['config-parameter-map'].map(d => DescriptorModelFactory.newVirtualNetworkFunctionAccessPointMap(d, this)).map((fg, i) => {
+//             return fg;
+//         });
+//     }
+
+//     set configParameterMap(obj) {
+//         const onVirtualNetworkFunctionAccessPointMap = (fg) => {
+
+//         };
+//         this.updateModelList('config-parameter-map', obj, VirtualNetworkFunctionAccessPointMap, onVirtualNetworkFunctionAccessPointMap);
+//     }
+
+//     createConfigParameterMap(model) {
+//         model = model || DescriptorModelMetaFactory.createModelInstanceForType('nsd.config-parameter-map');
+//         return this.configParameterMap = DescriptorModelFactory.newVirtualNetworkFunctionAccessPointMap(model, this);
+//     }
+// =======
+       get configParameterMap() {
+               if (!this.model['config-parameter-map']) {
+                       this.model['config-parameter-map'] = [];
+               }
+               return this.model['config-parameter-map'].map(d => DescriptorModelFactory.newVirtualNetworkFunctionAccessPointMap(d, this))
+       }
+
+       set configParameterMap(obj) {
+               this.updateModelList('config-parameter-map', obj, VirtualNetworkFunctionAccessPointMap);
+       }
+
+       createConfigParameterMap() {
+               const model = DescriptorModelMetaFactory.createModelInstanceForType('nsd.config-parameter-map');
+               return this.configParameterMap = DescriptorModelFactory.newVirtualNetworkFunctionAccessPointMap(model, this);
+       }
+// >>>>>>> Stashed changes
+
        get vnffgd() {
                if (!this.model.vnffgd) {
                        this.model.vnffgd = [];
@@ -159,6 +199,7 @@ export default class NetworkService extends DescriptorModel {
        }
 
 
+
        // NOTE temporarily disable NSD connection points
        // https://trello.com/c/crVgg2A1/88-do-not-render-nsd-connection-in-the-composer
        //get connectionPoint() {
index 5f3ad04..12bce38 100644 (file)
@@ -74,7 +74,7 @@ export default class VirtualNetworkFunction extends DescriptorModel {
 
        createVld() {
                const property = DescriptorModelMetaFactory.getModelMetaForType('vnfd.internal-vld');
-               const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(this['internal-vld'], property);
+               const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(this.vld, property);
                const model = DescriptorModelMetaFactory.createModelInstanceForType('vnfd.internal-vld', uniqueName);
                return this.vld = DescriptorModelFactory.newInternalVirtualLink(model, this);
        }
diff --git a/skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionAccessPoint.js b/skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionAccessPoint.js
new file mode 100644 (file)
index 0000000..defa317
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 11/23/15.
+ */
+
+'use strict';
+
+import DescriptorModel from '../DescriptorModel'
+import DescriptorModelFactory from '../DescriptorModelFactory'
+
+export default class VnfapMap extends DescriptorModel {
+
+    static get type() {
+        return 'vnfap-map';
+    }
+
+    static get className() {
+        return 'VnfapMap';
+    }
+
+    constructor(model, parent) {
+        super(model, parent);
+        this.type = 'vnfap-map';
+        this.uiState['qualified-type'] = 'nsd.vnfap-map';
+        this.className = 'VnfapMap';
+        // this.addProp('vnfdRef', DescriptorModelFactory.newVirtualNetworkFunctionReadOnlyWrapper({}, this));
+    }
+
+    get id() {
+        return this.model.id;
+    }
+    get capability() {
+        return []
+    }
+
+    get dependency() {
+
+    }
+
+
+}
diff --git a/skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionAccessPointMap.js b/skyquake/plugins/composer/src/src/libraries/model/descriptors/VirtualNetworkFunctionAccessPointMap.js
new file mode 100644 (file)
index 0000000..16b57dc
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ *
+ *   Copyright 2016 RIFT.IO Inc
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+/**
+ * Created by onvelocity on 11/23/15.
+ */
+
+'use strict';
+
+import DescriptorModel from '../DescriptorModel'
+import DescriptorModelFactory from '../DescriptorModelFactory'
+
+export default class ConfigParameterMap extends DescriptorModel {
+
+// <<<<<<< Updated upstream
+//     static get type() {
+//         return 'config-parameter-map';
+//     }
+
+//     static get className() {
+//         return 'ConfigParameterMap';
+//     }
+
+//     constructor(model, parent) {
+//         super(model, parent);
+//         this.type = 'config-parameter-map';
+//         this.uiState['qualified-type'] = 'nsd.config-parameter-map';
+//         this.className = 'ConfigParameterMap';
+//         // this.addProp('vnfdRef', DescriptorModelFactory.newVirtualNetworkFunctionReadOnlyWrapper({}, this));
+//     }
+
+//     get id() {
+//         return this.model.id;
+//     }
+//     get capability() {
+//         return {
+//             'member-vnf-index': this.model.capability['member-vnf-index'],
+//             'capability-ref': this.model.capability['capability-ref'],
+//         }
+//     }
+//     get availableCapabilities() {
+
+//     }
+
+//     get dependency() {
+
+//     }
+// =======
+       static get type() {
+               return 'config-parameter-map';
+       }
+
+       static get className() {
+               return 'ConfigParameterMap';
+       }
+       static get qualifiedType() {
+               return 'nsd.' + ConfigParameterMap.type;
+       }
+       constructor(model, parent) {
+               super(model, parent);
+               this.type = 'config-parameter-map';
+               this.uiState['qualified-type'] = 'nsd.config-parameter-map';
+               this.className = 'ConfigParameterMap';
+               // this.addProp('vnfdRef', DescriptorModelFactory.newVirtualNetworkFunctionReadOnlyWrapper({}, this));
+       }
+
+       get id() {
+               return this.model.id;
+       }
+       // get capability() {
+       //      return {
+       //              'member-vnf-index': this.model.capability['member-vnf-index'],
+       //              'capability-ref': this.model.capability['capability-ref'],
+       //      }
+       // }
+       // get availableCapabilities() {
+
+       // }
+
+       // get dependency() {
+
+       // }
+
+
+}
index bb8d21a..b1f5269 100644 (file)
@@ -27,21 +27,21 @@ import DescriptorModelFactory from '../DescriptorModelFactory'
 export default class VirtualNetworkFunctionReadOnlyWrapper extends DescriptorModel {
 
        constructor(model, parent) {
-               super(model, parent);
+               super(model, parent, true);
        }
 
        get vld() {
                if (!this.model['internal−vld']) {
                        this.model['internal−vld'] = [];
                }
-               return this.model['internal−vld'].map(d => DescriptorModelFactory.newInternalVirtualLink(d, this.parent));
+               return this.model['internal−vld'].map(d => DescriptorModelFactory.newInternalVirtualLink(d, this.parent, true));
        }
 
        get connectionPoint() {
                if (!this.model['connection-point']) {
                        this.model['connection-point'] = [];
                }
-               return this.model['connection-point'].map(d => DescriptorModelFactory.newConstituentVnfdConnectionPoint(d, this.parent));
+               return this.model['connection-point'].map(d => DescriptorModelFactory.newConstituentVnfdConnectionPoint(d, this.parent, true));
        }
 
        get connectors() {
index 3fa9eb3..5a36841 100644 (file)
@@ -69,7 +69,7 @@ export default {
                // last item in path used to assign value on the resolved object
                const name = path.pop();
                const resolvedObj = path.reduce((r, p, i) => {
-                       if (typeof(r[p]) !== 'object') {
+                       if (typeof (r[p]) !== 'object') {
                                // look-ahead to see if next path item is a number
                                const isArray = !isNaN(parseInt(pathCopy[i + 1], 10));
                                r[p] = isArray ? [] : {}
@@ -78,6 +78,25 @@ export default {
                }, obj);
                resolvedObj[name] = value;
        },
+       mergePathData(obj, path, data) {
+               path = path.split(/[\.\[\]]/).filter(d => d);
+               // enable look-ahead to determine if type is array or object
+               const pathCopy = path.slice();
+               let resolvedObj = obj;
+               if (path.length) {
+                       // last item in path used to assign value on the resolved object
+                       const name = path.pop();
+                       resolvedObj = path.reduce((r, p, i) => {
+                               if (typeof (r[p]) !== 'object') {
+                                       // look-ahead to see if next path item is a number
+                                       const isArray = !isNaN(parseInt(pathCopy[i + 1], 10));
+                                       r[p] = isArray ? [] : {}
+                               }
+                               return r[p];
+                       }, obj)[name];
+               }
+               Object.assign(resolvedObj, data);
+       },
        updatePathValue(obj, path, value, isCase) {
                // todo: replace implementation of assignPathValue with this impl and
                // remove updatePathValue (only need one function, not both)
@@ -92,21 +111,21 @@ export default {
                        // look-ahead to see if next path item is a number
                        const index = parseInt(pathCopy[i + 1], 10);
                        const isArray = !isNaN(index);
-                       if (typeof(r[p]) !== 'object') {
+                       if (typeof (r[p]) !== 'object') {
                                r[p] = isArray ? [] : {}
                        }
                        if (isRemove && ((i + 1) === path.length)) {
                                if (isArray) {
                                        r[p] = r[p].filter((d, i) => i !== index);
                                } else {
-                                       if(isCase) {
+                                       if (isCase) {
                                                delete r[name];
                                        } else {
                                                delete r[p][name];
                                        }
                                }
                        }
-                       if(isCase) {
+                       if (isCase) {
                                return r;
                        } else {
                                return r[p];
@@ -123,7 +142,7 @@ export default {
        },
 
        suffixAsInteger: (field) => {
-               return (obj) =>{
+               return (obj) => {
                        const str = String(obj[field]);
                        const value = str.replace(str.replace(/[\d]+$/, ''), '');
                        return 1 + parseInt(value, 10) || 0;
@@ -132,19 +151,19 @@ export default {
 
        toBiggestValue: (maxIndex, curIndex) => Math.max(maxIndex, curIndex),
 
-       isRelativePath (path) {
+       isRelativePath(path) {
                if (path.split('/')[0] == '..') {
                        return true;
                }
                return false;
        },
 
-       getResults (topLevelObject, pathArray) {
+       getResults(topLevelObject, pathArray) {
                let objectCopy = _cloneDeep(topLevelObject);
                let i = pathArray.length;
                let results = [];
 
-               while(pathArray[pathArray.length - i]) {
+               while (pathArray[pathArray.length - i]) {
                        if (_isArray(objectCopy[pathArray[pathArray.length - i]])) {
                                if (i == 2) {
                                        results = _map(objectCopy[pathArray[pathArray.length - i]], pathArray[pathArray.length - 1]);
@@ -166,7 +185,7 @@ export default {
                return results;
        },
 
-       getAbsoluteResults (topLevelObject, pathArray) {
+       getAbsoluteResults(topLevelObject, pathArray) {
                let i = pathArray.length;
                let objectCopy = _cloneDeep(topLevelObject);
                let results = [];
@@ -187,6 +206,9 @@ export default {
                                                console.log('Something went wrong while resolving a leafref. Reached a leaf with predicate.');
                                        } else {
                                                // contains no predicate
+                                               if (!objectCopy) {
+                                                       break;
+                                               }
                                                results.push(objectCopy[fragment]);
                                        }
                                }
@@ -243,6 +265,9 @@ export default {
                                                }
                                        } else {
                                                // contains no predicate
+                                               if (!objectCopy) {
+                                                       break;
+                                               }
                                                objectCopy = objectCopy[fragment];
                                                if (!objectCopy) {
                                                        // contains no value
@@ -258,7 +283,7 @@ export default {
                return results;
        },
 
-       resolveCurrentPredicate (leafRefPath, container, pathCopy) {
+       resolveCurrentPredicate(leafRefPath, container, pathCopy) {
                if (leafRefPath.indexOf('current()') != -1) {
                        // contains current
 
@@ -290,10 +315,10 @@ export default {
                return fieldKeyArray;
        },
 
-       resolveLeafRefPath (catalogs, leafRefPath, fieldKey, path, container) {
+       resolveLeafRefPath(catalogs, leafRefPath, fieldKey, path, container) {
                let pathCopy = _clone(path);
                // Strip any prefixes
-               let leafRefPathCopy = leafRefPath.replace(/[\w\d]*:/g, '');
+               let leafRefPathCopy = leafRefPath.replace(/[-\w]*:/g, '');
                // Strip any spaces
                leafRefPathCopy = leafRefPathCopy.replace(/\s/g, '');
 
@@ -311,11 +336,11 @@ export default {
 
                // Check if relative path or not
                // TODO: Below works but
-               // better to convert the pathCopy to absolute/rooted path
+               // better to convert the pathCopy to absolute/rooted path 
                // and use the absolute module instead
                if (this.isRelativePath(leafRefPathCopy)) {
                        let i = pathArray.length;
-                       while ((pathArray[pathArray.length - i] == '..') && fieldKeyArray.length > 1) {
+                       while (pathArray[pathArray.length - i] == '..') {
                                fieldKeyArray.splice(-1, 1);
                                if (!isNaN(Number(fieldKeyArray[fieldKeyArray.length - 1]))) {
                                        // found a number, so an index. strip it
@@ -328,7 +353,9 @@ export default {
                        if (fieldKeyArray.length == 1) {
                                for (let key in catalogs) {
                                        for (let subKey in catalogs[key]) {
-                                               let found = _find(catalogs[key][subKey], {id: fieldKeyArray[0]});
+                                               let found = _find(catalogs[key][subKey], {
+                                                       id: fieldKeyArray[0]
+                                               });
                                                if (found) {
                                                        results = this.getAbsoluteResults(found, pathArray.splice(-i, i));
                                                        return results;
@@ -339,11 +366,15 @@ export default {
                                for (let key in catalogs) {
                                        for (let subKey in catalogs[key]) {
                                                console.log(key, subKey);
-                                               var found = _find(catalogs[key][subKey], {id: fieldKeyArray[0]});
+                                               var found = _find(catalogs[key][subKey], {
+                                                       id: fieldKeyArray[0]
+                                               });
                                                if (found) {
                                                        for (let foundKey in found) {
                                                                if (_isArray(found[foundKey])) {
-                                                                       let topLevel = _find(found[foundKey], {id: fieldKeyArray[1]});
+                                                                       let topLevel = _find(found[foundKey], {
+                                                                               id: fieldKeyArray[1]
+                                                                       });
                                                                        if (topLevel) {
                                                                                results = this.getAbsoluteResults(topLevel, pathArray.splice(-i, i));
                                                                                return results;
@@ -361,11 +392,15 @@ export default {
                        } else if (fieldKeyArray.length == 3) {
                                for (let key in catalogs) {
                                        for (let subKey in catalogs[key]) {
-                                               let found = _find(catalogs[key][subKey], {id: fieldKeyArray[0]});
+                                               let found = _find(catalogs[key][subKey], {
+                                                       id: fieldKeyArray[0]
+                                               });
                                                if (found) {
                                                        for (let foundKey in found) {
                                                                if (_isArray(found[foundKey])) {
-                                                                       let topLevel = _find(found[foundKey], {id: fieldKeyArray[1]});
+                                                                       let topLevel = _find(found[foundKey], {
+                                                                               id: fieldKeyArray[1]
+                                                                       });
                                                                        if (topLevel) {
                                                                                results = this.getAbsoluteResults(topLevel, pathArray.splice(-i, i));
                                                                                return results;
@@ -401,4 +436,3 @@ export default {
                }
        }
 }
-
index 9c297ca..f409e26 100644 (file)
@@ -65,7 +65,7 @@ const CatalogDataSource = {
                        remote: function(state, catalogType, itemId) {
                                return new Promise(function(resolve, reject) {
                                        $.ajax({
-                                               url: 'api/catalog/' + catalogType + '/' + itemId + '?api_server=' + utils.getSearchParams(window.location).api_server,
+                                               url: 'api/catalog/' + catalogType + '/' + encodeURIComponent(itemId) + '?api_server=' + utils.getSearchParams(window.location).api_server,
                                                type: 'DELETE',
                                                beforeSend: utils.addAuthorizationStub,
                                                success: function(data) {
@@ -127,7 +127,7 @@ const CatalogDataSource = {
                                                });
                                        } else {
                                                $.ajax({
-                                                       url: 'api/catalog/' + item.uiState.type + '/' + payload.id + '?api_server=' + utils.getSearchParams(window.location).api_server,
+                                                       url: 'api/catalog/' + item.uiState.type + '/' + encodeURIComponent(payload.id) + '?api_server=' + utils.getSearchParams(window.location).api_server,
                                                        type: method,
                                                        beforeSend: utils.addAuthorizationStub,
                                                        data: payload,
index e059f94..00d3600 100644 (file)
@@ -23,10 +23,15 @@ import catalogUtils from '../libraries/utils'
 import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions'
 import Utils from 'utils/utils.js';
 
+let API_SERVER = catalogUtils.getSearchParams(window.location).api_server;
+const FILE_SERVER = window.location.hostname === 'localhost' 
+       ? API_SERVER : window.location.protocol + '//' + window.location.hostname;
+
+
 const getAuthorization = () => 'Basic ' + window.sessionStorage.getItem("auth");
 
 const getStateApiPath = (operation, id) => 
-       catalogUtils.getSearchParams(window.location).upload_server + ':4567/api/' + operation + '/' + id + '/state';
+       FILE_SERVER + ':8008/mano/' + operation + '/' + id + '/state';
 
 const getComposerApiPath = (api) =>
        window.location.origin + '/composer/api/' + api + '?api_server=' + catalogUtils.getSearchParams(window.location).api_server;
@@ -185,7 +190,7 @@ const CatalogPackageManagerSource = {
                                        const failHandler = (response) => {
                                                reject(Object.assign({}, operation, FAILED));
                                        };
-                                       const path = getComposerApiPath('package-manager/jobs/' + operation.transactionId);
+                                       const path = getComposerApiPath('package-copy/jobs/' + operation.transactionId);
                                        ajaxFetch(path, operation, successHandler, failHandler);
                                });
                        },
@@ -199,8 +204,8 @@ const CatalogPackageManagerSource = {
                        remote: function (state, upload) {
                                const transactionId = upload.transactionId;
                                return new Promise(function (resolve, reject) {
-                                       const action = upload.riftAction === 'onboard' ? 'upload' : 'update';
-                                       const path = getStateApiPath(action, transactionId);
+                                       const action = upload.riftAction === 'onboard' ? 'import' : 'update';
+                                       const path = getComposerApiPath('package-'+action+'/jobs/' + transactionId);
                                        ajaxFetch(path, upload, resolve, reject);
                                });
                        },
index 06d1342..714861f 100644 (file)
@@ -21,6 +21,8 @@
 import _pick from 'lodash/pick'
 import _isEqual from 'lodash/isEqual'
 import _cloneDeep from 'lodash/cloneDeep'
+import _merge from 'lodash/merge'
+import _debounce from 'lodash/debounce';
 import cc from 'change-case'
 import alt from '../alt'
 import UID from '../libraries/UniqueId'
@@ -28,6 +30,7 @@ import guid from '../libraries/guid'
 import React from 'react'
 import DescriptorModel from '../libraries/model/DescriptorModel'
 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
+import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
 import CatalogPackageManagerActions from '../actions/CatalogPackageManagerActions'
 import CatalogDataSourceActions from '../actions/CatalogDataSourceActions'
 import CatalogItemsActions from '../actions/CatalogItemsActions'
@@ -43,21 +46,27 @@ const defaults = {
        catalogItemExportGrammars: ['osm', 'tosca']
 };
 
-const areCatalogItemsMetaDataEqual = function (a, b) {
-       const metaProps = ['id', 'name', 'short-name', 'description', 'vendor', 'version'];
-       const aMetaData = _pick(a, metaProps);
-       const bMetaData = _pick(b, metaProps);
-       return _isEqual(aMetaData, bMetaData);
+const areCatalogItemsMetaDataEqual = function (catItem, activeItem) {
+       function getDefaultPositionMap() {
+               if (!activeItem.uiState.containerPositionMap) {
+                       return activeItem.uiState.containerPositionMap;
+               }
+               let defaultPositionMap = {};
+               defaultPositionMap[activeItem.id] = activeItem.uiState.defaultLayoutPosition;
+               return defaultPositionMap;
+       }
+       const activeItemMetaData = activeItem.uiState.containerPositionMap;
+       const catItemMetaData = catItem.uiState.containerPositionMap;
+       return catItemMetaData === undefined || _isEqual(catItemMetaData, activeItemMetaData);
 };
 
-function createItem (type) {
+function createItem(type) {
        let newItem = DescriptorModelMetaFactory.createModelInstanceForType(type);
-       if (newItem){
+       if (newItem) {
                newItem.id = guid();
-               UID.assignUniqueId(newItem.uiState);
+               UID.assignUniqueId(newItem);
                newItem.uiState.isNew = true;
                newItem.uiState.modified = true;
-               newItem.uiState['instance-ref-count'] = 0;
        }
        return newItem;
 }
@@ -67,7 +76,6 @@ class CatalogDataStore {
        constructor() {
                this.catalogs = defaults.catalogs;
                this.isLoading = true;
-               this.requiresSave = false;
                this.snapshots = {};
                this.selectedFormat = defaults.catalogItemExportFormats[0];
                this.selectedGrammar = defaults.catalogItemExportGrammars[0];
@@ -75,13 +83,15 @@ class CatalogDataStore {
                this.bindActions(CatalogDataSourceActions);
                this.bindActions(CatalogItemsActions);
                this.exportPublicMethods({
-            getCatalogs: this.getCatalogs,
-            getCatalogItemById: this.getCatalogItemById,
-            getCatalogItemByUid: this.getCatalogItemByUid,
-            getTransientCatalogs: this.getTransientCatalogs,
-            getTransientCatalogItemById: this.getTransientCatalogItemById,
-            getTransientCatalogItemByUid: this.getTransientCatalogItemByUid
-        });
+                       getCatalogs: this.getCatalogs,
+                       getCatalogItemById: this.getCatalogItemById,
+                       getCatalogItemByUid: this.getCatalogItemByUid,
+                       getTransientCatalogs: this.getTransientCatalogs,
+                       getTransientCatalogItemById: this.getTransientCatalogItemById,
+                       getTransientCatalogItemByUid: this.getTransientCatalogItemByUid,
+                       setUserProfile: this.setUserProfile
+               });
+               this.queueDirtyCheck = _debounce(() => this.saveDirtyDescriptorsToSessionStorage(), 500);
        }
 
        resetSelectionState = () => {
@@ -93,6 +103,61 @@ class CatalogDataStore {
                return this.catalogs || (this.catalogs = []);
        }
 
+       saveDirtyDescriptorsToSessionStorage() {
+               const dirtyCatalogs = this.catalogs.reduce((result, catalog) => {
+                       const dirtyDescriptors = catalog.descriptors.reduce((result, descriptor) => {
+                               if (descriptor.uiState.modified && !descriptor.uiState.deleted) {
+                                       result.push(descriptor);
+                               }
+                               return result;
+                       }, []);
+                       if (dirtyDescriptors.length) {
+                               let newCatalog = Object.assign({}, catalog);
+                               newCatalog.descriptors = dirtyDescriptors;
+                               result.push(newCatalog);
+                       }
+                       return result;
+               }, []);
+               window.sessionStorage.setItem(this.userProfile.userId + '@' + this.userProfile.domain, JSON.stringify({
+                       dirtyCatalogs
+               }));
+       }
+
+       mergeDirtyDescriptorsFromSessionStorage(catalogs) {
+               let userProfileDirtyCatalogs = window.sessionStorage.getItem(this.userProfile.userId + '@' + this.userProfile.domain);
+               let dirtyCatalogs = [];
+               if (userProfileDirtyCatalogs) {
+                       dirtyCatalogs = JSON.parse(userProfileDirtyCatalogs).dirtyCatalogs;
+               }
+               dirtyCatalogs.forEach((dirtyCatalog) => {
+                       let catalog = catalogs.find((c) => c.id === dirtyCatalog.id);
+                       dirtyCatalog.descriptors.forEach((dirtyDescriptor, index) => {
+                               let descriptor = catalog.descriptors.find((d) => d.id === dirtyDescriptor.id);
+                               if (descriptor) {
+                                       this.addSnapshot(descriptor);
+                                       _merge(descriptor, dirtyDescriptor);
+                               } else {
+                                       dirtyCatalog.descriptors.splice(index, 1);
+                                       this.queueDirtyCheck();
+                               }
+                       })
+               });
+               this.isNotMergedWithSessionStorage = false;
+               return catalogs;
+       }
+
+       setUserProfile = (userProfile) => {
+               if (!this.userProfile) {
+                       this.userProfile = userProfile;
+                       if (this.catalogs.length) {
+                               const catalogs = this.mergeDirtyDescriptorsFromSessionStorage(this.catalogs);
+                               this.setState({ catalogs });
+                       } else {
+                               this.isNotMergedWithSessionStorage = true;
+                       }
+               }
+       }
+
        getTransientCatalogs() {
                return this.state.catalogs || (this.state.catalogs = []);
        }
@@ -187,9 +252,8 @@ class CatalogDataStore {
                        });
                        return catalog;
                });
-               updatedCatalogs.filter(d => d.type === 'nsd').forEach(catalog =>  {
+               updatedCatalogs.filter(d => d.type === 'nsd').forEach(catalog => {
                        catalog.descriptors = catalog.descriptors.map(descriptor => {
-                               const instanceRefCount = parseInt(descriptor.uiState['instance-ref-count'], 10);
                                if (descriptor['constituent-vnfd']) {
                                        descriptor.vnfd = descriptor['constituent-vnfd'].map(d => {
                                                const vnfdId = d['vnfd-id-ref'];
@@ -197,10 +261,6 @@ class CatalogDataStore {
                                                if (!vnfd) {
                                                        throw new ReferenceError('no VNFD found in the VNFD Catalog for the constituent-vnfd: ' + d);
                                                }
-                                               if (!isNaN(instanceRefCount) && instanceRefCount > 0) {
-                                                       // this will notify user that this item cannot be updated when/if they make a change to it
-                                                       vnfd.uiState['instance-ref-count'] = instanceRefCount;
-                                               }
                                                // create an instance of this vnfd to carry transient ui state properties
                                                const instance = _cloneDeep(vnfd);
                                                instance.uiState['member-vnf-index'] = d['member-vnf-index'];
@@ -228,7 +288,7 @@ class CatalogDataStore {
                        }
                        return catalog;
                });
-               this.setState({catalogs: catalogs});
+               this.setState({ catalogs: catalogs });
        }
 
        mergeEditsIntoLatestFromServer(catalogsFromServer = []) {
@@ -279,30 +339,47 @@ class CatalogDataStore {
 
        loadCatalogsSuccess(context) {
                const fromServer = this.updateCatalogIndexes(context.data);
-               const catalogs = this.mergeEditsIntoLatestFromServer(fromServer);
+               let catalogs = this.mergeEditsIntoLatestFromServer(fromServer);
+               if (this.isNotMergedWithSessionStorage) {
+                       catalogs = this.mergeDirtyDescriptorsFromSessionStorage(catalogs);
+               }
                this.setState({
                        catalogs: catalogs,
                        isLoading: false
                });
        }
 
-       deleteCatalogItemSuccess (response) {
+       deleteCatalogItemSuccess(response) {
                let catalogType = response.catalogType;
                let itemId = response.itemId;
                const catalogs = this.getCatalogs().map(catalog => {
                        if (catalog.type === catalogType) {
-                               catalog.descriptors = catalog.descriptors.filter(d => d.id !== itemId);
+                               catalog.descriptors = catalog.descriptors.map(d => {
+                                       // We are just going to mark it as deleted here so it will be hidden from view.
+                                       // We will let the next catalog refresh actually remove it from the in memory store.
+                                       // This is to avoid having it reappear because a timing issue with a catalog refresh.
+                                       // The incoming refresh may still contain the item and it would then reappear till the next refresh.
+                                       if (d.id === itemId) {
+                                               d.uiState.deleted = true;
+                                               const activeItem = ComposerAppStore.getState().item;
+                                               if (activeItem && activeItem.id === itemId) {
+                                                       ComposerAppActions.showDescriptor.defer();
+                                                       CatalogItemsActions.editCatalogItem.defer(null);
+                                               }
+                                       }
+                                       return d;
+                               });
                        }
                        return catalog;
                });
-
-               this.setState({catalogs: catalogs});
+               this.setState({ catalogs: catalogs });
+               this.queueDirtyCheck();
        }
 
-       deleteCatalogItemError (data) {
+       deleteCatalogItemError(data) {
                console.log('Unable to delete', data.catalogType, 'id:', data.itemId, 'Error:', data.error.responseText);
                ComposerAppActions.showError.defer({
-                       errorMessage: 'Unable to delete ' + data.catalogType + ' id: ' + data.itemId + '. Check if it is in use'
+                       errorMessage: 'Unable to delete ' + data.catalogType + ' id: ' + data.itemId + '. Check to see if it is in use.'
                });
        }
 
@@ -312,34 +389,14 @@ class CatalogDataStore {
 
        catalogItemMetaDataChanged(item) {
                let requiresSave = false;
-               const catalogs = this.getCatalogs().map(catalog => {
-                       if (catalog.id === item.uiState.catalogId) {
-                               catalog.descriptors = catalog.descriptors.map(d => {
-                                       if (d.id === item.id) {
-                                               // compare just the catalog uiState data (id, name, short-name, description, etc.)
-                                               const modified = !areCatalogItemsMetaDataEqual(d, item);
-                                               if (modified) {
-                                                       if (d.uiState['instance-ref-count'] > 0) {
-                                                               console.log('cannot edit NSD/VNFD with references to instantiated Network Services');
-                                                               ComposerAppActions.showError.defer({
-                                                                       errorMessage: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
-                                                               });
-                                                               return _cloneDeep(d);
-                                                       } else {
-                                                               item.uiState.modified = modified;
-                                                               requiresSave = true;
-                                                               this.addSnapshot(item);
-                                                       }
-                                               }
-                                               return item;
-                                       }
-                                       return d;
-                               });
-                       }
-                       return catalog;
-               });
-               if (requiresSave) {
-                       this.setState({catalogs: catalogs, requiresSave: true});
+               let previousVersion = this.getLatestSnapshot(item);
+               // compare just the catalog uiState data 
+               const modified = !areCatalogItemsMetaDataEqual(previousVersion, item);
+               if (modified) {
+                       item.uiState.modified = true;
+                       this.updateCatalogItem(item);
+                       this.addSnapshot(item);
+                       this.queueDirtyCheck();
                }
        }
 
@@ -353,24 +410,17 @@ class CatalogDataStore {
                                // replace the old descriptor with the updated one
                                catalog.descriptors = catalog.descriptors.map(d => {
                                        if (d.id === descriptorId) {
-                                               if (d.uiState['instance-ref-count'] > 0) {
-                                                       console.log('cannot edit NSD/VNFD with references to instantiated Network Services');
-                                                       ComposerAppActions.showError.defer({
-                                                               errorMessage: 'Cannot edit NSD/VNFD with references to instantiated Network Services'
-                                                       });
-                                                       return _cloneDeep(d);
-                                               } else {
-                                                       itemDescriptor.model.uiState.modified = true;
-                                                       this.addSnapshot(itemDescriptor.model);
-                                                       return itemDescriptor.model;
-                                               }
+                                               itemDescriptor.model.uiState.modified = true;
+                                               this.addSnapshot(itemDescriptor.model);
+                                               return itemDescriptor.model;
                                        }
                                        return d;
                                });
                        }
                        return catalog;
                });
-               this.setState({catalogs: catalogs, requiresSave: true})
+               this.setState({ catalogs: catalogs })
+               this.queueDirtyCheck();
        }
 
        deleteSelectedCatalogItem() {
@@ -384,63 +434,21 @@ class CatalogDataStore {
        }
 
        deleteCatalogItem(item) {
-               const snapshot = JSON.stringify(item);
-               function confirmDeleteCancel(event) {
-                       undo();
-                       event.preventDefault();
-                       ModalOverlayActions.hideModalOverlay();
-               }
-               const remove = () => {
-                       // item is deleted or does not exist on server, so remove from ui
-                       this.removeCatalogItem(item);
-                       this.setState({catalogs: this.getCatalogs()});
-                       const activeItem = ComposerAppStore.getState().item;
-                       if (activeItem && activeItem.id === item.id) {
-                               CatalogItemsActions.editCatalogItem.defer(null);
-                       }
-                       ModalOverlayActions.hideModalOverlay();
-               };
-               const undo = () => {
-                       // item failed to delete on server so revert ui
-                       const revertTo = JSON.parse(snapshot);
-                       this.updateCatalogItem(revertTo);
-                       const activeItem = ComposerAppStore.getState().item;
-                       if (activeItem && activeItem.id === revertTo.id) {
-                               SelectionManager.select(activeItem);
-                               CatalogItemsActions.editCatalogItem.defer(revertTo);
-                               SelectionManager.refreshOutline();
-                       }
-               };
                if (item) {
-                       if (item.uiState.isNew) {
-                               CatalogDataStore.confirmDelete(remove, confirmDeleteCancel);
-                       } else {
-                               if (item.uiState['instance-ref-count'] > 0) {
-                                       console.log('cannot delete NSD/VNFD with references to instantiated Network Services');
-                                       ComposerAppActions.showError.defer({
-                                               errorMessage: 'Cannot delete NSD/VNFD with references to instantiated Network Services'
+                       CatalogDataStore.confirmDelete(event => {
+                               event.preventDefault();
+                               ModalOverlayActions.showModalOverlay.defer();
+                               this.getInstance().deleteCatalogItem(item.uiState.type, item.id)
+                                       .then(ModalOverlayActions.hideModalOverlay, ModalOverlayActions.hideModalOverlay)
+                                       .catch(function () {
+                                               console.log('overcoming ES6 unhandled rejection red herring');
                                        });
-                                       undo();
-                               } else {
-                                       const confirmDeleteOK = event => {
-                                               event.preventDefault();
-                                               item.uiState.deleted = true;
-                                               this.setState({catalogs: this.getCatalogs()});
-                                               ModalOverlayActions.showModalOverlay.defer();
-                                               this.getInstance().deleteCatalogItem(item.uiState.type, item.id)
-                                                       .then(remove, undo)
-                                                       .then(ModalOverlayActions.hideModalOverlay, ModalOverlayActions.hideModalOverlay)
-                                                       .catch(function() {
-                                                               console.log('overcoming ES6 unhandled rejection red herring');
-                                                       });
-                                       };
-                                       CatalogDataStore.confirmDelete(confirmDeleteOK, confirmDeleteCancel);
-                               }
-                       }
+                       }, );
                }
        }
 
        static confirmDelete(onClickYes, onClickCancel) {
+               const cancelDelete = onClickCancel || (e => ModalOverlayActions.hideModalOverlay.defer());
                ModalOverlayActions.showModalOverlay.defer((
                        <div className="actions panel">
                                <div className="panel-header">
@@ -448,7 +456,7 @@ class CatalogDataStore {
                                </div>
                                <div className="panel-body">
                                        <a className="action confirm-yes primary-action Button" onClick={onClickYes}>Yes, delete selected catalog item</a>
-                                       <a className="action cancel secondary-action Button" onClick={onClickCancel}>No, cancel</a>
+                                       <a className="action cancel secondary-action Button" onClick={cancelDelete}>No, cancel</a>
                                </div>
                        </div>
                ));
@@ -472,8 +480,28 @@ class CatalogDataStore {
                        if (!this.snapshots[item.id]) {
                                this.snapshots[item.id] = [];
                        }
+                       // save the snapshot with a new id for an in memory instance
+                       let uid = UID.from(item);
+                       UID.assignUniqueId(item);
                        this.snapshots[item.id].push(JSON.stringify(item));
+                       UID.assignUniqueId(item, uid);
+               }
+       }
+
+       getLatestSnapshot(item) {
+               if (this.snapshots[item.id]) {
+                       return JSON.parse(this.snapshots[item.id][this.snapshots[item.id].length - 1]);
                }
+               this.getCatalogs().forEach(catalog => {
+                       if (catalog.id === item.uiState.catalogId) {
+                               catalog.descriptors.forEach(d => {
+                                       if (d.id === item.id) {
+                                               return d;
+                                       }
+                               });
+                       }
+               });
+               return {};
        }
 
        resetSnapshots(item) {
@@ -499,7 +527,7 @@ class CatalogDataStore {
                                });
                                return catalog;
                        });
-                       this.setState({catalogs: catalogs});
+                       this.setState({ catalogs: catalogs });
                        this.catalogItemMetaDataChanged(item);
                }
        }
@@ -513,8 +541,8 @@ class CatalogDataStore {
                                this.updateCatalogItem(revertTo);
                                // TODO should the cancel action clear the undo/redo stack back to the beginning?
                                this.resetSnapshots(revertTo);
-                               this.setState({requiresSave: false});
                                CatalogItemsActions.editCatalogItem.defer(revertTo);
+                               this.queueDirtyCheck();
                        }
                }
        }
@@ -528,13 +556,6 @@ class CatalogDataStore {
 
        saveItem(item) {
                if (item) {
-                       if (item.uiState['instance-ref-count'] > 0) {
-                               console.log('cannot save NSD/VNFD with references to instantiated Network Services');
-                               ComposerAppActions.showError.defer({
-                                       errorMessage: 'Cannot save NSD/VNFD with references to instantiated Network Services'
-                               });
-                               return;
-                       }
                        const success = () => {
                                delete item.uiState.modified;
                                if (item.uiState.isNew) {
@@ -547,6 +568,7 @@ class CatalogDataStore {
                                this.resetSnapshots(item);
                                ModalOverlayActions.hideModalOverlay.defer();
                                CatalogItemsActions.editCatalogItem.defer(item);
+                               this.queueDirtyCheck();
                        };
                        const failure = () => {
                                ModalOverlayActions.hideModalOverlay.defer();
@@ -574,13 +596,32 @@ class CatalogDataStore {
                        this.resetSelectionState();
                }
        }
-       saveCatalogItemError(data){
-               let error = JSON.parse(data.error.responseText);
-               const errorMsg = error && error.body && error.body['rpc-reply'] && JSON.stringify(error.body['rpc-reply']['rpc-error'], null, ' ')
+       saveCatalogItemError(data) {
+               const descriptor = this.getCatalogs().reduce((gotIt, catalog) => {
+                       if (!gotIt && (catalog.type === data.catalogType)) {
+                               return catalog.descriptors.find(d => {
+                                       if (d.id === data.itemId) {
+                                               return d;
+                                       }
+                               });
+                       }
+                       return gotIt;
+               }, null);
+               const container = data.catalogType === 'nsd' ?
+                       DescriptorModelFactory.newNetworkService(descriptor, null)
+                       : DescriptorModelFactory.newVirtualNetworkFunction(descriptor, null)
                ComposerAppActions.showError.defer({
-                       errorMessage: 'Unable to save the descriptor.\n' + errorMsg
+                       errorMessage: 'Unable to save the descriptor.',
+                       rpcError: data.error.responseText
                });
+               ComposerAppActions.recordDescriptorError.defer({
+                       descriptor: container,
+                       type: data.catalogType,
+                       id: data.itemId,
+                       error: data.error.responseText
+               })
        }
 }
 
 export default alt.createStore(CatalogDataStore, 'CatalogDataStore');
+
index 9d23042..2ab785e 100644 (file)
@@ -68,9 +68,12 @@ const defaults = {
 const exception = function ignoreException() {};
 
 const packagePropertyNames = Object.keys(defaults.downloadPackage);
+const API_SERVER = utils.getSearchParams(window.location).api_server;
+const FILE_SERVER = window.location.hostname === 'localhost' 
+       ? API_SERVER : window.location.protocol + '//' + window.location.hostname;
 
 function getCatalogPackageManagerServerOrigin() {
-       return utils.getSearchParams(window.location).upload_server + ':4567';
+       return FILE_SERVER + ':8008';
 }
 
 function delayStatusCheck(statusCheckFunction, operation) {
@@ -244,7 +247,7 @@ function updateStatusInfo(response) {
                        break;
                case 'upload-success':
                        statusInfo.pending = true;
-                       statusInfo.progress = 100;
+                       statusInfo.progress = 50;
                        statusInfo.message = 'Upload completed.';
                        statusInfo.transactionId = responseData['transaction_id'] || responseData['transaction-id'] || operation.transactionId;
                        break;
@@ -260,7 +263,7 @@ function updateStatusInfo(response) {
                case 'pending':
                        statusInfo.pending = true;
                        statusInfo.progress = 50;
-                       statusInfo.message = responseData.events[responseData.events.length - 1].text;
+                       statusInfo.message = responseData.events && responseData.events.length && responseData.events[responseData.events.length - 1].text;
                        break;
                case 'success':
                        statusInfo.success = true;
@@ -269,9 +272,9 @@ function updateStatusInfo(response) {
                        if (operation.type === 'download') {
                                statusInfo.urlValidUntil = moment().add(defaults.downloadUrlTimeToLiveInMinutes, 'minutes').toISOString();
                                if (responseData.filename) {
-                                       statusInfo.url = getCatalogPackageManagerServerOrigin() + '/api/export/' + responseData.filename;
+                                       statusInfo.url = getCatalogPackageManagerServerOrigin() + '/mano/export/' + responseData.filename;
                                } else {
-                                       statusInfo.url = getCatalogPackageManagerServerOrigin() + '/api/export/' + operation.transactionId + '.tar.gz';
+                                       statusInfo.url = getCatalogPackageManagerServerOrigin() + '/mano/export/' + operation.transactionId + '.tar.gz';
                                }
                        }
                        break;
@@ -279,8 +282,23 @@ function updateStatusInfo(response) {
                        statusInfo.error = true;
                        statusInfo.message = responseData.errors[0].value;
                        break;
+               case 'COMPLETED':
+                       statusInfo.success = true;
+                       statusInfo.progress = 100;
+                       statusInfo.message = "Onboarding completed";
+                       break;
+               case 'IN_PROGRESS':
+                       statusInfo.pending = true;
+                       statusInfo.progress = (operation.progress || 25) + ((100 - operation.progress) / 2);
+                       statusInfo.message = "Onboarding in progress";
+                       break;
+               case 'FAILED':
+                       statusInfo.error = true;
+                       statusInfo.message = "Operation failed";
+                       break;
                default:
-                       throw new ReferenceError('a status of "request", "success", "failure", "pending", "upload-completed", "upload-error", "download-requested", "upload-progress", "upload-action" is required');
+                       statusInfo.error = true;
+                       statusInfo.message = responseData.message || "Operation failed";
                }
        } else {
                // typically get here due to unexpected development errors (backend exceptions, down/up load server access issues)
index c9675b7..27220d4 100644 (file)
@@ -1,4 +1,3 @@
-
 /*
  *
  *   Copyright 2016 RIFT.IO Inc
 import _isNumber from 'lodash/isNumber'
 import _cloneDeep from 'lodash/cloneDeep'
 import _isEmpty from 'lodash/isEmpty'
+import _isArray from 'lodash/isArray'
 import _mergeWith from 'lodash/mergeWith'
 import _uniqBy from 'lodash/uniqBy'
 import _isEqual from 'lodash/isEqual'
 import _findIndex from 'lodash/findIndex'
 import _remove from 'lodash/remove'
+import _get from 'lodash/get';
+import _set from 'lodash/set';
 import d3 from 'd3'
 import alt from '../alt'
 import UID from '../libraries/UniqueId'
+import DescriptorModel from '../libraries/model/DescriptorModel'
 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
 import PanelResizeAction from '../actions/PanelResizeAction'
 import CatalogItemsActions from '../actions/CatalogItemsActions'
 import CanvasEditorActions from '../actions/CanvasEditorActions'
+import DescriptorEditorActions from '../actions/DescriptorEditorActions'
 import ComposerAppActions from '../actions/ComposerAppActions'
 import CatalogFilterActions from '../actions/CatalogFilterActions'
 import CanvasPanelTrayActions from '../actions/CanvasPanelTrayActions'
@@ -43,6 +47,9 @@ import isFullScreen from '../libraries/isFullScreen'
 import FileManagerSource from '../components/filemanager/FileManagerSource';
 import FileManagerActions from '../components/filemanager/FileManagerActions';
 
+import Property from '../libraries/model/DescriptorModelMetaProperty'
+import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
+
 import React from 'react';
 
 //Hack for crouton fix. Should eventually put composer in skyquake alt context
@@ -52,13 +59,13 @@ let NotificationError = null;
 import utils from '../libraries/utils';
 
 class ComponentBridge extends React.Component {
-    constructor(props) {
-        super(props);
-        NotificationError = this.props.flux.actions.global.showNotification;
-    }
-    render(){
-        return <i></i>
-    }
+       constructor(props) {
+               super(props);
+               NotificationError = this.props.flux.actions.global.showNotification;
+       }
+       render() {
+               return <i > </i>
+       }
 }
 const getDefault = (name, defaultValue) => {
        const val = window.localStorage.getItem('defaults-' + name);
@@ -106,8 +113,8 @@ const uiTransientState = {};
 class ComposerAppStore {
 
        constructor() {
-        //Bridge for crouton fix
-        this.ComponentBridgeElement = SkyquakeComponent(ComponentBridge);
+               //Bridge for crouton fix
+               this.ComponentBridgeElement = SkyquakeComponent(ComponentBridge);
 
                this.exportAsync(FileManagerSource)
                // the catalog item currently being edited in the composer
@@ -126,6 +133,8 @@ class ComposerAppStore {
                this.drag = null;
                this.message = '';
                this.messageType = '';
+               this.showHelp = { onFocus: false, forAll: true, forNothing: false },
+                       this.detailPanel = { collapsed: true, hidden: false }
                this.showJSONViewer = false;
                this.showClassifiers = {};
                this.editPathsMode = false;
@@ -137,11 +146,11 @@ class ComposerAppStore {
                this.downloadJobs = {};
                this.containers = [];
                this.newPathName = '';
+               this.displayedPanel = 'forwarding'; //or parameter
                //End File  manager values
                this.bindListeners({
                        onResize: PanelResizeAction.RESIZE,
                        editCatalogItem: CatalogItemsActions.EDIT_CATALOG_ITEM,
-                       catalogItemMetaDataChanged: CatalogItemsActions.CATALOG_ITEM_META_DATA_CHANGED,
                        catalogItemDescriptorChanged: CatalogItemsActions.CATALOG_ITEM_DESCRIPTOR_CHANGED,
                        toggleShowMoreInfo: CanvasEditorActions.TOGGLE_SHOW_MORE_INFO,
                        showMoreInfo: CanvasEditorActions.SHOW_MORE_INFO,
@@ -152,6 +161,7 @@ class ComposerAppStore {
                        addVirtualDeploymentDescriptor: CanvasEditorActions.ADD_VIRTUAL_DEPLOYMENT_DESCRIPTOR,
                        selectModel: ComposerAppActions.SELECT_MODEL,
                        outlineModel: ComposerAppActions.OUTLINE_MODEL,
+                       modelSaveError: ComposerAppActions.RECORD_DESCRIPTOR_ERROR,
                        showError: ComposerAppActions.SHOW_ERROR,
                        clearError: ComposerAppActions.CLEAR_ERROR,
                        setDragState: ComposerAppActions.SET_DRAG_STATE,
@@ -178,38 +188,64 @@ class ComposerAppStore {
                        openDownloadMonitoringSocketSuccess: FileManagerActions.openDownloadMonitoringSocketSuccess,
                        getFilelistSocketSuccess: FileManagerActions.getFilelistSocketSuccess,
                        newPathNameUpdated: FileManagerActions.newPathNameUpdated,
-                       createDirectory: FileManagerActions.createDirectory
+                       createDirectory: FileManagerActions.createDirectory,
+                       modelSetOpenState: DescriptorEditorActions.setOpenState,
+                       modelError: DescriptorEditorActions.setError,
+                       modelSet: DescriptorEditorActions.setValue,
+                       modelAssign: DescriptorEditorActions.assignValue,
+                       modelListNew: DescriptorEditorActions.addListItem,
+                       modelListRemove: DescriptorEditorActions.removeListItem,
+                       showHelpForNothing: DescriptorEditorActions.showHelpForNothing,
+                       showHelpForAll: DescriptorEditorActions.showHelpForAll,
+                       showHelpOnFocus: DescriptorEditorActions.showHelpOnFocus,
+                       collapseAllPanels: DescriptorEditorActions.collapseAllPanels,
+                       expandAllPanels: DescriptorEditorActions.expandAllPanels,
+                       modelForceOpenState: DescriptorEditorActions.expandPanel,
+                       showPanelsWithData: DescriptorEditorActions.showPanelsWithData
+
                });
-        this.exportPublicMethods({
-            closeFileManagerSockets: this.closeFileManagerSockets.bind(this)
-        })
+               this.exportPublicMethods({
+                       closeFileManagerSockets: this.closeFileManagerSockets.bind(this)
+               })
        }
 
        onResize(e) {
+               const layout = Object.assign({}, this.layout);
                if (e.type === 'resize-manager.resize.catalog-panel') {
-                       const layout = Object.assign({}, this.layout);
                        layout.left = Math.max(0, layout.left - e.moved.x);
                        if (layout.left !== this.layout.left) {
-                               this.setState({layout: layout});
+                               this.setState({
+                                       layout: layout
+                               });
                        }
                } else if (e.type === 'resize-manager.resize.details-panel') {
-                       const layout = Object.assign({}, this.layout);
                        layout.right = Math.max(0, layout.right + e.moved.x);
                        if (layout.right !== this.layout.right) {
-                               this.setState({layout: layout});
+                               this.setState({
+                                       layout: layout
+                               });
                        }
                } else if (/^resize-manager\.resize\.canvas-panel-tray/.test(e.type)) {
-                       const layout = Object.assign({}, this.layout);
                        layout.bottom = Math.max(25, layout.bottom + e.moved.y);
                        if (layout.bottom !== this.layout.bottom) {
-                               const zoom = autoZoomCanvasScale(layout.bottom) ;
+                               const zoom = autoZoomCanvasScale(layout.bottom);
                                if (this.zoom !== zoom) {
-                                       this.setState({layout: layout, zoom: zoom});
+                                       this.setState({
+                                               layout: layout,
+                                               zoom: zoom
+                                       });
                                } else {
-                                       this.setState({layout: layout});
+                                       this.setState({
+                                               layout: layout
+                                       });
                                }
                        }
-               } else if (e.type !== 'resize') {
+               } else if (e.type == 'resize') {
+                       layout.height = e.target.innerHeight;
+                       this.setState({
+                               layout: layout
+                       });
+               } else {
                        console.log('no resize handler for ', e.type, '. Do you need to add a handler in ComposerAppStore::onResize()?')
                }
                SelectionManager.refreshOutline();
@@ -219,14 +255,17 @@ class ComposerAppStore {
                const self = this;
                let containers = [];
                let cpNumber = 0;
-               if(!document.body.classList.contains('resizing')) {
+               if (!document.body.classList.contains('resizing')) {
                        containers = [item].reduce(DescriptorModelFactory.buildCatalogItemFactory(CatalogDataStore.getState().catalogs), []);
 
                        containers.filter(d => DescriptorModelFactory.isConnectionPoint(d)).forEach(d => {
                                d.cpNumber = ++cpNumber;
                                containers.filter(d => DescriptorModelFactory.isVnfdConnectionPointRef(d)).filter(ref => ref.key === d.key).forEach(ref => ref.cpNumber = d.cpNumber);
                        });
-                       this.setState({containers: containers, item: _cloneDeep(item)});
+                       this.setState({
+                               containers: containers,
+                               item: _cloneDeep(item)
+                       });
                }
                SelectionManager.refreshOutline();
        }
@@ -246,44 +285,291 @@ class ComposerAppStore {
                        this.openFileManagerSockets(item);
                }
        }
-       catalogItemMetaDataChanged(item) {
-               this.updateItem(item);
-       }
 
        catalogItemDescriptorChanged(itemDescriptor) {
-               this.catalogItemMetaDataChanged(itemDescriptor.model);
+               this.updateItem(itemDescriptor.model);
+       }
+
+       setItemError(descriptor, path, message) {
+               // save locally and globally
+               descriptor.setUiState('error', path, message);
+               if (descriptor.parent) {
+                       let parentPath = [];
+                       while (descriptor.parent) {
+                               parentPath.push(descriptor.type);
+                               const index = descriptor.parent.model[descriptor.type].findIndex(item => item.id === descriptor.id);
+                               parentPath.push(index);
+                               descriptor = descriptor.parent;
+                       }
+                       parentPath.length && descriptor.setUiState('error', parentPath.concat(path), message);
+               } else if (path.length > 1 && descriptor[path[0]]) {
+                       // if we are indirectly editing a sub model set the error state in the sub model
+                       descriptor[path[0]][path[1]].setUiState('error', path.slice(2), message);
+               }
+       }
+
+       modelSaveError(input) {
+               const { descriptor, type, id } = input;
+               const errorMessage = {
+                       'data-missing': "Required value.",
+                       'missing-element': "Incomplete configuration."
+               }
+               if (descriptor.id === id) {
+                       let error = input.error;
+                       let rpcError = null;
+                       if (typeof error === 'string') {
+                               try {
+                                       error = JSON.parse(error);
+                               } catch (e) {
+                                       error = {};
+                               }
+                       }
+                       rpcError = error['rcp-error']
+                               || (error['rcp-reply'] && error['rcp-reply']['rcp-error'])
+                               || (error.body && error.body['rpc-reply'] && error.body['rpc-reply']['rpc-error']);
+                       if (rpcError) {
+                               const errorTag = rpcError['error-tag'];
+                               const message = errorMessage[errorTag] || errorTag;
+                               let errPath = rpcError['error-path'].trim();
+                               errPath = errPath.replace(/[-\w]*:/g, '');
+                               errPath = errPath.slice(errPath.indexOf('/' + type + '[') + 1);
+                               const path = [];
+                               let splitIndex = errPath.indexOf('/');
+                               function ripOutNodeExpression(str, complexNode) {
+                                       const expressionEnd = str.indexOf(']');
+                                       complexNode.push(str.slice(1, expressionEnd));
+                                       if (str.charAt(expressionEnd + 1) === '[') {
+                                               str = str.slice(expressionEnd + 1);
+                                               return ripOutNodeExpression(str, complexNode)
+                                       }
+                                       return str.slice(expressionEnd + 2);
+                               }
+                               while (splitIndex > -1) {
+                                       const expressionStart = errPath.indexOf('[');
+                                       if (expressionStart > 0 && expressionStart < splitIndex) {
+                                               const complexNode = [];
+                                               complexNode.push(errPath.slice(0, expressionStart));
+                                               errPath = errPath.slice(expressionStart);
+                                               errPath = ripOutNodeExpression(errPath, complexNode);
+                                               path.push(complexNode);
+                                       } else {
+                                               path.push(errPath.slice(0, splitIndex))
+                                               errPath = errPath.slice(splitIndex + 1);
+                                       }
+                                       splitIndex = errPath.indexOf('/');
+                               }
+                               const expressionStart = errPath.indexOf('[');
+                               if (expressionStart > 0) {
+                                       const complexNode = [];
+                                       complexNode.push(errPath.slice(0, expressionStart));
+                                       errPath = errPath.slice(expressionStart);
+                                       errPath = ripOutNodeExpression(errPath, complexNode);
+                               } else {
+                                       path.push(errPath.slice(0))
+                               }
+                               let model = descriptor.model;
+                               path.shift();
+                               let fullPath = path.reduce((a, p, i) => {
+                                       let element = p;
+                                       let subPath = [];
+                                       if (Array.isArray(p)) {
+                                               element = p.shift();
+                                               subPath = p;
+                                       }
+                                       a.push(element);
+                                       model = model[element];
+                                       const match = subPath.reduce((m, e) => {
+                                               const id = e.split('=');
+                                               const key = id[0];
+                                               let value = id[1];
+                                               value = value.charAt(0) === "'" ? value.split("'")[1] : value;
+                                               m.push({ key, value });
+                                               return m;
+                                       }, []);
+                                       if (match.length) {
+                                               const index = model.findIndex(obj => match.every(e => obj[e.key] == e.value));
+                                               a.push(index);
+                                               model = model[index];
+                                       }
+                                       return a;
+                               }, []);
+                               this.setItemError(descriptor, fullPath, message);
+                               this.updateItem(descriptor.getRoot().model);
+                       }
+               }
+       }
+
+       modelError(input) {
+               const { descriptor, path, message } = input;
+               this.setItemError(descriptor, path, message);
+               this.updateItem(descriptor.getRoot().model)
+       }
+
+       modelSet(input) {
+               const { descriptor, path, value } = input;
+               _set(descriptor.model, path, value);
+               this.setItemError(descriptor, path, null);
+               this.updateItem(descriptor.getRoot().model)
+               CatalogItemsActions.catalogItemDescriptorChanged.defer(descriptor.getRoot());
+       }
+
+       modelSetOpenState(input) {
+               const { descriptor, path, isOpen } = input;
+               descriptor.setUiState('opened', path, isOpen);
+               this.updateItem(descriptor.getRoot().model)
+       }
+
+       modelForceOpenState(input) {
+               const { descriptor, path } = input;
+               let openPath = [];
+               const targetPath = _isArray(path) ? path : [path];
+               targetPath.forEach(p => {
+                       openPath.push(p);
+                       descriptor.setUiState('opened', openPath, true);
+               });
+               this.updateItem(descriptor.getRoot().model)
+       }
+
+       modelAssign(input) {
+               const { descriptor, path, source } = input;
+               let obj = _get(descriptor.model, path) || {};
+               Object.assign(obj, source);
+               this.updateItem(descriptor.getRoot().model)
+               CatalogItemsActions.catalogItemDescriptorChanged.defer(descriptor.getRoot());
+       }
+
+       modelListNew(input) {
+               const { descriptor, path, property } = input;
+               const create = Property.getContainerCreateMethod(property, descriptor);
+               if (create) {
+                       const model = null;
+                       create(model, path, property);
+               } else {
+                       // get a unique name for the new list item based on the current list content
+                       // some lists, based on the key, may not get a uniqueName generated here
+                       const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(descriptor.model[property.name], property);
+                       const value = Property.createModelInstance(property, uniqueName);
+                       let list = _get(descriptor.model, path) || [];
+                       list.push(value);
+                       _set(descriptor.model, path, list);
+               }
+               const list = _get(descriptor.model, path);
+               let openPath = path.slice();
+               descriptor.setUiState('opened', openPath, true);
+               openPath.push((list.length - 1).toString());
+               descriptor.setUiState('opened', openPath, true);
+               function traverseProps(properties) {
+                       properties.forEach((p) => {
+                               if (p.type === 'list' || p.type === 'container') {
+                                       openPath.push(p.name);
+                                       descriptor.setUiState('opened', openPath, true);
+                                       p.type === 'container' && traverseProps(p.properties);
+                                       openPath.pop();
+                               }
+                       });
+               }
+               traverseProps(property.properties);
+               this.updateItem(descriptor.getRoot().model);
+               CatalogItemsActions.catalogItemDescriptorChanged.defer(descriptor.getRoot());
+       }
+
+       modelListRemove(input) {
+               const { descriptor, path, property } = input;
+               const removeMethod = Property.getContainerMethod(property, descriptor, 'remove');
+               if (removeMethod) {
+                       removeMethod(_get(descriptor.model, path));
+               } else {
+                       const index = path.pop();
+                       let list = _get(descriptor.model, path);
+                       list = list.splice(index, 1);
+               }
+               this.updateItem(descriptor.getRoot().model);
+               CatalogItemsActions.catalogItemDescriptorChanged.defer(descriptor.getRoot());
+       }
+
+       showHelpForNothing() {
+               this.setState({ showHelp: { onFocus: false, forAll: false, forNothing: true } })
+       }
+
+       showHelpForAll() {
+               this.setState({ showHelp: { onFocus: false, forAll: true, forNothing: false } })
+       }
+
+       showHelpOnFocus() {
+               this.setState({ showHelp: { onFocus: true, forAll: false, forNothing: false } })
+       }
+
+       clearOpenPanelState(descriptor) {
+               if (descriptor) {
+                       descriptor.setUiState('opened', [], {});
+                       this.updateItem(descriptor.getRoot().model);
+               }
+       }
+       collapseAllPanels(input) {
+               this.setState({ openPanelsWithData: false, collapsePanelsByDefault: true });
+               this.clearOpenPanelState(input.descriptor);
+       }
+
+       expandAllPanels(input) {
+               this.setState({ openPanelsWithData: false, collapsePanelsByDefault: false });
+               this.clearOpenPanelState(input.descriptor);
+       }
+
+       showPanelsWithData(input) {
+               this.setState({ openPanelsWithData: true, collapsePanelsByDefault: true });
+               this.clearOpenPanelState(input.descriptor);
        }
 
        showMoreInfo() {
-               this.setState({showMore: true});
+               this.setState({
+                       showMore: true
+               });
        }
 
        showLessInfo() {
-               this.setState({showMore: false});
+               this.setState({
+                       showMore: false
+               });
        }
 
        showError(data) {
-        NotificationError.defer({msg: data.errorMessage, type: 'error'})
-        // this.setState({message: data.errorMessage, messageType: 'error'});
+               NotificationError.defer({
+                       msg: data.errorMessage,
+                       type: 'error',
+                       rpcError: data.rpcError
+               })
+               // this.setState({message: data.errorMessage, messageType: 'error'});
        }
 
        clearError() {
-               this.setState({message: '', messageType: ''});
+               this.setState({
+                       message: '',
+                       messageType: ''
+               });
        }
 
        toggleShowMoreInfo() {
-               this.setState({showMore: !this.showMore});
+               this.setState({
+                       showMore: !this.showMore
+               });
        }
 
        applyDefaultLayout() {
                if (this.item && this.item.uiState && this.item.uiState.containerPositionMap) {
                        if (!_isEmpty(this.item.uiState.containerPositionMap)) {
                                this.item.uiState.containerPositionMap = {};
+                               this.updateItem(this.item);
                                CatalogItemsActions.catalogItemMetaDataChanged.defer(this.item);
                        }
                }
        }
 
+       updateItemNotifyLetSettleCheckMetaData(descriptor) {
+               this.updateItem(descriptor.model);
+               CatalogItemsActions.catalogItemDescriptorChanged.defer(descriptor);
+               setTimeout(() => CatalogItemsActions.catalogItemMetaDataChanged(this.item), 100);
+       }
+
        addVirtualLinkDescriptor(dropCoordinates = null) {
                let vld;
                if (this.item) {
@@ -298,8 +584,7 @@ class ComposerAppStore {
                                vld.uiState.dropCoordinates = dropCoordinates;
                                SelectionManager.clearSelectionAndRemoveOutline();
                                SelectionManager.addSelection(vld);
-                               this.updateItem(vld.getRoot().model);
-                               CatalogItemsActions.catalogItemDescriptorChanged.defer(vld.getRoot());
+                               this.updateItemNotifyLetSettleCheckMetaData(vld.getRoot());
                        }
                }
        }
@@ -311,8 +596,7 @@ class ComposerAppStore {
                        fg.uiState.dropCoordinates = dropCoordinates;
                        SelectionManager.clearSelectionAndRemoveOutline();
                        SelectionManager.addSelection(fg);
-                       this.updateItem(nsdc.model);
-                       CatalogItemsActions.catalogItemDescriptorChanged.defer(nsdc);
+                       this.updateItemNotifyLetSettleCheckMetaData(nsdc);
                }
        }
 
@@ -323,15 +607,14 @@ class ComposerAppStore {
                        vdu.uiState.dropCoordinates = dropCoordinates;
                        SelectionManager.clearSelectionAndRemoveOutline();
                        SelectionManager.addSelection(vdu);
-                       this.updateItem(vdu.getRoot().model);
-                       CatalogItemsActions.catalogItemDescriptorChanged.defer(vdu.getRoot());
+                       this.updateItemNotifyLetSettleCheckMetaData(vdu.getRoot());
                }
        }
 
        selectModel(container) {
                if (SelectionManager.select(container)) {
                        const model = DescriptorModelFactory.isContainer(container) ? container.getRoot().model : container;
-                       this.catalogItemMetaDataChanged(model);
+                       this.updateItem(model);
                }
        }
 
@@ -344,36 +627,57 @@ class ComposerAppStore {
 
        clearSelection() {
                SelectionManager.clearSelectionAndRemoveOutline();
-               this.catalogItemMetaDataChanged(this.item);
        }
 
        setDragState(dragState) {
-               this.setState({drag: dragState});
+               this.setState({
+                       drag: dragState
+               });
        }
 
        filterCatalogByType(typeValue) {
-               this.setState({filterCatalogByTypeValue: typeValue})
+               this.setState({
+                       filterCatalogByTypeValue: typeValue
+               })
        }
 
        setCanvasZoom(zoom) {
-               this.setState({zoom: zoom});
+               this.setState({
+                       zoom: zoom
+               });
        }
 
        showJsonViewer() {
-               this.setState({showJSONViewer: true});
+               this.setState({
+                       showJSONViewer: true
+               });
        }
 
        closeJsonViewer() {
-               this.setState({showJSONViewer: false});
+               this.setState({
+                       showJSONViewer: false
+               });
        }
 
-       toggleCanvasPanelTray() {
+       toggleCanvasPanelTray(event) {
                const layout = this.layout;
-               if (layout.bottom > 25) {
+               const attrMap = event.target.attributes;
+               let panelEvent = null;
+               for (let k in attrMap) {
+                       if (attrMap[k].name == 'data-event') {
+                               panelEvent = attrMap[k].nodeValue;
+                       }
+               }
+               if ((layout.bottom > 25) && ((panelEvent == this.displayedPanel) || panelEvent == 'arrow')) {
                        this.closeCanvasPanelTray();
                } else {
                        this.openCanvasPanelTray();
                }
+               if (panelEvent != 'arrow') {
+                       this.setState({
+                               displayedPanel: panelEvent
+                       })
+               }
        }
 
        openCanvasPanelTray() {
@@ -384,9 +688,15 @@ class ComposerAppStore {
                };
                const zoom = defaults.defaultPanelTrayOpenZoom;
                if (this.zoom !== zoom) {
-                       this.setState({layout: layout, zoom: zoom, restoreZoom: this.zoom});
+                       this.setState({
+                               layout: layout,
+                               zoom: zoom,
+                               restoreZoom: this.zoom
+                       });
                } else {
-                       this.setState({layout: layout});
+                       this.setState({
+                               layout: layout
+                       });
                }
        }
 
@@ -398,9 +708,16 @@ class ComposerAppStore {
                };
                const zoom = this.restoreZoom || autoZoomCanvasScale(layout.bottom);
                if (this.zoom !== zoom) {
-                       this.setState({layout: layout, zoom: zoom, restoreZoom: null});
+                       this.setState({
+                               layout: layout,
+                               zoom: zoom,
+                               restoreZoom: null
+                       });
                } else {
-                       this.setState({layout: layout, restoreZoom: null});
+                       this.setState({
+                               layout: layout,
+                               restoreZoom: null
+                       });
                }
        }
 
@@ -412,7 +729,7 @@ class ComposerAppStore {
                 */
                const eventNames = ['fullscreenchange', 'mozfullscreenchange', 'webkitfullscreenchange', 'msfullscreenchange'];
 
-               const appRoot = document.body;//.getElementById('RIFT_wareLaunchpadComposerAppRoot');
+               const appRoot = document.body; //.getElementById('RIFT_wareLaunchpadComposerAppRoot');
 
                const comp = this;
 
@@ -424,9 +741,16 @@ class ComposerAppStore {
                                uiTransientState.restoreLayout = restoreLayout;
                                layout.left = 0;
                                layout.right = 0;
-                               comp.setState({fullScreenMode: true, layout: layout, restoreLayout: restoreLayout});
+                               comp.setState({
+                                       fullScreenMode: true,
+                                       layout: layout,
+                                       restoreLayout: restoreLayout
+                               });
                        } else {
-                               comp.setState({fullScreenMode: false, layout: uiTransientState.restoreLayout});
+                               comp.setState({
+                                       fullScreenMode: false,
+                                       layout: uiTransientState.restoreLayout
+                               });
                        }
 
                }
@@ -464,7 +788,9 @@ class ComposerAppStore {
                        document.webkitExitFullscreen();
                }
 
-               this.setState({fullScreenMode: false});
+               this.setState({
+                       fullScreenMode: false
+               });
 
        }
        showAssets() {
@@ -482,39 +808,41 @@ class ComposerAppStore {
        getFilelistSuccess(data) {
                let self = this;
                let filesState = null;
-        if (self.fileMonitoringSocketID) {
-               let newState = {};
-               if(data.hasOwnProperty('contents')) {
-                       filesState = updateFileState( _cloneDeep(this.filesState),data);
+               if (self.fileMonitoringSocketID) {
+                       let newState = {};
+                       if (data.hasOwnProperty('contents')) {
+                               filesState = updateFileState(_cloneDeep(this.filesState), data);
                                let normalizedData = normalizeTree(data);
                                newState = {
                                        files: {
-                                               data: _mergeWith(normalizedData.data, self.files.data, function(obj, src) {
-                                                       return _uniqBy(obj? obj.concat(src) : src, 'name');
+                                               data: _mergeWith(normalizedData.data, self.files.data, function (obj, src) {
+                                                       return _uniqBy(obj ? obj.concat(src) : src, 'name');
                                                }),
                                                id: normalizedData.id
                                        },
                                        filesState: filesState
                                }
-               } else {
-                       newState = {
-                               files: false
-                       }
-               }
-               if(!_isEqual(newState.files, this.files) || ! _isEqual(newState.fileState, this.fileState)) {
-                       this.setState(newState);
-               }
-
-        }
+                       } else {
+                               newState = {
+                                       files: false
+                               }
+                       }
+                       if (!_isEqual(newState.files, this.files) || !_isEqual(newState.fileState, this.fileState)) {
+                               this.setState(newState);
+                       }
+
+               }
+
                function normalizeTree(data) {
                        let f = {
-                               id:[],
-                               data:{}
+                               id: [],
+                               data: {}
                        };
+
                        function getContents(d) {
-                               if(d.hasOwnProperty('contents')) {
+                               if (d.hasOwnProperty('contents')) {
                                        let contents = [];
-                                       d.contents.map(function(c,i) {
+                                       d.contents.map(function (c, i) {
                                                if (!c.hasOwnProperty('contents')) {
                                                        contents.push(c);
                                                } else {
@@ -526,11 +854,12 @@ class ComposerAppStore {
                                }
                        }
                        getContents(data);
-                       return f;
+                       return f;
                }
+
                function updateFileState(obj, d) {
                        d.newFile = '';
-                       if(d.hasOwnProperty('contents')) {
+                       if (d.hasOwnProperty('contents')) {
                                d.contents.map(updateFileState.bind(null, obj))
                        }
                        // override any "pending" state we may have initialized
@@ -556,7 +885,7 @@ class ComposerAppStore {
                });
        }
        addFileSuccess = (data) => {
-               if(!data.refresh) {
+               if (!data.refresh) {
                        let path = data.path;
                        if (path.startsWith('readme')) {
                                // this asset type stuff should be in a more common location
@@ -568,24 +897,29 @@ class ComposerAppStore {
                        let assetGroup = files.data[path] || [];
                        if (fileName) {
                                let name = path + '/' + fileName;
-                               if (assetGroup.findIndex(f => f.name === name) == -1){
-                                       assetGroup.push({name});
+                               if (assetGroup.findIndex(f => f.name === name) == -1) {
+                                       assetGroup.push({
+                                               name
+                                       });
                                }
                        }
                        files.data[path] = assetGroup;
-                       if (files.id.indexOf(path) == -1){
+                       if (files.id.indexOf(path) == -1) {
                                files.id.push(path);
                        }
                        let filesState = _cloneDeep(this.filesState);
                        filesState[name] = "DOWNLOADING";
-                       this.setState({files, filesState});
+                       this.setState({
+                               files,
+                               filesState
+                       });
                }
 
        }
        startWatchingJob = () => {
                let ws = window.multiplexer.channel(this.jobSocketId);
                this.setState({
-                       jobSocket:null
+                       jobSocket: null
                })
        }
        openDownloadMonitoringSocketSuccess = (id) => {
@@ -594,39 +928,39 @@ class ComposerAppStore {
                let downloadJobs = _cloneDeep(self.downloadJobs);
                let newFiles = false;
                ws.onmessage = (socket) => {
-            if (self.files && self.files.length > 0) {
-                let jobs = [];
-                try {
-                    jobs = JSON.parse(socket.data);
-                } catch(e) {}
-                newFiles = _cloneDeep(self.files);
-                jobs.map(function(j) {
-                    //check if not in completed state
-                    let fullPath = j['package-path'];
-                    let path = fullPath.split('/');
-                    let fileName = path.pop();
-                    path = path.join('/');
-                    let index = _findIndex(self.files.data[path], function(o){
-                        return fullPath == o.name
-                    });
-                    if((index > -1) && newFiles.data[path][index]) {
-                        newFiles.data[path][index].status = j.status
+                       if (self.files && self.files.length > 0) {
+                               let jobs = [];
+                               try {
+                                       jobs = JSON.parse(socket.data);
+                               } catch (e) { }
+                               newFiles = _cloneDeep(self.files);
+                               jobs.map(function (j) {
+                                       //check if not in completed state
+                                       let fullPath = j['package-path'];
+                                       let path = fullPath.split('/');
+                                       let fileName = path.pop();
+                                       path = path.join('/');
+                                       let index = _findIndex(self.files.data[path], function (o) {
+                                               return fullPath == o.name
+                                       });
+                                       if ((index > -1) && newFiles.data[path][index]) {
+                                               newFiles.data[path][index].status = j.status
                                        } else {
-                        if(j.status.toUpperCase() == 'LOADING...' || j.status.toUpperCase() == 'IN_PROGRESS') {
-                            newFiles.data[path].push({
-                                status: j.status,
-                                name: fullPath
-                            })
-                        } else {
-                            // if ()
-                        }
+                                               if (j.status.toUpperCase() == 'LOADING...' || j.status.toUpperCase() == 'IN_PROGRESS') {
+                                                       newFiles.data[path].push({
+                                                               status: j.status,
+                                                               name: fullPath
+                                                       })
+                                               } else {
+                                                       // if ()
+                                               }
                                        }
-                })
-                self.setState({
-                    files: newFiles
-                })
-                       // console.log(JSON.parse(socket.data));
-            }
+                               })
+                               self.setState({
+                                       files: newFiles
+                               })
+                               // console.log(JSON.parse(socket.data));
+                       }
                }
                this.setState({
                        jobSocketId: id,
@@ -638,20 +972,20 @@ class ComposerAppStore {
                let self = this;
                let ws = window.multiplexer.channel(id);
                ws.onmessage = (socket) => {
-            if (self.fileMonitoringSocketID) {
-                let data = [];
-                try {
-                    data = JSON.parse(socket.data);
-                } catch(e) {}
-                self.getFilelistSuccess(data)
-            }
+                       if (self.fileMonitoringSocketID) {
+                               let data = [];
+                               try {
+                                       data = JSON.parse(socket.data);
+                               } catch (e) { }
+                               self.getFilelistSuccess(data)
+                       }
                }
 
                this.setState({
                        filesState: [],
                        files: {
-                               id:[],
-                               data:{}
+                               id: [],
+                               data: {}
                        },
                        fileMonitoringSocketID: id,
                        fileMonitoringSocket: ws
@@ -659,31 +993,34 @@ class ComposerAppStore {
 
        }
        closeFileManagerSockets() {
-        this.fileMonitoringSocketID = null;
-        this.setState({
-                jobSocketId : null,
-                fileMonitoringSocketID : null
-                // jobSocket : null,
-                // fileMonitoringSocket : null,
-        });
+               this.fileMonitoringSocketID = null;
+               this.setState({
+                       jobSocketId: null,
+                       fileMonitoringSocketID: null
+                       // jobSocket : null,
+                       // fileMonitoringSocket : null,
+               });
                this.jobSocket && this.jobSocket.close();
                this.fileMonitoringSocket && this.fileMonitoringSocket.close();
-        console.log('closing');
+               console.log('closing');
        }
        openFileManagerSockets(i) {
                let self = this;
                let item = i || self.item;
-        // this.closeFileManagerSockets();
-               this.getInstance().openFileMonitoringSocket(item.id, item.uiState.type).then(function() {
-        //     // self.getInstance().openDownloadMonitoringSocket(item.id);
+               // this.closeFileManagerSockets();
+               this.getInstance().openFileMonitoringSocket(item.id, item.uiState.type).then(function () {
+                       //      // self.getInstance().openDownloadMonitoringSocket(item.id);
                });
-        this.getInstance().openDownloadMonitoringSocket(item.id);
+               this.getInstance().openDownloadMonitoringSocket(item.id);
        }
        endWatchingJob(id) {
 
        }
        deletePackageFile(asset) {
-               let {assetType, path} = asset;
+               let {
+                       assetType,
+                       path
+               } = asset;
                let id = this.item.id;
                let type = this.item.uiState.type;
                this.getInstance().deleteFile(id, type, assetType, path);
@@ -691,7 +1028,7 @@ class ComposerAppStore {
        deleteFileSuccess = (data) => {
                let name = null;
                let path = null;
-               if (data.assetFolder === 'readme'){
+               if (data.assetFolder === 'readme') {
                        // freak'n root folder is special
                        name = data.path;
                        path = ['.'];
@@ -702,7 +1039,7 @@ class ComposerAppStore {
                }
                let files = _cloneDeep(this.files);
                let filesForPath = files.data[path.join('/')]
-               _remove(filesForPath, function(c) {
+               _remove(filesForPath, function (c) {
                        return c.name == name;
                });
 
@@ -741,4 +1078,4 @@ class ComposerAppStore {
        }
 }
 
-export default alt.createStore(ComposerAppStore, 'ComposerAppStore');
+export default alt.createStore(ComposerAppStore, 'ComposerAppStore');
\ No newline at end of file
index 8220f0a..9af34bf 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -67,4 +67,4 @@
                        font-size: 11px;
                }
        }
-}
\ No newline at end of file
+}
index 9fa1149..2f684d2 100644 (file)
@@ -28,6 +28,7 @@
        min-width: 300px;
        overflow: hidden;
        z-index: 1;
+
        .CanvasPanelHeader {
                h1 {
                        margin: 0;
index 7e9e336..94f2b43 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,7 +29,8 @@ $tray-head-height: 25px;
        right: 0;
        height: 25px;
        min-width: 300px;
-       background-color: white;
+       /* background-color: white;*/
+       background: $panel-bg-color;
        &.-with-transitions {
                transition: height 300ms cubic-bezier(0.230, 1.000, 0.320, 1.000);
        }
@@ -60,4 +61,43 @@ $tray-head-height: 25px;
                }
        }
 
+
+       &-buttons {
+               display: -ms-flexbox;
+               display: flex;
+               margin-top: 2px;
+               button {
+                       padding: 6px 34px;
+               }
+       }
+       .tray-body {
+               top:31px;
+       }
+       .ConfigParameterMap {
+               padding: 10px 20px;
+               background: $panel-bg-color;
+
+               .toggleable {
+                       display:-ms-flexbox;
+                       display:flex;
+                       -ms-flex-wrap: wrap;
+                           flex-wrap: wrap;
+                   background: $panel-bg-color-contrast;
+                   margin:8px 0;
+                       & > .leaf-property {
+                           -ms-flex: 1 0 100%;
+                       flex: 1 0 100%;
+                       margin: 8px 0;
+                               padding: 0 8px;
+                               background: none;
+                       .property-label {
+                       }
+                       }
+                       & > .container-property {
+                               -ms-flex: 1 1;
+                                   flex: 1 1;
+                       }
+               }
+       }
+
 }
diff --git a/skyquake/plugins/composer/src/src/styles/CatalogItemDetailsEditor.scss b/skyquake/plugins/composer/src/src/styles/CatalogItemDetailsEditor.scss
new file mode 100644 (file)
index 0000000..bdbdb9c
--- /dev/null
@@ -0,0 +1,12 @@
+.CatalogItemDetailsEditor {
+    position: absolute;
+    overflow: hidden;
+    &:hover {
+        overflow: auto;
+    }
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    bottom: 0;
+    top: 0;
+    left: 0;
+    right: 0;
+}
\ No newline at end of file
index 5d020c0..5a34a6e 100644 (file)
 
                }
                &.-is-deleted {
-                       opacity: 0.25;
+                       display: none;
                }
 
                .-is-modified-indicator {
index 729f6de..04f1e78 100644 (file)
@@ -1,4 +1,3 @@
-
 /*
  * 
  *   Copyright 2016 RIFT.IO Inc
  *   limitations under the License.
  *
  */
+
 @import 'main';
 @import 'variables';
 .DetailsPanel {
        @extend .panel;
-       border-top: 1px solid mix($panel-border-color, white, 80%);
+       //border-top: 1px solid mix($panel-border-color, white, 80%);
        //border-left: 1px solid $panel-border-color;
        background-color: ($panel-bg-color-contrast);
        position: absolute;
        width: 300px;
        min-width: 6px;
        z-index: 2;
-       .DetailsPanelBody {
-               @extend .panel-body;
-               position: absolute;
-               overflow: hidden;
-               &:hover {
-                       overflow: auto;
-               }
-               -ms-overflow-style: -ms-autohiding-scrollbar;
-               bottom: 0;
-               top: 0;
-               left: 0;
-               right: 0;
-               min-width: 200px;
-       }
+       overflow: visible
 }
+
+.DetailsPanelBody {
+       overflow: hidden;
+       >div {
+               position: relative;
+               height: 100%;
+               width: 100%;
+       }
+       min-width: 200px;
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/styles/DetailsPanelErrors.scss b/skyquake/plugins/composer/src/src/styles/DetailsPanelErrors.scss
new file mode 100644 (file)
index 0000000..8351d47
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 
+ *   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 'main';
+@import 'variables';
+.DetailsPanelErrors {
+       overflow: hidden;
+       max-height: 300px;
+       position: relative;
+       height: auto;
+       width: 100%;
+       color: red;
+}
\ No newline at end of file
diff --git a/skyquake/plugins/composer/src/src/styles/DetailsPanelToolbar.scss b/skyquake/plugins/composer/src/src/styles/DetailsPanelToolbar.scss
new file mode 100644 (file)
index 0000000..5139f27
--- /dev/null
@@ -0,0 +1,50 @@
+
+/*
+ * 
+ *   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 '_variables';
+.DetailsPanelToolbar {
+    display: block;
+    padding: 0;
+    margin: 32px 0 0 0;
+       height: auto;
+       border-top: 1px solid mix($panel-border-color, white, 80%);
+       border-bottom: 0 solid $panel-border-color-light;
+       white-space: nowrap;
+       text-align: center;
+       h1 {
+               white-space: nowrap;
+               text-align: left;
+        font-size: 14px;
+        line-height: 16px;
+        padding: 20px;
+        text-transform: uppercase;
+        font-weight: bold;
+        background-color: #ffffff;
+       }
+       .btn-bar {
+               padding: 0;
+               background-color: rgba(203, 209, 209, 1);
+               .btn-group {
+                       padding: 0 8px;
+                       display: inline-block;
+                       input {
+                               margin: 0 4px;
+                       }
+               }
+       }
+}
diff --git a/skyquake/plugins/composer/src/src/styles/EditConfigParameterMap.scss b/skyquake/plugins/composer/src/src/styles/EditConfigParameterMap.scss
new file mode 100644 (file)
index 0000000..f0407d6
--- /dev/null
@@ -0,0 +1,357 @@
+/*
+ *
+ *   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 "main";
+@import "variables";
+@import "ColorGroups";
+
+.config-parameter-map {
+
+       $field-border-radius: 3px;
+       $field-background-color: white;
+       $child-indent-left-right-margin: 5px;
+
+       /* font-size: smaller;*/
+       display:-ms-flexbox;
+       display:flex;
+       -ms-flex-wrap: wrap;
+           flex-wrap: wrap;
+
+
+
+       max-width: 980px;
+       .config-parameter-titles {
+               display: -ms-flexbox;
+               display: flex;
+               -ms-flex: 1 1 100%;
+                   flex: 1 1 100%;
+           padding-bottom:10px;
+           color: #AEAEAE;
+           text-transform:uppercase;
+                   .config-parameter {
+                       padding-left: 10px;
+                   }
+       }
+       .config-parameter {
+               display:-ms-flexbox;
+               display:flex;
+               -ms-flex: 1 0 50%;
+                   flex: 1 0 50%;
+                   &-source, &-request {
+                       -ms-flex: 1 1 50%;
+                       flex: 1 1 50%;
+                               border-top:1px solid silver;
+                               padding: 10px 10px;
+                                                   }
+                   &-request{
+                       line-height: 25px;
+                   }
+                   &-group {
+                       -ms-flex: 1 1 100%;
+                           flex: 1 1 100%;
+                       display:-ms-flexbox;
+                       display:flex;
+                       &:nth-child(even) {
+                               background: $panel-bg-color-contrast;
+                       }
+                   }
+       }
+
+
+       h1 {
+               text-align: left;
+               span:last-child {
+                       i {
+                               display: none;
+                       }
+               }
+       }
+
+       h2 {
+               @extend h1;
+
+       }
+
+       .basic-properties-group {
+               > h1 {
+                       display: none;
+               }
+       }
+
+       .advanced-properties-group {
+       }
+
+       /* label is used as list item headers */
+       h3 {
+               @extend h2;
+               display: inline-block;
+               .name {
+                       color: #586e75;
+               }
+               .value {
+                       display: none;
+                       color: #002b36;
+               }
+               .info {
+                       margin: 4px;
+               }
+       }
+
+       val {
+               display: inline-block;
+               color: #073642;
+       }
+
+       a {
+               &.simple-list-item {
+                       display: inline-block;
+                       width: 230px;
+                       padding: 4px 16px 4px 4px;
+                       white-space: nowrap;
+               }
+               &.vld-list-item,
+               &.internal-vld-list-item{
+                       border: 1px solid $vld-primary-color;
+                       border-radius: 24px;
+                       background: white linear-gradient(to right, $vld-primary-color 34px, white 34px);;
+               }
+               &.vnfd-list-item,
+               &.constituent-vnfd-list-item{
+                       border: 1px solid $vnfd-primary-color;
+                       border-radius: 11px;
+                       background: white linear-gradient(to right, $vnfd-primary-color 34px, white 34px);;
+               }
+               &.vdu-list-item {
+                       border: 1px solid $vdu-primary-color;
+                       border-radius: 11px;
+                       background: white linear-gradient(to right, $vdu-primary-color 34px, white 34px);;
+               }
+               &.vnffgd-list-item {
+                       border: 1px solid $vnffgd-primary-color;
+                       border-radius: 11px;
+                       background: white linear-gradient(to right, $vnffgd-primary-color 34px, white 34px);;
+               }
+       }
+
+       &.-is-tree-view {
+               .property {
+
+                       position: relative;
+                       overflow: hidden;
+
+                       margin: 8px 8px;
+
+                       background-color: rgba(147, 161, 161, 0.5);
+                       border-radius: $field-border-radius;
+
+                       > h3 {
+                               position: absolute;
+                               top: 2px;
+                               right: 18px;
+                               height: 21px;
+                               pointer-events: none;
+                               border-radius: $field-border-radius;
+                               padding: 3px 0;
+                       }
+
+                       > val {
+                               width: 100%;
+                               > .property-content {
+                                       width: 100%;
+                               }
+
+                       }
+
+                       &.-is-focused {
+                               > h3 {
+                                       /*z-index: -1;*/
+                               }
+                       }
+
+                       &.leaf-property {
+                               overflow: hidden;
+                               min-height: 25px;
+                               > h3 {
+                                       background: linear-gradient(to right, transparent, $field-background-color 21px);
+                                       padding-left: 25px;
+                               }
+                               > val {
+                                       border-radius: $field-border-radius;
+                                       > .property-content {
+                                               border-radius: $field-border-radius;
+                                       }
+                               }
+                       }
+
+                       &.property:not(.leaf-property) {
+
+                               padding: 7px $child-indent-left-right-margin 0 $child-indent-left-right-margin;
+
+                               &.list-property {
+                                       > h3 {
+                                               padding: 4px 8px;
+                                       }
+                               }
+
+                               > h3 {
+                                       right: auto;
+                                       top: 0;
+                                       left: 0;
+                                       width: 100%;
+                                       height: 25px;
+                                       z-index: 1;
+                                       padding: 8px;
+                                       /*background: red;*/
+                                       pointer-events: all;
+                               }
+                               > val {
+                                       margin: 28px 0 8px 0;
+                                       > .property-content {
+                                               position: relative;
+                                               border-radius: $field-border-radius;
+                                               margin-top: 4px;
+                                               padding: 4px;
+                                               &:first-of-type {
+                                                       margin-top: 0;
+                                               }
+                                               &.simple-list {
+                                                       min-height: 15px;
+                                                       margin: 11px;
+                                                       .simple-list-item {
+                                                               img,
+                                                               span {
+                                                                       margin: 0 5px;
+                                                                       vertical-align: middle;
+                                                               }
+                                                       }
+                                               }
+                                       }
+                                       > .property-content:not(.simple-list) {
+                                               &:nth-of-type(odd) {
+                                                       background-color: rgba(238, 232, 213, 0.33);
+                                               }
+                                               &:nth-of-type(even) {
+                                                       background-color: rgba(147, 161, 161, 0.33);
+                                               }
+                                       }
+                                       .tip {
+                                               font-style: italic;
+                                               font-size: small;
+                                               color: #93a1a1;
+                                       }
+                               }
+                       }
+
+                       .actions {
+                               span {
+                                       vertical-align: middle;
+                                       padding: 0 5px;
+                               }
+                       }
+
+               }
+
+       }
+
+       .description {
+               display: none;
+       }
+
+       input,
+       select,
+       textarea {
+               height: 25px;
+               line-height: 25px;
+               max-width: 100%;
+               min-width: 100%;
+               margin: 0;
+               padding: 0 0px 4px 8px;
+               border: 1px solid $field-background-color;
+               border-radius: $field-border-radius;
+               color: #002b36;
+               background-color: $field-background-color;
+               vertical-align: top;
+               &:focus {
+                       color: #002b36;
+                       background-color: white !important;
+               }
+               &::-webkit-input-placeholder {
+                       color: #eee8d5 !important;
+               }
+
+               &:-moz-placeholder { /* Firefox 18- */
+                       color: #eee8d5 !important;
+               }
+
+               &::-moz-placeholder {  /* Firefox 19+ */
+                       color: #eee8d5 !important;
+               }
+
+               &:-ms-input-placeholder {
+                       color: #eee8d5 !important;
+               }
+       }
+
+       select {
+               padding-right: 0;
+               margin-right: 0;
+               -webkit-appearance: none;
+               -webkit-border-radius: $field-border-radius;
+               &.-value-not-set {
+                       color: #eee8d5;
+               }
+       }
+
+       select {
+               -webkit-appearance: none;
+                  -moz-appearance: none;
+                       appearance: none; /* using -prefix-free http://leaverou.github.io/prefixfree/*/
+        background: $field-background-color url(../../../node_modules/open-iconic/svg/caret-bottom.svg) no-repeat right 8px center;
+               background-size: 10px;
+               border: {
+                       color: $field-background-color;
+                       radius: $field-border-radius;
+                       style: solid;
+                       width: 1px;
+               }
+       }
+
+       /* Removes default arrow for IE10+*/
+       /* IE 8/9 get dafault arrow which covers caret image*/
+       /* as long as caret image is small than and positioned*/
+       /* behind default arrow*/
+       select::-ms-expand {
+               display: none;
+       }
+
+       textarea {
+               height: 50px;
+       }
+
+       input {
+               padding: 0 20px 0 8px;
+               line-height: 25px;
+       }
+
+       input[name$="id"],
+       input.-is-guid {
+               font-size: 10px;
+               font-family: monospace;
+       }
+
+}
index 48ce949..c85409c 100644 (file)
 
 @import "main";
 @import "ColorGroups";
-
 .EditDescriptorModelProperties {
-
        $field-border-radius: 3px;
        $field-background-color: white;
        $child-indent-left-right-margin: 5px;
-
        font-size: smaller;
-
        h1 {
                text-align: left;
                span:last-child {
                        }
                }
        }
-
        h2 {
                @extend h1;
-
        }
-
        .basic-properties-group {
-               > h1 {
+               >h1 {
                        display: none;
                }
        }
-
-       .advanced-properties-group {
-       }
-
+       .advanced-properties-group {}
        /* label is used as list item headers */
        h3 {
                @extend h2;
                        margin: 4px;
                }
        }
-
        val {
                display: inline-block;
                color: #073642;
        }
-
        a {
                &.simple-list-item {
                        display: inline-block;
                        white-space: nowrap;
                }
                &.vld-list-item,
-               &.internal-vld-list-item{
+               &.internal-vld-list-item {
                        border: 1px solid $vld-primary-color;
                        border-radius: 24px;
-                       background: white linear-gradient(to right, $vld-primary-color 34px, white 34px);;
+                       background: white linear-gradient(to right, $vld-primary-color 34px, white 34px);
+                       ;
                }
                &.vnfd-list-item,
-               &.constituent-vnfd-list-item{
+               &.constituent-vnfd-list-item {
                        border: 1px solid $vnfd-primary-color;
                        border-radius: 11px;
-                       background: white linear-gradient(to right, $vnfd-primary-color 34px, white 34px);;
+                       background: white linear-gradient(to right, $vnfd-primary-color 34px, white 34px);
+                       ;
                }
                &.vdu-list-item {
                        border: 1px solid $vdu-primary-color;
                        border-radius: 11px;
-                       background: white linear-gradient(to right, $vdu-primary-color 34px, white 34px);;
+                       background: white linear-gradient(to right, $vdu-primary-color 34px, white 34px);
+                       ;
                }
                &.vnffgd-list-item {
                        border: 1px solid $vnffgd-primary-color;
                        border-radius: 11px;
-                       background: white linear-gradient(to right, $vnffgd-primary-color 34px, white 34px);;
+                       background: white linear-gradient(to right, $vnffgd-primary-color 34px, white 34px);
+                       ;
                }
        }
-
        &.-is-tree-view {
                .property {
-
                        position: relative;
                        overflow: hidden;
-
                        margin: 8px 8px;
-
                        background-color: rgba(147, 161, 161, 0.5);
                        border-radius: $field-border-radius;
-
-                       > h3 {
+                       >h3 {
                                position: absolute;
                                top: 2px;
                                right: 18px;
                                border-radius: $field-border-radius;
                                padding: 3px 0;
                        }
-
-                       > val {
+                       >val {
                                width: 100%;
-                               > .property-content {
+                               >.property-content {
                                        width: 100%;
                                }
-
                        }
-
                        &.-is-focused {
-                               > h3 {
+                               >h3 {
                                        //z-index: -1;
                                }
                        }
-
                        &.leaf-property {
                                overflow: hidden;
                                min-height: 25px;
-                               > h3 {
+                               >h3 {
                                        background: linear-gradient(to right, transparent, $field-background-color 21px);
                                        padding-left: 25px;
                                }
-                               > val {
+                               >val {
                                        border-radius: $field-border-radius;
-                                       > .property-content {
+                                       >.property-content {
                                                border-radius: $field-border-radius;
                                        }
                                }
                        }
-
                        &.property:not(.leaf-property) {
-
                                padding: 7px $child-indent-left-right-margin 0 $child-indent-left-right-margin;
-
+                               min-height: 18px;
                                &.list-property {
-                                       > h3 {
+                                       >h3 {
                                                padding: 4px 8px;
                                        }
                                }
-
-                               > h3 {
+                               &.container-property {
+                                       >h3 {
+                                               padding: 4px 8px;
+                                       }
+                               }
+                               >h3 {
                                        right: auto;
                                        top: 0;
                                        left: 0;
                                        //background: red;
                                        pointer-events: all;
                                }
-                               > val {
+                               >val {
                                        margin: 28px 0 8px 0;
-                                       > .property-content {
+                                       >.property-content {
                                                position: relative;
                                                border-radius: $field-border-radius;
                                                margin-top: 4px;
                                                        }
                                                }
                                        }
-                                       > .property-content:not(.simple-list) {
+                                       >.property-content:not(.simple-list) {
                                                &:nth-of-type(odd) {
                                                        background-color: rgba(238, 232, 213, 0.33);
                                                }
                                        }
                                }
                        }
-
                        .actions {
                                span {
                                        vertical-align: middle;
                                        padding: 0 5px;
                                }
                        }
-
                }
-
        }
-
        .description {
-               display: none;
+               display: inline-block;
+               margin-left: 15px;
+               font-style: italic;
+               font-size: small;
+               color: #323232;
        }
-
        input,
        select,
        textarea {
                &::-webkit-input-placeholder {
                        color: #eee8d5 !important;
                }
-
-               &:-moz-placeholder { /* Firefox 18- */
+               &:-moz-placeholder {
+                       /* Firefox 18- */
                        color: #eee8d5 !important;
                }
-
-               &::-moz-placeholder {  /* Firefox 19+ */
+               &::-moz-placeholder {
+                       /* Firefox 19+ */
                        color: #eee8d5 !important;
                }
-
                &:-ms-input-placeholder {
                        color: #eee8d5 !important;
                }
        }
-
        select {
                padding-right: 0;
                margin-right: 0;
                        color: #eee8d5;
                }
        }
-
        select {
                appearance: none; // using -prefix-free http://leaverou.github.io/prefixfree/
-        background: $field-background-color url(../../../node_modules/open-iconic/svg/caret-bottom.svg) no-repeat right 8px center;
+               background: $field-background-color url(../../../node_modules/open-iconic/svg/caret-bottom.svg) no-repeat right 8px center;
                background-size: 10px;
                border: {
                        color: $field-background-color;
                        width: 1px;
                }
        }
-
        // Removes default arrow for IE10+
        // IE 8/9 get dafault arrow which covers caret image
        // as long as caret image is small than and positioned
        select::-ms-expand {
                display: none;
        }
-
        textarea {
                height: 50px;
        }
-
        input {
                padding: 0 20px 0 8px;
                line-height: 25px;
        }
-
        input[name$="id"],
        input.-is-guid {
                font-size: 10px;
                font-family: monospace;
        }
-
-}
+}
\ No newline at end of file
index e8cee07..9e48807 100644 (file)
@@ -415,7 +415,7 @@ input.-is-guid {
     .sub-menu {
         display: none;
         position: absolute;
-        top: 36px;
+        top: 30px;
         left: -4px;
         padding: 4px;
         background-color: #f1f1f1;
index 7839f10..ecfc6b6 100644 (file)
@@ -31,6 +31,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CompressionPlugin = require("compression-webpack-plugin");
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html'
 var config = {
        devtool: 'source-map',
        output: {
@@ -93,8 +94,8 @@ var config = {
        },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
-            templateContent: '<div id="app"></div>'
+            filename: '../' + htmlFilename
+            template: frameworkPath + '/plugin-index.html'
         })
     ]
 };
diff --git a/skyquake/plugins/config/CMakeLists.txt b/skyquake/plugins/config/CMakeLists.txt
deleted file mode 100644 (file)
index e12d4fd..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-# 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/config/api/ro.js b/skyquake/plugins/config/api/ro.js
deleted file mode 100644 (file)
index 99fb1d7..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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/config/config.json b/skyquake/plugins/config/config.json
deleted file mode 100644 (file)
index 6fd9f43..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-    "root": "public",
-    "name": "Config",
-    "dashboard": "./dashboard/dashboard.jsx",
-    "order": 1,
-    "priority":1,
-    "routes": [
-    {
-        "label": "Configuration Dashboard",
-        "route": "accounts",
-        "component": "./dashboard/dashboard.jsx",
-        "path": "accounts",
-        "type": "internal"
-    }]
-}
diff --git a/skyquake/plugins/config/images/OpenDaylight_logo.png b/skyquake/plugins/config/images/OpenDaylight_logo.png
deleted file mode 100644 (file)
index b9064ec..0000000
Binary files a/skyquake/plugins/config/images/OpenDaylight_logo.png and /dev/null differ
diff --git a/skyquake/plugins/config/images/aws.png b/skyquake/plugins/config/images/aws.png
deleted file mode 100644 (file)
index ca57e1c..0000000
Binary files a/skyquake/plugins/config/images/aws.png and /dev/null differ
diff --git a/skyquake/plugins/config/images/juju.svg b/skyquake/plugins/config/images/juju.svg
deleted file mode 100644 (file)
index 142f608..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-        width="1190.55px" height="841.89px" viewBox="0 0 1190.55 841.89" enable-background="new 0 0 1190.55 841.89"
-        xml:space="preserve">
-<g>
-       <g>
-               <path fill="#DD4814" d="M657.621,123.565c-73.488,0-133.063,59.573-133.063,133.062s59.576,133.069,133.063,133.069
-                       c73.49,0,133.064-59.58,133.064-133.069S731.111,123.565,657.621,123.565z"/>
-               <path fill="#FFFFFF" d="M593.043,338.909c-1.128,1.128-2.632,1.75-4.233,1.75l0,0c-1.605,0-3.112-0.624-4.244-1.757
-                       c-1.129-1.128-1.751-2.632-1.751-4.234c0-1.602,0.622-3.105,1.751-4.235c1.133-1.132,2.64-1.756,4.243-1.756
-                       c1.601,0,3.104,0.622,4.233,1.751c1.131,1.131,1.753,2.637,1.753,4.24C594.797,336.273,594.174,337.779,593.043,338.909z"/>
-               <path fill="#FFFFFF" d="M619.154,249.424c-0.603-1.92-1.47-3.498-2.579-4.69c-1.105-1.193-2.443-2.085-3.978-2.653
-                       c-1.543-0.574-3.291-0.865-5.194-0.865s-3.636,0.29-5.147,0.863c-1.509,0.569-2.834,1.462-3.939,2.654
-                       c-1.107,1.19-1.974,2.769-2.578,4.691c-0.606,1.933-0.914,4.299-0.914,7.032v69.735h-12.038V255.39
-                       c0-3.572,0.484-6.896,1.438-9.88c0.957-2.996,2.463-5.641,4.475-7.863c2.015-2.226,4.586-3.976,7.643-5.203
-                       c3.05-1.226,6.742-1.848,10.973-1.848c4.229,0,7.949,0.622,11.059,1.847c3.115,1.227,5.717,2.978,7.732,5.204
-                       c2.01,2.219,3.532,4.863,4.52,7.861c0.985,2.985,1.485,6.311,1.485,9.882V265h-12.038v-8.544
-                       C620.072,253.733,619.763,251.367,619.154,249.424z"/>
-               <path fill="#FFFFFF" d="M669.396,298.309c0,3.579-0.499,6.905-1.483,9.885c-0.985,2.99-2.507,5.634-4.521,7.86
-                       c-2.016,2.225-4.617,3.976-7.732,5.203c-3.112,1.226-6.833,1.848-11.06,1.848s-7.917-0.622-10.972-1.849
-                       c-3.055-1.226-5.626-2.976-7.642-5.201c-2.014-2.226-3.52-4.872-4.476-7.864c-0.954-2.983-1.437-6.308-1.437-9.882v-30.425h12.037
-                       v29.361c0,2.734,0.309,5.099,0.914,7.029c0.603,1.924,1.471,3.502,2.578,4.694c1.106,1.194,2.434,2.087,3.938,2.654
-                       c1.511,0.573,3.242,0.863,5.147,0.863c1.903,0,3.651-0.291,5.193-0.864c1.533-0.568,2.871-1.461,3.979-2.653
-                       c1.108-1.194,1.979-2.773,2.579-4.693c0.607-1.94,0.917-4.305,0.917-7.03v-29.361h12.038V298.309z"/>
-               <path fill="#FFFFFF" d="M657.387,259.099c0-3.302,2.685-5.989,5.986-5.989c3.305,0,5.995,2.687,5.995,5.989
-                       c0,3.305-2.69,5.994-5.995,5.994C660.071,265.093,657.387,262.404,657.387,259.099z"/>
-               <path fill="#FFFFFF" d="M693.726,174.163c-0.603-1.92-1.472-3.499-2.581-4.693c-1.107-1.193-2.446-2.085-3.979-2.652
-                       c-1.54-0.573-3.288-0.863-5.193-0.863c-1.903,0-3.635,0.29-5.146,0.862c-1.508,0.569-2.834,1.462-3.941,2.653
-                       c-1.105,1.193-1.973,2.772-2.575,4.693c-0.606,1.932-0.914,4.298-0.914,7.032v69.734h-12.038v-70.8
-                       c0-3.571,0.482-6.896,1.438-9.881c0.958-2.996,2.463-5.642,4.477-7.863c2.012-2.224,4.584-3.975,7.643-5.202
-                       c3.055-1.227,6.744-1.849,10.969-1.849c4.227,0,7.948,0.622,11.06,1.848c3.119,1.228,5.719,2.979,7.731,5.203
-                       c2.014,2.22,3.535,4.865,4.522,7.86c0.983,2.988,1.483,6.314,1.483,9.885v9.609h-12.04v-8.543
-                       C694.64,178.467,694.332,176.101,693.726,174.163z"/>
-               <path fill="#FFFFFF" d="M743.963,223.048c0,3.577-0.499,6.903-1.482,9.884c-0.986,2.993-2.506,5.638-4.518,7.86
-                       c-2.018,2.226-4.619,3.976-7.732,5.203c-3.115,1.226-6.837,1.848-11.06,1.848c-4.229,0-7.921-0.622-10.974-1.849
-                       c-3.057-1.226-5.626-2.976-7.642-5.201c-2.012-2.223-3.518-4.868-4.476-7.863c-0.954-2.989-1.438-6.313-1.438-9.881v-30.426
-                       h12.038v29.361c0,2.729,0.308,5.095,0.915,7.031c0.603,1.92,1.469,3.499,2.58,4.692c1.106,1.194,2.432,2.087,3.937,2.654
-                       c1.513,0.573,3.243,0.863,5.146,0.863c1.907,0,3.654-0.291,5.194-0.864c1.534-0.566,2.872-1.459,3.981-2.653
-                       c1.106-1.19,1.975-2.769,2.577-4.692c0.607-1.936,0.916-4.301,0.916-7.031v-29.361h12.037V223.048z"/>
-       </g>
-       <g>
-               <path d="M360.49,646.299c-2.566,0-5.878-0.32-9.943-0.962c-4.06-0.642-7.482-1.498-10.264-2.565l3.849-24.377
-                       c2.14,0.641,4.601,1.172,7.377,1.603c2.781,0.427,5.347,0.642,7.698,0.642c10.265,0,17.586-3.157,21.972-9.461
-                       c4.38-6.311,6.575-15.557,6.575-27.746V419.527h29.83v163.584c0,21.38-4.866,37.257-14.594,47.631
-                       C393.257,641.112,379.094,646.299,360.49,646.299z"/>
-               <path d="M595.598,581.507c-6.847,1.714-15.877,3.528-27.103,5.454c-11.227,1.924-24.217,2.887-38.972,2.887
-                       c-12.83,0-23.63-1.875-32.396-5.614c-8.771-3.738-15.826-9.03-21.168-15.876c-5.348-6.842-9.197-14.916-11.547-24.218
-                       c-2.356-9.302-3.528-19.616-3.528-30.952v-93.66h29.83v87.245c0,20.317,3.208,34.856,9.622,43.622
-                       c6.416,8.771,17.21,13.15,32.396,13.15c3.208,0,6.52-0.105,9.943-0.32c3.417-0.211,6.625-0.481,9.623-0.802
-                       c2.992-0.321,5.718-0.642,8.179-0.963c2.457-0.32,4.22-0.691,5.293-1.122V419.527h29.83V581.507z"/>
-               <path d="M615.162,646.299c-2.566,0-5.879-0.32-9.943-0.962c-4.06-0.642-7.484-1.498-10.265-2.565l3.85-24.377
-                       c2.14,0.641,4.601,1.172,7.377,1.603c2.782,0.427,5.348,0.642,7.698,0.642c10.265,0,17.587-3.157,21.972-9.461
-                       c4.38-6.311,6.575-15.557,6.575-27.746V419.527h29.83v163.584c0,21.38-4.866,37.257-14.595,47.631
-                       C647.929,641.112,633.766,646.299,615.162,646.299z"/>
-               <path d="M850.27,581.507c-6.846,1.714-15.878,3.528-27.104,5.454c-11.227,1.924-24.217,2.887-38.972,2.887
-                       c-12.83,0-23.63-1.875-32.396-5.614c-8.771-3.738-15.827-9.03-21.17-15.876c-5.347-6.842-9.196-14.916-11.547-24.218
-                       c-2.355-9.302-3.528-19.616-3.528-30.952v-93.66h29.83v87.245c0,20.317,3.207,34.856,9.622,43.622
-                       c6.415,8.771,17.211,13.15,32.396,13.15c3.207,0,6.521-0.105,9.942-0.32c3.419-0.211,6.627-0.481,9.624-0.802
-                       c2.991-0.321,5.718-0.642,8.179-0.963c2.455-0.32,4.22-0.691,5.292-1.122V419.527h29.83V581.507z"/>
-       </g>
-</g>
-</svg>
diff --git a/skyquake/plugins/config/images/openmano.png b/skyquake/plugins/config/images/openmano.png
deleted file mode 100644 (file)
index 0557c00..0000000
Binary files a/skyquake/plugins/config/images/openmano.png and /dev/null differ
diff --git a/skyquake/plugins/config/images/openstack-horizontal.png b/skyquake/plugins/config/images/openstack-horizontal.png
deleted file mode 100644 (file)
index e1426e5..0000000
Binary files a/skyquake/plugins/config/images/openstack-horizontal.png and /dev/null differ
diff --git a/skyquake/plugins/config/images/openstack.png b/skyquake/plugins/config/images/openstack.png
deleted file mode 100644 (file)
index 9ee6d77..0000000
Binary files a/skyquake/plugins/config/images/openstack.png and /dev/null differ
diff --git a/skyquake/plugins/config/images/riftio.png b/skyquake/plugins/config/images/riftio.png
deleted file mode 100644 (file)
index 19bdf36..0000000
Binary files a/skyquake/plugins/config/images/riftio.png and /dev/null differ
diff --git a/skyquake/plugins/config/package.json b/skyquake/plugins/config/package.json
deleted file mode 100644 (file)
index c459845..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-{
-  "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",
-    "compression-webpack-plugin": "^0.3.2",
-    "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.10.1"
-  }
-}
diff --git a/skyquake/plugins/config/routes.js b/skyquake/plugins/config/routes.js
deleted file mode 100644 (file)
index bf5915d..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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/config/scripts/build.sh b/skyquake/plugins/config/scripts/build.sh
deleted file mode 100755 (executable)
index 4a5f8c6..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/bash
-
-# STANDARD_RIFT_IO_COPYRIGHT
-
-PLUGIN_NAME=accounts
-# 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
diff --git a/skyquake/plugins/config/scripts/install.sh b/skyquake/plugins/config/scripts/install.sh
deleted file mode 100755 (executable)
index ca3d9ca..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-
-# STANDARD_RIFT_IO_COPYRIGHT
-
-plugin=config
-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/config/server.js b/skyquake/plugins/config/server.js
deleted file mode 100644 (file)
index f72199c..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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);
-});
diff --git a/skyquake/plugins/config/src/dashboard/config.scss b/skyquake/plugins/config/src/dashboard/config.scss
deleted file mode 100644 (file)
index 3637338..0000000
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * STANDARD_RIFT_IO_COPYRIGHT
- */
-@import "style/_colors.scss";
-.Config {
-    -ms-flex: 1 1 100%;
-        flex: 1 1 100%;
-
-    .refreshStatus {
-        margin-left: 0.5rem;
-        padding: 1rem 0;
-
-        .currentStatus {
-            text-transform: uppercase;
-            display:-ms-flexbox;
-            display:flex;
-            -ms-flex-line-pack:center;
-                align-content:center;
-        }
-        span.oi {
-            cursor: pointer;
-        }
-    }
-    .delete, .cancel {
-        cursor: pointer;
-    }
-    .associateSdnAccount {
-        margin: 1rem;
-    }
-    .create > .flex-row > li {
-        -ms-flex: 1 1 auto;
-            flex: 1 1 auto;
-        margin: 0 .25rem;
-    }
-    .create > .flex-row > li:first-child {
-        margin-left: .5rem;
-    }
-    .create > .flex-row > li:last-child {
-        margin-right: .5rem;
-    }
-    /* .flex-row > li h3 {*/
-    /*     background-color: #ffffff;*/
-    /*     padding: 12px 18px;*/
-    /*     text-transform: uppercase;*/
-    /* }*/
-    .create .options {
-        -ms-flex-wrap: wrap;
-            flex-wrap: wrap;
-        text-align: center;
-        margin: 24px auto;
-        width: 90%;
-        flex-wrap: wrap;
-        margin: 24px auto;
-        width: 90%;
-        -ms-flex-pack: start;
-            justify-content: flex-start;
-    }
-    .create .options a {
-        background-color: #ffffff;
-        -ms-flex: 0 1 32%;
-            flex: 0 1 32%;
-        margin: 0 4px 4px 0;
-        padding: 4px;
-        box-shadow: 2px 2px rgba(0, 0, 0, 0.15);
-    }
-    .create .options a h5 {
-        font-size: 10px;
-        margin: 5px 0 0 5px;
-        text-align: left;
-    }
-    .create .options a img {
-        margin-top: 10px;
-        width: 70%;
-    }
-    .list-pools {
-        padding: 24px;
-    }
-    .list-pools li a {
-        background-color: #ffffff;
-        display: block;
-        font-size: 12px;
-        margin-bottom: 24px;
-        padding: 12px;
-        text-align: center;
-        box-shadow: 2px 2px rgba(0, 0, 0, 0.15);
-    }
-    .list-pools li h4 {
-        text-align: left;
-    }
-    .list-pools li img {
-        margin-top: 24px;
-        width: 65%;
-    }
-    .form-actions {
-        clear: both;
-        margin-top: 36px;
-        text-align: center;
-        margin-bottom:1rem;
-    }
-    .form-actions a {
-        color: #000000;
-        display: inline-block;
-        font-size: 12px;
-        padding: 8px 64px;
-        text-decoration: none;
-        text-transform: uppercase;
-        box-shadow: 2px 2px rgba(0, 0, 0, 0.15);
-    }
-    .form-actions a.save {
-        background-color: #ffffff;
-        border: 1px solid #cccccc;
-        border-top: 0;
-        cursor: pointer;
-        margin-right: 48px;
-    }
-    .form-actions a.launch {
-        background-color: #333333;
-        border: 1px solid #000000;
-        border-top: 0;
-        color: #ffffff;
-    }
-    .form-actions a.launch:hover, .form-actions a.launch:active {
-        background: #00acee;
-        color: #ffffff;
-    }
-    .create .select-type {
-        margin: 0.5rem;
-    }
-    /* .create-fleet-pool label {*/
-    /*     padding: 0.125rem;*/
-    /*     display: block;*/
-    /* }*/
-    /* .create-fleet-pool input {*/
-    /*     width: 350px;*/
-    /*     height: 35px;*/
-    /*     margin: 0px 20px 10px 15px;*/
-    /*     box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.39), 0 -1px 1px #ffffff, 0 1px 0 #ffffff;*/
-    /*     font-size: 20px;*/
-    /* }*/
-    .create .optional {
-        font-style: italic;
-    }
-    .name-input input {
-      background:white !important;
-      box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.39), 0 -1px 1px #ffffff, 0 1px 0 #ffffff;
-      display: block;
-      font-size: 120%;
-      height: 35px;
-      margin: 0;
-      margin-top: 0.25rem;
-      width: 350px;
-    }
-    .select-type {
-        display:-ms-flexbox;
-        display:flex;
-    }
-    .accountSelection {
-        display:-ms-flexbox;
-        display:flex;
-        -ms-flex:0 1;
-            flex:0 1;
-        position:relative;
-        -ms-flex-direction:column;
-        flex-direction:column;
-        margin-right:0.5rem;
-        -ms-flex-align:center;
-        align-items:center;
-        border-top:$gray-light 1px solid;
-        border-left:$gray-light 1px solid;
-        border-right:$gray-dark 1px solid;
-        border-bottom:$gray-dark 1px solid;
-        box-shadow: $gray 2px 2px 3px;
-        cursor:pointer;
-        background:white;
-        &:last-child {
-            margin-right:0;
-        }
-
-        input[type="radio"] {
-            opacity: 0.01;
-            position:absolute;
-            top:0;
-        }
-        &-imageWrapper {
-            display:-ms-flexbox;
-            display:flex;
-            -ms-flex-direction:column;
-                flex-direction:column;
-            margin:0.5rem;
-            padding:0.25rem;
-            background:white;
-            width:100px;
-            height:50px;
-            -ms-flex-align:center;
-                align-items:center;
-            -ms-flex-pack:center;
-                justify-content:center;
-            img{
-                max-width:100px;
-                max-height:50px;
-            }
-        }
-        span {
-            padding-bottom:0.5rem;
-        }
-        &-overlay {
-            position:absolute;
-            top:0;
-            left:0;
-            right:0;
-            bottom:0;
-            height:100%;
-            width:100%;
-            background: rgba(0, 0, 0, 0.25);
-            opacity: 0.2;
-        }
-        &--isSelected{
-            border-bottom:$gray-light 1px solid;
-            border-right:$gray-light 1px solid;
-            border-top:$gray-dark 1px solid;
-            border-left:$gray-dark 1px solid;
-            box-shadow: $gray 2px 2px 3px inset;
-        }
-    }
-    .configForm {
-        margin:0.5rem 0;
-        width:100%;
-        background: $gray-lighter;
-        &-title {
-            background-color: #ffffff;
-            padding: 12px 0.5rem;
-            text-transform: uppercase;
-            &--edit {
-                display:-ms-flexbox;
-                display:flex;
-                -ms-flex-pack:justify;
-                    justify-content:space-between;
-                -ms-flex-align: center;
-                    align-items: center;
-                >div {
-                    display:-ms-flexbox;
-                    display:flex;
-                    -ms-flex-align:center;
-                        align-items:center;
-                }
-            }
-        }
-        img {
-            height:2rem;
-            margin:0 0.5rem;
-        }
-        &-content {
-            padding:0.5rem;
-        }
-        &-nestedParams {
-            display:-ms-flexbox;
-            display:flex;
-            > label {
-                margin-right:0.5rem;
-            }
-            > label:last-child {
-                margin-right:0rem;
-            }
-
-        }
-    }
-    .row {
-        display:-ms-flexbox;
-        display:flex;
-    }
-}
diff --git a/skyquake/plugins/config/src/dashboard/configActions.js b/skyquake/plugins/config/src/dashboard/configActions.js
deleted file mode 100644 (file)
index 0956e80..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-/*
- * STANDARD_RIFT_IO_COPYRIGHT
- */
-module.exports = function(Alt) {
-   return Alt.generateActions(
-                                       'getResourceOrchestratorSuccess',
-                                       'updateResourceOrchestratorSuccess'
-                                       );
-}
diff --git a/skyquake/plugins/config/src/dashboard/configSource.js b/skyquake/plugins/config/src/dashboard/configSource.js
deleted file mode 100644 (file)
index 860d1f7..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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;
-}
-
-
-module.exports = function(Alt) {
-    return {
-        getResourceOrchestrator: {
-          remote: function() {
-              return new Promise(function(resolve, reject) {
-                $.ajax({
-                  url: 'passthrough/data/api/running/resource-orchestrator' + '?api_server=' + API_SERVER,
-                  type: 'GET',
-                  beforeSend: Utils.addAuthorizationStub,
-                  contentType: "application/json",
-                  success: function(data) {
-                    resolve(data["rw-launchpad:resource-orchestrator"]);
-                  },
-                  error: function(error) {
-                    console.log("There was an error updating the account: ", arguments);
-
-                  }
-                }).fail(function(xhr){
-                  //Authentication and the handling of fail states should be wrapped up into a connection class.
-                  Utils.checkAuthentication(xhr.status);
-                  return reject('error');
-                });
-              });
-          },
-          interceptResponse: interceptResponse({
-            'error': 'There was an error retrieving the resource orchestrator information.'
-          }),
-          success: Alt.actions.global.getResourceOrchestratorSuccess,
-                    loading: Alt.actions.global.showScreenLoader,
-          error: Alt.actions.global.showNotification
-        },
-        update: {
-          remote: function(state, account) {
-
-            return new Promise(function(resolve, reject) {
-              $.ajax({
-                url: 'resource-orchestrator' + '?api_server=' + API_SERVER,
-                type:'PUT',
-                beforeSend: Utils.addAuthorizationStub,
-                data: JSON.stringify(account),
-                contentType: "application/json",
-                success: function(data) {
-                  resolve({data});
-                },
-                error: function(error) {
-                  console.log("There was an error updating the account: ", arguments);
-                  return null;
-                }
-              }).fail(function(xhr){
-                //Authentication and the handling of fail states should be wrapped up into a connection class.
-                Utils.checkAuthentication(xhr.status);
-                return reject('error');
-              });
-
-            });
-          },
-          interceptResponse: interceptResponse({
-            'error': 'There was an error updating the account.'
-          }),
-          success: Alt.actions.global.updateResourceOrchestratorSuccess,
-          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;
-    }
-  }
-}
diff --git a/skyquake/plugins/config/src/dashboard/configStore.js b/skyquake/plugins/config/src/dashboard/configStore.js
deleted file mode 100644 (file)
index 54f3568..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * STANDARD_RIFT_IO_COPYRIGHT
- */
-import AccountActions from './configActions.js';
-import AccountSource from './configSource.js';
-
-let tempData = {
-    "rw-launchpad:resource-orchestrator": {
-        "name": "test",
-        "account-type": "openmano",
-        "openmano": {
-            "port": 9090,
-            "tenant-id": "d2581f60-4f28-11e6-9732-fa163e4bfd3e",
-            "host": "10.0.55.39"
-        }
-    }
-}
-
-var rw = require('utils/rw.js');
-var altImage = rw.getSearchParams(window.location).alt_image;
-
-let Params = {
-    //Config Agent
-    ConfigAgent: {
-
-    }
-}
-
-
-
-let AccountMeta = {
-    'account-types': ['openmano', 'rift-ro'],
-    'rift-ro' : [],
-    'openmano' : [{
-        label: "Host",
-        ref: 'host'
-    }, {
-        label: "Port",
-        ref: 'port'
-    }, {
-        label: "Tenant ID",
-        ref: 'tenant-id'
-    }],
-    imageByType: {
-        "openmano": altImage || require("../../images/openmano.png"),
-        "rift-ro": require("../../images/riftio.png")
-
-    },
-    labelByType: {
-        "openmano": "OpenStack",
-        "rift-ro": "Cloudsim"
-    }
-}
-
-export default class ConfigStore {
-    constructor() {
-        this.account = {};
-        this.accountType = 'openmano';
-        this.refreshingAll = false;
-        this.sdnOptions = [];
-        this.AccountMeta = AccountMeta;
-        this.bindActions(AccountActions(this.alt));
-        this.registerAsync(AccountSource);
-        this.exportPublicMethods({
-            getROAccount: this.getROAccount,
-            handleParamChange: this.handleParamChange,
-            handleNameChange: this.handleNameChange,
-            handleAccountTypeChange: this.handleAccountTypeChange,
-            updateAccount: this.updateAccount,
-            getImage: this.getImage
-        })
-    }
-    setAccountTemplate = (AccountType) => {
-        let state = {};
-        let account = {
-            name: '',
-            'account-type': AccountType || 'rift-ro',
-        };
-        account[AccountType || 'rift-ro'] = {};
-        state.account = account;
-        state.accountType = AccountType;
-        if (AccountType == this.initialAccountType) {
-            state.account = this.initialAccount;
-        }
-        this.setState(state)
-    }
-    updateAccount = (account) => {
-        this.setState({account:account})
-    }
-    getROAccount = () => {
-        let data = tempData["rw-launchpad:resource-orchestrator"]
-        this.setState({
-            account: data
-        })
-    }
-    handleNameChange = (event) => {
-        var account = this.account;
-        account.name = event.target.value;
-        this.setState(
-             {
-                account:account
-             }
-        );
-    }
-    handleAccountTypeChange = (event) => {
-        var accountType = event.target.value;
-        this.setAccountTemplate(accountType);
-    }
-    handleParamChange(node, event) {
-        return function(event) {
-            var account = this.state.account;
-            account[account['account-type']][node.ref] = event.target.value;
-            this.updateAccount(account);
-        }.bind(this);
-    }
-    getImage = (type) => {
-        return AccountMeta.imageByType[type];
-    }
-    getResourceOrchestratorSuccess = (data) => {
-        this.alt.actions.global.hideScreenLoader.defer();
-        if(!data) {
-            this.setAccountTemplate(false)
-        } else {
-            this.setState({
-                initialAccount: data,
-                initialAccountType: data['account-type'],
-                account: data,
-                accountType: data['account-type'] || 'rift-ro'
-            });
-        }
-    }
-    updateResourceOrchestratorSuccess = (data) => {
-        this.alt.actions.global.showNotification.defer({type:'success', msg: 'Resource Orchestrator has been succesfully updated'});
-    }
-}
diff --git a/skyquake/plugins/config/src/dashboard/dashboard.jsx b/skyquake/plugins/config/src/dashboard/dashboard.jsx
deleted file mode 100644 (file)
index 7430994..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * STANDARD_RIFT_IO_COPYRIGHT
- */
-
-import React from 'react';
-import AppHeader from 'widgets/header/header.jsx';
-import ConfigStore from './configStore.js';
-import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
-import 'style/layout.scss';
-import './config.scss';
-import {Panel, PanelWrapper} from 'widgets/panel/panel';
-import TextInput from 'widgets/form_controls/textInput.jsx';
-import Button from 'widgets/button/rw.button.js';
-
-class ConfigDashboard extends React.Component {
-    constructor(props) {
-        super(props);
-        this.Store = this.props.flux.stores.hasOwnProperty('ConfigStore') ? this.props.flux.stores.ConfigStore : this.props.flux.createStore(ConfigStore, "ConfigStore");
-        this.state = this.Store.getState();
-    }
-    componentWillMount() {
-        this.Store.listen(this.updateState);
-        this.Store.getResourceOrchestrator();
-    }
-    componentWillUnmount() {
-        this.Store.unlisten(this.updateState);
-    }
-    updateState = (state) => {
-        this.setState(state);
-    }
-    updateAccount = (e) => {
-        e.preventDefault();
-        e.stopPropagation();
-        this.Store.update(this.state.account);
-    }
-    render() {
-        let self = this;
-        let html;
-        let Account = this.state.account;
-        let AccountMeta = this.state.AccountMeta;
-        let AccountType = this.state.accountType;
-        let isEdit = true;
-        let ParamsHTML = null;
-        let Store = this.Store;
-        let Types = this.state.AccountMeta['account-types'];
-
-        let selectAccountStack = [];
-        let selectAccountHTML = null;
-
-
-        if (Account['account-type']) {
-            for (var i = 0; i < Types.length; i++) {
-                var node = Types[i];
-                var isSelected = (Account['account-type'] == node);
-                selectAccountStack.push(
-                  <label key={i} className={"accountSelection " + (isSelected ? "accountSelection--isSelected" : "")}>
-                    <div className={"accountSelection-overlay" + (isSelected ? "accountSelection-overlay--isSelected" : "")}></div>
-                    <div className="accountSelection-imageWrapper">
-                        <img src={Store.getImage(node)}/>
-                    </div>
-                    <input type="radio" name="account"
-                        onChange={Store.handleAccountTypeChange} defaultChecked={node == Types[0]} value={node} />{node}
-                  </label>
-                )
-            }
-            selectAccountHTML = (
-                <Panel className="accountForm" title="Select Account Type" no-corners>
-                    <div className="select-type accountForm-content row" >
-                        {selectAccountStack}
-                    </div>
-                </Panel>
-            );
-        }
-
-        if (AccountMeta[AccountType] && AccountMeta[AccountType].length > 0) {
-            var paramsStack = [];
-            var optionalField = '';
-            for (var i = 0; i < AccountMeta[AccountType].length; i++) {
-                var node = AccountMeta[AccountType][i];
-                var value = ""
-                if (Account[AccountType]) {
-                    value = Account[AccountType][node.ref]
-                }
-                paramsStack.push(
-                    <TextInput key={node.label} className="accountForm-input" label={node.label} required={!node.optional}  onChange={this.Store.handleParamChange(node)} value={value} />
-                );
-            }
-            ParamsHTML = (
-                <Panel className="create-fleet-pool accountForm" title={(isEdit ? 'Update' : 'Enter') +  ' Account Details'} no-corners>
-                    <div className="accountForm-content">
-                        {paramsStack}
-                    </div>
-                </Panel>
-            )
-        } else {
-            ParamsHTML = (
-                <Panel className="create-fleet-pool accountForm" title={(isEdit ? 'Update' : 'Enter') +  ' Account Details'} no-corners>
-                    <label style={{'marginLeft':'17px', color:'#888'}}>No Details Required</label>
-                </Panel>
-            )
-        }
-
-
-
-        html = (
-            <PanelWrapper className="column Config" style={{'alignContent': 'center', 'flexDirection': 'column'}}>
-            <form className="app-body create Accounts"  onSubmit={this.preventDefault} onKeyDown={this.evaluateSubmit}>
-                <div className="noticeSubText noticeSubText_right">
-                    * required
-                </div>
-                <div>
-                    <Panel className="create-fleet-pool accountForm" title="Resource Orchestrator" no-corners>
-                        <div className="accountForm-content">
-                             <TextInput className="accountForm-input" label={"Name"} onChange={this.Store.handleNameChange} value={Account.name} />
-                        </div>
-                    </Panel>
-
-                            {
-                                selectAccountHTML
-                            }
-                            {
-                                ParamsHTML
-                            }
-                </div>
-                <div className="form-actions">
-                    <Button key="4" role="button" className="update dark" label="Update"  onClick={this.updateAccount} />
-                </div>
-            </form>
-            </PanelWrapper>
-        );
-        return html;
-    }
-}
-// onClick={this.Store.update.bind(null, Account)}
-ConfigDashboard.contextTypes = {
-    router: React.PropTypes.object
-};
-
-export default SkyquakeComponent(ConfigDashboard);
diff --git a/skyquake/plugins/config/src/dashboard/inputs.jsx b/skyquake/plugins/config/src/dashboard/inputs.jsx
deleted file mode 100644 (file)
index 1636f4c..0000000
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
- * STANDARD_RIFT_IO_COPYRIGHT
- */
-
-import React from 'react';
-import Button from 'widgets/button/rw.button.js';
-import _cloneDeep from 'lodash/cloneDeep';
-import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
-import Crouton from 'react-crouton';
-import TextInput from 'widgets/form_controls/textInput.jsx';
-import 'style/common.scss';
-import './config.scss';
-class Account extends React.Component {
-    constructor(props) {
-        super(props);
-        this.state = {};
-        this.state.account = {};
-    }
-    storeListener = (state) => {
-        if(!state.account) {
-            this.setUp(this.props)
-        }
-        state.account && this.setState({account: state.account,accountType: state.accountType, types: state.types, sdnOptions: state.sdnOptions})
-    }
-    componentWillMount() {
-        this.props.store.listen(this.storeListener);
-        this.setUp(this.props);
-    }
-    componentWillReceiveProps(nextProps) {
-        if(JSON.stringify(nextProps.params) != JSON.stringify(this.props.params)){
-              this.setUp(nextProps);
-        }
-    }
-    componentWillUnmount() {
-        this.props.store.unlisten(this.storeListener);
-    }
-    setUp(props){
-        if(props.params.name != 'create') {
-            this.props.store.viewAccount({type: props.params.type, name: props.params.name});
-        } else {
-            this.props.store.setAccountTemplate(props.params.type);
-        }
-    }
-    create(e) {
-        e.preventDefault();
-        var self = this;
-        var Account = this.state.account;
-        let AccountType = this.state.accountType;
-        if (Account.name == "") {
-            self.props.flux.actions.global.showNotification("Please give the account a name");
-            return;
-        } else {
-            var type = Account['account-type'];
-            var params = Account.params;
-
-            if(params) {
-                for (var i = 0; i < params.length; i++) {
-                    var param = params[i].ref;
-                    if (typeof(Account[type]) == 'undefined' || typeof(Account[type][param]) == 'undefined' || Account[type][param] == "") {
-                        if (!params[i].optional) {
-                            self.props.flux.actions.global.showNotification("Please fill all account details");
-                            return;
-                        }
-                    }
-                }
-            }
-
-            let nestedParams = Account.nestedParams && Account.nestedParams;
-            if (nestedParams && nestedParams.params) {
-                for (let i = 0; i < nestedParams.params.length; i++) {
-                    let nestedParam = nestedParams.params[i].ref;
-                    if (typeof(Account[type]) == 'undefined' || typeof(Account[type][nestedParams['container-name']][nestedParam]) == 'undefined' || Account[type][nestedParams['container-name']][nestedParam] == "") {
-                        if (!nestedParams.params[i].optional) {
-                            self.props.flux.actions.global.showNotification("Please fill all account details");
-                            return;
-                        }
-                    }
-                }
-            }
-        }
-
-        let newAccount = _cloneDeep(removeTrailingWhitespace(Account));
-        delete newAccount.params;
-        newAccount.nestedParams &&
-            newAccount.nestedParams['container-name'] &&
-            delete newAccount[newAccount.nestedParams['container-name']];
-        delete newAccount.nestedParams;
-
-        this.props.flux.actions.global.showScreenLoader();
-        this.props.store.create(newAccount, AccountType).then(function() {
-            self.props.router.push({pathname:'accounts'});
-            self.props.flux.actions.global.hideScreenLoader.defer();
-        },
-         function() {
-            self.props.flux.actions.global.showNotification("There was an error creating your account. Please contact your system administrator.");
-            self.props.flux.actions.global.hideScreenLoader.defer();
-         });
-    }
-    update(e) {
-        e.preventDefault();
-        var self = this;
-        var Account = this.state.account;
-        let AccountType = this.state.accountType;
-        this.props.flux.actions.global.showScreenLoader();
-        this.props.store.update(Account, AccountType).then(function() {
-            self.props.router.push({pathname:'accounts'});
-             self.props.flux.actions.global.hideScreenLoader();
-        },
-        function() {
-
-        });
-    }
-    cancel = (e) => {
-        e.preventDefault();
-        e.stopPropagation();
-        this.props.router.push({pathname:'accounts'});
-    }
-    handleDelete = () => {
-        let self = this;
-        let msg = 'Preparing to delete "' + self.state.account.name + '"' +
-        ' Are you sure you want to delete this ' + self.state.accountType + ' account?"';
-        if (window.confirm(msg)) {
-            this.props.store.delete(self.state.accountType, self.state.account.name).then(function() {
-                self.props.flux.actions.global.hideScreenLoader();
-                self.props.router.push({pathname:'accounts'});
-            }, function(){
-                // self.props.flux.actions.global.hideScreenLoader.defer();
-                // console.log('Delete Account Fail');
-            });
-        } else {
-           self.props.flux.actions.global.hideScreenLoader();
-        }
-    }
-    handleNameChange(event) {
-       this.props.store.handleNameChange(event);
-    }
-    handleAccountTypeChange(node, event) {
-        this.props.store.handleAccountTypeChange(node, event);
-    }
-    handleSelectSdnAccount = (e) => {
-        var tmp = this.state.account;
-        if(e) {
-            tmp['sdn-account'] = e;
-        } else {
-            if(tmp['sdn-account']) {
-                delete tmp['sdn-account'];
-            }
-        }
-        console.log(e, tmp)
-    }
-    preventDefault = (e) => {
-        e.preventDefault();
-        e.stopPropagation();
-    }
-    evaluateSubmit = (e) => {
-        if (e.keyCode == 13) {
-            if (this.props.edit) {
-                this.update(e);
-            } else {
-                this.create(e);
-            }
-            e.preventDefault();
-            e.stopPropagation();
-        }
-    }
-
-    render() {
-        let self = this;
-        let {store, ...props} = this.props;
-        // This section builds elements that only show up on the create page.
-        // var name = <label>Name <input type="text" onChange={this.handleNameChange.bind(this)} style={{'textAlign':'left'}} /></label>;
-        var name = <TextInput label="Name"  onChange={this.handleNameChange.bind(this)} required={true} />;
-        let params = null;
-        let selectAccount = null;
-        let resfreshStatus = null;
-        let Account = this.state.account;
-        // AccountType is for the view, not the data account-type value;
-        let AccountType = this.state.accountType;
-        let Types = this.state.types;
-        let isEdit = this.props.params.name != 'create';
-        var buttons;
-        let cloudResources = Account['cloud-resources-state'] && Account['cloud-resources-state'][Account['account-type']];
-        let cloudResourcesStateHTML = null;
-
-        // Account Type Radio
-        var selectAccountStack = [];
-        if (!isEdit) {
-            buttons = [
-                <Button key="0" onClick={this.cancel} className="cancel light" label="Cancel"></Button>,
-                <Button key="1" role="button" onClick={this.create.bind(this)} className="save dark" label="Save" />
-            ]
-            for (var i = 0; i < Types.length; i++) {
-                var node = Types[i];
-                var isSelected = (Account['account-type'] == node['account-type']);
-                selectAccountStack.push(
-                  <label key={i} className={"accountSelection " + (isSelected ? "accountSelection--isSelected" : "")}>
-                    <div className={"accountSelection-overlay" + (isSelected ? "accountSelection-overlay--isSelected" : "")}></div>
-                    <div className="accountSelection-imageWrapper">
-                        <img src={store.getImage(node['account-type'])}/>
-                    </div>
-                    <input type="radio" name="account" onChange={this.handleAccountTypeChange.bind(this, node)} defaultChecked={node.name == Types[0].name} value={node['account-type']} />{node.name}
-                  </label>
-                )
-            }
-            selectAccount = (
-                <div className="accountForm">
-                    <h3 className="accountForm-title">Select Account Type</h3>
-                    <div className="select-type accountForm-content" >
-                        {selectAccountStack}
-                    </div>
-                </div>
-            );
-        } else {
-            selectAccount = null
-        }
-
-         //
-        // This sections builds the parameters for the account details.
-        if (Account.params) {
-            var paramsStack = [];
-            var optionalField = '';
-            for (var i = 0; i < Account.params.length; i++) {
-                var node = Account.params[i];
-                var value = ""
-                if (Account[Account['account-type']]) {
-                    value = Account[Account['account-type']][node.ref]
-                }
-                if (this.props.edit && Account.params) {
-                    value = Account.params[node.ref];
-                }
-                paramsStack.push(
-                    <TextInput key={node.label} className="accountForm-input" label={node.label} required={!node.optional}  onChange={this.props.store.handleParamChange(node)} value={value} />
-                );
-            }
-            params = (
-                <li className="create-fleet-pool accountForm">
-                    <h3  className="accountForm-title"> {isEdit ? 'Update' : 'Enter'} Account Details</h3>
-                    <div className="accountForm-content">
-                        {paramsStack}
-                    </div>
-                </li>
-            )
-        } else {
-            params = (
-                <li className="create-fleet-pool accountForm">
-                    <h3 className="accountForm-title"> {isEdit ? 'Update' : 'Enter'}</h3>
-                    <label style={{'marginLeft':'17px', color:'#888'}}>No Details Required</label>
-                </li>
-            )
-        }
-
-        // This section builds elements that only show up in the edit page.
-        if (isEdit) {
-            name = <label>{Account.name}</label>;
-            buttons = [
-                <Button key="2" onClick={this.handleDelete} className="light" label="Remove Account" />,
-                <Button key="3" onClick={this.cancel} className="light" label="Cancel" />,
-                <Button key="4" role="button" onClick={this.update.bind(this)} className="update dark" label="Update" />
-            ];
-            resfreshStatus = Account['connection-status'] ? (
-                <div className="accountForm">
-                    <div className="accountForm-title accountForm-title--edit">
-                        Connection Status
-                    </div>
-                    <div className="accountForm-content" style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
-                        <div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
-                            <AccountConnectivityStatus status={Account['connection-status'].status} />
-                            {Account['connection-status'].status.toUpperCase()}
-                        </div>
-                            <Button className="refreshList light" onClick={this.props.store.refreshAccount.bind(this, Account.name, AccountType)} label="REFRESH STATUS"></Button>
-                    </div>
-                    {
-                        Account['connection-status'].status.toUpperCase() === 'FAILURE' ?
-                        displayFailureMessage(Account['connection-status'].details) : null
-                    }
-                </div>
-            ) : null;
-            // cloudResourcesStateHTML = (
-            //     <div className="accountForm">
-            //         <h3 className="accountForm-title">Resources Status</h3>
-            //         <div className="accountForm-content" >
-            //         <ul>
-            //             {
-            //                 cloudResources && props.AccountMeta.resources[Account['account-type']].map(function(r, i) {
-
-            //                     return (
-            //                         <li key={i}>
-            //                             {r}: {cloudResources[r]}
-            //                         </li>
-            //                     )
-            //                 }) || 'No Additional Resources'
-            //             }
-            //         </ul>
-            //         </div>
-            //     </div>
-            // )
-        }
-
-        var html = (
-
-              <form className="app-body create Accounts"  onSubmit={this.preventDefault} onKeyDown={this.evaluateSubmit}>
-                <div className="noticeSubText noticeSubText_right">
-                    * required
-                </div>
-                <div className="associateSdnAccount accountForm">
-                    <h3 className="accountForm-title">Account</h3>
-                    <div className="accountForm-content" style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
-                        <h4 style={{flex: '1'}}>{name}</h4>
-                        { isEdit ?
-                            (
-                                <div style={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
-                                    <img src={store.getImage(Account['account-type'])}/> {props.AccountMeta.labelByType[Account['account-type']]}
-                                </div>)
-                            : null
-                        }
-                    </div>
-                </div>
-
-                  {selectAccount}
-                  {sdnAccounts}
-                  {resfreshStatus}
-                  {cloudResourcesStateHTML}
-                  <ol className="flex-row">
-                      {params}
-                  </ol>
-                  <div className="form-actions">
-                      {buttons}
-                  </div>
-              </form>
-        )
-        return html;
-    }
-}
-
-function displayFailureMessage(msg) {
-    return (
-        <div className="accountForm-content" style={{maxWidth: '600px'}}>
-            <div style={{paddingBottom: '1rem'}}>Details:</div>
-            <div>
-                {msg}
-            </div>
-
-        </div>
-    )
-}
-
-class SelectOption extends React.Component {
-  constructor(props){
-    super(props);
-  }
-  handleOnChange = (e) => {
-    this.props.onChange(JSON.parse(e.target.value));
-  }
-  render() {
-    let html;
-    html = (
-      <select className={this.props.className} onChange={this.handleOnChange}>
-        {
-          this.props.options.map(function(op, i) {
-            return <option key={i} value={JSON.stringify(op.value)}>{op.label}</option>
-          })
-        }
-      </select>
-    );
-    return html;
-  }
-}
-SelectOption.defaultProps = {
-  options: [],
-  onChange: function(e) {
-    console.dir(e)
-  }
-}
-
-function removeTrailingWhitespace(Account) {
-             var type = Account['account-type'];
-            var params = Account.params;
-
-            if(params) {
-                for (var i = 0; i < params.length; i++) {
-                    var param = params[i].ref;
-                    if(typeof(Account[type][param]) == 'string') {
-                        Account[type][param] = Account[type][param].trim();
-                    }
-                }
-            }
-
-            let nestedParams = Account.nestedParams;
-            if (nestedParams && nestedParams.params) {
-                for (let i = 0; i < nestedParams.params.length; i++) {
-                    let nestedParam = nestedParams.params[i].ref;
-                    let nestedParamValue = Account[type][nestedParams['container-name']][nestedParam];
-                    if (typeof(nestedParamValue) == 'string') {
-                        Account[type][nestedParams['container-name']][nestedParam] = nestedParamValue.trim();
-                    }
-                }
-            }
-            return Account;
-}
-
-export default SkyquakeComponent(Account)
diff --git a/skyquake/plugins/config/src/main.js b/skyquake/plugins/config/src/main.js
deleted file mode 100644 (file)
index 5dc626f..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * 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/config/webpack.production.config.js b/skyquake/plugins/config/webpack.production.config.js
deleted file mode 100644 (file)
index f78ff42..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 CompressionPlugin = require("compression-webpack-plugin");
-// 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>'
-        })
-    ]
-};
-
-if (process.argv.indexOf('--optimize-minimize') !== -1) {
-    // we are going to output a gzip file in the production process
-    config.output.filename = "gzip-" + config.output.filename;
-    config.plugins.push(new webpack.DefinePlugin({ // <-- key to reducing React's size
-      'process.env': {
-        'NODE_ENV': JSON.stringify('production')
-      }
-    }));
-    config.plugins.push(new CompressionPlugin({
-        asset: "[path]", // overwrite js file with gz file
-        algorithm: "gzip",
-        test: /\.(js)$/
-    }));
-}
-module.exports = config;
index 5d6e0ea..b02ae88 100644 (file)
@@ -33,7 +33,7 @@ crashDetails.get = function(req) {
     var requestHeaders = {};
     _.extend(requestHeaders,
       constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
       });
     request({
         url: utils.confdPort(api_server) + APIVersion +'/api/operational/crash?deep',
index 6643aca..e195ddd 100644 (file)
@@ -2,8 +2,14 @@
     "root": "public",
     "name": "Debug",
     "dashboard": "./crash.jsx",
-    "order": 100,
+    "order": 4,
     "priority":2,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper"
+    ],
     "routes" : [{
         "label": "Debug",
         "route": "/",
index 7bac1bf..97545a6 100755 (executable)
 
 PLUGIN_NAME=debug
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index 6e27c86..11759e8 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,5 +44,5 @@ 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
+tar -cf $bcache_dir/plugins/$plugin/node_modules.tar node_modules package.json -C $source_dir
 #cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
index 2ef01be..f23b8d2 100644 (file)
@@ -26,6 +26,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CompressionPlugin = require("compression-webpack-plugin");
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -66,8 +67,8 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
-            templateContent: '<div id="app"></div>'
+            filename: '../' + htmlFilename
+            template: frameworkPath + '/plugin-index.html'
         })
     ]
 };
index 3298c2a..c05cb30 100755 (executable)
 
 PLUGIN_NAME=goodbyeworld
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index 7db6acc..82ac524 100644 (file)
@@ -26,6 +26,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CompressionPlugin = require("compression-webpack-plugin");
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -65,7 +66,7 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
+            filename: '../' + htmlFilename
             templateContent: '<div id="content"></div>'
         })
     ]
index aec9106..c3867af 100755 (executable)
 
 PLUGIN_NAME=helloworld
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index 7db6acc..82ac524 100644 (file)
@@ -26,6 +26,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CompressionPlugin = require("compression-webpack-plugin");
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -65,7 +66,7 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
+            filename: '../' + htmlFilename
             templateContent: '<div id="content"></div>'
         })
     ]
index d74bb3e..d8777d5 100644 (file)
@@ -38,6 +38,7 @@ var ComputeTopology = {};
 var NetworkTopology = {};
 var VDUR = {};
 var CloudAccount = {};
+var ResourceOrchestratorAccount = {};
 var ConfigAgentAccount = {};
 var RPC = {};
 var SSHkey = {};
@@ -48,18 +49,19 @@ APIConfig.NfviMetrics = ['vcpu', 'memory'];
 RPC.executeNSServicePrimitive = function(req) {
     var api_server = req.query['api_server'];
     return new Promise(function(resolve, reject) {
+        var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operations/exec-ns-service-primitive');
         var jsonData = {
-            "input": req.body
+            "input": utils.addProjectContextToRPCPayload(req, uri, req.body)
         };
 
         var headers = _.extend({},
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
         request({
-            url: utils.confdPort(api_server) + APIVersion + '/api/operations/exec-ns-service-primitive',
+            url: uri,
             method: 'POST',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -81,18 +83,20 @@ RPC.getNSServicePrimitiveValues = function(req) {
     // var nsr_id = req.body['nsr_id_ref'];
     // var nsConfigPrimitiveName = req.body['name'];
     return new Promise(function(resolve, reject) {
+        var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operations/get-ns-service-primitive-values');
+
         var jsonData = {
-            "input": req.body
+            "input": utils.addProjectContextToRPCPayload(req, uri, req.body)
         };
 
         var headers = _.extend({},
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/operations/get-ns-service-primitive-values',
+            uri: uri,
             method: 'POST',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -133,16 +137,20 @@ RPC.refreshAccountConnectionStatus = function(req) {
         }
     }
     jsonData.input[rpcInfo[Type].label] = Name;
+
+    var uri = utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operations/' + rpcInfo[Type].rpc);
+
+    jsonData.input = utils.addProjectContextToRPCPayload(req, uri, jsonData.input);
+
     var headers = _.extend({},
         constants.HTTP_HEADERS.accept.data,
         constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         }
     );
     return new Promise(function(resolve, reject) {
-
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/operations/' + rpcInfo[Type].rpc,
+            uri: uri,
             method: 'POST',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -168,33 +176,34 @@ var DataCenters = {};
 Catalog.get = function(req) {
     var api_server = req.query['api_server'];
     var results = {}
+    var projectPrefix = req.session.projectId ? "project-" : "";
     return new Promise(function(resolve, reject) {
         Promise.all([
             rp({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/config/nsd-catalog/nsd?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/nsd-catalog/nsd?deep'),
                 method: 'GET',
                 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
             }),
             rp({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd?deep'),
                 method: 'GET',
                 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
             }),
             rp({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata?deep'),
                 method: 'GET',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 }),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
@@ -207,7 +216,7 @@ Catalog.get = function(req) {
             //   headers: _.extend({},
             //     constants.HTTP_HEADERS.accept.collection,
             //     {
-            //       'Authorization': req.get('Authorization')
+            //       'Authorization': req.session && req.session.authorization
             //     }),
             //   forever: constants.FOREVER_ON,
             // rejectUnauthorized: false,
@@ -258,16 +267,16 @@ Catalog.get = function(req) {
             var vnfdCatalog = null;
             var vnfdDict = {};
             if (result[1].body) {
-                vnfdCatalog = JSON.parse(result[1].body).collection['vnfd:vnfd'].map(function(v, i) {
+                response[1].descriptors = utils.dataToJsonSansPropNameNamespace(result[1].body).collection['vnfd'];
+                vnfdCatalog = response[1].descriptors.map(function(v, i) {
                     vnfdDict[v.id] = v['short-name'] || v.name;
                 })
             }
             if (result[0].body) {
-                response[0].descriptors = JSON.parse(result[0].body).collection['nsd:nsd'];
+                response[0].descriptors = utils.dataToJsonSansPropNameNamespace(result[0].body).collection['nsd'];
                 if (result[2].body) {
                     var data = JSON.parse(result[2].body);
-                    if (data && data["nsr:ns-instance-opdata"] && data["nsr:ns-instance-opdata"]["rw-nsr:nsd-ref-count"]) {
-                        var nsdRefCountCollection = data["nsr:ns-instance-opdata"]["rw-nsr:nsd-ref-count"];
+                    if (data && data["ns-instance-opdata"]) {
                         response[0].descriptors.map(function(nsd) {
                             if (!nsd["meta"]) {
                                 nsd["meta"] = {};
@@ -275,9 +284,6 @@ Catalog.get = function(req) {
                             if (typeof nsd['meta'] == 'string') {
                                 nsd['meta'] = JSON.parse(nsd['meta']);
                             }
-                            nsd["meta"]["instance-ref-count"] = _.findWhere(nsdRefCountCollection, {
-                                "nsd-id-ref": nsd.id
-                            })["instance-ref-count"];
                             nsd["constituent-vnfd"] && nsd["constituent-vnfd"].map(function(v) {
                                 v.name = vnfdDict[v["vnfd-id-ref"]];
                             })
@@ -285,12 +291,6 @@ Catalog.get = function(req) {
                     }
                 }
             };
-            if (result[1].body) {
-                response[1].descriptors = JSON.parse(result[1].body).collection['vnfd:vnfd'];
-            };
-            // if (result[2].body) {
-            //   response[2].descriptors = JSON.parse(result[2].body).collection['pnfd:pnfd'];
-            // };
             resolve({
                 statusCode: response.statusCode || 200,
                 data: JSON.stringify(response)
@@ -315,10 +315,10 @@ Catalog.delete = function(req) {
     console.log('Deleting', catalogType, id, 'from', api_server);
     return new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog/' + catalogType + '/' + id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog/' + catalogType + '/' + encodeURIComponent(id)),
             method: 'DELETE',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
@@ -334,7 +334,7 @@ Catalog.delete = function(req) {
 Catalog.getVNFD = function(req) {
     var api_server = req.query['api_server'];
     var vnfdID = req.body.data;
-    var authorization = req.get('Authorization');
+    var authorization = req.session && req.session.authorization;
     var VNFDs = [];
     if (typeof(vnfdID) == "object" && vnfdID.constructor.name == "Array") {
         vnfdID.map(function(id) {
@@ -361,9 +361,9 @@ Catalog.getVNFD = function(req) {
 
     function requestVNFD(id) {
         return new Promise(function(resolve, reject) {
-            var url = utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd' + (id ? '/' + id : '') + '?deep';
+            var url = utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd' + (id ? '/' + encodeURIComponent(id) : '') + '?deep';
             request({
-                uri: url,
+                uri: utils.projectContextUrl(req, url),
                 method: 'GET',
                 headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
                     'Authorization': authorization
@@ -399,10 +399,10 @@ Catalog.create = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog'),
             method: 'POST',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -429,10 +429,10 @@ Catalog.update = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog' + '/' + catalogType + '/' + id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/' + catalogType + '-catalog' + '/' + catalogType + '/' + encodeURIComponent(id)),
             method: 'PUT',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -508,35 +508,40 @@ NSR.get = function(req) {
     var nsrPromises = [];
     var api_server = req.query["api_server"];
     var id = req.params.id;
-    var nsdInfo = new Promise(function(resolve, reject) {
+    var projectPrefix = req.session.projectId ? "project-" : "";
+    var vnfdInfo = new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/nsd-catalog/nsd?deep',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/vnfd-catalog/vnfd'),
             method: 'GET',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
         }, function(error, response, body) {
-            if (utils.validateResponse('NSR.get nsd-catalog', error, response, body, resolve, reject)) {
+            if (utils.validateResponse('NSR.get vnfd-catalog', error, response, body, resolve, reject)) {
                 var data;
                 var isString = typeof(response.body) == "string";
                 if (isString && response.body == '') return resolve('empty');
                 data = isString ? JSON.parse(response.body) : response.body;
-                var nsdData = data.collection["nsd:nsd"];
-                if (nsdData.constructor.name == "Object") {
-                    nsdData = [nsdData];
+                var vnfdData = data.collection[projectPrefix + "vnfd:vnfd"];
+                if (vnfdData.constructor.name == "Object") {
+                    vnfdData = [vnfdData];
                 }
-                resolve(nsdData);
+                var vnfdDict = {};
+                vnfdData.map(function(v, i) {
+                    vnfdDict[v.id] = v;
+                })
+                resolve(vnfdDict);
             };
-        })
+        })//
     })
     var config = new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-config/nsr' + (id ? '/' + id : '') + '?deep',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-config/nsr' + (id ? '/' + encodeURIComponent(id) : '') + '?deep'),
             method: 'GET',
             headers: _.extend({}, id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
@@ -557,10 +562,10 @@ NSR.get = function(req) {
     });
     var opData = new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata/nsr' + (id ? '/' + id : '') + '?deep',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata/nsr' + (id ? '/' + encodeURIComponent(id) : '') + '?deep'),
             method: 'GET',
             headers: _.extend({}, id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
@@ -591,13 +596,14 @@ NSR.get = function(req) {
     });
     return new Promise(function(resolve, reject) {
         //Need smarter error handling here
-        Promise.all([config, opData]).then(function(resolves) {
+        Promise.all([config, opData, vnfdInfo]).then(function(resolves) {
             var aggregate = {};
             // resolves[0] ==> ns-instance-config
             // resolves[1] ==> ns-instance-opdata
 
             var nsInstanceConfig = resolves[0] && resolves[0];
             var nsInstanceOpdata = resolves[1] && resolves[1];
+            var vnfdCatalog = resolves[2];
 
             if (!nsInstanceConfig && !nsInstanceOpdata) {
                 return resolve({
@@ -620,7 +626,9 @@ NSR.get = function(req) {
                                 'member-vnf-index-ref': vnfd['member-vnf-index-ref']
                             });
                             if (vnfrObj) {
+                                var vnfdId = _.findWhere(v.nsd['constituent-vnfd'], {'member-vnf-index': vnfrObj['member-vnf-index-ref']})['vnfd-id-ref'];                                
                                 vnfd['short-name'] = vnfrObj['short-name'];
+                                vnfd['vnfd'] = {id: vnfrObj.id, logo: vnfdCatalog[vnfdId]['logo']}
                             }
                         })
                     })
@@ -651,7 +659,12 @@ NSR.get = function(req) {
                             }
                         });
                     });
-                })
+                });
+
+                v['vnfrs'] && v['vnfrs'].map(function(vnfrObj){
+                    var vnfdId = _.findWhere(v.nsd['constituent-vnfd'], {'member-vnf-index': vnfrObj['member-vnf-index-ref']})['vnfd-id-ref'];
+                    vnfrObj.vnfd = vnfdCatalog[vnfdId];
+                });
             });
             var nsrsData = nsInstanceConfig;
             nsrsData.sort(function(a, b) {
@@ -739,7 +752,7 @@ NSR.addVlrDataPromise = function(req, nsrs) {
     function decorateNSRWithVLR(nsr, nsrVLRObject, vlr) {
         var vlrObject = _.extend(nsrVLRObject, vlr);
         vlrObject['vnfr-connection-point-ref'] && vlrObject['vnfr-connection-point-ref'].map(function(vnfrCP) {
-            var vnfrName = nsr['vnfrs'] && _.find(nsr['vnfrs'], {id: vnfrCP['vnfr-id']})['name'];
+            var vnfrName = nsr['vnfrs'] && nsr['vnfrs'].length && _.findWhere(nsr['vnfrs'], {id: vnfrCP['vnfr-id']})['name'];
             vnfrName && (vnfrCP['vnfr-name'] = vnfrName);
         });
         nsr['decorated-vlrs'].splice(_.sortedIndex(nsr['decorated-vlrs'], vlrObject, 'name'), 0, vlrObject);
@@ -816,7 +829,7 @@ NSR.addVnfrDataPromise = function(req, nsrs) {
 
         vnfr && vnfr['vdur'] && vnfr['vdur'].map(function(vdur) {
             // This console-url is what front-end will hit to generate a real console-url
-            vdur['console-url'] = 'api/vnfr/' + vnfr.id + '/vdur/' + vdur.id + '/console-url';
+            vdur['console-url'] = 'api/vnfr/' + encodeURIComponent(vnfr.id) + '/vdur/' + encodeURIComponent(vdur.id) + '/console-url';
             nsr['console-urls'].push({
                 id: vdur.id,
                 name: vnfr.name,
@@ -834,7 +847,7 @@ NSR.addVnfrDataPromise = function(req, nsrs) {
             "nsr-id": nsr['ns-instance-config-ref'],
             "name": vnfr['name'],
             "vdur": vnfr["vdur"],
-            "cloud-account": vnfr["cloud-account"]
+            "datacenter": vnfr["datacenter"]
         };
         var vnfrSg = nsr['vnfr-scaling-groups'];
         var vnfrName = vnfr["name"];
@@ -844,7 +857,7 @@ NSR.addVnfrDataPromise = function(req, nsrs) {
             }
         }
         var vnfrNfviMetrics = buildNfviGraphs(vnfr.vdur, vnfrName);
-        if (vnfr['vnf-configuration'] && vnfr['vnf-configuration']['service-primitive'] && vnfr['vnf-configuration']['service-primitive'].length > 0) {
+        if (vnfr['vnf-configuration'] && vnfr['vnf-configuration']['config-primitive'] && vnfr['vnf-configuration']['config-primitive'].length > 0) {
             vnfrObj['service-primitives-present'] = true;
         } else {
             vnfrObj['service-primitives-present'] = false;
@@ -866,10 +879,10 @@ NSR.create = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config'),
             method: 'POST',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -910,10 +923,10 @@ NSR.delete = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(id)),
             method: 'DELETE',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1006,10 +1019,10 @@ NSR.setStatus = function(req) {
         }
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + id + '/admin-status/',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(id) + '/admin-status/'),
             method: 'PUT',
             headers: requestHeaders,
             json: {
@@ -1059,12 +1072,12 @@ NSR.createScalingGroupInstance = function(req) {
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data,
             {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
 
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + id + '/scaling-group/' + scaling_group_id + '/instance',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(id) + '/scaling-group/' + encodeURIComponent(scaling_group_id) + '/instance'),
             method: 'POST',
             headers: requestHeaders,
             json: jsonData,
@@ -1108,12 +1121,12 @@ NSR.deleteScalingGroupInstance = function(req) {
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data,
             {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
 
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + id + '/scaling-group/' + scaling_group_id + '/instance/' + scaling_instance_id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(id) + '/scaling-group/' + encodeURIComponent(scaling_group_id) + '/instance/' + encodeURIComponent(scaling_instance_id)),
             method: 'DELETE',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1152,12 +1165,12 @@ NSR.nsd.vld.get = function(req) {
         _.extend(requestHeaders,
             vld_id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection,
             {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
 
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + nsr_id + '/nsd/vld' + (vld_id ? '/' + vld_id : '')  +'?deep',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(nsr_id) + '/nsd/vld' + (vld_id ? '/' + encodeURIComponent(vld_id) : '')  +'?deep'),
             method: 'GET',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1197,10 +1210,10 @@ NSR.nsd.vld.create = function(req) {
     return new Promise(function(resolve, reject) {
         var requestHeaders = {};
         _.extend(requestHeaders, constants.HTTP_HEADERS.accept.data, constants.HTTP_HEADERS.content_type.data, {
-            'Authorization': req.get('Authorization')
+            'Authorization': req.session && req.session.authorization
         });
         request({
-            uri: utils.confdPort(api_server) + '/api/config/ns-instance-config/nsr/' + nsr_id + '/nsd/vld' + (vld_id ? '/' + vld_id : ''),
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(nsr_id) + '/nsd/vld' + (vld_id ? '/' + encodeURIComponent(vld_id) : '')),
             method: vld_id ? 'PUT' : 'POST',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1239,12 +1252,12 @@ NSR.nsd.vld.delete = function(req) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }
         );
 
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + nsr_id + '/nsd/vld/' + vld_id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/ns-instance-config/nsr/' + encodeURIComponent(nsr_id) + '/nsd/vld/' + encodeURIComponent(vld_id)),
             method: 'DELETE',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1264,13 +1277,13 @@ VNFR.get = function(req) {
     var api_server = req.query["api_server"];
     var id = req.params.id;
     var uri = utils.confdPort(api_server);
-    uri += APIVersion + '/api/operational/vnfr-catalog/vnfr' + (id ? '/' + id : '') + '?deep';
+    uri += APIVersion + '/api/operational/vnfr-catalog/vnfr' + (id ? '/' + encodeURIComponent(id) : '') + '?deep';
     var headers = _.extend({}, id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         request({
-            url: uri,
+            url: utils.projectContextUrl(req, uri),
             method: 'GET',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -1278,14 +1291,14 @@ VNFR.get = function(req) {
         }, function(error, response, body) {
             if (utils.validateResponse('VNFR.get', error, response, body, resolve, reject)) {
                 var data = JSON.parse(response.body);
-                var returnData = id ? [data["vnfr:vnfr"]] : data.collection["vnfr:vnfr"];
+                var returnData = id ? (data["vnfr:vnfr"] ? [data["vnfr:vnfr"]] : []) : data.collection["vnfr:vnfr"];
                 returnData.forEach(function(vnfr) {
-                    vnfr['nfvi-metrics'] = buildNfviGraphs(vnfr.vdur);
-                    vnfr['epa-params'] = epa_aggregator(vnfr.vdur);
-                    vnfr['service-primitives-present'] = (vnfr['vnf-configuration'] && vnfr['vnf-configuration']['service-primitive'] && vnfr['vnf-configuration']['service-primitive'].length > 0) ? true : false;
+                    vnfr['nfvi-metrics'] = vnfr.vdur ? buildNfviGraphs(vnfr.vdur) : [];
+                    vnfr['epa-params'] = vnfr.vdur ? epa_aggregator(vnfr.vdur) : [];
+                    vnfr['service-primitives-present'] = (vnfr['vnf-configuration'] && vnfr['vnf-configuration']['config-primitive'] && vnfr['vnf-configuration']['config-primitive'].length > 0) ? true : false;
                     vnfr['vdur'] && vnfr['vdur'].map(function(vdur, vdurIndex) {
                         // This console-url is what front-end will hit to generate a real console-url
-                        vdur['console-url'] = 'api/vnfr/' + vnfr.id + '/vdur/' + vdur.id + '/console-url';
+                        vdur['console-url'] = 'api/vnfr/' + encodeURIComponent(vnfr.id) + '/vdur/' + encodeURIComponent(vdur.id) + '/console-url';
                     });
                 });
                 return resolve(returnData);
@@ -1339,9 +1352,9 @@ VNFR.getByNSR = function(req) {
     var uri = utils.confdPort(api_server);
     var reqClone = _.clone(req);
     delete reqClone.params.id;
-    uri += APIVersion + '/api/operational/ns-instance-opdata/nsr/' + id + '?deep';
+    uri += APIVersion + '/api/operational/ns-instance-opdata/nsr/' + encodeURIComponent(id) + '?deep';
     var headers = _.extend({}, id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         if (VNFR.cachedNSR[id]) {
@@ -1352,7 +1365,7 @@ VNFR.getByNSR = function(req) {
             });
         } else {
             request({
-                url: uri,
+                url: utils.projectContextUrl(req, uri),
                 method: 'GET',
                 headers: headers,
                 forever: constants.FOREVER_ON,
@@ -1386,13 +1399,13 @@ VLR.get = function(req) {
     var api_server = req.query["api_server"];
     var id = req.params.id;
     var uri = utils.confdPort(api_server);
-    uri += APIVersion + '/api/operational/vlr-catalog/vlr' + (id ? '/' + id : '') + '?deep';
+    uri += APIVersion + '/api/operational/vlr-catalog/vlr' + (id ? '/' + encodeURIComponent(id) : '') + '?deep';
     var headers = _.extend({}, id ? constants.HTTP_HEADERS.accept.data : constants.HTTP_HEADERS.accept.collection, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         request({
-            url: uri,
+            url: utils.projectContextUrl(req, uri),
             method: 'GET',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -1416,10 +1429,10 @@ RIFT.api = function(req) {
     var url = req.path;
     return new Promise(function(resolve, reject) {
         request({
-            url: uri + url + '?deep',
+            url: utils.projectContextUrl(req, uri + url + '?deep'),
             method: 'GET',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
@@ -1444,11 +1457,11 @@ ComputeTopology.get = function(req) {
     return new Promise(function(resolve, reject) {
         var nsrPromise = new Promise(function(success, failure) {
             request({
-                uri: utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata/nsr/' + nsr_id + '?deep',
+                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ns-instance-opdata/nsr/' + encodeURIComponent(nsr_id) + '?deep'),
                 method: 'GET',
                 headers: _.extend({},
                     constants.HTTP_HEADERS.accept.data, {
-                        'Authorization': req.get('Authorization')
+                        'Authorization': req.session && req.session.authorization
                     }),
                 forever: constants.FOREVER_ON,
                 rejectUnauthorized: false,
@@ -1493,10 +1506,10 @@ ComputeTopology.get = function(req) {
                     vnfrPromises.push(
                         new Promise(function(success, failure) {
                             rp({
-                                uri: utils.confdPort(api_server) + APIVersion + '/api/operational/vnfr-catalog/vnfr/' + vnfrId + '?deep',
+                                uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/vnfr-catalog/vnfr/' + encodeURIComponent(vnfrId) + '?deep'),
                                 method: 'GET',
                                 headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                                    'Authorization': req.get('Authorization')
+                                    'Authorization': req.session && req.session.authorization
                                 }),
                                 forever: constants.FOREVER_ON,
                                 rejectUnauthorized: false,
@@ -1587,11 +1600,11 @@ NetworkTopology.get = function(req) {
     var uri = utils.confdPort(api_server);
     uri += APIVersion + '/api/operational/network?deep';
     var headers = _.extend({}, constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         request({
-            url: uri,
+            url: utils.projectContextUrl(req, uri),
             method: 'GET',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -1616,13 +1629,13 @@ VDUR.get = function(req) {
     var vnfrID = req.params.vnfr_id;
     var vdurID = req.params.vdur_id;
     var uri = utils.confdPort(api_server);
-    uri += APIVersion + '/api/operational/vnfr-catalog/vnfr/' + vnfrID + '/vdur/' + vdurID + '?deep';
+    uri += APIVersion + '/api/operational/vnfr-catalog/vnfr/' + encodeURIComponent(vnfrID) + '/vdur/' + encodeURIComponent(vdurID) + '?deep';
     var headers = _.extend({}, constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         request({
-            url: uri,
+            url: utils.projectContextUrl(req, uri),
             method: 'GET',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -1643,13 +1656,13 @@ VDUR.consoleUrl.get = function(req) {
     var vnfrID = req.params.vnfr_id;
     var vdurID = req.params.vdur_id;
     var uri = utils.confdPort(api_server);
-    uri += APIVersion + '/api/operational/vnfr-console/vnfr/' + vnfrID + '/vdur/' + vdurID + '/console-url' + '?deep';
+    uri += APIVersion + '/api/operational/vnfr-console/vnfr/' + encodeURIComponent(vnfrID) + '/vdur/' + encodeURIComponent(vdurID) + '/console-url' + '?deep';
     var headers = _.extend({}, constants.HTTP_HEADERS.accept.data, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         request({
-            url: uri,
+            url: utils.projectContextUrl(req, uri),
             method: 'GET',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -1672,11 +1685,11 @@ CloudAccount.get = function(req) {
     var uri = utils.confdPort(api_server);
     uri += APIVersion + '/api/operational/cloud/account?deep';
     var headers = _.extend({}, constants.HTTP_HEADERS.accept.collection, {
-        'Authorization': req.get('Authorization')
+        'Authorization': req.session && req.session.authorization
     });
     return new Promise(function(resolve, reject) {
         request({
-            url: uri,
+            url: utils.projectContextUrl(req, uri),
             method: 'GET',
             headers: headers,
             forever: constants.FOREVER_ON,
@@ -1693,6 +1706,114 @@ CloudAccount.get = function(req) {
         });
     });
 }
+ResourceOrchestratorAccount.get = function(req) {
+var self = this;
+    var api_server = req.query["api_server"];
+    var accountID = req.params.id || req.params.name;
+
+    return new Promise(function(resolve, reject) {
+        var requestHeaders = {};
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.collection, {
+                'Authorization': req.session && req.session.authorization
+            }
+        );
+        var urlOp =  utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ro-account/account');
+        var urlConfig =  utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/ro-account-state/account');
+        if(accountID) {
+            urlOp = url + '/' + encodeURIComponent(accountID);
+            urlConfig = url + '/' + encodeURIComponent(accountID);
+        }
+        var allRequests = [];
+        var roOpData = new Promise(function(resolve, reject) {
+            request({
+                url: urlOp,
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+                },
+                function(error, response, body) {
+                    var data;
+                    if (utils.validateResponse('RoAccount.get', error, response, body, resolve, reject)) {
+                        try {
+                            data = JSON.parse(response.body).collection['rw-ro-account:account']
+                        } catch (e) {
+                            console.log('Problem with "RoAccount.get"', e);
+                            var err = {};
+                            err.statusCode = 500;
+                            err.errorMessage = {
+                                error: 'Problem with "RoAccount.get": ' + e // + e.toString()
+                            }
+                            return reject(err);
+                        }
+                        return resolve({
+                            statusCode: response.statusCode,
+                            data: data
+                        });
+                    };
+                }
+            );
+        });
+        var roConfigData = new Promise(function(resolve, reject){
+            request({
+                url: urlConfig,
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+                },
+                function(error, response, body) {
+                    var data;
+                    if (utils.validateResponse('RoAccount.get', error, response, body, resolve, reject)) {
+                        try {
+                            data = JSON.parse(response.body).collection['rw-ro-account:account']
+                        } catch (e) {
+                            console.log('Problem with "RoAccount.get"', e);
+                            var err = {};
+                            err.statusCode = 500;
+                            err.errorMessage = {
+                                error: 'Problem with "RoAccount.get": ' + e // + e.toString()
+                            }
+                            return reject(err);
+                        }
+                        return resolve({
+                            statusCode: response.statusCode,
+                            data: data
+                        });
+                    };
+                }
+            );
+        });
+
+        allRequests.push(roOpData);
+        allRequests.push(roConfigData);
+        Promise.all(allRequests).then(function(data) {
+            var state = data[1].data;
+            var op = data[0].data;
+            var result = [];
+            var dict = {"rift":{}};
+            if (!accountID) {
+                state.length && state.map(function(s){
+                    dict[s.name] = s;
+                });
+                op.length && op.map(function(o) {
+                    dict[o.name] = _.extend(dict[o.name], o);
+                });
+                Object.keys(dict).map(function(d) {
+                    result.push(dict[d]);
+                })
+            } else {
+                result = _.extend(op, state);
+            }
+            resolve({
+                statusCode: 200,
+                data: result
+            })
+        })
+
+    })
+}
 
 
 // Config-Agent Account APIs
@@ -1709,11 +1830,11 @@ ConfigAgentAccount.get = function(req) {
             var requestHeaders = {};
             _.extend(requestHeaders,
                 constants.HTTP_HEADERS.accept.collection, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 });
 
             request({
-                    url: utils.confdPort(api_server) + APIVersion + '/api/operational/config-agent/account',
+                    url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/config-agent/account'),
                     type: 'GET',
                     headers: requestHeaders,
                     forever: constants.FOREVER_ON,
@@ -1749,11 +1870,11 @@ ConfigAgentAccount.get = function(req) {
             var requestHeaders = {};
             _.extend(requestHeaders,
                 constants.HTTP_HEADERS.accept.data, {
-                    'Authorization': req.get('Authorization')
+                    'Authorization': req.session && req.session.authorization
                 });
 
             request({
-                    url: utils.confdPort(api_server) + APIVersion + '/api/operational/config-agent/account/' + id,
+                    url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/config-agent/account/' + encodeURIComponent(id)),
                     type: 'GET',
                     headers: requestHeaders,
                     forever: constants.FOREVER_ON,
@@ -1802,11 +1923,11 @@ ConfigAgentAccount.create = function(req) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
 
         request({
-            url: utils.confdPort(api_server) + APIVersion + '/api/config/config-agent',
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/config-agent'),
             method: 'POST',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1841,11 +1962,11 @@ ConfigAgentAccount.update = function(req) {
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data,
             constants.HTTP_HEADERS.content_type.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
 
         request({
-            url: utils.confdPort(api_server) + APIVersion + '/api/config/config-agent/account/' + id,
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/config-agent/account/' + encodeURIComponent(id)),
             method: 'PUT',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1883,10 +2004,10 @@ ConfigAgentAccount.delete = function(req) {
         var requestHeaders = {};
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
         request({
-            url: utils.confdPort(api_server) + APIVersion + '/api/config/config-agent/account/' + id,
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/config-agent/account/' + encodeURIComponent(id)),
             method: 'DELETE',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1909,10 +2030,10 @@ DataCenters.get = function(req) {
         var requestHeaders = {};
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
         request({
-            url: utils.confdPort(api_server) + APIVersion + '/api/operational/datacenters?deep',
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/operational/datacenters?deep'),
             method: 'GET',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1950,10 +2071,10 @@ SSHkey.get  = function(req) {
         var requestHeaders = {};
         _.extend(requestHeaders,
             constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             });
         request({
-            url: utils.confdPort(api_server) + APIVersion + '/api/config/key-pair?deep',
+            url: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/key-pair?deep'),
             method: 'GET',
             headers: requestHeaders,
             forever: constants.FOREVER_ON,
@@ -1987,10 +2108,10 @@ SSHkey.delete = function(req) {
     console.log('Deleting ssk-key', id);
     return new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/key-pair/' + id,
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/key-pair/' + encodeURIComponent(id)),
             method: 'DELETE',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             forever: constants.FOREVER_ON,
             rejectUnauthorized: false,
@@ -2008,10 +2129,10 @@ SSHkey.post = function(req) {
     var data = req.body;
     return new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/key-pair/',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/key-pair/'),
             method: 'POST',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             json: data,
             forever: constants.FOREVER_ON,
@@ -2031,10 +2152,10 @@ SSHkey.put = function(req) {
     var data = req.body;
     return new Promise(function(resolve, reject) {
         request({
-            uri: utils.confdPort(api_server) + APIVersion + '/api/config/key-pair/',
+            uri: utils.projectContextUrl(req, utils.confdPort(api_server) + APIVersion + '/api/config/key-pair/'),
             method: 'PUT',
             headers: _.extend({}, constants.HTTP_HEADERS.accept.data, {
-                'Authorization': req.get('Authorization')
+                'Authorization': req.session && req.session.authorization
             }),
             json: data,
             forever: constants.FOREVER_ON,
@@ -2063,6 +2184,7 @@ module.exports.computeTopology = ComputeTopology;
 module.exports.networkTopology = NetworkTopology;
 module.exports.config = Config;
 module.exports.cloud_account = CloudAccount;
+module.exports.ResourceOrchestratorAccount = ResourceOrchestratorAccount;
 module.exports['config-agent-account'] = ConfigAgentAccount;
 module.exports.rpc = RPC;
 module.exports.data_centers = DataCenters;
index 851239f..4b10980 100644 (file)
@@ -4,6 +4,12 @@
     "dashboard": "./launchpad.jsx",
     "order": 1,
     "priority":1,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-project:project-admin",
+        "rw-project:project-oper",
+        "rw-project-mano:lcm-oper",
+        "rw-project-mano:lcm-admin"],
     "routes": [
     {
         "label": "Dashboard",
         "component": "./instantiate/instantiateDashboard.jsx",
         "path": "",
         "type": "external",
+        "allow": [
+            "rw-rbac-platform:super-admin",
+            "rw-project:project-admin",
+            "rw-project-mano:lcm-admin"
+        ],
         "routes": [
             {
                 "label": "Instantiate",
                 "route": ":nsd",
                 "component": "./instantiate/instantiateParameters.jsx",
                 "path": ":nsd",
-                "type": "internal"
+                "type": "internal",
+                "allow": [
+                    "rw-rbac-platform:super-admin",
+                    "rw-project:project-admin",
+                    "rw-project-mano:lcm-admin"
+                ]
             }
         ]
     },{
index 22d4f79..8126f59 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",
index 06ee5da..081ab98 100644 (file)
@@ -271,6 +271,13 @@ app.get('/api/nsr', cors(), function(req, res) {
             utils.sendErrorResponse(error, res);
         })
     });
+    app.get('/api/ro-account', cors(), function(req, res) {
+        launchpadAPI['ResourceOrchestratorAccount'].get(req).then(function(data) {
+            utils.sendSuccessResponse(data, res);
+        }, function(error) {
+            utils.sendErrorResponse(error, res);
+        })
+    });
     app.get('/api/config', cors(), function(req, res) {
         launchpadAPI['config'].get(req).then(function(data) {
             utils.sendSuccessResponse(data, res);
index c59d28f..071d73f 100755 (executable)
 
 PLUGIN_NAME=launchpad
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index ac8bff8..3c982dd 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,5 +44,5 @@ 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
+tar -cf $bcache_dir/plugins/$plugin/node_modules.tar node_modules package.json -C $source_dir
 #cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
index 7bed778..ef93bb0 100644 (file)
@@ -24,10 +24,15 @@ import InstantiateSelectDescriptorPanel from './instantiateSelectDescriptorPanel
 import CatalogDescriptorRaw from './catalogDescriptorRaw.jsx'
 import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
 import {Panel, PanelWrapper} from 'widgets/panel/panel';
-import Button from 'widgets/button/rw.button.js'
+import Button from 'widgets/button/rw.button.js';
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
 import 'style/layout.scss';
 import './instantiateDashboard.scss';
 
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
 class InstantiateDashboard extends React.Component {
     constructor(props) {
         super(props);
@@ -38,19 +43,15 @@ class InstantiateDashboard extends React.Component {
         let self = this;
         let asyncOperations = []
         asyncOperations.push(this.Store.getCatalog());
-        asyncOperations.push(this.Store.getCloudAccount(function() {
-          asyncOperations.push(self.Store.getDataCenters());
-          asyncOperations.push(self.Store.getResourceOrchestrator());
+        asyncOperations.push(this.Store.getResourceOrchestratorAccounts(function() {
           asyncOperations.push(self.Store.getSshKey());
           asyncOperations.push(self.Store.getConfigAgent());
-          asyncOperations.push(self.Store.getResourceOrchestrator());
         }));
         Promise.all(asyncOperations).then(function(resolve, reject) {
             if(self.props.params.nsd) {
                 self.Store.descriptorSelected(self.state.nsdDict[self.props.params.nsd]);
             }
         })
-
     }
     componentWillMount() {
         this.Store.listen(this.updateState);
@@ -107,6 +108,7 @@ class InstantiateDashboard extends React.Component {
         let html;
         let selectedNSDid = self.state.selectedNSDid;
         let isPreviewing = self.state.isPreviewing;
+        const hasAccess = isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]);
         let descriptorPreview = (
             <Panel title={(self.state.selectedNSD['short-name'] || self.state.selectedNSD.name ) + ' Descriptor Preview'} className="CatalogDescriptorPreview">
             <span className="oi CatalogDescriptorPreview-button" data-glyph={"circle-x"} onClick={self.Store.deselectDescriptor}></span>
@@ -136,10 +138,11 @@ class InstantiateDashboard extends React.Component {
 
                     <Button label="Cancel" onClick={this.handleCancel}/>
                     {this.isSelectPage() ?
-                        <Button label="Next" isLoading={this.state.isSaving} onClick={this.state.selectedNSD && self.openDescriptor} className="dark"  type="submit"/>
+                        <Button label="Next" isLoading={this.state.isSaving} onClick={this.state.selectedNSD && self.openDescriptor} className="dark" type="submit"/>
                         : <div>
                             <Button label="Back" onClick={this.handleBack}/>
-                            <Button label="Launch" isLoading={this.state.isSaving} onClick={self.handleSave.bind(self, true)} className="dark"  type="submit"/>
+                            { hasAccess ? <Button label="Launch" isLoading={this.state.isSaving} onClick={self.handleSave.bind(self, true)} className="dark"  type="submit"
+                            /> : null}
                         </div>
                     }
                 </div>
@@ -148,6 +151,7 @@ class InstantiateDashboard extends React.Component {
     }
 }
 InstantiateDashboard.contextTypes = {
-    router: React.PropTypes.object
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
 };
 export default SkyquakeComponent(InstantiateDashboard);
index b9ce7fb..7e63377 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +15,7 @@
  *   limitations under the License.
  *
  */
+
 .application.instantiate {
     /* overflow:visible !important;*/
     height:100%;
index 9d7b7eb..624829c 100644 (file)
@@ -31,22 +31,22 @@ export default class InstantiateInputParams extends Component {
   nsConfigHTML = (props) => {
     return (
       <div className="configure-nsd_section">
+        <div className="noticeSubText noticeSubText_right">
+            * required
+        </div>
         <div className="inputControls">
-            <TextInput label="Instance Name" type="text" pattern="^[a-zA-Z0-9_]*$" style={{textAlign:'left'}} onChange={props.updateName} value={props.name}/>
-          {
-            !isOpenMano(props.ro) ?
-              (
-                <label>Select VIM Account
-                  <SelectOption options={constructCloudAccountOptions(props.cloudAccounts)} onChange={props.nsFn.updateSelectedCloudAccount} />
-                </label>
-              )
-            : null
-          }
+            <TextInput label="Instance Name" required={true} type="text" pattern="^[a-zA-Z0-9_]*$" style={{textAlign:'left'}} onChange={props.updateName} value={props.name}/>
+            <label>Resource Orchestrator
+              <SelectOption options={constructROOptions(props.resourceOrchestrators)} onChange={props.nsFn.updateSelectedRoAccount} />
+            </label>
           {
-            isOpenMano(props.ro) ?
-              dataCentersHTML(props.dataCenters[props.ro.name],
-                              props.nsFn.updateSelectedDataCenter)
-              : null
+            (props.selectedResourceOrchestrator.datacenters && props.selectedResourceOrchestrator.datacenters.datacenters) ?
+                        (
+                          <label>Datacenter
+                            <SelectOption options={constructDataCenterOptions(props.selectedResourceOrchestrator.datacenters.datacenters)} onChange={props.nsFn.updateSelectedDataCenter} />
+                          </label>
+                        )
+                      : <span>No datacenters configured</span>
           }
         </div>
       </div>
@@ -68,26 +68,19 @@ export default class InstantiateInputParams extends Component {
                 }
                 return (
                     <div className="inputControls" key={i}>
-                    <h4 className="inputControls-title">VNFD: {v.name}</h4>
+                    <h4 className="inputControls-title">VNFD: {v['vnf-name']}</h4>
                   {
-                    !isOpenMano(props.ro) ?
-                      (
-                        <label>Select VIM Account
-                          <SelectOption options={constructCloudAccountOptions(props.cloudAccounts)} initial={true} onChange={props.vnfFn.updateSelectedCloudAccount.bind(null, v['member-vnf-index'])} defaultValue={defaultValue} />
-                        </label>
-                      )
-                    : null
+                   (props.selectedResourceOrchestrator.datacenters && props.selectedResourceOrchestrator.datacenters.datacenters) ?
+                        (
+                          <label>Datacenter
+                            <SelectOption options={constructDataCenterOptions(props.selectedResourceOrchestrator.datacenters.datacenters)} onChange={props.vnfFn.updateSelectedDataCenter.bind(null, v['member-vnf-index'])}  initial={true} />
+                          </label>
+                        )
+                      : <span>No datacenters configured</span>
                   }
-                      {
-                        isOpenMano(props.ro) ?
-                          dataCentersHTML(
-                                          props.dataCenters[props.ro.name],
-                                          props.vnfFn.updateSelectedDataCenter.bind(null, v['member-vnf-index']), true)
-                          : null
-                      }
                       {
                         (props.configAgentAccounts && props.configAgentAccounts.length > 0) ?
-                        <label>Select Config Agent Account
+                        <label>Config Agent Account
                           <SelectOption options={props.configAgentAccounts && props.configAgentAccounts.map(function(c) {
                             return {
                               label: c.name,
@@ -103,6 +96,8 @@ export default class InstantiateInputParams extends Component {
       </div>
     )
   }
+  //"vnfd-catalog/vnfd[id=../../../constituent-vnfd[0]/vnfd-id-ref]/version";
+
   inputParametersHTML = (props) => {
     let inputParameters = props.inputParameters;
     const handleChange = (i, event) => props.updateInputParam(i, event.target.value);
@@ -124,6 +119,43 @@ export default class InstantiateInputParams extends Component {
     );
     return nsinput;
   }
+  //"vnfd-catalog/vnfd[id=../../../constituent-vnfd[0]/vnfd-id-ref]/version";
+
+  vnfInputParametersHTML = (props) => {
+    let vnfInputParams = props.vnfInputParams;
+    const handleChange = (vnfIndex, parameterIndex, event) => props.updateVnfInputParam(vnfIndex, parameterIndex, event.target.value);
+    let vnfInputParamsHTML = [];
+    vnfInputParams && vnfInputParams.map(function(input, i) {
+        vnfInputParamsHTML.push(
+          <div className="configure-nsd_section" key={i}>
+            <h3 className="launchpadCard_title">{`VNF Input Parameters: ${input['name']}:${input['member-vnf-index-ref']}`}</h3>
+            {
+              input['input-parameter'].filter(function(p){
+                let regex = /vnfd\[(?:vnfd:)?id\s*=\s*'?"?(\S+?)'?"?\s*\]/
+                // if has id, then it belongs in a vnfd section
+                if (p.xpath.match(regex)) {
+                  // look for matching vnfd
+                  if ((p.xpath.match(regex)[1] == input['vnfd-id-ref'])) {
+                    return true
+                  } else {
+                    return false;
+                  }
+                } else {
+                  return true;
+                }
+              }).map(function(p, j){
+                let param = vnfInputParams[i]['input-parameter'][j];
+                return (<div className="inputControls" key={j}>
+                        <TextInput label={(p.label || p.xpath)} value={param.value || param['default-value']} type="text" onChange={handleChange.bind(this, i, j)} />
+                    </div>)
+              })
+            }
+         </div>
+        );
+    })
+
+    return <div>{vnfInputParamsHTML}</div>;
+  }
   nsPlacementGroupsHTML = (props) => {
     let nsPlacementGroups = props.nsPlacementGroups;
     let displayPlacementGroups = props.displayPlacementGroups;
@@ -175,8 +207,8 @@ export default class InstantiateInputParams extends Component {
                             return (
 
                                   <div className="input_group" key={j}>
-                                    <TextInput type="text" onChange={props.nsFn.placementGroupUpdate.bind(self, i, j, 'key')} placeholder="KEY" value={key} />
-                                    <TextInput type="text" onChange={props.nsFn.placementGroupUpdate.bind(self, i, j, 'value')} placeholder="VALUE"  value={value} />
+                                    <TextInput type="text" onChange={props.nsFn.hostAggregateUpdate.bind(self, i, j, 'key')} placeholder="KEY" value={key} />
+                                    <TextInput type="text" onChange={props.nsFn.hostAggregateUpdate.bind(self, i, j, 'value')} placeholder="VALUE"  value={value} />
                                     <span onClick={props.nsFn.removeHostAggregate.bind(self, i, j)} className="removeInput"><img src={imgRemove} />Remove</span>
                                   </div>
                             )
@@ -288,8 +320,15 @@ export default class InstantiateInputParams extends Component {
                         </div>
                           {
                             isUnknown ? null : isVIM ?
-                            <TextInput label="Network Name" onChange={self.props.vldFn.updateValue(i, currentType)} value={v[currentType]} /> :
-                            <div>
+                              <div>
+                                <TextInput label="Network Name" onChange={self.props.vldFn.updateValue(i, currentType)} value={v[currentType]} />
+                                {
+                                  v['mgmt-network'].toUpperCase() == "TRUE" ?
+                                    <TextInput label="IPV4 NAT POOL NAME" placeholder={self.props.dataCenterID} onChange={self.props.vldFn.updateValue(i, 'ipv4-nat-pool-name')} value={v['ipv4-nat-pool-name']} />
+                                    : null
+                                }
+                              </div>
+                            : <div>
                               <SelectOption
                               label="IP PROFILE NAME"
                                 options={ipProfileList && ipProfileList.map(function(ip) {
@@ -492,6 +531,7 @@ export default class InstantiateInputParams extends Component {
         <div className="input_group input_group-users" key={i}>
           <div className="inputControls">
           <div style={{fontWeight: 'bold', display: 'flex'}}>USER <span onClick={usersFn.remove(i)} className="removeInput"><img src={imgRemove} />Remove</span></div>
+
             <TextInput onChange={usersFn.update(i, 'name')} label="USERNAME" value={i.name} />
             <TextInput onChange={usersFn.update(i, 'user-info')} label="REAL NAME" value={i.gecos} />
             {
@@ -578,12 +618,20 @@ export default class InstantiateInputParams extends Component {
             this.inputParametersHTML(props)
           }
           {
+            //VNF INPUT PARAMETERS
+            this.vnfInputParametersHTML(props)
+          }
+          {
+            true ?
+            // self.props.selectedResourceOrchestrator['ro-account-type'] == 'rift-ro' ?
             //NS PLACEMENTGROUPS
-            this.nsPlacementGroupsHTML(props)
+            this.nsPlacementGroupsHTML(props) : null
           }
           {
+            true ?
+            // self.props.selectedResourceOrchestrator['ro-account-type'] == 'rift-ro' ?
             //VNF PLACEMENTGROUPS
-            this.vnfPlacementGroupsHTML(props)
+            this.vnfPlacementGroupsHTML(props) : null
           }
           {
             //VLD CONFIGURATION
@@ -626,6 +674,15 @@ function hideInput(e){
 }
 function addDNS(){}
 function removeDNS(){}
+function constructROOptions(resourceOrchestrators){
+  let ROOptions = resourceOrchestrators && resourceOrchestrators.map(function(ro, index) {
+    return {
+      label: ro.name,
+      value: ro
+    }
+  });
+  return ROOptions;
+}
 function constructCloudAccountOptions(cloudAccounts){
   let CloudAccountOptions = cloudAccounts && cloudAccounts.map(function(ca, index) {
     return {
@@ -635,6 +692,15 @@ function constructCloudAccountOptions(cloudAccounts){
   });
   return CloudAccountOptions;
 }
+function constructDataCenterOptions(dataCenters){
+  let DataCenterOptions = dataCenters && dataCenters.map(function(dc, index) {
+    return {
+      label: dc.name,
+      value: dc.name
+    }
+  });
+  return DataCenterOptions;
+}
 function dataCentersHTML(dataCenters, onChange, initial) {
   //Build DataCenter options
   //Relook at this, why is it an object?
@@ -647,23 +713,12 @@ function dataCentersHTML(dataCenters, onChange, initial) {
   });
   if (dataCenters && dataCenters.length > 0) {
     return (
-      <label>Select Data Center
+      <label>Data Center
         <SelectOption initial={!!initial} options={DataCenterOptions} onChange={onChange} />
       </label>
     )
   }
 }
-function isOpenMano(account) {
-  if (account) {
-    let a = account;
-    if (a.constructor.name == 'String') {
-      a = JSON.parse(a);
-    }
-    return a['account-type'] == 'openmano';
-  } else {
-    return false;
-  }
-}
 function updateNewSshKeyRefSelection(e) {
   this.setState({
     newRefSelection: e.target.value
index 5681851..454a5c8 100644 (file)
@@ -55,7 +55,7 @@ class Instantiate extends Component {
                     sshFn={this.props.sshFn()}
                     updateName={this.props.nameUpdated}
                     updateInputParam={this.props.updateInputParam}
-
+                    updateVnfInputParam={this.props.updateVnfInputParam}
                     nsd={selectedNSD}
                     selectedNSDid={this.props.selectedNSDid}
                     name={this.props.name}
@@ -67,6 +67,7 @@ class Instantiate extends Component {
                     dataCenters={this.props.dataCenters}
                     configAgentAccounts={this.props.configAgentAccounts}
                     inputParameters={this.props['input-parameters']}
+                    vnfInputParams={this.props['vnf-input-parameter']}
 
                     displayPlacementGroups={this.props.displayPlacementGroups}
 
@@ -89,6 +90,13 @@ class Instantiate extends Component {
 
                     isOpenMano={this.props.isOpenMano}
 
+                    displayVIMAccounts={this.props.displayVIMAccounts}
+                    resourceOrchestrators={this.props.resourceOrchestrators}
+
+                    selectedResourceOrchestrator={this.props.selectedResourceOrchestrator}
+                    selectedDataCenterID={this.props.dataCenterID}
+
+                    vnfDataCenters={this.props.vnfDataCenters}
                 />
             </Panel>
         </PanelWrapper>
index a8d4b67..760fc52 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -29,21 +29,23 @@ export default class InstantiateSelectDescriptorPanel extends Component {
         return (
             <Panel title="Select Descriptor"  className={"InstantiateSelectDescriptorPanel" + (isPreviewing ? " InstantiateSelectDescriptorPanel--previewmode" : '')}>
             {
-                catalog.descriptors && catalog.descriptors.map(function(descriptor, i) {
-                    let isSelected = (descriptor.id === selectedDescriptorID);
-                    return (
-                        <CatalogCard
-                            key={i}
-                            isActive={isPreviewing && isSelected}
-                            isSelected={isSelected}
-                            descriptor={descriptor}
-                            onClick={onSelectDescriptor.bind(null, descriptor)}
-                            onDoubleClick={openDescriptor.bind(null, descriptor)}
-                            onPreviewDescriptor={onPreviewDescriptor}
-                            onCloseCard={closeCard}
-                        />
-                    )
-                })
+                catalog.descriptors && (catalog.descriptors.length > 0) ?
+                    catalog.descriptors.map(function(descriptor, i) {
+                        let isSelected = (descriptor.id === selectedDescriptorID);
+                        return (
+                            <CatalogCard
+                                key={i}
+                                isActive={isPreviewing && isSelected}
+                                isSelected={isSelected}
+                                descriptor={descriptor}
+                                onClick={onSelectDescriptor.bind(null, descriptor)}
+                                onDoubleClick={openDescriptor.bind(null, descriptor)}
+                                onPreviewDescriptor={onPreviewDescriptor}
+                                onCloseCard={closeCard}
+                            />
+                        )
+                    })
+                    : <div className="InstantiateSelectDescriptorPanel-message"><h2>No Descriptors Onboarded</h2></div>
             }
             </Panel>
         )
index b49132e..8a89dfb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +15,8 @@
  *   limitations under the License.
  *
  */
+@import "style/_colors.scss";
+
 .InstantiateSelectDescriptorPanel {
     display: -ms-flexbox;
     display: flex;
             box-sizing: border-box;
             overflow:auto;
     }
+    &-message {
+        display:-ms-flexbox;
+        display:flex;
+        -ms-flex-pack: center;
+            justify-content: center;
+        -ms-flex-align: center;
+            align-items: center;
+        -ms-flex: 1;
+            flex: 1;
+
+        h2 {
+            text-transform: uppercase;
+            color: $neutral-dark-2;
+        }
+    }
 }
index 59fb003..3f13acb 100644 (file)
@@ -22,7 +22,7 @@ import AppHeaderActions from 'widgets/header/headerActions.js';
 import Alt from '../alt';
 import _cloneDeep from 'lodash/cloneDeep';
 import _find from 'lodash/find';
-
+import _merge from 'lodash/merge';
 
 class LaunchNetworkServiceStore {
     constructor() {
@@ -39,29 +39,34 @@ class LaunchNetworkServiceStore {
         this.sla_parameters = [];
         this.selectedNSDid;
         this.selectedNSD = {};
-        this.selectedCloudAccount = {};
         this.dataCenters = [];
-        this.cloudAccounts = [];
         this.isLoading = false;
         this.hasConfigureNSD = false;
+        this.hasConfigureVNFD = false;
         this['input-parameters'] = [];
-        this.displayPlacementGroups = false;
-        this.ro = {};
+        this.displayPlacementGroups = true;
         this.bindActions(NetworkServiceActions);
         this.nsdConfiguration = {
             name:'',
-            selectedCloudAccount: {},
             dataCenterID: null
         };
         /*Collection of vnf state containting cloud account and datacenter info.
         keyed off vnfd-id-ref
         */
-        this.vnfdCloudAccounts = {};
         this.usersList = [];
         this.configAgentAccounts = [];
 
         this.isPreviewing = false;
         this.isOpenMano = false;
+
+        this.displayVIMAccounts = false;
+        this.resourceOrchestrators = [{
+            name: 'rift',
+            'ro-account-type': 'rift-ro'
+        }];
+        this.selectedResourceOrchestrator = this.resourceOrchestrators[0];
+        this.dataCenterID = null;
+        this.vnfDataCenters = {};
         this.registerAsync(NetworkServiceSource);
         this.exportPublicMethods({
             getMockData: getMockData.bind(this),
@@ -91,8 +96,7 @@ class LaunchNetworkServiceStore {
             name: '',
             'input-parameter-xpath': null,
             'ns-placement-groups': null,
-            'vnf-placement-groups':null,
-            vnfdCloudAccounts: {}
+            'vnf-placement-groups':null
         })
     }
 
@@ -130,28 +134,6 @@ class LaunchNetworkServiceStore {
 
         });
     }
-    getLaunchCloudAccountSuccess(cloudAccounts) {
-        let newState = {};
-        newState.cloudAccounts = cloudAccounts.filter(function(v) {
-            console.log(v)
-                return v['connection-status'].status == 'success';
-            }) || [];
-        if(cloudAccounts.length != newState.cloudAccounts.length) {
-            Alt.actions.global.showNotification.defer({type: 'warning', msg: 'One or more VIM accounts have failed to connect'});
-        }
-        if(cloudAccounts && cloudAccounts.length > 0) {
-            newState.selectedCloudAccount = newState.cloudAccounts[0];
-            if (cloudAccounts[0]['account-type'] == 'openstack') {
-                newState.displayPlacementGroups = true;
-            } else {
-             newState.displayPlacementGroups = false;
-            }
-        } else {
-            newState.selectedCloudAccount = {};
-        }
-
-        this.setState(newState);
-    }
     getConfigAgentSuccess(configAgentAccounts) {
         this.setState({
             configAgentAccounts: configAgentAccounts
@@ -163,7 +145,7 @@ class LaunchNetworkServiceStore {
         let newState = {
             dataCenters: dataCenters || []
         };
-       if (this.ro && this.ro['account-type'] == 'openmano') {
+    if (this.ro && this.ro['account-type'] == 'openmano') {
             newState.dataCenterID = dataCenters[this.ro.name][0].uuid
         }
         this.setState(newState)
@@ -190,14 +172,7 @@ class LaunchNetworkServiceStore {
         return window.location.hash = 'launchpad/' + tokenizedHash[2];
     }
     launchNSRError(data) {
-        var msg = 'Something went wrong while trying to instantiate. Check the error logs for more information';
-        if(data) {
-            msg = data;
-        }
-        if (data.error) {
-            msg = data.error;
-        }
-        Alt.actions.global.showNotification.defer(msg);
+        Alt.actions.global.showNotification.defer(data);
         Alt.actions.global.hideScreenLoader.defer();
         this.setState({
             isLoading: false
@@ -209,10 +184,16 @@ class LaunchNetworkServiceStore {
             sshKeysRef: []
         })
     }
-    getResourceOrchestratorSuccess = (data) => {
+    getResourceOrchestratorAccountsSuccess = (data) => {
+        let self = this;
         Alt.actions.global.hideScreenLoader.defer();
+        let ROAccounts = [];
         this.setState({
-            ro: data
+            resourceOrchestrators: ROAccounts.concat(data),
+            selectedResourceOrchestrator: data[0],
+            dataCenterID: data && data[0] && data[0].datacenters && data[0].datacenters.datacenters && data[0].datacenters.datacenters[0] && data[0].datacenters.datacenters[0].name,
+            dataCenterType:  data && data[0] && data[0].datacenters && data[0].datacenters.datacenters && data[0].datacenters.datacenters[0] && data[0].datacenters.datacenters[0]['datacenter-type'],
+            displayVIMAccounts: false
         })
     }
     getResourceOrchestratorError = (data) => {
@@ -256,15 +237,30 @@ class LaunchNetworkServiceStore {
         };
         newState.selectedNSD = data;
         newState['input-parameters'] = [];
+        newState['vnf-input-parameter'] = [];
         if (NSD['input-parameter-xpath']) {
+            let vnfParameters = [];
             newState.hasConfigureNSD = true;
             NSD['input-parameter-xpath'].map(function(p) {
-                newState.hasConfigureNSD = true;
-                newState['input-parameters'].push(_cloneDeep(p));
+                if (isVNFDInputParameter(p)) {
+                    newState.hasConfigureVNFD = true;
+                    vnfParameters.push(p);
+                } else {
+                    newState.hasConfigureNSD = true;
+                    newState['input-parameters'].push(p);
+                }
+            })
+            newState['vnf-input-parameter'] = NSD['constituent-vnfd'].map(function(vnf) {
+                return {
+                    'name': vnf['vnf-name'],
+                    'member-vnf-index-ref': vnf['member-vnf-index'],
+                    'vnfd-id-ref': vnf['vnfd-id-ref'],
+                    'input-parameter':_cloneDeep(vnfParameters)
+                }
             })
         } else {
             newState.hasConfigureNSD = false;
-            newState['input-parameters'] = null;
+            newState['input-parameter'] = null;
         }
         if(NSD['ns-placement-groups'] && NSD['ns-placement-groups'].length > 0 ) {
             newState['ns-placement-groups'] = NSD['ns-placement-groups'];
@@ -272,11 +268,15 @@ class LaunchNetworkServiceStore {
         if(NSD['vnf-placement-groups'] && NSD['vnf-placement-groups'].length > 0 ) {
             newState['vnf-placement-groups'] = NSD['vnf-placement-groups'];
         }
-        NSD["constituent-vnfd"].map((v) => {
+        NSD["constituent-vnfd"] && NSD["constituent-vnfd"].map((v) => {
             VNFIDs.push(v["vnfd-id-ref"]);
         })
         this.getInstance().getVDU(VNFIDs);
         this.setState(newState);
+
+        function isVNFDInputParameter(p){
+            return p.xpath.match('vnfd-catalog');
+        }
     }
     previewDescriptor = (data) => {
         let self = this;
@@ -294,34 +294,50 @@ class LaunchNetworkServiceStore {
             'input-parameters': ip
         });
     }
+    updateVnfInputParam = (i, j, value) => {
+        let ip = this['vnf-input-parameter'];
+        ip[i]['input-parameter'][j].value = value;
+        this.setState({
+            'vnf-input-parameter': ip
+        });
+    }
     nsFn = () => {
         let self = this;
         return {
-            updateSelectedCloudAccount: (cloudAccount) => {
+            updateSelectedRoAccount: (resourceOrchestrator) => {
                 let nsd = self.nsd[0];
                 var newState = {
-                    selectedCloudAccount: JSON.parse(cloudAccount.target.value)
+                    selectedResourceOrchestrator: JSON.parse(JSON.parse(resourceOrchestrator.target.value))
                 };
-                if (cloudAccount['account-type'] == 'openstack') {
-                    newState.displayPlacementGroups = true;
+                // if no datacenters and ro is not rift-ro
+                if (!(newState.selectedResourceOrchestrator.datacenters && newState.selectedResourceOrchestrator.datacenters.datacenters) ) {
+                    Alt.actions.global.showNotification.defer("No data centers configured in resource orchestrator");
                 } else {
-                     newState.displayPlacementGroups = false;
+                    newState.dataCenterID = newState.selectedResourceOrchestrator.datacenters.datacenters[0].name;
+                    newState.dataCenterType = newState.selectedResourceOrchestrator.datacenters.datacenters[0]['datacenter-type'];
+                    newState.displayPlacementGroups = newState.selectedResourceOrchestrator.name == "rift" ? true : false;
                 }
                 self.setState(newState);
             },
             updateSelectedDataCenter: (dataCenter) => {
+                let dataCenterID = JSON.parse(JSON.parse(dataCenter.target.value));
+                let dataCenterType = _find(self.selectedResourceOrchestrator.datacenters.datacenters, {name: dataCenterID})
+
                 self.setState({
-                    dataCenterID: JSON.parse(dataCenter.target.value)
+                    dataCenterID,
+                    dataCenterType
                 });
             },
-            placementGroupUpdate: (i, k, value) => {
+            placementGroupUpdate: (i, k, event) => {
+                let value = event.target.value;
                 let pg = self['ns-placement-groups'];
                 pg[i][k] = value;
                 self.setState({
                     'ns-placement-groups': pg
                 })
             },
-            hostAggregateUpdate: (pgi, hai, k, value) => {
+            hostAggregateUpdate: (pgi, hai, k, event) => {
+                let value = event.target.value;
                 let pg = self['ns-placement-groups'];
                 let ha = pg[pgi]['host-aggregate'][hai];
                 ha[k] = value;
@@ -353,14 +369,16 @@ class LaunchNetworkServiceStore {
     vnfFn = () => {
         let self = this;
         return {
-            placementGroupUpdate: (i, k, value) => {
+            placementGroupUpdate: (i, k, event) => {
+                let value = event.target.value;
                 let pg = self['vnf-placement-groups'];
                 pg[i][k] = value;
                 self.setState({
                     'vnf-placement-groups': pg
                 })
             },
-            hostAggregateUpdate: (pgi, hai, k, value) => {
+            hostAggregateUpdate: (pgi, hai, k, event) => {
+                let value = event.target.value;
                 let pg = self['vnf-placement-groups'];
                 let ha = pg[pgi]['host-aggregate'][hai];
                 ha[k] = value;
@@ -384,64 +402,42 @@ class LaunchNetworkServiceStore {
                     'vnf-placement-groups': pg
                 })
             },
-            updateSelectedCloudAccount: (id, cloudAccount) => {
-                let vnfCA = self.vnfdCloudAccounts;
-                if(cloudAccount) {
-                    if(!vnfCA.hasOwnProperty(id)) {
-                        vnfCA[id] = {};
-                    }
-                    vnfCA[id].account = JSON.parse(cloudAccount.target.value);
-
-                    if (cloudAccount['account-type'] == 'openmano' && this.dataCenters && self.dataCenters[cloudAccount['name']]) {
-                        let datacenter = self.dataCenters[cloudAccount['name']][0];
-                        vnfCA[id].datacenter = datacenter.uuid;
-                    } else {
-                        if (vnfCA[id].datacenter) {
-                            delete vnfCA[id].datacenter;
-                        }
-                    }
-                } else {
-                    if(vnfCA.hasOwnProperty(id)) {
-                        if(vnfCA[id].hasOwnProperty('config-agent-account')) {
-                            delete vnfCA[id].account;
-                        } else {
-                            delete vnfCA[id];
-                        }
-                    }
-                }
-                self.setState({
-                    vnfdCloudAccounts: vnfCA
-                });
-            },
             updateSelectedConfigAgent:  (id) => {
                 return function(e) {
                     let configAgentRef = JSON.parse(e.target.value);
-                    let vnfCA = self.vnfdCloudAccounts;
+                    let vnfDC = self.vnfDataCenters;
                     if(configAgentRef) {
-                        if(!vnfCA.hasOwnProperty(id)) {
-                            vnfCA[id] = {};
+                        if (!vnfDC[id]) {
+                            vnfDC[id] = {};
                         }
-                        vnfCA[id]['config-agent-account'] = configAgentRef;
+                        vnfDC[id]['config-agent-account'] = configAgentRef;
                     } else {
-                        if(vnfCA[id].hasOwnProperty('account')) {
+                        if(vnfDC[id].hasOwnProperty('datacenter')) {
                             delete vnfCA[id]['config-agent-account'];
                         } else {
                             delete vnfCA[id];
                         }
                     }
                     self.setState({
-                        vnfdCloudAccounts: vnfCA
+                        vnfDataCenters: vnfDC
                     });
                 }
             },
             updateSelectedDataCenter: (id, dataCenter) => {
-                let vnfCA = self.vnfdCloudAccounts;
-                if (!vnfCA[id]) {
-                    vnfCA[id] = {};
+                let vnfDC = self.vnfDataCenters;
+                let dc = JSON.parse(JSON.parse(dataCenter.target.value));
+                if (!vnfDC[id]) {
+                    vnfDC[id] = {};
+                }
+                vnfDC[id]['member-vnf-index-ref'];
+                if (dc) {
+                    vnfDC[id].datacenter = dc;
+                } else {
+                    delete vnfDC[id];
                 }
-                vnfCA[id].datacenter = JSON.parse(dataCenter.target.value);
+
                 self.setState({
-                    vnfdCloudAccounts: vnfCA
+                    vnfDataCenters: vnfDC
                 });
             }
         }
@@ -462,12 +458,15 @@ class LaunchNetworkServiceStore {
                         let IPProfile = self.ipProfiles;
                         vld[i][type] = IPProfile[0] && IPProfile[0].name;
                         delete vld[i]['vim-network-name'];
+                        delete vld[i]['ipv4-nat-pool-name'];
                     } else {
                         delete vld[i]['dns-server'];
+                        vld[i]['ipv4-nat-pool-name'] = self.dataCenterID
                     }
                     if(type == 'none') {
                         delete vld[i]['ip-profile-ref'];
                         delete vld[i]['vim-network-name'];
+                        delete vld[i]['ipv4-nat-pool-name'];
                     }
                     self.setState({vld:vld});
                 }
@@ -668,6 +667,7 @@ class LaunchNetworkServiceStore {
         }
     }
     saveNetworkServiceRecord(name, launch) {
+        let self = this;
         //input-parameter: [{uuid: < some_unique_name>, xpath: <same as you got from nsd>, value: <user_entered_value>}]
         /*
         'input-parameter-xpath':[{
@@ -699,9 +699,9 @@ class LaunchNetworkServiceStore {
             nsdPayload.vld && nsdPayload.vld.map(function(v){
                 delete v['none'];
                 delete v.type;
-            })
+            });
+            nsdPayload['input-parameter-xpath'] && nsdPayload['input-parameter-xpath'].map((x) => delete x.value);
         }
-        let vnfdCloudAccounts = this.state.vnfdCloudAccounts;
         let payload = {
             id: guuid,
             "name": name,
@@ -711,15 +711,20 @@ class LaunchNetworkServiceStore {
             "nsd": nsdPayload
         }
 
-        if (this.state.ro && this.state.ro['account-type'] == 'openmano') {
-            payload['om-datacenter'] = this.state.dataCenterID;
-        } else {
-            if(!this.state.selectedCloudAccount) {
-                Alt.actions.global.showNotification.defer("No VIM Account Selected");
-                return;
-            }
-            payload["cloud-account"] = this.state.selectedCloudAccount.name;
+        if(!this.state.selectedResourceOrchestrator) {
+            Alt.actions.global.showNotification.defer("No Resource Orchestrator selected");
+            return;
+        }
+        if (this.state.selectedResourceOrchestrator.name != "rift") {
+            payload["resource-orchestrator"] = this.state.selectedResourceOrchestrator.name;
+        }
+
+        if(!this.state.dataCenterID) {
+            Alt.actions.global.showNotification.defer("No Data Center selected");
+            return;
         }
+        payload["datacenter"] = this.state.dataCenterID;
+
         //Clean Input Parameters
         if (this.state.hasConfigureNSD) {
             let ips = _cloneDeep(this.state['input-parameters']);
@@ -738,6 +743,38 @@ class LaunchNetworkServiceStore {
                 payload['input-parameter'] = ipsToSend;
             }
         }
+        //Clean VNF Input Parameters
+        if (this.state.hasConfigureVNFD) {
+            let vnf = _cloneDeep(this.state['vnf-input-parameter']);
+            vnf = vnf.filter(function(v){
+                delete v.name;
+                v['input-parameter'] = v['input-parameter'].filter(
+                    function(i) {
+                        if(i.value && i.value != "") {
+                            delete i.label;
+                            delete i['default-value'];
+                            return true;
+                        }
+                        return false;
+                });
+                if (v['input-parameter'].length) {
+                    return true;
+                }
+                    return false;
+            })
+            if (vnf.length > 0) {
+                payload['vnf-input-parameter'] = vnf;
+            }
+        }
+        let VnfDataCenters = this.state.vnfDataCenters;
+        if (Object.keys(VnfDataCenters).length) {
+            payload['vnf-datacenter-map'] = Object.keys(VnfDataCenters).map(function(k) {
+                return {
+                    'member-vnf-index-ref' : k,
+                    datacenter: VnfDataCenters[k].datacenter
+                }
+            })
+        }
         // These placement groups need to be refactored. Too much boilerplate.
         if (this.state.displayPlacementGroups) {
             nsPg = this.state['ns-placement-groups'];
@@ -746,7 +783,7 @@ class LaunchNetworkServiceStore {
                 payload['nsd-placement-group-maps'] = nsPg.map(function(n, i) {
                     if(n['availability-zone'] || n['server-group'] || (n['host-aggregate'].length > 0)) {
                         var obj = {
-                            'cloud-type': 'openstack'
+                            'cloud-type': self.state.dataCenterType
                         };
                         if(n['host-aggregate'].length > 0) {
                             obj['host-aggregate'] = n['host-aggregate'].map(function(h, j) {
@@ -774,10 +811,15 @@ class LaunchNetworkServiceStore {
                 });
             };
             if(vnfPg && (vnfPg.length > 0)) {
+                let vnfDataCenterDictionary = {};
+                payload['vnf-datacenter-map'] && payload['vnf-datacenter-map'].map(function(d) {
+                    vnfDataCenterDictionary[d['member-vnf-index-ref']] = d.datacenter
+                })
                 payload['vnfd-placement-group-maps'] = vnfPg.map(function(n, i) {
                     if(n['availability-zone'] || n['server-group'] || (n['host-aggregate'].length > 0)) {
+                        let DC = vnfDataCenterDictionary[n['member-vnf-index']];
                         var obj = {
-                            'cloud-type': 'openstack'
+                            'cloud-type': DC ? _find(self.state.selectedResourceOrchestrator.datacenters.datacenters, {name: DC})['datacenter-type'] : self.state.dataCenterType
                         };
                         if(n['host-aggregate'].length > 0) {
                             obj['host-aggregate'] = n['host-aggregate'].map(function(h, j) {
@@ -806,31 +848,14 @@ class LaunchNetworkServiceStore {
                 });
             }
         }
-        //Construct VNF cloud accounts
-        payload['vnf-cloud-account-map'] = [];
-        for(let k in vnfdCloudAccounts) {
-            let vnf = {};
-            vnf['member-vnf-index-ref'] = k;
-            if(vnfdCloudAccounts[k].hasOwnProperty('account') && (vnfdCloudAccounts[k]['account'] && vnfdCloudAccounts[k]['account'].name)) {
-                vnf['cloud-account'] = vnfdCloudAccounts[k].account.name;
-            }
-            if(vnfdCloudAccounts[k].hasOwnProperty('config-agent-account') && vnfdCloudAccounts[k]['config-agent-account']) {
-                vnf['config-agent-account'] = vnfdCloudAccounts[k]['config-agent-account'];
-            }
-            if(vnfdCloudAccounts[k].hasOwnProperty('datacenter')) {
-                vnf['om-datacenter'] = vnfdCloudAccounts[k].datacenter;
-            }
-            if(vnf['om-datacenter'] || vnf['cloud-account'] || vnf['config-agent-account']) {
-                payload['vnf-cloud-account-map'].push(vnf);
-            }
-        }
+
         //Add SSH-Keys
         payload['ssh-authorized-key'] = this.state.sshKeysRef.map(function(k) {
             return {'key-pair-ref': JSON.parse(k).name};
         });
         //Add Users
         payload['user'] = addKeyPairRefToUsers(this.state.usersList);
-        // console.log(payload)
+        console.log(payload)
         this.launchNSR({
             'nsr': [payload]
         });
index a769246..f04b98c 100644 (file)
@@ -25,8 +25,6 @@ export default Alt.generateActions(
                                    'getCatalogError',
                                    'getVDUSuccess',
                                    'getVDUError',
-                                   'getLaunchCloudAccountSuccess',
-                                   'getLaunchCloudAccountError',
                                    'getDataCentersSuccess',
                                    'getDataCentersError',
                                    'launchNSRLoading',
@@ -37,6 +35,6 @@ export default Alt.generateActions(
                                    'getInstantiateSshKeyError',
                                    'getConfigAgentSuccess',
                                    'getConfigAgentError',
-                                   'getResourceOrchestratorSuccess',
+                                   'getResourceOrchestratorAccountsSuccess',
                                    'getResourceOrchestratorAgentError'
                                    )
index d5c33c1..16eb884 100644 (file)
@@ -46,26 +46,6 @@ export default function(Alt){
       success: Alt.actions.global.getCatalogSuccess,
       error: Alt.actions.global.getCatalogError
   },
-  getCloudAccount:{
-      remote (state, cb) {
-        return new Promise((resolve, reject) => {
-          $.ajax({
-            url: 'api/cloud-account?api_server=' +
-              API_SERVER,
-              type: 'GET',
-              beforeSend: Utils.addAuthorizationStub,
-              success: function (data) {
-                resolve(data);
-                if(cb) {
-                  cb();
-                }
-              }
-          })
-        })
-      },
-      success: Alt.actions.global.getLaunchCloudAccountSuccess,
-      error: Alt.actions.global.getLaunchCloudAccountError
-  },
   getDataCenters:{
       remote () {
         return new Promise((resolve, reject) => {
@@ -128,16 +108,7 @@ export default function(Alt){
           }).fail(function(xhr){
             //Authentication and the handling of fail states should be wrapped up into a connection class.
             Utils.checkAuthentication(xhr.status);
-            var error = null;
-            if(xhr.responseText) {
-              try {
-                error = JSON.parse(xhr.responseText);
-                error = JSON.parse(error.error)['rpc-reply']['rpc-error']['error-message'];
-              } catch(e){
-                console.log(e);
-              }
-            }
-            reject(error);
+            reject(xhr.responseText || 'An error occurred. Check your logs for more information');
           });
         })
       },
@@ -170,7 +141,7 @@ export default function(Alt){
       remote(state) {
         return new Promise((resolve, reject) => {
           $.ajax({
-            url: 'api/nsd/' + NSDId + '/input-param?api_server=' + API_SERVER,
+            url: 'api/nsd/' + encodeURIComponent(NSDId) + '/input-param?api_server=' + API_SERVER,
             type: 'GET',
               beforeSend: Utils.addAuthorizationStub,
               success: function (data) {
@@ -200,22 +171,19 @@ export default function(Alt){
       success: Alt.actions.global.getConfigAgentSuccess,
       error: Alt.actions.global.getConfigAgentError
   },
-  getResourceOrchestrator: {
-          remote: function() {
+  getResourceOrchestratorAccounts: {
+          remote: function(state, cb) {
               return new Promise(function(resolve, reject) {
                 $.ajax({
-                  url: 'passthrough/data/api/running/resource-orchestrator' + '?api_server=' + API_SERVER,
+                  url: 'api/ro-account' + '?api_server=' + API_SERVER,
                   type: 'GET',
                   beforeSend: Utils.addAuthorizationStub,
                   contentType: "application/json",
                   success: function(data) {
-                    let returnedData;
-                    if (data.hasOwnProperty("rw-launchpad:resource-orchestrator")) {
-                      returnedData = data['rw-launchpad:resource-orchestrator'];
-                    } else {
-                      returnedData = {};
+                    if(cb) {
+                      cb();
                     }
-                    resolve(returnedData);
+                    resolve(data);
                   },
                   error: function(error) {
                     console.log("There was an error updating the account: ", arguments);
@@ -231,11 +199,10 @@ export default function(Alt){
           interceptResponse: interceptResponse({
             'error': 'There was an error retrieving the resource orchestrator information.'
           }),
-          success: Alt.actions.global.getResourceOrchestratorSuccess,
+          success: Alt.actions.global.getResourceOrchestratorAccountsSuccess,
                     loading: Alt.actions.global.showScreenLoader,
-          error: Alt.actions.global.showNotification
+          error: Alt.actions.global.handleServerReportedError
         }
-
   }
 }
 function interceptResponse (responses) {
diff --git a/skyquake/plugins/launchpad/src/launchpad-create.js b/skyquake/plugins/launchpad/src/launchpad-create.js
deleted file mode 100644 (file)
index cbe6d4b..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-
-/*
- * 
- *   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.
- *
- */
-require('../components/form-controls.js');
-require('./launchpad-launch-fleet.html');
-var createStore = require('./createStore.js');
-var createActions = require('./createActions.js');
-
-
-angular.module('launchpad')
-    .controller('launchpadCreateCtrl', function($timeout, $stateParams, $state) {
-        var self = this;
-        self.createStore =
-        // var createChannel = $rw.radio.channel('createFleet');
-        var apiServer = self.isOnline = require('utils/rw.js').getSearchParams(window.location).api_server;
-        // var federationChannel = $rw.radio.channel('federationChannel');
-        self.fleet = {
-            template_id: null,
-            pool_id: null,
-            description: '',
-            epa_attributes: {},
-            status: "active",
-            name: 'NEW FLEET'
-        };
-        self.slaParams = [];
-        self.federation = $stateParams.id;
-        createStore.getNetworkServices();
-        createStore.getSlaParams();
-        createStore.getPools();
-        createStore.listen(function(state) {
-                $timeout(function() {
-                    self.networkServices = state.networkServices;
-                    self.slaParams = state.slaParams;
-                    self.fleet.pool = state.pools[0];
-                        self.pools = state.pools;
-                    angular.forEach(self.slaParams, function(v) {
-                        if (!v.hasOwnProperty('value')) {
-                            v.value = v.options.second;
-                        };
-                        return v;
-                    });
-                })
-            })
-            // federationChannel.request("federation:services").then(function(data) {
-            //     $timeout(function() {
-            //         // self.fleet.service = 'cag';
-            //         self.networkServices = data;
-            //         createChannel.request('vnfParams', 'cag').then(function(data) {
-            //         $timeout(function() {
-            //             self.vnfParams = data;
-            //         });
-            //     });
-            //     });
-            // });
-            //     federationChannel.request('federation:pools', apiServer).then(function(data) {
-            //         $timeout(function() {
-            //             console.log('pools:', data)
-            //             self.fleet.pool = data[0];
-            //             self.pools = data;
-            //         })
-            //     });
-
-        //         federationChannel.request('federation:sla-params').then(function(data) {
-        //             $timeout(function() {
-        //                 self.slaParams = data;
-        //                 angular.forEach(self.slaParams, function(v) {
-        //                     if (!v.hasOwnProperty('value')) {
-        //                         v.value = v.options.second;
-        //                     };
-        //                     return v;
-        //                 });
-        //             }
-        //             );
-        //         });
-
-        // federationChannel.on("launchpadCreate", function() {
-        //         $state.go('launchpad', null, {reload: false});
-
-        // });
-
-        self.generateServiceImage = function(service) {
-            return ('assets/img/svg/' + service.src + (self.isSelectedService(service.id) ? '-active' : '-inactive') + '.svg');
-        };
-        self.generatePoolImage = function(pool) {
-            return ('assets/img/svg/' + self.refsDB.resources.openstackCloud.pools[pool.ref].src + (self.isSelectedPool(pool) ? '-active' : '-inactive') + '.svg');
-        };
-        self.isSelectedPool = function(id) {
-            return id == self.fleet.pool_id;
-        };
-        self.isSelectedService = function(id) {
-            return id == self.fleet.template_id;
-        };
-        self.launch = function(launch) {
-            if (self.fleet.name == "") {
-                createActions.validateError('Plase Name the Service')
-            }
-            createActions.validateReset();
-            self.slaParams.forEach(function(v) {
-                if (v.value.indexOf("RRC") > -1) {
-                    v.value = "RRC";
-                }
-                self.fleet.epa_attributes[v.ref] = v.value;
-            });
-            delete self.fleet.pool;
-            self.fleet.status = launch ? 'active' : 'inactive';
-            createStore.createEnvironment(self.fleet)
-        };
-        self.selectPool = function(id) {
-            self.fleet.pool_id = id;
-            // createChannel.command("pool:select", id);
-        };
-        self.selectService = function(id) {
-
-            self.fleet.template_id = id;
-            // createChannel.command("service:select", id);
-        };
-    });
index 759c7d6..e335cad 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,6 +26,12 @@ import NsListPanel from './nsListPanel/nsListPanel.jsx';
 import Crouton from 'react-crouton'
 import AppHeader from 'widgets/header/header.jsx';
 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');
@@ -90,6 +96,7 @@ export default class LaunchpadApp extends React.Component {
 
   render () {
     var self = this;
+    const hasAccess = isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]);
     let mgmtDomainName = window.location.hash.split('/')[2];
     let navItems = [];
     if(!mgmtDomainName) {
@@ -110,14 +117,16 @@ export default class LaunchpadApp extends React.Component {
             isVisible={self.state.isNsListPanelVisible}
             />
           <NsCardPanel nsrs={self.state.nsrs}
-            openedNsrIDs={self.state.openedNsrIDs} />
+            openedNsrIDs={self.state.openedNsrIDs}
+            hasAccess={hasAccess} />
         </div>
       </div>
     );
     }
 }
 LaunchpadApp.contextTypes = {
-    router: React.PropTypes.object
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
 };
 LaunchpadApp.defaultProps = {
   // name: 'Loading...',
index fff6fae..9f72147 100644 (file)
@@ -86,7 +86,7 @@ module.exports = function(Alt) {
         console.log(id)
         return new Promise(function(resolve, reject) {
           $.ajax({
-            url: 'api/nsr/' + id + '?api_server=' + API_SERVER,
+            url: 'api/nsr/' + encodeURIComponent(id) + '?api_server=' + API_SERVER,
             type: 'DELETE',
             beforeSend: Utils.addAuthorizationStub,
             success: function(data) {
@@ -106,7 +106,7 @@ module.exports = function(Alt) {
             return resolve(false);
           }
            $.ajax({
-            url: '/socket-polling?api_server=' + API_SERVER,
+            url: '/socket-polling',
             type: 'POST',
             beforeSend: Utils.addAuthorizationStub,
             data: {
@@ -129,7 +129,7 @@ module.exports = function(Alt) {
       remote: function(state, id, status) {
         return new Promise(function(resolve, reject) {
           $.ajax({
-            url: 'api/nsr/' + id + '/admin-status?api_server=' + API_SERVER ,
+            url: 'api/nsr/' + encodeURIComponent(id) + '/admin-status?api_server=' + API_SERVER ,
             type:'PUT',
             beforeSend: Utils.addAuthorizationStub,
             data: {
diff --git a/skyquake/plugins/launchpad/src/launchpad_card/launchpad-card.js b/skyquake/plugins/launchpad/src/launchpad_card/launchpad-card.js
deleted file mode 100644 (file)
index de27638..0000000
+++ /dev/null
@@ -1,370 +0,0 @@
-
-/*
- * 
- *   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.
- *
- */
-require('./launchpad-fleet-card-params.html');
-var React = require('react');
-var LaunchpadCard = require('./launchpadCard.jsx');
-// var LaunchpadCard = require('../../components/dashboard_card/dashboard_card.jsx');
-angular.module('launchpad')
-  .directive('fleetCard', function($state, $stateParams) {
-    return {
-      restrict: 'AE',
-      replace: true,
-      template: '<div></div>',
-      scope: {
-        fleet: '=data',
-        $index: '=',
-        metric: '=?',
-        slaParam: '=?',
-        nfviMetric: '=?'
-      },
-
-      bindToController: true,
-      controllerAs: 'card',
-      link: function(scope, element, attributes) {
-
-      },
-      controller: function($timeout, $interval, $scope, $rootScope, $element) {
-        var self = this;
-
-        console.log(self.fleet)
-         $scope.$watch(function() {
-          return self.fleet}, reactRender)
-        function reactRender() {
-          console.log('rendering', self.fleet)
-          React.render(
-            React.createElement(LaunchpadCard, {className:'launchpadCard'}
-            ),
-           $element[0]
-          );
-        }
-        //FOR WAG ONLY
-        //   self.isOn = false;
-
-        //   //END WAG ONLY
-        //   //Remove for testing only
-        //   $scope.$watch(function() {
-        //     return self.fleet;
-        //   }, function() {});
-
-        //   // var fleetChannel = $rw.radio.channel('fleetChannel');
-        //   var FleetStore = require('../launchpadFleetStore.js');
-        //   self.valueFormat = {
-        //     "int": 1,
-        //     "dec": 0
-        //   };
-        //   self.federationID = $stateParams.id;
-        //   //if this is true, set gauges to start
-        //   if (require('utils/rw.js').getSearchParams(window.location).api_server) {
-        //     self.fleet.started = true;
-        //   }
-        //   self.apiServer = require('utils/rw.js').getSearchParams(window.location).api_server;
-        //   self.isNoisy = false;
-        //   var rateChanged = function(rate) {
-        //     self.rate = rate;
-        //     fleetChannel.command('fleet:change:rate', rate)
-        //   }
-        //   var packetSizeChanged = function(packetSize) {
-        //     self.packetSize = packetSize
-        //     fleetChannel.command('fleet:change:packetSize', packetSize)
-        //   };
-        //   self.openFleet = function(index) {
-        //     var params = require('utils/rw.js').getSearchParams(window.location);
-        //     if (params.api_server) {
-        //       if (params.api_server == "http://10.0.201.25:5000") {
-        //         var newLoc = window.location.origin + window.location.pathname + '?config=' + params.config + '&api_server=' + 'http://10.0.201.25:5050' + '#/wag';
-        //         window.open(newLoc);
-        //         return;
-        //       }
-        //       if (self.fleet.api == "10.0.117.22") self.fleet.api = "10.0.117.17";
-        //       var newLoc = window.location.origin + window.location.pathname + '?config=' + params.config + '&api_server=' + 'http://' + self.fleet.api + ':5050' + '&name=' + self.fleet.name + '&env_id=' + self.fleet.id + '#/dashboard/' + $stateParams.id + '/' + index;;
-        //       console.log('Opening new window at: %s', newLoc)
-        //       window.open(newLoc);
-        //     } else {
-        //       if (self.fleet.id == 'wag-fleet') {
-        //         var newLoc = window.location.origin + window.location.pathname + '?config=' + params.config + '#/wag';
-        //         window.open(newLoc);
-        //       }
-        //       window.open('#/dashboard/' + $stateParams.id + '/' + index);
-        //     }
-        //   };
-        //   self.openConsole = function(index) {
-        //     console.log(self);
-        //     var params = require('utils/rw.js').getSearchParams(window.location);
-        //     if (params.api_server) {
-        //       if (self.fleet.api == "10.0.117.22") self.fleet.api = "10.0.117.17";
-        //       var newLoc = self.apiServer + '/api/environments/' + self.fleet.id + '/console';
-        //       window.open(newLoc);
-        //     } else {
-        //       window.open('#/dashboard/' + $stateParams.id + '/' + index);
-        //     }
-        //   };
-
-
-        //   self.noisyToggle = function() {
-        //     var action;
-        //     if (self.isNoisy) {
-        //       action = 'stop';
-        //       self.isNoisy = false;
-        //     } else {
-        //       action = 'start';
-        //       self.isNoisy = true;
-        //     }
-        //     $.ajax({
-        //       url: "http://" + self.fleet.api + ':5050/api/operations/' + action + '-stream',
-        //       type: "POST",
-        //       headers: {
-        //         "Content-Type": "application/vnd.yang.operation+json"
-        //       },
-        //       dataType: "json",
-        //       data: JSON.stringify({
-        //         "input": {
-        //           "now": ""
-        //         }
-        //       })
-        //     });
-        //   };
-
-        //   self.toggleStatus = function() {
-        //     var state;
-        //     var status = self.fleet.status;
-        //     console.log(self.fleet)
-        //     if (status == "starting" || status == "stopping") {
-        //       return;
-        //     }
-        //     if (status == "active") {
-        //       state = "inactive";
-        //     } else {
-        //       state = "active";
-        //     }
-        //     self.fleet.status = state;
-        //     FleetStore.setFleetState(self.fleet.id, state);
-        //   };
-        //   //CAT + NOISY NEIGHBOR ONLY
-
-        //   self.catStarted = false;
-        //   self.catToggle = function() {
-        //     var action;
-        //     if (self.catStarted) {
-        //       action = 'stop';
-        //       self.catStarted = false;
-        //     } else {
-        //       action = 'start';
-        //       self.catStarted = true;
-        //     }
-        //     $.ajax({
-        //       url: "http://" + self.fleet.api + ':5050/api/operations/' + action + '-cat',
-        //       type: "POST",
-        //       headers: {
-        //         "Content-Type": "application/vnd.yang.operation+json"
-        //       },
-        //       dataType: "json",
-        //       data: JSON.stringify({
-        //         "input": {
-        //           "now": ""
-        //         }
-        //       })
-        //     });
-        //   };
-
-        //   self.deleteFleet = function() {
-        //     if (confirm("Do you really want to delete this fleet?")) {
-        //       // if (confirm("Seriously, you REALLY want to delete this?")) {
-        //         // fleetChannel.request('fleet:delete', self.fleet);
-        //         FleetStore.deleteFleet(self.fleet.id)
-        //       // }
-        //     }
-        //   };
-
-
-        //   /**
-        //    *  WAG Code Start
-        //    **/
-        //   // var wagChannel = $rw.radio.channel('wag');
-
-        //   //If Wag page Offline
-        //   if (!self.apiServer && self.fleet.id == 'wag-fleet') {
-        //     self.offline = setInterval(function() {
-        //       if (self.fleet.started) {
-        //         self.fleet.wagpage.clientsim['traffic-gen']['rx-pps'] = 100 * (Math.random() - .5) + 200;
-        //         self.fleet.wagpage.clientsim['traffic-gen']['tx-pps'] = 100 * (Math.random() - .5) + 200;
-        //       }
-        //     }, 2000);
-        //   }
-
-        //   // If Wag page Online
-        //   if (self.fleet.template_name == 'Wireless Access Gateway') {
-        //     wagChannel.command('wag-poll');
-        //     wagChannel.on('wag-update', function(data) {
-        //       $timeout(function() {
-        //         self.fleet.wagpage = {};
-        //         self.fleet.wagpage.clientsim = data.clientsim
-        //           // self.data.clientsim['traffic-sink']['rx-pps'] / 1000000;
-        //         self.isOn = data.clientsim['traffic-gen-status'].running;
-        //       });
-        //     });
-        //   }
-
-        //   $rootScope.$on('$stateChangeStart', function() {
-        //     wagChannel.command('wag-poll:kill');
-        //   });
-
-        //   /**
-        //    *  WAG Code End
-        //    **/
-
-
-      }
-    };
-  })
-
-.directive('launchpadFleetCardParams', function() {
-  return {
-    restrict: 'AE',
-    replace: true,
-    templateUrl: '/modules/launchpad/launchpad_card/launchpad-fleet-card-params.html',
-    controllerAs: 'params',
-    scope: {
-      $index: "=",
-      cardData: '=',
-      slaParam: '=?',
-      nfviMetric: '=?'
-    },
-    bindToController: true,
-    controller: function($scope, $stateParams, $timeout, $interval, $rootScope) {
-      //remove watch
-      $scope.$watch(function() {
-        return self.cardData;
-      }, function() {})
-      var self = this;
-      var apiServer = require('utils/rw.js').getSearchParams(window.location).api_server;
-      self.apiServer = apiServer;
-      // self.refs = refsDB;
-
-
-      // var LaunchpadFleetActions = require('../launchpadFleetActions.js');
-      // var LaunchpadFleetStore = require('../launchpadFleetStore.js');
-      //var LaunchpadFleetActions = require('./launchpadFleetActions.js');
-
-      self.rate = 40;
-      self.packetSize = 256;
-      self.refParam = $rw.refParams;
-      var currentNFVIRef;
-
-      this.visible = 'default';
-      if (self.cardData.id == "wag-fleet") {
-        self.cardData.started = self.cardData.wagpage.clientsim['traffic-gen-status'].running;
-      }
-
-
-      self.selectedNFVI = 0;
-      self.selectedSLAParam = {};
-      self.refPools = $rw.refPools;
-
-      // Event Listeners
-      // View Actions
-      self.load = function(panel) {
-        self.visible = panel;
-      };
-      self.filterSLA = function(ref) {
-        // fleetChannel.command('filter:SLAParams', ref);
-        FleetActions.filterSLAParams(ref);
-      };
-      self.filterNFVI = function(ref) {
-        FleetActions.filterNfviMetrics(ref)
-          // fleetChannel.command('filter:NFVIMetrics', ref);
-      };
-      self.serviceToggle = function(fleet, control) {
-        if (apiServer) {
-          var vnfrChannel = $rw.radio.channel('vnfr');
-          switch (control.ref) {
-            case "trafgen":
-              var action = (control.started) ? 'stop' : 'start';
-              vnfrChannel.command("vnf:command", action, control.data.api, "http://" + fleet.api + ':5050');
-              break;
-            case "iot_army":
-              var action = (control.started) ? 'stop' : 'start';
-              vnfrChannel.command("vnf:command", action, control.data.api, "http://" + fleet.api + ':5050');
-              break;
-          }
-        } else {
-          fleetChannel.command('fleet:start', $stateParams.id, self.$index);
-          if ($stateParams.id == 'wag-federation') {
-            if (!self.cardData.wagpage.clientsim['traffic-gen-status']) {
-              self.cardData.wagpage.clientsim['traffic-gen-status'] = {};
-            }
-            self.cardData.wagpage.clientsim['traffic-gen-status'].running = !self.cardData.wagpage.clientsim['traffic-gen-status'].running;
-          }
-        }
-
-      };
-      // Init Params
-
-
-      //WAG ONLY
-      if (self.apiServer == "http://10.0.201.25:5000") {
-        self.isWag = true;
-      }
-      self.toggleOn = function(e) {
-        // self.setPacket(e, self.packetSize);
-        // self.setRate(e, self.rate);
-        // self.setSubscribers(e, self.subscribers);
-        // self.setAP(e, self.ap);
-        var wagServer = ""
-        if (self.apiServer) {
-          if (self.isOn) {
-            $.ajax({
-              type: "POST",
-              url: "http://10.0.201.25:5050/api/running/stop-device-group/",
-              success: (function() {
-                console.log('stahp post sent')
-              })
-            });
-            $.ajax({
-              type: "POST",
-              url: "http://10.0.201.25:5050/api/running/stop-sink-server/",
-              success: (function() {
-                console.log('stahp post sent')
-              })
-            });
-          } else {
-            $.ajax({
-              type: "POST",
-              url: "http://10.0.201.25:5050/api/running/start-sink-server/",
-              success: (function() {
-                console.log('start post sent')
-              })
-            });
-            $.ajax({
-              type: "POST",
-              url: "http://10.0.201.25:5050/api/running/start-device-group/",
-              success: (function() {
-                console.log('start post sent')
-              })
-            });
-          }
-        }
-        self.isOn = !self.isOn;
-      }
-
-      //END WAG ONLY
-
-    },
-    link: function(s, e, a) {}
-  }
-});
index e68a659..8caa2f4 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -69,13 +69,13 @@ class LaunchpadCard extends React.Component {
     if(true) {
       // metricsAndParameters.push(<LaunchpadControls controlSets={this.props.nsr.nsControls} />)
       if (this.props.nsr) {
-        if (this.props.nsr["nfvi-metrics"]) {
-          metricsAndParameters.push((
-                                     <div className="nfvi-metrics" key="nfviMetrics">
-                                      <LpCardNfviMetrics key="nfvi-metrics" name="NFVI METRICS" id={this.props.id} data={this.props.nsr["nfvi-metrics"]} />
-                                     </div>
-                                     ))
-        }
+        // if (this.props.nsr["nfvi-metrics"]) {
+        //   metricsAndParameters.push((
+        //                              <div className="nfvi-metrics" key="nfviMetrics">
+        //                               <LpCardNfviMetrics key="nfvi-metrics" name="NFVI METRICS" id={this.props.id} data={this.props.nsr["nfvi-metrics"]} />
+        //                              </div>
+        //                              ))
+        // }
         if (this.props.nsr["epa-params"]) {
           metricsAndParameters.push(<EpaParams key="epa-params" data={this.props.nsr["epa-params"]} />);
         }
@@ -101,7 +101,7 @@ class LaunchpadCard extends React.Component {
 
       html = (
         <DashboardCard className={'launchpadCard'} closeCard={closeButton}>
-          <LaunchpadHeader nsr={this.props.nsr} name={this.props.name} isActive={this.props.isActive} id={this.props.id}/>
+          <LaunchpadHeader hasAccess={this.props.hasAccess} nsr={this.props.nsr} name={this.props.name} isActive={this.props.isActive} id={this.props.id}/>
           {
           deleting ?
           <div className={'deletingIndicator'}>
index 39397ff..ff02395 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -30,25 +30,38 @@ export default class LaunchpadCardCloudAccount extends React.Component {
   render() {
     let html;
     let isDisplayed = this.props.display;
-    let status = [];
-    if (this.props.nsr['cloud-account']) {
-      status.push(
-        (<li key="nsr"><h3>NSR: {this.props.nsr['cloud-account']}</h3></li>)
-      )
-    }
-    this.props.nsr['vnfrs'] && this.props.nsr['vnfrs'].map(function(v,i) {
-      if(v.hasOwnProperty('cloud-account')) {
-        status.push(
-          (<li key={i}><h3>VNFR {v['short-name']}: {v['cloud-account']}</h3></li>)
-        )
-      }
-    })
-    html = (
-          <ul>
-            {status}
-          </ul>
-      )
-    return (<div className={this.props.className + (isDisplayed ? '_open':'_close')}><h2>VIM Accounts</h2>{html}</div>);
+    let nsrDataCenter = this.props.nsr['resource-orchestrator'] ? this.props.nsr['resource-orchestrator'] : 'RIFT';
+    return (
+            <div className={this.props.className + (isDisplayed ? '_open':'_close')}>
+              <h2>Accounts</h2>
+              <div className={'dataCenterTable'}>
+                <div className="dataCenterTable-header">
+                  <div>TYPE</div>
+                  <div>NAME</div>
+                  <div>RESOURCE ORCHESTRATOR</div>
+                  <div>DATACENTER</div>
+                </div>
+                <div>
+                  <div>NSR</div>
+                  <div>{this.props.nsr['short-name']}</div>
+                  <div>{nsrDataCenter}</div>
+                  <div>{this.props.nsr['datacenter']}</div>
+                </div>
+                {
+                  this.props.nsr && this.props.nsr['vnfrs'] && this.props.nsr['vnfrs'].map(function(v,i) {
+                    if(v.hasOwnProperty('datacenter')) {
+                      return  <div>
+                                <div>VNFR</div>
+                                <div>{v['short-name']}</div>
+                                <div>{nsrDataCenter}</div>
+                                <div>{v['datacenter']}</div>
+                              </div>
+                    }
+                  })
+                }
+              </div>
+            </div>
+    );
   }
 }
 
index 9ee7ec4..f7a1965 100644 (file)
@@ -151,8 +151,14 @@ class LaunchpadHeader extends React.Component {
             <h3 style={{display: isLoading ? 'none' : 'inherit'}}>
                 <UpTime initialTime={nsrCreateTime} run={true} />
             </h3>
-            <h3 className="launchpadCard_header-link" style={{display: this.props.nsr['cloud-account'] ? 'inherit' : 'none'}}>
-              <a onClick={this.openCloudAccountPanel} title="VIM Account" className={self.state.displayCloudAccount ? 'activeIcon' : 'inActiveIcon'}>
+            <h3 style={{display: isLoading ? this.props.nsr["operational-status"] == 'vl-init-phase' ? 'inherit' : 'none' : 'none'}}>
+                {this.props.nsr["rw-nsr:orchestration-progress"]["networks"]["active"]} / {this.props.nsr["rw-nsr:orchestration-progress"]["networks"]["total"]}
+            </h3>
+            <h3 style={{display: isLoading ? this.props.nsr["operational-status"] == 'vnf-init-phase' ? 'inherit' : 'none' : 'none'}}>
+                {this.props.nsr["rw-nsr:orchestration-progress"]["vms"]["active"]} / {this.props.nsr["rw-nsr:orchestration-progress"]["vms"]["total"]}
+            </h3>
+            <h3 className="launchpadCard_header-link" style={{display: this.props.nsr['datacenter'] ? 'inherit' : 'none'}}>
+              <a onClick={this.openCloudAccountPanel} title="Datacenters" className={self.state.displayCloudAccount ? 'activeIcon' : 'inActiveIcon'}>
                 <span className="oi" data-glyph="cloud" aria-hidden="true"></span>
               </a>
             </h3>
@@ -163,11 +169,15 @@ class LaunchpadHeader extends React.Component {
                 </a>
             </h3>
             {toggleStatus}
-            <h3 className="launchpadCard_header-link" style={{display: 'inherit'}}>
-              <a onClick={this.deleteLaunchpad} title="Delete">
-                <span className="oi" data-glyph="trash" aria-hidden="true"></span>
-              </a>
-            </h3>
+            {this.props.hasAccess ?
+                (
+                  <h3 className="launchpadCard_header-link" style={{display: 'inherit'}}>
+                    <a onClick={this.deleteLaunchpad} title="Delete">
+                      <span className="oi" data-glyph="trash" aria-hidden="true"></span>
+                    </a>
+                  </h3>
+                )
+              : null}
           </div>
         </div>
         <div className="launchpadCard_header-status">
index 1f2680d..38b2887 100644 (file)
  */
 @import 'style/_colors.scss';
 @import '../../node_modules/open-iconic/font/css/open-iconic.css';
+$LaunchpadCardWidth: 693px;
 .launchpadCard {
   display:-ms-flexbox;
+  display:-webkit-box;
   display:flex;
   margin-top: 0.75rem;
   &-body{
   }
   .deletingIndicator {
     display: -ms-flexbox;
+    display: -webkit-box;
     display: flex;
     -ms-flex:1;
-        flex:1;
+        -webkit-box-flex:1;
+            flex:1;
     -ms-flex-align: center;
-        align-items: center;
+        -webkit-box-align: center;
+            align-items: center;
     -ms-flex-pack: center;
-        justify-content: center;
+        -webkit-box-pack: center;
+            justify-content: center;
   }
   &_launch {
     display:-ms-flexbox;
+    display:-webkit-box;
     display:flex;
     -ms-flex-direction:column;
-        flex-direction:column;
+        -webkit-box-orient:vertical;
+        -webkit-box-direction:normal;
+            flex-direction:column;
     -ms-flex: 1;
-        flex: 1;
+        -webkit-box-flex: 1;
+            flex: 1;
     -ms-flex-pack: center;
-        justify-content: center;
+        -webkit-box-pack: center;
+            justify-content: center;
     -ms-flex-align: center;
-        align-items: center;
+        -webkit-box-align: center;
+            align-items: center;
     img {
       width: 88px;
       padding:1rem;
   &_header {
     color:white;
     padding-right: 1rem;
-    min-width: 693px;
-    max-width: 693px;
+    width: $LaunchpadCardWidth;
     &-link, a {
       display:-ms-flexbox;
+      display:-webkit-box;
       display:flex;
       -ms-flex-align:center;
-          align-items:center;
+          -webkit-box-align:center;
+              align-items:center;
       text-decoration: none;
       color: inherit;
     }
     }
     &-title {
       display: -ms-flexbox;
+      display: -webkit-box;
       display: flex;
       line-height: 3rem;
       background-color: $brand-green-light;
       /*background-color: $primary-header;*/
       position: relative;
       -ms-flex-direction: row;
-          flex-direction: row;
+          -webkit-box-orient: horizontal;
+          -webkit-box-direction: normal;
+              flex-direction: row;
       -ms-flex-align: center;
-          align-items: center;
+          -webkit-box-align: center;
+              align-items: center;
       -ms-flex-pack: start;
-          justify-content: flex-start;
+          -webkit-box-pack: start;
+              justify-content: flex-start;
       padding-right:1rem;
       &-off {
         background-color:white;
       }
     &-actions {
       -ms-flex: 1;
-          flex: 1;
+          -webkit-box-flex: 1;
+              flex: 1;
       display: -ms-flexbox;
+      display: -webkit-box;
       display: flex;
       -ms-flex-pack: end;
-          justify-content: flex-end;
+          -webkit-box-pack: end;
+              justify-content: flex-end;
       padding-right: 1rem;
       h3{
         padding-right:0.25rem;
     }
     &-status {
       background-color: $brand-green;
+      width:$LaunchpadCardWidth;
       &-current {
         display:-ms-flexbox;
+        display:-webkit-box;
         display:flex;
         -ms-flex-pack:justify;
-            justify-content:space-between;
+            -webkit-box-pack:justify;
+                justify-content:space-between;
         padding: 0.5rem 0;
+        width: $LaunchpadCardWidth;
         span,a {
           padding: 0 0.25rem;
         }
     &-operational-status {
       &_loading {
         display:-ms-flexbox;
+        display:-webkit-box;
         display:flex;
         -ms-flex-direction:column;
-            flex-direction:column;
+            -webkit-box-orient:vertical;
+            -webkit-box-direction:normal;
+                flex-direction:column;
         -ms-flex-pack: center;
-            justify-content: center;
+            -webkit-box-pack: center;
+                justify-content: center;
         justify-content: center;
         -ms-flex-align: center;
-            align-items: center;
+            -webkit-box-align: center;
+                align-items: center;
         color:$gray-darkest;
+        width: $LaunchpadCardWidth;
      }
      &_open {
         overflow: hidden;
         width: 100%;
         z-index: 99;
         display: -ms-flexbox;
+        display: -webkit-box;
         display: flex;
         -ms-flex-direction: column;
-            flex-direction: column;
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                flex-direction: column;
         -ms-flex-align: center;
-            align-items: center;
+            -webkit-box-align: center;
+                align-items: center;
         -ms-flex-pack: center;
-            justify-content: center;
+            -webkit-box-pack: center;
+                justify-content: center;
         -ms-flex-line-pack: center;
             align-content: center;
+        width: $LaunchpadCardWidth;
         color: black;
         h2{
               padding: 2rem;
 
       &_loading {
         display:-ms-flexbox;
+        display:-webkit-box;
         display:flex;
         -ms-flex-direction:column;
-            flex-direction:column;
+            -webkit-box-orient:vertical;
+            -webkit-box-direction:normal;
+                flex-direction:column;
         -ms-flex-pack: center;
-            justify-content: center;
+            -webkit-box-pack: center;
+                justify-content: center;
         -ms-flex-align: center;
-            align-items: center;
+            -webkit-box-align: center;
+                align-items: center;
         color:$gray-darkest;
+
+
      }
       &_open {
         overflow:hidden;
         width:100%;
         z-index: 99;
         display: -ms-flexbox;
+        display: -webkit-box;
         display: flex;
         -ms-flex-direction: column;
-            flex-direction: column;
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                flex-direction: column;
         -ms-flex-align: center;
-            align-items: center;
-        -ms-flex-pack: center;
-            justify-content: center;
+            -webkit-box-align: center;
+                align-items: center;
+        justify-content: flex-start;
         -ms-flex-line-pack: center;
             align-content: center;
         color:black;
         }
         h2 {
           padding:2rem;
+          padding-left: 0;
         }
         ul,li {
           margin:0.25rem 1rem 0 0.825rem;
         }
         li {
           -ms-flex-pack: justify;
-              justify-content: space-between;
+              -webkit-box-pack: justify;
+                  justify-content: space-between;
           display: -ms-flexbox;
+          display: -webkit-box;
           display: flex;
         }
       }
       min-height:50px;
       max-width: 100%;
       display:-ms-flexbox;
+      display:-webkit-box;
       display:flex;
       -ms-flex-pack: center;
-          justify-content: center;
+          -webkit-box-pack: center;
+              justify-content: center;
       -ms-flex-align: center;
-          align-items: center;
+          -webkit-box-align: center;
+              align-items: center;
     }
   }
   &_title {
   &_data-list {
     ul, dl{
       display:-ms-flexbox;
+      display:-webkit-box;
       display:flex;
       -ms-flex-direction:row;
-          flex-direction:row;
+          -webkit-box-orient:horizontal;
+          -webkit-box-direction:normal;
+              flex-direction:row;
       -ms-flex-wrap:wrap;
           flex-wrap:wrap;
       -ms-flex-align:start;
-          align-items:flex-start;
+          -webkit-box-align:start;
+              align-items:flex-start;
        font-size: 0.825rem;
       li{
         -ms-flex:1 1 30%;
-            flex:1 1 30%;
+            -webkit-box-flex:1;
+                flex:1 1 30%;
       }
     }
     dl {
   }
   &_controls{
     display:-ms-flexbox;
+    display:-webkit-box;
     display:flex;
     -ms-flex-direction:row;
-        flex-direction:row;
+        -webkit-box-orient:horizontal;
+        -webkit-box-direction:normal;
+            flex-direction:row;
     .react-tabs {
       -ms-flex: auto;
-          flex: auto;
+          -webkit-box-flex: 1;
+              flex: auto;
     [role=tab][aria-selected=true] {
       background:none;
       border:none;
       border:none;
       &:focus{
         outline:none;
-        box-shadow:none;
+        -webkit-box-shadow:none;
+                box-shadow:none;
         border:none;
         &:after {
            background:none;
       margin: 0 0 10px;
       padding: 0;
       display: -ms-flexbox;
+      display: -webkit-box;
       display: flex;
       -ms-flex-pack: end;
-          justify-content: flex-end;
+          -webkit-box-pack: end;
+              justify-content: flex-end;
 
          [role=tab]:first-child {
             -ms-flex: 1;
-                flex: 1;
+                -webkit-box-flex: 1;
+                    flex: 1;
          }
     }
 
       border-radius: 15px;
     }
   }
+  .dataCenterTable {
+    display: flex;
+    flex-direction: column;
+    width: 88%;
+    &-header {
+      align-items: flex-end;
+      font-weight:bold;
+    }
+      > div {
+        display: flex;
+
+        &:not(:first-child) {
+          margin-top:1rem;
+          align-items: flex-start;
+        }
+        > div {
+          flex: 1 0 25%;
+        }
+      }
+  }
 }
index 9d0cdba..acafdd0 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -162,6 +162,7 @@ class NsrConfigPrimitives extends React.Component {
     }
     constructConfigPrimitiveTabs = (tabList, tabPanels) => {
         let self = this;
+        const hasAccess = self.props.hasAccess;
         let defaultFromRpc = '';
         //coded here for dev purposes
         let mandatoryFieldValue = 'true';
@@ -307,7 +308,11 @@ class NsrConfigPrimitives extends React.Component {
                                     </div>
                                 )
                             })}
-                            <Button label="Submit" isLoading={this.state.isSaving} onClick={this.handleExecuteClick.bind(this, nsConfigPrimitiveIndex)} className="dark"/>
+                            {
+                                hasAccess ?
+                                    <Button label="Submit" isLoading={this.state.isSaving} onClick={this.handleExecuteClick.bind(this, nsConfigPrimitiveIndex)} className="dark"/>
+                                : null
+                            }
                         </TabPanel>
                     )
                 );
index db94ded..ffd21bb 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -65,6 +65,7 @@ class NsrScalingGroups extends React.Component {
                                                        <td>{id}</td>
                                                        <td><UpTime initialTime={sgrInstance['create-time']} run={true} /></td>
                                                        <td>{sgrInstance['op-status']}</td>
+                                                        <td>{sgrInstance['config-status']}</td>
                                                        <td>
                                                                {sgrInstance['is-default'] == 'false' ? <a onClick={this.handleDeleteClick.bind(this, this.props.data.id, sgrName, id)} title="Delete">
                                                        <span className="oi" data-glyph="trash" aria-hidden="true"></span>
@@ -92,8 +93,9 @@ class NsrScalingGroups extends React.Component {
                                <tr>
                                        <th style={{width: '6%'}}></th>
                                    <th style={{width: '12%'}}>ID</th>
-                                   <th style={{width: '37%'}}>Uptime</th>
-                                   <th style={{width: '37%'}}>Status</th>
+                                   <th style={{width: '24%'}}>Uptime</th>
+                                   <th style={{width: '25%'}}>Status</th>
+                                    <th style={{width: '25%'}}>Config-Status</th>
                                    <th style={{width: '7%'}}> </th>
                                </tr>
                            </thead>
index d9b7182..a9d2058 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -46,7 +46,7 @@
 
     handleParamChange = (paramIndex, configPrimitiveIndex, vnfrIndex, e) => {
         let vnfrs = this.state.vnfrs;
-        vnfrs[vnfrIndex]["vnf-configuration"]["service-primitive"][configPrimitiveIndex]["parameter"][paramIndex].value = e.target.value
+        vnfrs[vnfrIndex]["vnf-configuration"]["config-primitive"][configPrimitiveIndex]["parameter"][paramIndex].value = e.target.value
         this.setState({
             vnfrs: vnfrs
         })
     }
 
     constructConfigPrimitiveTabs = (tabList, tabPanels) => {
+        const hasAccess = this.props.hasAccess;
         let mandatoryFieldValue = 'true';
         this.state.vnfrs && this.state.vnfrs.map((vnfr, vnfrIndex) => {
-            if (vnfr['vnf-configuration'] && vnfr['vnf-configuration']['service-primitive'] && vnfr['vnf-configuration']['service-primitive'].length > 0) {
-                vnfr['vnf-configuration']['service-primitive'].map((configPrimitive, configPrimitiveIndex) => {
+            if (vnfr['vnf-configuration'] && vnfr['vnf-configuration']['config-primitive'] && vnfr['vnf-configuration']['config-primitive'].length > 0) {
+                vnfr['vnf-configuration']['config-primitive'].map((configPrimitive, configPrimitiveIndex) => {
                     let params = [];
                     if (configPrimitive['parameter'] && configPrimitive['parameter'].length > 0) {
                         configPrimitive['parameter'].map((param, paramIndex) => {
                                     {params}
                                 </ul>
                             </div>
-                            <button className="dark" role="button" onClick={this.handleExecuteClick.bind(this, configPrimitiveIndex, vnfrIndex)}>{configPrimitive.name}</button>
+                            {hasAccess ? <button className="dark" role="button" onClick={this.handleExecuteClick.bind(this, configPrimitiveIndex, vnfrIndex)}>{configPrimitive.name}</button> : null}
                         </TabPanel>
                     )
                 });
     }
 
     render() {
-
         let tabList = [];
         let tabPanels = [];
         let isConfiguring = (this.props.data['config-status'] && this.props.data['config-status'] != 'configured') || false;
         return (
             <div className="nsConfigPrimitives vnfrConfigPrimitives">
                 <div className="launchpadCard_title">
-                  SERVICE-PRIMITIVES {displayConfigStatus}
+                  CONFIG-PRIMITIVES {displayConfigStatus}
                 </div>
                 <div className={isConfiguring ? 'configuring': 'nsConfigPrimitiveTabs'}>
                     <Tabs onSelect={this.handleSelect}>
index ab12fcd..f840b44 100644 (file)
@@ -49,15 +49,15 @@ function Component(node, prop, count, length) {
     //' + String((100/length) - 5) + '%'
     switch(node["widget-type"]) {
       case 'GAUGE':
-        // Disabled for OSM
+        // Disabked fir OSM
         // return (
-        //     <div
-        //     className="monitoring_params_component"
-        //     key={prop + '-' + prop.name+ '-' + count}
-        //     mp={node["mp-id"]}>
-        //         <div>{node.name}</div>
-        //         <Components.default.Gauge value={node[valueProperty] || 0} min={numericConstraintsMinPresent ? node['numeric-constraints']['min-value'] : 0} max={numericConstraintsMaxPresent ? node['numeric-constraints']['max-value'] : 100} units={node['units'] || ''} />
-        //     </div>);
+        //    <div
+        //    className="monitoring_params_component"
+        //    key={prop + '-' + prop.name+ '-' + count}
+        //    mp={node["mp-id"]}>
+        //        <div>{node.name}</div>
+        //        <Components.default.Gauge value={node[valueProperty] || 0} min={numericConstraintsMinPresent ? node['numeric-constraints']['min-value'] : 0} max={numericConstraintsMaxPresent ? node['numeric-constraints']['max-value'] : 100} units={node['units'] || ''} />
+        //    </div>);
         // break;
       case 'TEXTBOX':
       case 'COUNTER':
index 43c871b..7314a5b 100644 (file)
@@ -84,7 +84,8 @@
     display: flex;
 }
 .monitoring_params_component {
-  flex: 1 1;
+    flex: 1 1;
+    flex-basis: 20%;
     text-align:center;
     display: flex;
     flex-direction: column;
index 2b9b4c1..228b741 100644 (file)
@@ -26,13 +26,14 @@ export default class NsCardPanel extends React.Component {
                         return  (
                           <LaunchpadCard deleting={nsr.deleting}
                                 slideno={this.props.slideno}
-                                key={index}
+                                key={nsr_id}
                                 id={nsr_id}
                                 name={nsr.name}
                                 data={nsr.data}
                                 nsr={nsr}
                                 isActive={nsr["admin-status"] == "ENABLED"}
-                                closeButtonAction={this.onCloseCard(nsr.id)}/>
+                                closeButtonAction={this.onCloseCard(nsr.id)}
+                                hasAccess={this.props.hasAccess}/>
                         );
                       }
                     }
index c391392..8a1334d 100644 (file)
@@ -6,7 +6,9 @@ import DashboardCard from 'widgets/dashboard_card/dashboard_card.jsx';
 import LaunchpadFleetActions from'../launchpadFleetActions';
 import LaunchpadFleetStore from '../launchpadFleetStore';
 import UpTime from 'widgets/uptime/uptime.jsx';
-
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+const PROJECT_ROLES = ROLES.PROJECT;
 /*
  * TODO: Handle when page is loading. See recordView for ref
  */
@@ -367,7 +369,7 @@ export default class NsListPanel extends React.Component {
             return (
                 <DashboardCard className="nsListPanel" showHeader={true}
                     title={title}>
-                    {this.panelToolbar()}
+                    {isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]) ? this.panelToolbar() : null}
                     <a onClick={this.handleShowHideToggle(!isVisible)}
                         className={"nsListPanelToggle"}>
                         <span className="oi"
@@ -396,7 +398,8 @@ export default class NsListPanel extends React.Component {
     }
 }
 NsListPanel.contextTypes = {
-    router: React.PropTypes.object
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
 };
 NsListPanel.defaultProps = {
     isVisible: true
index ddb4779..06110f7 100644 (file)
@@ -35,9 +35,14 @@ import LaunchpadFleetStore from '../launchpadFleetStore.js';
 import _forEach from 'lodash/forEach';
 import Prism from 'prismjs';
 import 'prismjs/themes/prism.css';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
 
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
 
-export default class RecordCard extends React.Component {
+const PROJECT_ROLES = ROLES.PROJECT;
+
+class RecordCard extends React.Component {
   constructor(props) {
     super(props)
   }
@@ -77,6 +82,8 @@ export default class RecordCard extends React.Component {
 
     let notice = null;
 
+    let hasAccess = isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]);
+
     switch(this.props.type) {
       case 'vnfr' :
         cardData = this.props.data[0];
@@ -86,7 +93,7 @@ export default class RecordCard extends React.Component {
         if (displayConfigPrimitives) {
           configPrimitiveComponent = (
             <div className="flex vnfrConfigPrimitiveContainer">
-              <VnfrConfigPrimitives data={configPrimitivesProps} />
+              <VnfrConfigPrimitives data={configPrimitivesProps} hasAccess={hasAccess} />
             {/* <NsrPrimitiveJobList jobs={cardData['config-agent-job']}/> */}
             <div style={{display:'flex', flexDirection: 'column',     flex: '1 1 40%'}}>
                 <div className="launchpadCard_title">
@@ -193,7 +200,7 @@ export default class RecordCard extends React.Component {
         if (displayConfigPrimitives) {
           configPrimitiveComponent = (
             <div className="flex nsConfigPrimitiveContainer">
-              <NsrConfigPrimitives data={configPrimitivesProps} />
+              <NsrConfigPrimitives data={configPrimitivesProps} hasAccess={hasAccess}  />
               <div style={{display:'flex', flexDirection: 'column',     flex: '1 1 40%'}}>
                 <div className="launchpadCard_title">
                   JOB LIST
@@ -249,7 +256,7 @@ export default class RecordCard extends React.Component {
     let metricsAndParams = [];
 
 
-    let nfviMetrics = <LpCardNfviMetrics data={cardData["nfvi-metrics"]} />;
+    let nfviMetrics = null //<LpCardNfviMetrics data={cardData["nfvi-metrics"]} />;
     metricsAndParams.push(<div className="monitoringParams" key="mp">
                           {components.map(function(c, k) {
                             return <div key={k} className="mpSlide">{c.title}{c.component}</div>
@@ -331,7 +338,7 @@ export default class RecordCard extends React.Component {
           if (this.props.type == 'nsr') {
             primitivesTabTitle = 'Service Primitive';
           } else if (this.props.type == 'vnfr') {
-            primitivesTabTitle = 'Service Primitive'
+            primitivesTabTitle = 'Config Primitive'
           }
 
           tabList.push(
@@ -421,3 +428,9 @@ RecordCard.defaultProps = {
   isLoading: true,
   jobData: []
 }
+RecordCard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+export default SkyquakeComponent(RecordCard);
index 224bad5..05c6e5e 100644 (file)
@@ -1,6 +1,6 @@
 
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -89,7 +89,7 @@ export default class RecordView extends React.Component {
 
     let nav = <AppHeader nav={navItems} />
     if (this.state.showRecordDetails) {
-      recordDetails = <RecordDetails isLoading={this.state.detailLoading} data={this.state.rawData} />
+    recordDetails = <RecordDetails isLoading={this.state.detailLoading} data={this.state.rawData} />
     }
     html = (
       <div className="app-body recordView">
index 49c3122..6af92ce 100644 (file)
@@ -33,7 +33,7 @@ export default {
             remote: function(state, recordID) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + recordID + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(recordID) + '?api_server=' + API_SERVER,
                         type: 'GET',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -55,7 +55,7 @@ export default {
             remote: function(state, vnfrID) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'passthrough/data/api/operational/vnfr-catalog/vnfr/' + vnfrID + '?api_server=' + API_SERVER,
+                        url: 'passthrough/data/api/operational/vnfr-catalog/vnfr/' + encodeURIComponent(vnfrID) + '?api_server=' + API_SERVER,
                         type: 'GET',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -74,7 +74,7 @@ export default {
             remote: function(state, nsrID) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'passthrough/data/api/operational/ns-instance-opdata/nsr/' + nsrID + '?api_server=' + API_SERVER,
+                        url: 'passthrough/data/api/operational/ns-instance-opdata/nsr/' + encodeURIComponent(nsrID) + '?api_server=' + API_SERVER,
                         type: 'GET',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -94,11 +94,11 @@ export default {
                 return new Promise(function(resolve, reject) {
                     console.log('Getting NSR Socket');
                     $.ajax({
-                        url: '/socket-polling?api_server=' + API_SERVER,
+                        url: '/socket-polling',
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         data: {
-                            url: '/launchpad/api/nsr/' + recordID + '?api_server=' + API_SERVER,
+                            url: '/launchpad/api/nsr/' + encodeURIComponent(recordID) + '?api_server=' + API_SERVER,
                         },
                         success: function(data) {
                             Utils.checkAndResolveSocketRequest(data, resolve, reject);
@@ -117,11 +117,11 @@ export default {
                 return new Promise(function(resolve, reject) {
                     console.log('Getting Job Socket');
                     $.ajax({
-                        url: '/socket-polling?api_server=' + API_SERVER,
+                        url: '/socket-polling',
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         data: {
-                            url: '/launchpad/api/nsr/' + recordID + '?api_server=' + API_SERVER,
+                            url: '/launchpad/api/nsr/' + encodeURIComponent(recordID) + '?api_server=' + API_SERVER,
                         },
                         success: function(data) {
                             Utils.checkAndResolveSocketRequest(data, resolve, reject);
@@ -139,11 +139,11 @@ export default {
                 return new Promise(function(resolve, reject) {
                     console.log('Getting VNFR Socket for: ' + state.recordID);
                     $.ajax({
-                        url: '/socket-polling?api_server=' + API_SERVER,
+                        url: '/socket-polling',
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         data: {
-                            url: '/launchpad/api/vnfr/' + state.recordID + '?api_server=' + API_SERVER,
+                            url: '/launchpad/api/vnfr/' + encodeURIComponent(state.recordID) + '?api_server=' + API_SERVER,
                         },
                         success: function(data) {
                             Utils.checkAndResolveSocketRequest(data, resolve, reject);
@@ -182,7 +182,7 @@ export default {
             remote(state, params) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + params.nsr_id + '/' + params.scaling_group_id + '/instance' + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(params.nsr_id) + '/' + encodeURIComponent(params.scaling_group_id) + '/instance' + '?api_server=' + API_SERVER,
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -201,7 +201,7 @@ export default {
             remote(state, params) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + params.nsr_id + '/' + params.scaling_group_id + '/instance/' + params.scaling_instance_index + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(params.nsr_id) + '/' + encodeURIComponent(params.scaling_group_id) + '/instance/' + params.scaling_instance_index + '?api_server=' + API_SERVER,
                         type: 'DELETE',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -220,7 +220,7 @@ export default {
             remote(state, params) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + params.nsr_id + '/vld' + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(params.nsr_id) + '/vld' + '?api_server=' + API_SERVER,
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -239,7 +239,7 @@ export default {
             remote(state, params) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + params.nsr_id + '/vld/' + params.vldId + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(params.nsr_id) + '/vld/' + encodeURIComponent(params.vldId) + '?api_server=' + API_SERVER,
                         type: 'DELETE',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -258,7 +258,7 @@ export default {
             remote(state, params) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + params.nsr_id + '/vld/' + params.vldId + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(params.nsr_id) + '/vld/' + encodeURIComponent(params.vldId) + '?api_server=' + API_SERVER,
                         type: 'PUT',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
index 72609e4..b9cd86c 100644 (file)
@@ -139,7 +139,7 @@ class RecordViewStore {
         let configPrimitiveIndex = data.configPrimitiveIndex;
         let payload = {};
         let isValid = true;
-        let configPrimitive = vnfrs[vnfrIndex]['vnf-configuration']['service-primitive'][configPrimitiveIndex];
+        let configPrimitive = vnfrs[vnfrIndex]['vnf-configuration']['config-primitive'][configPrimitiveIndex];
 
         payload['name'] = '';
         payload['nsr_id_ref'] = vnfrs[vnfrIndex]['nsr-id-ref'];
index 756c775..fd38d84 100644 (file)
@@ -41,7 +41,7 @@ export default {
             return resolve(false);
           }
            $.ajax({
-            url: '/socket-polling?api_server=' + API_SERVER,
+            url: '/socket-polling',
             type: 'POST',
             beforeSend: Utils.addAuthorizationStub,
             data: {
index b570082..a02c56e 100644 (file)
@@ -42,11 +42,11 @@ export default {
             return resolve(false);
           }
            $.ajax({
-            url: '/socket-polling?api_server=' + API_SERVER,
+            url: '/socket-polling',
             type: 'POST',
             beforeSend: Utils.addAuthorizationStub,
             data: {
-              url: '/launchpad/api/nsr/' + id + '/compute-topology?api_server=' + API_SERVER
+              url: '/launchpad/api/nsr/' + encodeURIComponent(id) + '/compute-topology?api_server=' + API_SERVER
             },
             success: function(data, textStatus, jqXHR) {
               Utils.checkAndResolveSocketRequest(data, resolve, reject);
@@ -68,7 +68,7 @@ export default {
         id = 0;
         return new Promise(function (resolve, reject) {
           $.ajax({
-            url: '/launchpad/api/nsr/' + id + '/compute-topology?api_server=' + API_SERVER,
+            url: '/launchpad/api/nsr/' + encodeURIComponent(id) + '/compute-topology?api_server=' + API_SERVER,
             type: 'GET',
             beforeSend: Utils.addAuthorizationStub,
             contentType: "application/json",
@@ -95,7 +95,7 @@ export default {
       remote: function(state, vnfrID) {
         return new Promise(function(resolve, reject) {
           $.ajax({
-            url: 'passthrough/data/api/operational/vnfr-catalog/vnfr/' + vnfrID + '?api_server=' + API_SERVER,
+            url: 'passthrough/data/api/operational/vnfr-catalog/vnfr/' + encodeURIComponent(vnfrID) + '?api_server=' + API_SERVER,
             type: 'GET',
             beforeSend: Utils.addAuthorizationStub,
             success: function(data) {
@@ -114,7 +114,7 @@ export default {
       remote: function(state, nsrID) {
         return new Promise(function(resolve, reject) {
           $.ajax({
-            url: 'passthrough/data/api/operational/ns-instance-opdata/nsr/' + nsrID + '?api_server=' + API_SERVER,
+            url: 'passthrough/data/api/operational/ns-instance-opdata/nsr/' + encodeURIComponent(nsrID) + '?api_server=' + API_SERVER,
             type: 'GET',
             beforeSend: Utils.addAuthorizationStub,
             success: function(data) {
@@ -133,7 +133,7 @@ export default {
       remote: function(state, vdurID, vnfrID) {
         return new Promise(function(resolve, reject) {
           $.ajax({
-            url: 'passthrough/data/api/operational/vnfr-catalog/vnfr/' + vnfrID + '/vdur/' + vdurID + '?api_server=' + API_SERVER,
+            url: 'passthrough/data/api/operational/vnfr-catalog/vnfr/' + encodeURIComponent(vnfrID) + '/vdur/' + encodeURIComponent(vdurID) + '?api_server=' + API_SERVER,
             type: 'GET',
             beforeSend: Utils.addAuthorizationStub,
             success: function(data) {
index f4f8eab..7e4f02a 100644 (file)
@@ -34,7 +34,7 @@ export default {
             remote(state, nsrId, payload) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + nsrId + '/vld' + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(nsrId) + '/vld' + '?api_server=' + API_SERVER,
                         type: 'POST',
                         beforeSend: Utils.addAuthorizationStub,
                         dataType:'json',
@@ -59,7 +59,7 @@ export default {
             remote(state, nsrId, vldId) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + nsrId + '/vld/' + vldId + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(nsrId) + '/vld/' + encodeURIComponent(vldId) + '?api_server=' + API_SERVER,
                         type: 'DELETE',
                         beforeSend: Utils.addAuthorizationStub,
                         success: function(data) {
@@ -82,7 +82,7 @@ export default {
             remote(state, nsrId, vldId, vld) {
                 return new Promise(function(resolve, reject) {
                     $.ajax({
-                        url: 'api/nsr/' + nsrId + '/vld/' + vldId + '?api_server=' + API_SERVER,
+                        url: 'api/nsr/' + encodeURIComponent(nsrId) + '/vld/' + encodeURIComponent(vldId) + '?api_server=' + API_SERVER,
                         type: 'PUT',
                         beforeSend: Utils.addAuthorizationStub,
                         dataType:'json',
index 37cf0b3..aa4fe43 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 
+ *
  *   Copyright 2016 RIFT.IO Inc
  *
  *   Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,11 +25,16 @@ import UpTime from 'widgets/uptime/uptime.jsx';
 import NSVirtualLinkDetails from './nsVirtualLinkDetails.jsx';
 import NSVirtualLinkCreate from './nsVirtualLinkCreate.jsx';
 import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import ROLES from 'utils/roleConstants.js';
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
 
 class NsVirtualLinks extends React.Component {
        constructor(props) {
                super(props);
-           this.Store = this.props.flux.stores.hasOwnProperty('NSVirtualLinkCreateStore') ? 
+           this.Store = this.props.flux.stores.hasOwnProperty('NSVirtualLinkCreateStore') ?
                                this.props.flux.stores.NSVirtualLinkCreateStore : this.props.flux.createStore(NSVirtualLinkCreateStore, 'NSVirtualLinkCreateStore');
                this.state = {};
                this.state.mode = 'viewing';    // Can be 'viewing'/'creating'/'editing'/'deleting'. Default is 'viewing'
@@ -59,7 +64,7 @@ class NsVirtualLinks extends React.Component {
                if (!this.state.nsd) {
                        this.setState({
                                nsd: this.props.data.nsd
-                       });     
+                       });
                }
 
                if (!this.state.nsrId) {
@@ -78,7 +83,7 @@ class NsVirtualLinks extends React.Component {
                if (!this.state.nsd) {
                        this.setState({
                                nsd: nextProps.data.nsd
-                       });     
+                       });
                }
 
                if (!this.state.nsrId) {
@@ -149,14 +154,18 @@ class NsVirtualLinks extends React.Component {
                                <tr key={vlrIndex} className={selectedClassName} onClick={this.handleSelectVirtualLinkClick.bind(this, vlrId)}>
                                        <td>{name}</td>
                                        <td>{operationalStatus}</td>
-                                       <td>
-                                               <a onClick={this.handleEditVirtualLinkClick.bind(this, this.props.data.id, vlrId, vldId)}>
-                                                       <span className="oi" data-glyph="pencil" aria-hidden="true"></span>
-                                               </a>
-                                               <a onClick={this.handleDeleteVirtualLinkClick.bind(this, this.props.data.id, vldId)}>
-                                                       <span className="oi" data-glyph="trash" aria-hidden="true"></span>
-                                               </a>
-                                       </td>
+                                       {
+                                               isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]) ?
+                                               <td>
+                                                       <a onClick={this.handleEditVirtualLinkClick.bind(this, this.props.data.id, vlrId, vldId)}>
+                                                               <span className="oi" data-glyph="pencil" aria-hidden="true"></span>
+                                                       </a>
+                                                       <a onClick={this.handleDeleteVirtualLinkClick.bind(this, this.props.data.id, vldId)}>
+                                                               <span className="oi" data-glyph="trash" aria-hidden="true"></span>
+                                                       </a>
+                                               </td>
+                                               : null
+                                       }
                                </tr>
                        );
                });
@@ -173,7 +182,10 @@ class NsVirtualLinks extends React.Component {
                                <tr>
                                        <th style={{width: '50%'}}>Name</th>
                                    <th style={{width: '35%'}}>Status</th>
-                                   <th style={{width: '15%'}}> </th>
+                                   {
+                                               isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]) ?
+                                   <th style={{width: '15%'}}> </th> : null
+                               }
                                </tr>
                            </thead>
                            {tbody}
@@ -224,7 +236,7 @@ class NsVirtualLinks extends React.Component {
                                {nsVirtualLinksTable}
                        </div>
                        <div className='nsVirtualLinksCreateButtonWrapper'>
-                               {nsVirtualLinkCreateButton}
+                               { isRBACValid(this.context.userProfile, [PROJECT_ROLES.LCM_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]) ? nsVirtualLinkCreateButton : null}
                        </div>
                 </div>
                 {nsVirtualLinkDetails}
@@ -232,4 +244,4 @@ class NsVirtualLinks extends React.Component {
                );
        }
 }
-export default SkyquakeComponent(NsVirtualLinks);
\ No newline at end of file
+export default SkyquakeComponent(NsVirtualLinks);
index 327bdb9..17384b8 100644 (file)
@@ -42,11 +42,11 @@ export default {
           }
           console.log(nsr_id)
           $.ajax({
-            url: '//' + window.location.hostname + ':' + window.location.port + '/socket-polling?api_server=' + API_SERVER,
+            url: '//' + window.location.hostname + ':' + window.location.port + '/socket-polling',
             type: 'POST',
             beforeSend: Utils.addAuthorizationStub,
             data: {
-              url: API_SERVER + ':' + NODE_PORT + '/launchpad/nsr/' + nsr_id + '/vnfr?api_server=' + API_SERVER,
+              url: API_SERVER + ':' + NODE_PORT + '/launchpad/nsr/' + encodeURIComponent(nsr_id) + '/vnfr?api_server=' + API_SERVER,
             },
             success: function(data) {
               Utils.checkAndResolveSocketRequest(data, resolve, reject);
index ed2a23f..486ac81 100644 (file)
@@ -27,6 +27,7 @@ var CompressionPlugin = require("compression-webpack-plugin");
 
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -67,8 +68,8 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
-            templateContent: '<div id="app"></div>'
+            filename: '../' + htmlFilename
+            template: frameworkPath + '/plugin-index.html'
         })
     ]
 };
index 06c27d8..08103d8 100644 (file)
@@ -43,7 +43,7 @@ var Test = {};
 function buildGetRequestOptions(req, endpoint) {
   var headers = _.extend({},
     constants.HTTP_HEADERS.accept.data, {
-    'Authorization': req.get('Authorization')
+    'Authorization': req.session && req.session.authorization
   });
   var api_server = req.query["api_server"];
   var requestOptions = {
@@ -60,7 +60,7 @@ function buildPutRequestOptions(req, endpoint, jsonData) {
   var headers = _.extend({},
     constants.HTTP_HEADERS.accept.data,
     constants.HTTP_HEADERS.content_type.data, {
-    'Authorization': req.get('Authorization')
+    'Authorization': req.session && req.session.authorization
   });
   var api_server = req.query["api_server"];
   var requestOptions = {
@@ -79,7 +79,7 @@ function buildDeleteRequestOptions(req, endpoint) {
   var headers = _.extend({},
     constants.HTTP_HEADERS.accept.data,
     constants.HTTP_HEADERS.content_type.data, {
-    'Authorization': req.get('Authorization')
+    'Authorization': req.session && req.session.authorization
   });
   var api_server = req.query["api_server"];
   var requestOptions = {
@@ -447,7 +447,7 @@ SysLogViewer.get = function(req) {
       headers: _.extend({},
         constants.HTTP_HEADERS.accept.data,
         {
-          'Authorization': req.get('Authorization')
+          'Authorization': req.session && req.session.authorization
         }),
       forever: foreverOn,
       rejectUnauthorized: false
index 2e2b8dc..301b1eb 100644 (file)
@@ -40,7 +40,8 @@ Support.severities = function() {
  * Class to convert RESTConf data to logging plugin
  */
 
-
+var LoggingConfigDecoder = {};
+var LoggingConfigEncoder = {};
 
 LoggingConfigDecoder = function(debugMode) {
   this.debugMode = debugMode || false
index f68358d..3c9ba98 100644 (file)
@@ -2,8 +2,14 @@
     "root": "public",
     "name": "Logging",
     "dashboard": "./loggingGeneral.jsx",
-    "order": 101,
+    "order": 3,
     "priority":2,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper"
+    ],
     "routes": [
         {
             "label": "Logging",
index 056f0f2..5efe68f 100644 (file)
@@ -6,6 +6,7 @@
   "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",
index 569bc06..f99da36 100755 (executable)
 
 PLUGIN_NAME=logging
 # 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 --optimize-minimize --progress --config webpack.production.config.js
-echo 'Packaging '$PLUGIN_NAME' using webpack... done'
-echo 'Building plugin '$PLUGIN_NAME'... done'
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
index e6fdb86..2e824fd 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# 
+#
 #   Copyright 2016 RIFT.IO Inc
 #
 #   Licensed under the Apache License, Version 2.0 (the "License");
@@ -44,5 +44,5 @@ 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
+tar -cf $bcache_dir/plugins/$plugin/node_modules.tar node_modules package.json -C $source_dir
 #cp -Lrp $source_dir/node_modules $bcache_dir/plugins/$plugin/.
index 2810ac8..dfba167 100644 (file)
@@ -75,6 +75,9 @@ export default {
                 allowDuplicateEvents: loggingConfig.allowDuplicateEvents
               },
               success: function(data) {
+                if (!loggingConfig.allowDuplicateEvents) {
+                  delete state.loggingConfig.allowDuplicateEvents
+                }
                 resolve(data);
               },
               error: function(error) {
index 3c306ae..a8bb337 100644 (file)
@@ -63,6 +63,7 @@ class LoggingStore {
     console.log("LoggingStore.putLoggingConfigSuccess called. data=", data);
     const initialLoggingConfig = _cloneDeep(this.loggingConfig);
     this.setState({
+      nulledCategories: [],
       isLoading: false,
       initialLoggingConfig: initialLoggingConfig
     });
index a4e4834..9723428 100644 (file)
@@ -26,6 +26,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
 var CompressionPlugin = require("compression-webpack-plugin");
 // Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
 process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
 var config = {
     devtool: 'source-map',
     entry: mainPath,
@@ -66,8 +67,8 @@ var config = {
     },
     plugins: [
         new HtmlWebpackPlugin({
-            filename: '../index.html'
-            templateContent: '<div id="app"></div>'
+            filename: '../' + htmlFilename
+            template: frameworkPath + '/plugin-index.html'
         })
     ]
 };
index c92589f..2cfab02 100644 (file)
@@ -15,6 +15,13 @@ acorn@^3.0.0, acorn@^3.1.0:
   version "3.3.0"
   resolved "https://npm.riftio.com:4873/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
+ajv@^4.9.1:
+  version "4.11.8"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+  dependencies:
+    co "^4.6.0"
+    json-stable-stringify "^1.0.1"
+
 align-text@^0.1.1, align-text@^0.1.3:
   version "0.1.4"
   resolved "https://npm.riftio.com:4873/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -146,7 +153,7 @@ async@^1.3.0, async@^1.4.0:
   version "1.5.2"
   resolved "https://npm.riftio.com:4873/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
-async@~0.2.6:
+async@~0.2.6, async@0.2.x:
   version "0.2.10"
   resolved "https://npm.riftio.com:4873/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
@@ -941,7 +948,7 @@ browserslist@~1.4.0:
   dependencies:
     caniuse-db "^1.0.30000539"
 
-buffer-shims@^1.0.0:
+buffer-shims@^1.0.0, buffer-shims@~1.0.0:
   version "1.0.0"
   resolved "https://npm.riftio.com:4873/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
 
@@ -1003,6 +1010,10 @@ caseless@~0.11.0:
   version "0.11.0"
   resolved "https://npm.riftio.com:4873/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
 
+caseless@~0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
 center-align@^0.1.1:
   version "0.1.3"
   resolved "https://npm.riftio.com:4873/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
@@ -1080,6 +1091,10 @@ clone@^1.0.2:
   version "1.0.2"
   resolved "https://npm.riftio.com:4873/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
 
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
 coa@~1.0.1:
   version "1.0.1"
   resolved "https://npm.riftio.com:4873/coa/-/coa-1.0.1.tgz#7f959346cfc8719e3f7233cd6852854a7c67d8a3"
@@ -1132,7 +1147,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.5.0, commander@^2.9.0, commander@2.9.x:
+commander@^2.5.0, commander@^2.8.1, commander@^2.9.0, commander@2.9.x:
   version "2.9.0"
   resolved "https://npm.riftio.com:4873/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
   dependencies:
@@ -1168,6 +1183,15 @@ compressible@~2.0.8:
   dependencies:
     mime-db ">= 1.24.0 < 2"
 
+compression-webpack-plugin@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-0.3.2.tgz#1edfb0e749d7366d3e701670c463359b2c0cf704"
+  dependencies:
+    async "0.2.x"
+    webpack-sources "^0.1.0"
+  optionalDependencies:
+    node-zopfli "^2.0.0"
+
 compression@^1.5.2:
   version "1.6.2"
   resolved "https://npm.riftio.com:4873/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3"
@@ -1403,6 +1427,12 @@ deep-extend@~0.4.0:
   version "0.4.1"
   resolved "https://npm.riftio.com:4873/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253"
 
+defaults@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
+  dependencies:
+    clone "^1.0.2"
+
 defined@^1.0.0:
   version "1.0.0"
   resolved "https://npm.riftio.com:4873/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -1818,7 +1848,7 @@ fsevents@^1.0.0:
     nan "^2.3.0"
     node-pre-gyp "^0.6.29"
 
-fstream-ignore@~1.0.5:
+fstream-ignore@^1.0.5, fstream-ignore@~1.0.5:
   version "1.0.5"
   resolved "https://npm.riftio.com:4873/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
   dependencies:
@@ -1835,6 +1865,15 @@ fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10:
     mkdirp ">=0.5 0"
     rimraf "2"
 
+fstream@^1.0.10:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+  dependencies:
+    graceful-fs "^4.1.2"
+    inherits "~2.0.0"
+    mkdirp ">=0.5 0"
+    rimraf "2"
+
 function-bind@^1.0.2:
   version "1.1.0"
   resolved "https://npm.riftio.com:4873/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
@@ -1957,6 +1996,10 @@ graceful-fs@^4.1.2:
   version "1.0.1"
   resolved "https://npm.riftio.com:4873/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
 
+har-schema@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
 har-validator@~2.0.6:
   version "2.0.6"
   resolved "https://npm.riftio.com:4873/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
@@ -1966,6 +2009,13 @@ har-validator@~2.0.6:
     is-my-json-valid "^2.12.4"
     pinkie-promise "^2.0.0"
 
+har-validator@~4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+  dependencies:
+    ajv "^4.9.1"
+    har-schema "^1.0.5"
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://npm.riftio.com:4873/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -2389,6 +2439,12 @@ json-schema@0.2.3:
   version "0.2.3"
   resolved "https://npm.riftio.com:4873/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
 
+json-stable-stringify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+  dependencies:
+    jsonify "~0.0.0"
+
 json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://npm.riftio.com:4873/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -2407,6 +2463,10 @@ json5@^0.5.0:
   version "0.5.1"
   resolved "https://npm.riftio.com:4873/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
 
+jsonify@~0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
 jsonpointer@^4.0.0:
   version "4.0.0"
   resolved "https://npm.riftio.com:4873/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5"
@@ -2668,6 +2728,10 @@ ms@0.7.2:
   version "0.7.2"
   resolved "https://npm.riftio.com:4873/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"
 
+nan@^2.0.0:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
+
 nan@^2.3.0, nan@^2.3.2:
   version "2.4.0"
   resolved "https://npm.riftio.com:4873/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232"
@@ -2756,6 +2820,20 @@ node-pre-gyp@^0.6.29:
     tar "~2.2.1"
     tar-pack "~3.3.0"
 
+node-pre-gyp@^0.6.4:
+  version "0.6.34"
+  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.34.tgz#94ad1c798a11d7fc67381b50d47f8cc18d9799f7"
+  dependencies:
+    mkdirp "^0.5.1"
+    nopt "^4.0.1"
+    npmlog "^4.0.2"
+    rc "^1.1.7"
+    request "^2.81.0"
+    rimraf "^2.6.1"
+    semver "^5.3.0"
+    tar "^2.2.1"
+    tar-pack "^3.4.0"
+
 node-sass@^3.4.2:
   version "3.13.1"
   resolved "https://npm.riftio.com:4873/node-sass/-/node-sass-3.13.1.tgz#7240fbbff2396304b4223527ed3020589c004fc2"
@@ -2777,6 +2855,22 @@ node-sass@^3.4.2:
     request "^2.61.0"
     sass-graph "^2.1.1"
 
+node-zopfli@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/node-zopfli/-/node-zopfli-2.0.2.tgz#a7a473ae92aaea85d4c68d45bbf2c944c46116b8"
+  dependencies:
+    commander "^2.8.1"
+    defaults "^1.0.2"
+    nan "^2.0.0"
+    node-pre-gyp "^0.6.4"
+
+nopt@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+  dependencies:
+    abbrev "1"
+    osenv "^0.1.4"
+
 nopt@~3.0.6, "nopt@2 || 3":
   version "3.0.6"
   resolved "https://npm.riftio.com:4873/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
@@ -2815,7 +2909,7 @@ normalizr@^2.1.0:
   dependencies:
     lodash "^4.17.2"
 
-npmlog@^4.0.0, npmlog@^4.0.1:
+npmlog@^4.0.0, npmlog@^4.0.1, npmlog@^4.0.2:
   version "4.0.2"
   resolved "https://npm.riftio.com:4873/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
   dependencies:
@@ -2876,7 +2970,7 @@ on-headers@~1.0.1:
   version "1.0.1"
   resolved "https://npm.riftio.com:4873/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
 
-once@^1.3.0:
+once@^1.3.0, once@^1.3.3:
   version "1.4.0"
   resolved "https://npm.riftio.com:4873/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   dependencies:
@@ -2927,7 +3021,7 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
   version "1.0.2"
   resolved "https://npm.riftio.com:4873/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
 
-osenv@0:
+osenv@^0.1.4, osenv@0:
   version "0.1.4"
   resolved "https://npm.riftio.com:4873/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
   dependencies:
@@ -2999,6 +3093,10 @@ pbkdf2-compat@2.0.1:
   version "2.0.1"
   resolved "https://npm.riftio.com:4873/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz#b6e0c8fa99494d94e0511575802a59a5c142f288"
 
+performance-now@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://npm.riftio.com:4873/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -3324,6 +3422,10 @@ qs@~6.3.0:
   version "6.3.0"
   resolved "https://npm.riftio.com:4873/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
 
+qs@~6.4.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
 qs@6.2.0:
   version "6.2.0"
   resolved "https://npm.riftio.com:4873/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
@@ -3364,6 +3466,15 @@ range-parser@^1.0.3, range-parser@~1.2.0:
   version "1.2.0"
   resolved "https://npm.riftio.com:4873/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
 
+rc@^1.1.7:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
+  dependencies:
+    deep-extend "~0.4.0"
+    ini "~1.3.0"
+    minimist "^1.2.0"
+    strip-json-comments "~2.0.1"
+
 rc@~1.1.6:
   version "1.1.6"
   resolved "https://npm.riftio.com:4873/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
@@ -3464,6 +3575,18 @@ read-pkg@^1.0.0:
     string_decoder "~0.10.x"
     util-deprecate "~1.0.1"
 
+readable-stream@^2.1.4:
+  version "2.2.9"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.9.tgz#cf78ec6f4a6d1eb43d26488cac97f042e74b7fc8"
+  dependencies:
+    buffer-shims "~1.0.0"
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "~1.0.0"
+    process-nextick-args "~1.0.6"
+    string_decoder "~1.0.0"
+    util-deprecate "~1.0.1"
+
 readable-stream@~2.1.4:
   version "2.1.5"
   resolved "https://npm.riftio.com:4873/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
@@ -3634,6 +3757,33 @@ request@^2.34, request@^2.61.0, request@^2.79.0, request@2:
     tunnel-agent "~0.4.1"
     uuid "^3.0.0"
 
+request@^2.81.0:
+  version "2.81.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+  dependencies:
+    aws-sign2 "~0.6.0"
+    aws4 "^1.2.1"
+    caseless "~0.12.0"
+    combined-stream "~1.0.5"
+    extend "~3.0.0"
+    forever-agent "~0.6.1"
+    form-data "~2.1.1"
+    har-validator "~4.2.1"
+    hawk "~3.1.3"
+    http-signature "~1.1.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.7"
+    oauth-sign "~0.8.1"
+    performance-now "^0.2.0"
+    qs "~6.4.0"
+    safe-buffer "^5.0.1"
+    stringstream "~0.0.4"
+    tough-cookie "~2.3.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.0.0"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://npm.riftio.com:4873/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -3652,6 +3802,12 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
+rimraf@^2.5.1, rimraf@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+  dependencies:
+    glob "^7.0.5"
+
 rimraf@~2.5.1, rimraf@~2.5.4, rimraf@2:
   version "2.5.4"
   resolved "https://npm.riftio.com:4873/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
@@ -3662,6 +3818,10 @@ ripemd160@0.2.0:
   version "0.2.0"
   resolved "https://npm.riftio.com:4873/ripemd160/-/ripemd160-0.2.0.tgz#2bf198bde167cacfa51c0a928e84b68bbe171fce"
 
+safe-buffer@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
+
 sass-graph@^2.1.1:
   version "2.1.2"
   resolved "https://npm.riftio.com:4873/sass-graph/-/sass-graph-2.1.2.tgz#965104be23e8103cb7e5f710df65935b317da57b"
@@ -3686,7 +3846,7 @@ select@^1.0.6:
   version "1.1.0"
   resolved "https://npm.riftio.com:4873/select/-/select-1.1.0.tgz#a6c520cd9ab919ad81c7d1a273e0452f504dd7a2"
 
-semver@~5.3.0, "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5":
+semver@^5.3.0, semver@~5.3.0, "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5":
   version "5.3.0"
   resolved "https://npm.riftio.com:4873/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
 
@@ -3809,7 +3969,7 @@ source-map@^0.4.2, source-map@~0.4.1, source-map@0.4.x:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1:
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
   version "0.5.6"
   resolved "https://npm.riftio.com:4873/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
 
@@ -3879,6 +4039,12 @@ string_decoder@^0.10.25, string_decoder@~0.10.x:
   version "0.10.31"
   resolved "https://npm.riftio.com:4873/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
 
+string_decoder@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.0.tgz#f06f41157b664d86069f84bdbdc9b0d8ab281667"
+  dependencies:
+    buffer-shims "~1.0.0"
+
 string-convert@^0.2.0:
   version "0.2.1"
   resolved "https://npm.riftio.com:4873/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
@@ -3917,6 +4083,10 @@ strip-json-comments@~1.0.4:
   version "1.0.4"
   resolved "https://npm.riftio.com:4873/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
 
+strip-json-comments@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
 style-loader@^0.13.0:
   version "0.13.1"
   resolved "https://npm.riftio.com:4873/style-loader/-/style-loader-0.13.1.tgz#468280efbc0473023cd3a6cd56e33b5a1d7fc3a9"
@@ -3953,6 +4123,19 @@ tapable@^0.1.8, tapable@~0.1.8:
   version "0.1.10"
   resolved "https://npm.riftio.com:4873/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
 
+tar-pack@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+  dependencies:
+    debug "^2.2.0"
+    fstream "^1.0.10"
+    fstream-ignore "^1.0.5"
+    once "^1.3.3"
+    readable-stream "^2.1.4"
+    rimraf "^2.5.1"
+    tar "^2.2.1"
+    uid-number "^0.0.6"
+
 tar-pack@~3.3.0:
   version "3.3.0"
   resolved "https://npm.riftio.com:4873/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae"
@@ -3966,7 +4149,7 @@ tar-pack@~3.3.0:
     tar "~2.2.1"
     uid-number "~0.0.6"
 
-tar@^2.0.0, tar@~2.2.1:
+tar@^2.0.0, tar@^2.2.1, tar@~2.2.1:
   version "2.2.1"
   resolved "https://npm.riftio.com:4873/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
   dependencies:
@@ -4018,6 +4201,12 @@ tty-browserify@0.0.0:
   version "0.0.0"
   resolved "https://npm.riftio.com:4873/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
 
+tunnel-agent@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+  dependencies:
+    safe-buffer "^5.0.1"
+
 tunnel-agent@~0.4.1:
   version "0.4.3"
   resolved "https://npm.riftio.com:4873/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
@@ -4050,7 +4239,7 @@ uglify-to-browserify@~1.0.0:
   version "1.0.2"
   resolved "https://npm.riftio.com:4873/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
 
-uid-number@~0.0.6:
+uid-number@^0.0.6, uid-number@~0.0.6:
   version "0.0.6"
   resolved "https://npm.riftio.com:4873/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
 
@@ -4212,6 +4401,13 @@ webpack-dev-server@^1.10.1:
     supports-color "^3.1.1"
     webpack-dev-middleware "^1.4.0"
 
+webpack-sources@^0.1.0:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.5.tgz#aa1f3abf0f0d74db7111c40e500b84f966640750"
+  dependencies:
+    source-list-map "~0.1.7"
+    source-map "~0.5.3"
+
 webpack@^1.3.0:
   version "1.14.0"
   resolved "https://npm.riftio.com:4873/webpack/-/webpack-1.14.0.tgz#54f1ffb92051a328a5b2057d6ae33c289462c823"
diff --git a/skyquake/plugins/project_management/CMakeLists.txt b/skyquake/plugins/project_management/CMakeLists.txt
new file mode 100644 (file)
index 0000000..f994a55
--- /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(
+  project_management
+  DEPENDS skyquake
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
+  CONFIGURE_COMMAND echo
+  BUILD_COMMAND
+    ${CMAKE_CURRENT_BINARY_DIR}/project_management/project_management-build/scripts/build.sh
+  INSTALL_COMMAND
+    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/install.sh
+    ${CMAKE_CURRENT_BINARY_DIR}/project_management/project_management-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/project_management/config.json b/skyquake/plugins/project_management/config.json
new file mode 100644 (file)
index 0000000..f11bd2f
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "root": "public",
+    "name": "Project Management",
+    "dashboard": "./dashboard/dashboard.jsx",
+    "order": 1,
+    "priority":2,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-project:project-admin",
+        "rw-project:project-oper"],
+    "routes": [
+    {
+        "label": "Project Management Dashboard",
+        "route": "project-management",
+        "component": "./dashboard/dashboard.jsx",
+        "type": "internal"
+    }]
+}
diff --git a/skyquake/plugins/project_management/package.json b/skyquake/plugins/project_management/package.json
new file mode 100644 (file)
index 0000000..c459845
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "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",
+    "compression-webpack-plugin": "^0.3.2",
+    "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.10.1"
+  }
+}
diff --git a/skyquake/plugins/project_management/routes.js b/skyquake/plugins/project_management/routes.js
new file mode 100644 (file)
index 0000000..8640a99
--- /dev/null
@@ -0,0 +1,12 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+var app = require('express').Router();
+var cors = require('cors');
+var utils = require('../../framework/core/api_utils/utils.js')
+ // Begin Accounts API
+
+    utils.passThroughConstructor(app);
+
+module.exports = app;
diff --git a/skyquake/plugins/project_management/scripts/build.sh b/skyquake/plugins/project_management/scripts/build.sh
new file mode 100755 (executable)
index 0000000..fc31e0f
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+PLUGIN_NAME=project_management
+# change to the directory of this script
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
diff --git a/skyquake/plugins/project_management/scripts/install.sh b/skyquake/plugins/project_management/scripts/install.sh
new file mode 100755 (executable)
index 0000000..c093337
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+plugin=project_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/project_management/server.js b/skyquake/plugins/project_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/project_management/src/dashboard/dashboard.jsx b/skyquake/plugins/project_management/src/dashboard/dashboard.jsx
new file mode 100644 (file)
index 0000000..fc3e0a5
--- /dev/null
@@ -0,0 +1,475 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import ProjectManagementStore from './projectMgmtStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import SkyquakeRBAC from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import 'style/layout.scss';
+import './projectMgmt.scss';
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+import {InputCollection, FormSection} from 'widgets/form_controls/formControls.jsx';
+
+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'
+import _  from 'lodash';
+import ROLES from 'utils/roleConstants.js';
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
+class ProjectManagementDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('ProjectManagementStore') ? this.props.flux.stores.ProjectManagementStore : this.props.flux.createStore(ProjectManagementStore);
+        this.Store.getProjects();
+        this.Store.getUsers();
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+    }
+    componentDidUpdate() {
+        let self = this;
+        ReactDOM.findDOMNode(this.projectList).addEventListener('transitionend', this.onTransitionEnd, false);
+        setTimeout(function() {
+            let element = self[`project-ref-${self.state.activeIndex}`]
+            element && !isElementInView(element) && element.scrollIntoView({block: 'end', behavior: 'smooth'});
+        })
+    }
+    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)
+    }
+    addProject = () => {
+        this.actions.handleAddProject();
+    }
+    viewProject = (un, index) => {
+        this.actions.viewProject(un, index, true);
+    }
+    editProject = () => {
+        this.actions.editProject(false);
+    }
+    cancelEditProject = () => {
+        this.actions.editProject(true)
+    }
+    closePanel = () => {
+        this.actions.handleCloseProjectPanel();
+    }
+
+    deleteProject = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (confirm('Are you sure you want to delete this project?')) {
+            this.Store.deleteProject({
+                'name': this.state['name']
+            });
+        }
+    }
+    createProject = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let projectName = self.state['name'];
+        let projectUsers = self.state.projectUsers;
+        let cleanUsers = this.cleanUsers(projectUsers, projectName);
+
+
+        this.Store.createProject({
+            'name': projectName,
+            'description': self.state.description,
+            'project-config' : {
+                'user': cleanUsers
+            }
+        });
+    }
+    updateProject = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let projectName = self.state['name'];
+        let projectUsers = _.cloneDeep(self.state.projectUsers);
+        let cleanUsers = this.cleanUsers(projectUsers, projectName);
+
+
+        this.Store.updateProject({
+            'name': projectName,
+            'description': self.state.description,
+            'project-config' : {
+                'user': cleanUsers
+            }
+        });
+    }
+    cleanUsers(projectUsers, projectName) {
+        let self = this;
+        let cleanUsers = [];
+        //Remove null values from role
+        projectUsers.map((u) => {
+            let cleanRoles = [];
+            let cleanManoRoles = [];
+           u.role && u.role.map((r,i) => {
+             let role = {};
+             //you may add a user without a role or a keys, but if one is present then the other must be as well.
+            if(r.role) {
+                delete r.keys;
+                // r.keys = projectName;
+                switch(ROLES.PROJECT.TYPE[r.role]) {
+                    case 'rw-project-mano' : cleanManoRoles.push(r); break;
+                    case 'rw-project' : cleanRoles.push(r); break;
+                }
+            }
+           });
+           u.role = cleanRoles;
+           u["rw-project-mano:mano-role"] = u["rw-project-mano:mano-role"] || [];
+           u["rw-project-mano:mano-role"] = u["rw-project-mano:mano-role"].concat(cleanManoRoles);
+           //if (u['user-name'] != self.context.userProfile.userId) {
+                cleanUsers.push(u);
+           //}
+        });
+        return cleanUsers;
+    }
+     evaluateSubmit = (e) => {
+        if (e.keyCode == 13) {
+            if (this.props.isEdit) {
+                this.updateProject(e);
+            } else {
+                this.createProject(e);
+            }
+            e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    updateSelectedUser = (e) => {
+        this.setState({
+            selected
+        })
+    }
+    addUserToProject = (e) => {
+        let selectUserList = this.selectUserList;
+        console.log(ReactDOM.findDOMNode(selectUserList))
+        this.actions.handleAddUser(e);
+    }
+    removeUserFromProject = (userIndex, e) => {
+        this.actions.handleRemoveUserFromProject(userIndex);
+    }
+    toggleUserRoleInProject = (userIndex, roleIndex, e) => {
+        this.actions.handleToggleUserRoleInProject({
+            userIndex,
+            roleIndex,
+            checked: JSON.parse(e.currentTarget.checked)
+        })
+    }
+    removeRoleFromUserInProject = (userIndex, roleIndex, e) => {
+        this.actions.handleRemoveRoleFromUserInProject({
+            userIndex,
+            roleIndex
+        })
+    }
+    addRoleToUserInProject = (userIndex, e) => {
+        this.actions.addRoleToUserInProject(userIndex);
+    }
+    onTransitionEnd = (e) => {
+        this.actions.handleHideColumns(e);
+        console.log('transition end')
+    }
+    disableChange = (e) => {
+        let value = e.target.value;
+        value = value.toUpperCase();
+        if (value=="TRUE") {
+            value = true;
+        } else {
+            value = false;
+        }
+        console.log(value)
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let passwordSectionHTML = null;
+        let formButtonsHTML = (
+                <ButtonGroup className="buttonGroup">
+                    <Button label="EDIT" type="submit" onClick={this.editProject} />
+                </ButtonGroup>
+        );
+        let projectUsers = [];
+        self.state.projectUsers.map((u) => {
+            projectUsers.push(u);
+        });
+        let availableDomains = state.domains;
+        let availableUsers = state.users && state.users.filter((u) => {
+            return state.selectedDomain == u['user-domain'] && _.findIndex(projectUsers, (s) => {return (s['user-name'] == u['user-name']) && (u['user-domain'] == s['user-domain'])}) == -1
+        }).map((u) => {
+            return {
+                label: `${u['user-name']}`,
+                value: u
+            }
+        });
+
+
+        if(!this.state.isReadOnly) {
+            formButtonsHTML = (
+                                state.isEdit ?
+                                (
+                                    <ButtonGroup className="buttonGroup">
+                                        <Button label="Update" type="submit" onClick={this.updateProject} />
+                                        <Button label="Delete" onClick={this.deleteProject} />
+                                        <Button label="Cancel" onClick={this.cancelEditProject} />
+                                    </ButtonGroup>
+                                )
+                                : (
+                                    <ButtonGroup className="buttonGroup">
+                                        <Button label="Create" type="submit" onClick={this.createProject}  />
+                                    </ButtonGroup>
+                                )
+                            )
+        }
+
+        html = (
+            <PanelWrapper className={`row projectManagement ${!this.state.projectOpen ? 'projectList-open' : ''}`} style={{'alignContent': 'center', 'flexDirection': 'row'}} >
+                <PanelWrapper ref={(div) => { this.projectList = div}} className={`column projectList expanded ${this.state.projectOpen ? 'collapsed ' : ' '} ${this.state.hideColumns ? 'hideColumns ' : ' '}`}>
+                    <Panel title="Project List" style={{marginBottom: 0}} no-corners>
+                        <div className="tableRow tableRow--header">
+                            <div className="projectName">
+                                Project Name
+                            </div>
+                            <div>
+                                Description
+                            </div>
+                        </div>
+                        {state.projects && state.projects.map((u, k) => {
+                            let platformRoles = [];
+                            for(let role in u.platformRoles) {
+                                platformRoles.push(<div>{`${role}: ${u.platformRoles[role]}`}</div>)
+                            }
+                            return (
+                                <div onClick={self.viewProject.bind(null, u, k)} ref={(el) => this[`project-ref-${k}`] = el} className={`tableRow tableRow--data ${((self.state.activeIndex == k) && self.state.projectOpen) ? 'tableRow--data-active' : ''}`} key={k}>
+                                    <div
+                                        className={`projectName projectName-header ${((self.state.activeIndex == k) && self.state.projectOpen) ? 'activeProject' : ''}`}
+                                        >
+                                        {u['name']}
+                                    </div>
+                                    <div>
+                                        {u['description']}
+                                    </div>
+
+
+                                </div>
+                            )
+                        })}
+                    </Panel>
+                    <SkyquakeRBAC className="rbacButtonGroup">
+                        <ButtonGroup  className="buttonGroup">
+                            <Button label="Add Project" onClick={this.addProject} />
+                        </ButtonGroup>
+                    </SkyquakeRBAC>
+                </PanelWrapper>
+                <PanelWrapper onKeyUp={this.evaluateSubmit}
+                    className={`ProjectAdmin column`}>
+                    <Panel
+                        title={state.isEdit ? state['name'] : 'Create Project'}
+                        style={{marginBottom: 0}}
+                        hasCloseButton={this.closePanel}
+                        no-corners>
+                        <FormSection title="PROJECT INFO">
+                        {
+                            (state.isEditProject ||  state.isReadOnly) ?
+                                <Input readonly={state.isReadOnly || this.state.isEdit}  label="Name" value={state['name']} onChange={this.updateInput.bind(null, 'name')} />
+                                : null
+                            }
+                            <Input readonly={state.isReadOnly} type="textarea" label="Description" value={state['description']}  onChange={this.updateInput.bind(null, 'description')}></Input>
+                        </FormSection>
+                        <FormSection title="USER ROLES"  className="userTable">
+
+                        <table className="projectTable">
+                            <thead>
+                                <tr>
+                                    <td>Domain</td>
+                                    <td>User Name</td>
+                                    {
+                                        state.roles.map((r,i) => {
+                                            return <td key={i}>{r}</td>
+                                        })
+                                    }
+                                </tr>
+                            </thead>
+                            <tbody>
+                                {
+                            state.projectUsers.map((u,i)=> {
+                                let userRoles = []
+                                u.role && u.role.map((r) => {
+                                    userRoles.push(r.role);
+                                });
+                                u["rw-project-mano:mano-role"] && u["rw-project-mano:mano-role"].map((r) => {
+                                    userRoles.push(r.role);
+                                });
+                                return (
+                                    <tr key={i}>
+                                        <td>
+                                            {u['user-domain']}
+                                        </td>
+                                        <td>
+                                            {u['user-name']}
+                                        </td>
+                                        {
+                                            state.roles.map((r,j) => {
+                                                return <td key={j}><Input readonly={state.isReadOnly} type="checkbox" onChange={self.toggleUserRoleInProject.bind(self, i, j)} checked={(userRoles.indexOf(r) > -1)} /></td>
+                                            })
+                                        }
+                                        {
+                                            !state.isReadOnly ? <td><span
+                                                                    className="removeInput"
+                                                                    onClick={self.removeUserFromProject.bind(self, i)}
+                                                                >
+                                                                    <img src={imgRemove} />
+
+                                                                </span></td> : null
+                                        }
+
+                                    </tr>
+                                )
+                            })
+                        }
+                            </tbody>
+                        </table>
+                        <SkyquakeRBAC allow={[PLATFORM.ADMIN, PLATFORM.SUPER]} className="rbacButtonGroup" style={{marginLeft: 0}}>
+                            {
+                                !state.isReadOnly ?
+                                    <div className="tableRow tableRow--header">
+                                        <div>
+                                            <div className="addUser">
+                                                {
+                                                    availableDomains.length == 1 ?
+                                                        <SelectOption
+                                                            label="Domain"
+                                                            onChange={this.actions.handleSelectedDomain}
+                                                            defaultValue={state.selectedDomain || availableDomains[0]}
+                                                            initial={false}
+                                                            readonly={true}
+                                                            options={availableDomains}
+                                                            ref={(el) => self.selectUserList = el}
+                                                        /> :
+                                                        <SelectOption
+                                                            label="Domain"
+                                                            onChange={this.actions.handleSelectedDomain}
+                                                            value={state.selectedDomain || availableDomains[0]}
+                                                            initial={false}
+                                                            options={availableDomains}
+                                                            ref={(el) => self.selectUserList = el}
+                                                        />
+                                                }
+                                                {
+                                                    availableUsers.length ?
+                                                        <SelectOption
+                                                            label="Username"
+                                                            onChange={this.actions.handleSelectedUser}
+                                                            value={state.selectedUser}
+                                                            initial={true}
+                                                            options={availableUsers}
+                                                            ref={(el) => self.selectUserList = el}
+                                                        /> :
+                                                        <label className="noUsersAvailable">
+                                                        <span>Username</span>
+                                                        <span style={{display: 'block',
+    marginTop: '0.8rem', color: '#666'}}>No Available Users for this Domain</span></label>
+                                                }
+                                                {
+                                                    availableUsers.length ?
+                                                        <span className="addInput" onClick={this.addUserToProject}><img src={imgAdd} />
+                                                            Add User
+                                                        </span> :
+                                                        null
+                                                }
+
+                                            </div>
+                                        </div>
+                                    </div> : null
+                            }
+                        </SkyquakeRBAC>
+                        </FormSection>
+
+                    </Panel>
+                     <SkyquakeRBAC allow={[PROJECT_ROLES.PROJECT_ADMIN]} project={this.state.name} className="rbacButtonGroup">
+                        {formButtonsHTML}
+                     </SkyquakeRBAC>
+                </PanelWrapper>
+
+
+            </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+ProjectManagementDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+ProjectManagementDashboard.defaultProps = {
+    projectList: [],
+    selectedProject: {}
+}
+
+export default SkyquakeComponent(ProjectManagementDashboard);
+
+
+function isElementInView(el) {
+    var rect = el && el.getBoundingClientRect() || {};
+
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
+    );
+}
+
+
+// isReadOnly={state.isReadOnly} disabled={state.disabled} onChange={this.disableChange}
+
+class isDisabled extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        let props = this.props;
+        return (<div/>)
+    }
+}
+
+
+
+
diff --git a/skyquake/plugins/project_management/src/dashboard/projectMgmt.scss b/skyquake/plugins/project_management/src/dashboard/projectMgmt.scss
new file mode 100644 (file)
index 0000000..d9aee62
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+@import "style/_colors.scss";
+
+.projectManagement {
+        max-width: 1200px;
+
+    .skyquakePanel-wrapper {
+        overflow-x: hidden;
+    }
+    .projectList {
+
+        -ms-flex: 0 1 200px;
+        -webkit-box-flex: 0;
+                flex: 0 1 200px;
+
+        .activeUser {
+            font-weight:bold;
+        }
+
+        /* transition: all 2s;*/
+        &.expanded {
+            -ms-flex: 1 1 100%;
+            -webkit-box-flex: 1;
+                    flex: 1 1 100%;
+            /* transition: all 300ms;*/
+            .tableRow>div:not(.projectName) {
+                opacity: 1;
+                /* width:auto;*/
+                /* transition: width 600ms;*/
+                /* transition: opacity 300ms;*/
+            }
+            &.collapsed {
+                -ms-flex: 0 1 200px;
+                -webkit-box-flex: 0;
+                        flex: 0 1 200px;
+                /* transition: all 2s;*/
+                .tableRow>div:not(.projectName) {
+                    /* opacity: 0;*/
+                    /* width:0px;*/
+                    display:none;
+                    overflow:hidden;
+                    /* transition: all 600ms;*/
+                }
+            }
+        }
+        &.hideColumns {
+            overflow:hidden;
+            >div {
+                overflow:hidden;
+            }
+            .tableRow>div:not(.projectName) {
+                width: 0px;
+                /* transition: all 600ms;*/
+            }
+            .projectName {
+                &--header {
+                    /* display:none;*/
+             }
+            }
+        }
+        .projectName {
+            &:not(:first-child) {
+                cursor:pointer;
+            }
+        }
+
+
+    }
+
+    .projectAdmin {
+            -ms-flex: 1 1;
+            -webkit-box-flex: 1;
+                    flex: 1 1;
+            width:auto;
+            opacity:1;
+
+        textarea{
+            height: 100px;
+        }
+    }
+    &.projectList-open {
+        .projectAdmin {
+            -ms-flex: 0 1 0px;
+                -webkit-box-flex: 0;
+                    flex: 0 1 0px;
+            opacity:0;
+            /* width: 0px;*/
+            display:none;
+            /* transition: opacity 300ms;*/
+            /* transition: width 600ms;*/
+
+        }
+    }
+    .rbacButtonGroup {
+        margin: 0 0.5rem 0.5rem;
+        background: #ddd;
+        padding-bottom: 0.5rem;
+        padding: 0.5rem 0;
+    }
+    .buttonGroup {
+        border-top: #d3d3d3 1px solid;
+        padding-top:0.5rem;
+    }
+    .addUser {
+        display:-ms-flexbox;
+        display:-webkit-box;
+        display:flex;
+        -ms-flex-direction:row;
+            -webkit-box-orient:horizontal;
+            -webkit-box-direction:normal;
+                flex-direction:row;
+        label {
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                -ms-flex-direction: column;
+                    flex-direction: column;
+            display: -webkit-box;
+            display: -ms-flexbox;
+            display: flex;
+            -ms-flex: 0 1;
+            -webkit-box-flex: 0;
+                flex: 0 1;
+
+            width:150px;
+            span {
+                margin-bottom: 0.5rem;
+            }
+            span:nth-child(2) {
+                -webkit-box-align: center;
+                    -ms-flex-align: center;
+                        align-items: center;
+                -webkit-box-flex: 1;
+                    -ms-flex: 1;
+                        flex: 1;
+                display: -webkit-box;
+                display: -ms-flexbox;
+                display: flex;
+            }
+
+            margin-right: 1rem;
+            select {
+                width:150px;
+            }
+        }
+        .noUsersAvailable {
+            display:-webkit-box;
+            display:-ms-flexbox;
+            display:flex;
+            -webkit-box-align: start;
+                -ms-flex-align: start;
+                    align-items: flex-start;
+            margin-bottom: 0.75rem;
+            -webkit-box-flex: 1;
+                -ms-flex: 1;
+                    flex: 1;
+            div {
+                width:260px;
+                margin-bottom: 0.5rem;
+            }
+        }
+    }
+    .projectUsers {
+        .userName {
+            -ms-flex-pack: start;
+            -webkit-box-pack: start;
+                    justify-content: flex-start;
+            padding-top: 0.75rem;
+        }
+        select {
+            margin-bottom:0.5rem;
+        }
+        .addRole {
+            margin:.25rem 0;
+        }
+        .buttonGroup {
+            display:-ms-flexbox;
+            display:-webkit-box;
+            display:flex;
+        }
+
+    }
+    .projectUsers.tableRow--data:hover {
+            background:none;
+            color: black;
+        }
+
+    table {
+        font-size: 0.8rem;
+        thead {
+            border-bottom:1px solid #d3d3d3;
+            td{
+                font-weight:bold;
+            }
+        }
+        td{
+            padding:0.25rem 0.5rem;
+            vertical-align: middle;
+            .checkbox {
+                -ms-flex-pack:center;
+                    -webkit-box-pack:center;
+                        justify-content:center;
+            }
+        }
+        tbody tr {
+            &:nth-child(even) {
+                background:$neutral-dark-0;
+            }
+        }
+    }
+    .userTable {
+        .FormSection-body {
+            overflow-x: auto;
+        }
+    }
+}
+
+
+
+.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;
+            -webkit-box-flex: 1;
+                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:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+    -ms-flex-align: center;
+        -webkit-box-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:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    padding: 0.25rem;
+    >div {
+        padding: 0.25rem;
+        -ms-flex: 1 1 33%;
+            -webkit-box-flex: 1;
+                flex: 1 1 33%;
+        display: -ms-flexbox;
+        display: -webkit-box;
+        display: flex;
+        -ms-flex-direction: column;
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                flex-direction: column;
+        -ms-flex-pack: center;
+            -webkit-box-pack: center;
+                justify-content: center;
+    }
+    &--header {
+        font-weight:bold;
+    }
+    &--data {
+        &:nth-child(even) {
+            background:$neutral-dark-0;
+        }
+        &:hover:not(&-active) {
+            background:$neutral-dark-1;
+        }
+        &:hover, .activeUser, &-active{
+            cursor:pointer;
+            color:white;
+        }
+        .activeUser, &-active{
+            background: #00acee;
+        }
+    }
+}
+
+.addInput, .removeInput {
+    display:-ms-flexbox;
+    display:-webkit-box;
+    display:flex;
+    -webkit-box-align: end;
+    -ms-flex-align: end;
+    align-items: flex-end;
+    margin-bottom: 0.75rem;
+    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/project_management/src/dashboard/projectMgmtActions.js b/skyquake/plugins/project_management/src/dashboard/projectMgmtActions.js
new file mode 100644 (file)
index 0000000..306d7ce
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+module.exports = function(Alt) {
+   return Alt.generateActions(
+                                       'handleUpdateInput',
+                                       'handleAddProjectItem',
+                                       'handleRemoveProjectItem',
+                                       'handleUpdateProjectRole',
+                                       'viewProject',
+                                       'editProject',
+                                       'handleCloseProjectPanel',
+                                       'handleHideColumns',
+                                       'handleSelectedUser',
+                                       'handleSelectedRole',
+                                       'handleAddUser',
+                                       'handleRemoveUserFromProject',
+                                       'getProjectsSuccess',
+                                       'getUsersSuccess',
+                                       'getProjectsNotification',
+                                       'handleDisabledChange',
+                                       'handlePlatformRoleUpdate',
+                                       'handleAddProject',
+                                       'handleCreateProject',
+                                       'handleUpdateProject',
+                                       'handleUpdateSelectedUser',
+                                       'handleSelectedDomain',
+                                       'handleUpdateUserRoleInProject',
+                                       'handleToggleUserRoleInProject',
+                                       'addRoleToUserInProject',
+                                       'handleRemoveRoleFromUserInProject',
+                                       'updateProjectSuccess',
+                                       'createProjectSuccess',
+                                       'deleteProjectSuccess'
+                                       );
+}
diff --git a/skyquake/plugins/project_management/src/dashboard/projectMgmtSource.js b/skyquake/plugins/project_management/src/dashboard/projectMgmtSource.js
new file mode 100644 (file)
index 0000000..14083fa
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * 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;
+}
+
+
+
+module.exports = function(Alt) {
+    return {
+
+        getUsers: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.user);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          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.handleServerReportedError
+        },
+        getProjects: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `/project?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.project);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error retrieving the resource orchestrator information.'
+          }),
+          success: Alt.actions.global.getProjectsSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        updateProject: {
+          remote: function(state, project) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                  url: `/project?api_server=${API_SERVER}`,
+                  type: 'PUT',
+                  data: project,
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the project.'
+          }),
+          success: Alt.actions.global.updateProjectSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        deleteProject: {
+          remote: function(state, project) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                url: `/project/${encodeURIComponent(project['name'])}?api_server=${API_SERVER}`,
+                type: 'DELETE',
+                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);
+                let msg = xhr.responseText;
+                if(xhr.errorMessage) {
+                  msg = xhr.errorMessage
+                }
+                reject(msg);
+              });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error deleting the user.'
+          }),
+          success: Alt.actions.global.deleteProjectSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        createProject: {
+            remote: function(state, project) {
+
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `/project?api_server=${API_SERVER}`,
+                  type: 'POST',
+                  data: project,
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+            },
+            interceptResponse: interceptResponse({
+              'error': 'There was an error updating the account.'
+            }),
+            success: Alt.actions.global.createProjectSuccess,
+            loading: Alt.actions.global.showScreenLoader,
+            error: Alt.actions.global.handleServerReportedError
+        }
+      }
+}
+
+function interceptResponse (responses) {
+  return function(data, action, args) {
+    if(responses.hasOwnProperty(data)) {
+      return {
+        type: data,
+        msg: responses[data]
+      }
+    } else {
+      return data;
+    }
+  }
+}
+
diff --git a/skyquake/plugins/project_management/src/dashboard/projectMgmtStore.js b/skyquake/plugins/project_management/src/dashboard/projectMgmtStore.js
new file mode 100644 (file)
index 0000000..4162759
--- /dev/null
@@ -0,0 +1,299 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import ProjectManagementActions from './projectMgmtActions.js';
+import ProjectManagementSource from './projectMgmtSource.js';
+import ROLES from 'utils/roleConstants.js';
+import _ from 'lodash';
+export default class ProjectManagementStore {
+    constructor() {
+        this.actions = ProjectManagementActions(this.alt);
+        this.bindActions(this.actions);
+        this.registerAsync(ProjectManagementSource);
+        this.projects = [];
+        this['name'] = '';
+        this['description'] = 'Some Description';
+        this.projectUsers = [];
+        this.selectedUser = null;
+        this.selectedDomain = null;
+        this.selectedRole = null;
+        this.roles = Object.keys(ROLES.PROJECT).filter((p) => {
+            return p != "TYPE";
+        }).map((p) => {
+            return ROLES.PROJECT[p];
+        })
+        // this.roles = ['rw-project:project-admin', 'rw-project:project-oper', 'rw-project:project-create'];
+        this.users = [];
+        this.domains = [];
+        this.activeIndex = null;
+        this.isReadOnly = true;
+        this.projectOpen = false;
+        this.hideColumns = false;
+        this.isEdit = false;
+        this.isEditProject = true;
+        // 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
+        });
+    }
+    viewProject() {
+        let data = arguments[0];
+        let project = data[0];
+        let projectIndex = data[1];
+        let isReadOnly = data[2];
+
+        let ProjectData = {
+            'name': project['name'],
+            'description': project['description'],
+            'projectUsers': (project['project-config'] && project['project-config']['user'] || [])
+        }
+        let state = _.merge({
+            activeIndex: projectIndex,
+            projectOpen: true,
+            isEdit: true,
+            isReadOnly: isReadOnly,
+            isEditProject: isReadOnly
+        }, ProjectData);
+        this.setState(state)
+    }
+    editProject(isReadOnly) {
+        this.viewProject([this.projects[this.activeIndex], this.activeIndex, isReadOnly]);
+
+    }
+    handleCloseProjectPanel() {
+        this.setState({
+            projectOpen: false,
+            isEdit: false,
+            isReadOnly: true
+        })
+    }
+    handleHideColumns(e) {
+        if(this.projectOpen && 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
+        })
+    }
+    handleSelectedUser(event) {
+        this.setState({
+            selectedUser: JSON.parse(event.currentTarget.value)
+        })
+    }
+
+    handleSelectedRole(event) {
+        this.setState({
+            selectedRole: JSON.parse(event.currentTarget.value)
+        })
+    }
+    resetProject() {
+        let name = '';
+        let description = '';
+        return {
+            'name' : name,
+            'description' : description
+        }
+    }
+    handleAddProject() {
+        this.setState(_.merge( this.resetProject() ,
+              {
+                isEdit: false,
+                projectOpen: true,
+                activeIndex: null,
+                isEditProject: true,
+                isReadOnly: false,
+                projectUsers: []
+            }
+        ))
+    }
+
+    handleUpdateSelectedUser(user) {
+        this.setState({
+            selectedUser: JSON.parse(user)
+        });
+    }
+
+    handleSelectedDomain(event) {
+        let domain = JSON.parse(event.target.value);
+        this.setState({
+            selectedDomain: domain
+        });
+    }
+    handleAddUser(e) {
+        let self = this;
+        let u = JSON.parse(this.selectedUser);
+        let r = this.selectedRole;
+        let projectUsers = this.projectUsers;
+        console.log('adding user')
+        projectUsers.push({
+          'user-name': u['user-name'],
+          'user-domain': u['user-domain'],
+          "role":[{
+                      "role": r,
+                      "keys": self.name
+            }
+          ]
+        })
+        this.setState({projectUsers, selectedUser: JSON.stringify(null)})
+    }
+    handleToggleUserRoleInProject(data) {
+        let self = this;
+        let {userIndex, roleIndex, checked} = data;
+        let projectUsers = this.projectUsers;
+        let selectedRole = self.roles[roleIndex];
+        let roleType = (ROLES.PROJECT.TYPE[selectedRole] == 'rw-project-mano') ? "rw-project-mano:mano-role" : "role";
+        //
+        if(checked) {
+            if (!projectUsers[userIndex][roleType]) {
+                projectUsers[userIndex][roleType] = [];
+            }
+            projectUsers[userIndex][roleType].push({
+                role: self.roles[roleIndex]
+            })
+        } else {
+            let role = projectUsers[userIndex][roleType];
+            let roleIndex = _.findIndex(role, {role:selectedRole})
+            projectUsers[userIndex][roleType].splice(roleIndex, 1)
+        }
+       self.setState({projectUsers});
+
+    }
+    handleUpdateUserRoleInProject(data) {
+        let {userIndex, roleIndex, value} = data;
+        let projectUsers = this.projectUsers;
+        projectUsers[userIndex].role[roleIndex].role = value;
+
+    }
+    addRoleToUserInProject(userIndex) {
+        let projectUsers = this.projectUsers;
+        if(!projectUsers[userIndex].role) {
+            projectUsers[userIndex].role = [];
+        }
+        projectUsers[userIndex].role.push({
+              'role': null
+            });
+        this.setState({
+            projectUsers
+        })
+    }
+    handleRemoveRoleFromUserInProject (data) {
+        let {userIndex, roleIndex} = data;
+        let projectUsers = this.projectUsers;
+        projectUsers[userIndex].role.splice(roleIndex, 1);
+        this.setState({
+            projectUsers
+        })
+    }
+    handleRemoveUserFromProject (userIndex) {
+        let projectUsers = this.projectUsers;
+        projectUsers.splice(userIndex, 1);
+        this.setState({
+            projectUsers
+        })
+    }
+    getProjectsSuccess(projects) {
+        this.alt.actions.global.hideScreenLoader.defer();
+        this.setState({projects: projects});
+    }
+    getUsersSuccess(users) {
+        console.log(users)
+        this.alt.actions.global.hideScreenLoader.defer();
+        let domains =  users && users.reduce(function(arr, u) {
+            if (arr.indexOf(u['user-domain']) == -1) {
+                arr.push(u['user-domain']);
+                return arr;
+            } else {
+                return arr
+            }
+        }, []);
+        this.setState({users, domains, selectedDomain: domains[0]});
+    }
+    updateProjectSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let self = this;
+        let projects = this.projects || [];
+        projects[this.activeIndex] = {
+            'name': this['name'],
+            'description': this['description'],
+            'project-config': {
+                'user': self.projectUsers
+            }
+        }
+        this.setState({
+            projects,
+            isEdit: true,
+            isReadOnly: true
+        });
+    }
+    deleteProjectSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let projects = this.projects;
+        projects.splice(this.activeIndex, 1);
+        this.setState({projects, projectOpen: false,isEdit: true,
+            isReadOnly: false,})
+    }
+    createProjectSuccess() {
+        let self = this;
+        this.alt.actions.global.hideScreenLoader.defer();
+        let projects = this.projects || [];
+        projects.push({
+            'name': self['name'],
+            'description': self['description'],
+            'project-config': {
+                'user': self.projectUsers
+            }
+         });
+        let newState = {
+            projects,
+            isEdit: true,
+            isReadOnly: true,
+            activeIndex: projects.length - 1
+        };
+        _.merge(newState)
+        this.setState(newState);
+    }
+}
diff --git a/skyquake/plugins/project_management/src/main.js b/skyquake/plugins/project_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/project_management/webpack.production.config.js b/skyquake/plugins/project_management/webpack.production.config.js
new file mode 100644 (file)
index 0000000..23b6a5c
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 CompressionPlugin = require("compression-webpack-plugin");
+
+// Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
+process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
+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: '../' + htmlFilename,
+            template: frameworkPath + '/plugin-index.html'
+        }),
+    ]
+};
+
+if (process.argv.indexOf('--optimize-minimize') !== -1) {
+    // we are going to output a gzip file in the production process
+    config.output.filename = "gzip-" + config.output.filename;
+    config.plugins.push(new webpack.DefinePlugin({ // <-- key to reducing React's size
+      'process.env': {
+        'NODE_ENV': JSON.stringify('production')
+      }
+    }));
+    config.plugins.push(new CompressionPlugin({
+        asset: "[path]", // overwrite js file with gz file
+        algorithm: "gzip",
+        test: /\.(js)$/
+    }));
+}
+
+module.exports = config;
diff --git a/skyquake/plugins/redundancy/CMakeLists.txt b/skyquake/plugins/redundancy/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5691557
--- /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(
+  redundancy
+  DEPENDS skyquake
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
+  CONFIGURE_COMMAND echo
+  BUILD_COMMAND
+    ${CMAKE_CURRENT_BINARY_DIR}/redundancy/redundancy-build/scripts/build.sh
+  INSTALL_COMMAND
+    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/install.sh
+    ${CMAKE_CURRENT_BINARY_DIR}/redundancy/redundancy-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/redundancy/api/redundancy.js b/skyquake/plugins/redundancy/api/redundancy.js
new file mode 100644 (file)
index 0000000..8a70738
--- /dev/null
@@ -0,0 +1,368 @@
+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 APIVersion = '/v2'
+var Redundancy = {};
+
+Redundancy.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/operational/redundancy-config';
+
+        _.extend(
+            requestHeaders,
+            constants.HTTP_HEADERS.accept.data, {
+                'Authorization': req.session && req.session.authorization
+            }
+        );
+
+        request({
+                url: url + '?deep',
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+            },
+            function(error, response, body) {
+                var data;
+                var objKey = 'rw-redundancy:redundancy-config';
+                //SDN model doesn't follow convention
+                if (utils.validateResponse('Redundancy.get', error, response, body, resolve, reject)) {
+                    try {
+                        data = JSON.parse(response.body);
+                        data = data[objKey]
+                    } catch (e) {
+                        console.log('Problem with "Redundancy.get"', e);
+                        var err = {};
+                        err.statusCode = 500;
+                        err.errorMessage = {
+                            error: 'Problem with "Redundancy.get": ' + e
+                        }
+                        return reject(err);
+                    }
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: data
+                    });
+                };
+            });
+    });
+}
+
+
+Redundancy.getState = 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/operational/redundancy-state';
+        _.extend(
+            requestHeaders,
+            constants.HTTP_HEADERS.accept.data, {
+                'Authorization': req.session && req.session.authorization
+            }
+        );
+        request({
+                url: url + '?deep',
+                type: 'GET',
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false
+            },
+            function(error, response, body) {
+                var data;
+                var objKey = 'rw-redundancy:redundancy-state';
+                //SDN model doesn't follow convention
+                if (utils.validateResponse('Redundancy.getState', error, response, body, resolve, reject)) {
+                    try {
+                        data = JSON.parse(response.body);
+                        data = data[objKey]
+                    } catch (e) {
+                        console.log('Problem with "Redundancy.getState"', e);
+                        var err = {};
+                        err.statusCode = 500;
+                        err.errorMessage = {
+                            error: 'Problem with "Redundancy.getState": ' + e
+                        }
+                        return reject(err);
+                    }
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: data
+                    });
+                };
+            });
+    });
+}
+
+Redundancy.configUpdate = function(req) {
+    var self = this;
+    var id = req.params.id || req.params.name;
+    var api_server = req.query["api_server"];
+    var data = req.body;
+    var requestHeaders = {};
+    var createData = {};
+    var updateTasks = [];
+    var method = 'PUT';
+    if(data.hasOwnProperty('revertive-preference')) {
+        var revertivePreferenceUrl = utils.confdPort(api_server) + '/api/config/redundancy-config/revertive-preference';
+        var revertuvePreferenceData = {
+            'preferred-site-name': data['revertive-preference']['preferred-site-name']
+        };
+        var revertivePreferencePromise = new Promise(function(resolve, reject) {
+            _.extend(requestHeaders,
+                constants.HTTP_HEADERS.accept.data,
+                constants.HTTP_HEADERS.content_type.data, {
+                    'Authorization': req.session && req.session.authorization
+                });
+            request({
+                url: revertivePreferenceUrl,
+                method: method,
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                json: revertuvePreferenceData,
+            }, function(error, response, body) {
+                if (utils.validateResponse('revertivePreferencePromise.' + method, error, response, body, resolve, reject)) {
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: JSON.stringify(response.body)
+                    });
+                };
+            });
+        });
+        updateTasks.push(revertivePreferencePromise);
+    }
+
+    if(data.hasOwnProperty('geographic-failover-decision')) {
+        var geoFailDecisionConfigUrl = utils.confdPort(api_server) + '/api/config/redundancy-config';
+        var geoFailDecisionConfigData = {'geographic-failover-decision' : data['geographic-failover-decision']};
+        var geoFailDecisionPromise = new Promise(function(resolve, reject) {
+            _.extend(requestHeaders,
+                constants.HTTP_HEADERS.accept.data,
+                constants.HTTP_HEADERS.content_type.data, {
+                    'Authorization': req.session && req.session.authorization
+                });
+            request({
+                url: geoFailDecisionConfigUrl,
+                method: method,
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                json: geoFailDecisionConfigData,
+            }, function(error, response, body) {
+                if (utils.validateResponse('geoFailDecisionPromise.' + method, error, response, body, resolve, reject)) {
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: JSON.stringify(response.body)
+                    });
+                };
+            });
+        });
+        updateTasks.push(geoFailDecisionPromise);
+    }
+
+    if(data.hasOwnProperty('user-credentials')) {
+        var userCredentialsUrl = utils.confdPort(api_server) + '/api/config/redundancy-config';
+        var userCredentialsData = {'user-credentials' : data['user-credentials']};
+        var userCredentialsPromise = new Promise(function(resolve, reject) {
+            _.extend(requestHeaders,
+                constants.HTTP_HEADERS.accept.data,
+                constants.HTTP_HEADERS.content_type.data, {
+                    'Authorization': req.session && req.session.authorization
+                });
+            request({
+                url: userCredentialsUrl,
+                method: method,
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                json: userCredentialsData,
+            }, function(error, response, body) {
+                if (utils.validateResponse('userCredentials.' + method, error, response, body, resolve, reject)) {
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: JSON.stringify(response.body)
+                    });
+                };
+            });
+        });
+        updateTasks.push(userCredentialsPromise);
+    }
+
+    if(data.hasOwnProperty('polling-config')) {
+        var pollingConfigUrl = utils.confdPort(api_server) + '/api/config/redundancy-config/polling-config';
+        var pollingConfigData = data['polling-config'];
+        var revertivePreferencePromise = new Promise(function(resolve, reject) {
+            _.extend(requestHeaders,
+                constants.HTTP_HEADERS.accept.data,
+                constants.HTTP_HEADERS.content_type.data, {
+                    'Authorization': req.session && req.session.authorization
+                });
+            request({
+                url: pollingConfigUrl,
+                method: method,
+                headers: requestHeaders,
+                forever: constants.FOREVER_ON,
+                rejectUnauthorized: false,
+                json: pollingConfigData,
+            }, function(error, response, body) {
+                if (utils.validateResponse('pollingConfigPromise.' + method, error, response, body, resolve, reject)) {
+                    return resolve({
+                        statusCode: response.statusCode,
+                        data: JSON.stringify(response.body)
+                    });
+                };
+            });
+        });
+        updateTasks.push(revertivePreferencePromise);
+    }
+
+    if(data.hasOwnProperty('dns-ip-fqdn')) {
+        var dnsIpFqdn = data['dns-ip-fqdn'];
+        var dnsIpFqdnUrl = utils.confdPort(api_server) + '/api/config/redundancy-config';
+        if(dnsIpFqdn.trim() != '') {
+            var dnsIpFqdnData = {'dns-ip-fqdn' : dnsIpFqdn}
+            var dnsIpFqdnPromise = new Promise(function(resolve, reject) {
+                _.extend(requestHeaders,
+                    constants.HTTP_HEADERS.accept.data,
+                    constants.HTTP_HEADERS.content_type.data, {
+                        'Authorization': req.session && req.session.authorization
+                    });
+                request({
+                    url: dnsIpFqdnUrl,
+                    method: method,
+                    headers: requestHeaders,
+                    forever: constants.FOREVER_ON,
+                    rejectUnauthorized: false,
+                    json: dnsIpFqdnData,
+                }, function(error, response, body) {
+                    if (utils.validateResponse('dnsIpFqdnPromise.' + method, error, response, body, resolve, reject)) {
+                        return resolve({
+                            statusCode: response.statusCode,
+                            data: JSON.stringify(response.body)
+                        });
+                    };
+                });
+            });
+        } else {
+            //Delete config item
+            var dnsIpFqdnPromise = new Promise(function(resolve, reject) {
+                _.extend(requestHeaders,
+                    constants.HTTP_HEADERS.accept.data,
+                    constants.HTTP_HEADERS.content_type.data, {
+                        'Authorization': req.session && req.session.authorization
+                    });
+                request({
+                    url: dnsIpFqdnUrl + '/dns-ip-fqdn',
+                    method: 'DELETE',
+                    headers: requestHeaders,
+                    forever: constants.FOREVER_ON,
+                    rejectUnauthorized: false
+                }, function(error, response, body) {
+                    if (utils.validateResponse('dnsIpFqdnDelete.' + method, error, response, body, resolve, reject)) {
+                        return resolve({
+                            statusCode: response.statusCode,
+                            data: JSON.stringify(response.body)
+                        });
+                    };
+                });
+            });
+        }
+        updateTasks.push(dnsIpFqdnPromise);
+    }
+    return new Promise(function(resolve, reject) {
+        Promise.all(updateTasks).then(function(results) {
+            if(results && results[0]) {
+                resolve({
+                    statusCode: results[0].statusCode,
+                    data: {}
+                });
+            }
+        })
+    })
+}
+
+
+Redundancy.siteUpdate = function(req) {
+    var self = this;
+    var id = req.params.id || req.params.name;
+    var api_server = req.query["api_server"];
+    var data = req.body;
+    var requestHeaders = {};
+    var createData = {};
+    var url = utils.confdPort(api_server) + '/api/config/redundancy-config/site'
+    var method = 'POST'
+    createData = {};
+    if (!id) {
+        createData = {site: data}
+    } else {
+        method = 'PUT';
+        url += '/' + encodeURIComponent(id);
+        createData['rw-redundancy:site'] = data;
+    }
+
+
+
+    return new Promise(function(resolve, reject) {
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.data,
+            constants.HTTP_HEADERS.content_type.data, {
+                'Authorization': req.session && req.session.authorization
+            });
+        request({
+            url: url,
+            method: method,
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+            json: createData,
+        }, function(error, response, body) {
+            if (utils.validateResponse('siteUpdate.' + method, error, response, body, resolve, reject)) {
+                return resolve({
+                    statusCode: response.statusCode,
+                    data: JSON.stringify(response.body)
+                });
+            };
+        });
+    })
+}
+
+Redundancy.siteDelete = function(req) {
+    var self = this;
+    var id = req.params.id || req.params.name;
+    var api_server = req.query["api_server"];
+    var data = req.body;
+    var requestHeaders = {};
+    var createData = {};
+    var url = utils.confdPort(api_server) + '/api/config/redundancy-config/site/';
+    url += encodeURIComponent(id);
+    return new Promise(function(resolve, reject) {
+        _.extend(requestHeaders,
+            constants.HTTP_HEADERS.accept.data,
+            constants.HTTP_HEADERS.content_type.data, {
+                'Authorization': req.session && req.session.authorization
+            });
+        request({
+            url: url,
+            method: 'DELETE',
+            headers: requestHeaders,
+            forever: constants.FOREVER_ON,
+            rejectUnauthorized: false,
+        }, function(error, response, body) {
+            if (utils.validateResponse( 'siteUpdate.DELETE', error, response, body, resolve, reject)) {
+                return resolve({
+                    statusCode: response.statusCode,
+                    data: JSON.stringify(response.body)
+                });
+            };
+        });
+    })
+}
+
+module.exports = Redundancy;
diff --git a/skyquake/plugins/redundancy/config.json b/skyquake/plugins/redundancy/config.json
new file mode 100644 (file)
index 0000000..239579e
--- /dev/null
@@ -0,0 +1,41 @@
+{
+    "root": "public",
+    "name": "Redundancy",
+    "dashboard": "./dashboard/sites.jsx",
+    "order": 3,
+    "priority":2,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper"
+    ],
+    "routes": [
+    {
+        "label": "Sites",
+        "route": "sites",
+        "component": "./dashboard/sites.jsx",
+        "path": "sites",
+        "type": "internal",
+        "routes": [
+
+        ]
+    },{
+        "label": "Config",
+        "route": "config",
+        "component": "./dashboard/config.jsx",
+        "path": "config",
+        "type": "internal",
+        "routes": [
+
+        ]
+    },{
+        "label": "Status",
+        "route": "status",
+        "component": "./dashboard/status.jsx",
+        "path": "status",
+        "type": "internal",
+        "routes": [
+        ]
+    }]
+}
diff --git a/skyquake/plugins/redundancy/package.json b/skyquake/plugins/redundancy/package.json
new file mode 100644 (file)
index 0000000..7389264
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "name": "redundancy",
+  "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",
+    "compression-webpack-plugin": "^0.3.2",
+    "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.10.1"
+  }
+}
diff --git a/skyquake/plugins/redundancy/routes.js b/skyquake/plugins/redundancy/routes.js
new file mode 100644 (file)
index 0000000..1f25246
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+var app = require('express').Router();
+var cors = require('cors');
+var utils = require('../../framework/core/api_utils/utils.js')
+
+var redundancyAPI = require('./api/redundancy.js');
+
+app.get('/config', cors(), function(req, res) {
+    redundancyAPI.get(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+app.get('/state', cors(), function(req, res) {
+    redundancyAPI.getState(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+app.post('/site', cors(), function(req, res) {
+    redundancyAPI.siteUpdate(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+app.put('/config', cors(), function(req, res) {
+    redundancyAPI.configUpdate(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+app.put('/site/:id', cors(), function(req, res) {
+    redundancyAPI.siteUpdate(req).then(function(data) {
+        utils.sendSuccessResponse(data, res);
+    }, function(error) {
+        utils.sendErrorResponse(error, res);
+    });
+});
+
+app.delete('/site/:id', cors(), function(req, res) {
+    redundancyAPI.siteDelete(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/redundancy/scripts/build.sh b/skyquake/plugins/redundancy/scripts/build.sh
new file mode 100755 (executable)
index 0000000..c27889c
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+PLUGIN_NAME=redundancy
+# change to the directory of this script
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
diff --git a/skyquake/plugins/redundancy/scripts/install.sh b/skyquake/plugins/redundancy/scripts/install.sh
new file mode 100755 (executable)
index 0000000..c6b6a32
--- /dev/null
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+plugin=redundancy
+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/redundancy/server.js b/skyquake/plugins/redundancy/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/redundancy/src/dashboard/config.jsx b/skyquake/plugins/redundancy/src/dashboard/config.jsx
new file mode 100644 (file)
index 0000000..5db3e5a
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import RedundancyStore from './redundancyStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import SkyquakeRBAC from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import 'style/layout.scss';
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+import {InputCollection, FormSection} from 'widgets/form_controls/formControls.jsx';
+
+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'
+import _  from 'lodash';
+import ROLES from 'utils/roleConstants.js';
+
+import './redundancy.scss';
+const PLATFORM = ROLES.PLATFORM;
+
+class ConfigDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('RedundancyStore') ? this.props.flux.stores.RedundancyStore : this.props.flux.createStore(RedundancyStore, 'RedundancyStore');
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+    }
+    componentDidUpdate() {
+    }
+    componentWillMount() {
+        this.state = this.Store.getState();
+        this.Store.getRedundancy();
+        this.Store.listen(this.updateState);
+    }
+    componentWillUnmount() {
+        this.Store.unlisten(this.updateState);
+    }
+    updateState = (state) => {
+        this.setState(state);
+    }
+    updateConfigInput = (containerName, key, e) => {
+        let configData = this.state[containerName];
+        if(!configData) {
+            configData = {};
+        }
+        configData[key] = e.target.value;
+        this.actions.handleUpdateConfigInput({[containerName]:configData})
+    }
+    updateDnsIpFqdnConfigInput = (e) => {
+        let value = e.target.value;
+        this.actions.handleUpdateConfigInput({'dns-ip-fqdn':value})
+    }
+    updateConfig = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let configData = self.state.configData;
+        let userCredentials = configData['user-credentials'];
+        if (!userCredentials || (userCredentials['username'].trim() == '' ) || (userCredentials['password'].trim() == '' )) {
+            self.props.flux.actions.global.showNotification("Please enter your user credentials");
+            return;
+        }
+        this.Store.updateConfig(configData);
+    }
+     evaluateSubmit = (e) => {
+        if (e.keyCode == 13) {
+            if (this.props.isEdit) {
+                this.updateConfig(e);
+            } else {
+                this.updateConfig(e);
+            }
+            e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    failOverDecisionChange = (e) => {
+        let value = e.target.value;
+        value = value.toUpperCase();
+        this.actions.handleFailOverDecisionChange(value);
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let passwordSectionHTML = null;
+        let configData = state.configData;
+        let formButtonsHTML = (
+            <ButtonGroup className="buttonGroup">
+                <Button label="Update" type="submit" onClick={this.updateConfig} />
+            </ButtonGroup>
+        )
+        let GeoFailoverDecision = this.state.configData['geographic-failover-decision'];
+
+        html = (
+                <PanelWrapper onKeyUp={this.evaluateSubmit}
+                    className={`SiteAdmin column`} column>
+                    <AppHeader nav={[{ name: 'SITES', onClick: this.context.router.push.bind(this, { pathname: '/sites' })  }, { name: 'CONFIG'}, { name: 'STATUS', onClick: this.context.router.push.bind(this, { pathname: '/status' }) }]} />
+                    <PanelWrapper onKeyUp={this.evaluateSubmit}
+                    className={`SiteAdmin column`} style={{overflow: 'auto'}} column>
+                    <Panel
+                        title="GEOGRAPHIC FAILOVER DECISION"
+                        style={{flex: `0 0 ${GeoFailoverDecision != "INDIRECT" ? '150px' : '220px'}`}}
+                        no-corners>
+                        <Input className="userInfo-section"
+                            type="radiogroup"
+                            onChange={this.failOverDecisionChange}
+                            value={GeoFailoverDecision}
+                            options={state.failOverDecisionOptions}
+                        />
+                        {
+                            GeoFailoverDecision != "INDIRECT" ? null :
+                            <Input type="text"  onChange={self.updateDnsIpFqdnConfigInput.bind(self)} label="DNS FQDN/IP Address" value={state.configData['dns-ip-fqdn']} />
+                        }
+                    </Panel>
+                    {
+                        GeoFailoverDecision == "DIRECT" ?
+                            <Panel
+                                title="Preferred Failback Site"
+                                style={{flex: '0 0 160px'}}
+                                no-corners>
+                                <Input type="text"  onChange={self.updateConfigInput.bind(self, 'revertive-preference', 'preferred-site-name')} label="Site Name" value={state.configData['revertive-preference'] && state.configData['revertive-preference']['preferred-site-name']} />
+                            </Panel>
+                       : null
+                    }
+                     <Panel
+                        title="User Credentials"
+                        style={{flex: '0 0 250px'}}
+                        no-corners>
+                        <Input type="text"  onChange={self.updateConfigInput.bind(self, 'user-credentials', 'username')} label="Username" value={state.configData['user-credentials'] && state.configData['user-credentials']['username']}
+                        required />
+                        <Input type="password"  onChange={self.updateConfigInput.bind(self, 'user-credentials', 'password')} label="Password" value={state.configData['user-credentials'] && state.configData['user-credentials']['password']}
+                        required />
+                    </Panel>
+                    <Panel
+                        title="Polling Configuration"
+                        style={{flex: `0 0 ${GeoFailoverDecision != "INDIRECT" ? '390px' : '230px'}`}}
+                        no-corners>
+                            <Input type="text"  onChange={self.updateConfigInput.bind(self, 'polling-config', 'poll-interval')} label="polling interval (s)" value={state.configData['polling-config'] && state.configData['polling-config']['poll-interval']} />
+                            {
+                                GeoFailoverDecision == "DIRECT" ?
+                                    <Input type="text"  onChange={self.updateConfigInput.bind(self, 'polling-config', 'failover-timeo')} label="Failover Timeout (s)" value={state.configData['polling-config'] && state.configData['polling-config']['failover-timeo']} />
+                               : null
+                            }
+                            {
+                                GeoFailoverDecision == "DIRECT" ?
+                                    <Input type="text"  onChange={self.updateConfigInput.bind(self, 'polling-config', 'failback-timeo')} label="Failback Timeout (s)" value={state.configData['polling-config'] && state.configData['polling-config']['failback-timeo']} />
+                               : null
+                            }
+                            <Input type="text" onChange={self.updateConfigInput.bind(self,'polling-config', 'no-response-counter')} label="No Response Counter" value={state.configData['polling-config'] && state.configData['polling-config']['no-response-counter']} />
+
+
+                    </Panel>
+                    </PanelWrapper>
+                     <SkyquakeRBAC allow={[PLATFORM.SUPER, PLATFORM.ADMIN]} site={this.state.name} className="rbacButtonGroup">
+                        {formButtonsHTML}
+                     </SkyquakeRBAC>
+                </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+ConfigDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+ConfigDashboard.defaultProps = {
+    siteList: [],
+    selectedSite: {}
+}
+
+export default SkyquakeComponent(ConfigDashboard);
+
+
+function isElementInView(el) {
+    var rect = el && el.getBoundingClientRect() || {};
+
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
+    );
+}
+
+
+// isReadOnly={state.isReadOnly} disabled={state.disabled} onChange={this.disableChange}
+
+class isDisabled extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        let props = this.props;
+        return (<div/>)
+    }
+}
+
+function showInput(e){
+  let target = e.target;
+  if(target.parentElement.classList.contains("addInput")) {
+    target = target.parentElement;
+  }
+  target.style.display = 'none';
+  target.parentElement.nextElementSibling.style.display = 'flex';
+  // e.target.parentElement.nextElementSibling.children[1].style.display = 'initial';
+}
+function hideInput(e){
+  let target = e.target;
+  if(target.parentElement.classList.contains("removeInput")) {
+    target = target.parentElement;
+  }
+  target.parentElement.style.display = 'none';
+  target.parentElement.previousElementSibling.children[1].style.display = 'inline';
+  target.previousSibling.value = '';
+}
+
+
diff --git a/skyquake/plugins/redundancy/src/dashboard/dashboard.jsx b/skyquake/plugins/redundancy/src/dashboard/dashboard.jsx
new file mode 100644 (file)
index 0000000..3dfc570
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ *
+ *   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 AppHeader from 'widgets/header/header.jsx';
+import RedundancyStore from './redundancyStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import {SkyquakeRBAC, isRBACValid} from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import ROLES from 'utils/roleConstants.js';
+
+
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
+//Delete this line after testing is done
+// PROJECT_ROLES.ACCOUNT_ADMIN = '';
+import 'style/layout.scss';
+
+class AccountsDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('RedundancyStore') ? this.props.flux.stores.RedundancyStore : this.props.flux.createStore(RedundancyStore, "RedundancyStore");
+        this.state = this.Store.getState();
+    }
+    componentWillMount() {
+        this.Store.listen(this.updateState);
+    }
+    componentWillUnmount() {
+        this.Store.unlisten(this.updateState);
+    }
+    updateState = (state) => {
+        this.setState(state);
+    }
+    render() {
+        let self = this;
+        let html;
+        let READONLY = !isRBACValid(this.context.userProfile, [PROJECT_ROLES.ACCOUNT_ADMIN, PROJECT_ROLES.PROJECT_ADMIN]);
+        html = (<div className="launchpad-account-dashboard content-wrapper">
+                <AppHeader nav={[{ name: 'SITES', onClick: this.context.router.push.bind(this, { pathname: '/sites' })  }, { name: 'CONFIG', onClick: this.context.router.push.bind(this, { pathname: '/config' }) }, { name: 'STATUS', onClick: this.context.router.push.bind(this, { pathname: '/status' }) }]} />
+                    <div className="flex">
+                      <div>
+                        { this.props.children ? React.cloneElement(this.props.children, {readonly: READONLY, store: self.Store, ...self.state}) : 'Edit or Create New Accounts'
+                        }
+                      </div>
+                    </div>
+              </div>);
+        return html;
+    }
+}
+AccountsDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+export default SkyquakeComponent(AccountsDashboard);
diff --git a/skyquake/plugins/redundancy/src/dashboard/redundancy.scss b/skyquake/plugins/redundancy/src/dashboard/redundancy.scss
new file mode 100644 (file)
index 0000000..b9b161b
--- /dev/null
@@ -0,0 +1,382 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+@import "style/_colors.scss";
+
+.siteManagement {
+        max-width: 1200px;
+
+    .skyquakePanel-wrapper {
+        overflow-x: hidden;
+    }
+    .siteList {
+
+        -ms-flex: 0 1 200px;
+        -webkit-box-flex: 0;
+                flex: 0 1 200px;
+
+        .activeUser {
+            font-weight:bold;
+        }
+
+        /* transition: all 2s;*/
+        &.expanded {
+            -ms-flex: 1 1 100%;
+            -webkit-box-flex: 1;
+                    flex: 1 1 100%;
+            /* transition: all 300ms;*/
+            .tableRow>div:not(.siteName) {
+                opacity: 1;
+                /* width:auto;*/
+                /* transition: width 600ms;*/
+                /* transition: opacity 300ms;*/
+            }
+            &.collapsed {
+                -ms-flex: 0 1 200px;
+                -webkit-box-flex: 0;
+                        flex: 0 1 200px;
+                /* transition: all 2s;*/
+                .tableRow>div:not(.siteName) {
+                    /* opacity: 0;*/
+                    /* width:0px;*/
+                    display:none;
+                    overflow:hidden;
+                    /* transition: all 600ms;*/
+                }
+            }
+        }
+        &.hideColumns {
+            overflow:hidden;
+            >div {
+                overflow:hidden;
+            }
+            .tableRow>div:not(.siteName) {
+                width: 0px;
+                /* transition: all 600ms;*/
+            }
+            .siteName {
+                &--header {
+                    /* display:none;*/
+             }
+            }
+        }
+        .siteName {
+            &:not(:first-child) {
+                cursor:pointer;
+            }
+        }
+
+
+    }
+
+    .siteAdmin {
+            -ms-flex: 1 1;
+            -webkit-box-flex: 1;
+                    flex: 1 1;
+            width:auto;
+            opacity:1;
+
+        textarea{
+            height: 100px;
+        }
+    }
+    &.siteList-open {
+        .siteAdmin {
+            -ms-flex: 0 1 0px;
+                -webkit-box-flex: 0;
+                    flex: 0 1 0px;
+            opacity:0;
+            /* width: 0px;*/
+            display:none;
+            /* transition: opacity 300ms;*/
+            /* transition: width 600ms;*/
+
+        }
+    }
+    .rbacButtonGroup {
+        margin: 0 0.5rem 0.5rem;
+        background: #ddd;
+        padding-bottom: 0.5rem;
+        padding: 0.5rem 0;
+    }
+    .buttonGroup {
+        border-top: #d3d3d3 1px solid;
+        padding-top:0.5rem;
+    }
+    .addUser {
+        display:-ms-flexbox;
+        display:-webkit-box;
+        display:flex;
+        -ms-flex-direction:row;
+            -webkit-box-orient:horizontal;
+            -webkit-box-direction:normal;
+                flex-direction:row;
+        label {
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                -ms-flex-direction: column;
+                    flex-direction: column;
+            display: -webkit-box;
+            display: -ms-flexbox;
+            display: flex;
+            -ms-flex: 0 1;
+            -webkit-box-flex: 0;
+                flex: 0 1;
+
+            width:150px;
+            span {
+                margin-bottom: 0.5rem;
+            }
+            span:nth-child(2) {
+                -webkit-box-align: center;
+                    -ms-flex-align: center;
+                        align-items: center;
+                -webkit-box-flex: 1;
+                    -ms-flex: 1;
+                        flex: 1;
+                display: -webkit-box;
+                display: -ms-flexbox;
+                display: flex;
+            }
+
+            margin-right: 1rem;
+            select {
+                width:150px;
+            }
+        }
+        .noUsersAvailable {
+            display:-webkit-box;
+            display:-ms-flexbox;
+            display:flex;
+            -webkit-box-align: start;
+                -ms-flex-align: start;
+                    align-items: flex-start;
+            margin-bottom: 0.75rem;
+            -webkit-box-flex: 1;
+                -ms-flex: 1;
+                    flex: 1;
+            div {
+                width:260px;
+                margin-bottom: 0.5rem;
+            }
+        }
+    }
+    .siteUsers {
+        .userName {
+            -ms-flex-pack: start;
+            -webkit-box-pack: start;
+                    justify-content: flex-start;
+            padding-top: 0.75rem;
+        }
+        select {
+            margin-bottom:0.5rem;
+        }
+        .addRole {
+            margin:.25rem 0;
+        }
+        .buttonGroup {
+            display:-ms-flexbox;
+            display:-webkit-box;
+            display:flex;
+        }
+
+    }
+    .siteUsers.tableRow--data:hover {
+            background:none;
+            color: black;
+        }
+
+    table {
+        font-size: 0.8rem;
+        thead {
+            border-bottom:1px solid #d3d3d3;
+            td{
+                font-weight:bold;
+            }
+        }
+        td{
+            padding:0.25rem 0.5rem;
+            vertical-align: middle;
+            .checkbox {
+                -ms-flex-pack:center;
+                    -webkit-box-pack:center;
+                        justify-content:center;
+            }
+        }
+        tbody tr {
+            &:nth-child(even) {
+                background:$neutral-dark-0;
+            }
+        }
+    }
+    .userTable {
+        .FormSection-body {
+            overflow-x: auto;
+        }
+    }
+
+
+    .FormSection {
+        &-title, &-body {
+            padding-left: 0px;
+        }
+    }
+    .subSection {
+        .FormSection-title {
+            color: #000;
+            background: none;
+            padding: 0.5rem;
+            border-top: none;
+            border-bottom: 1px solid $neutral-dark-2;
+            text-transform:uppercase;
+            width: 50%;
+            padding-left: 0;
+
+        }
+        .FormSection-body {
+            padding-left: 0;
+        }
+        label {
+            -ms-flex: 1 0;
+                -webkit-box-flex: 1;
+                    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;
+        }
+    }
+    .sqCheckBox {
+        width: 480px;
+    }
+}
+
+
+
+.InputCollection {
+    display:-ms-flexbox;
+    display:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+    -ms-flex-align: center;
+        -webkit-box-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:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    padding: 0.25rem;
+    >div {
+        padding: 0.25rem;
+        -ms-flex: 1 1 33%;
+            -webkit-box-flex: 1;
+                flex: 1 1 33%;
+        display: -ms-flexbox;
+        display: -webkit-box;
+        display: flex;
+        -ms-flex-direction: column;
+        -webkit-box-orient: vertical;
+        -webkit-box-direction: normal;
+        flex-direction: column;
+        -ms-flex-pack: center;
+        -webkit-box-pack: center;
+        justify-content: center;
+    }
+    &--header {
+        font-weight:bold;
+    }
+    &--data {
+        &:nth-child(even) {
+            background:$neutral-dark-0;
+        }
+        &:hover:not(&-active) {
+            background:$neutral-dark-1;
+        }
+        &:hover, .activeUser, &-active{
+            cursor:pointer;
+            color:white;
+        }
+        .activeUser, &-active{
+            background: #00acee;
+        }
+    }
+}
+.rwInstance {
+    width:100%;
+    border-bottom: 1px solid $neutral-dark-5;
+    margin-bottom: 1rem;
+    h3 {
+        display: -webkit-box;
+        display: -ms-flexbox;
+        display: flex;
+        justify-content: space-between;
+        color: $neutral-dark-5;
+        margin-bottom: 1rem;
+    }
+    span.title {
+        color: $gray-darker;
+    }
+    .sqTextInput {
+        max-width: 400px;
+    }
+}
+
+.sqCheckBox label input {
+    min-width: auto;
+    margin: 1rem;
+}
+.addInput, .removeInput {
+    display:-ms-flexbox;
+    display:-webkit-box;
+    display:flex;
+    -webkit-box-align: end;
+    -ms-flex-align: end;
+    align-items: flex-end;
+    margin-bottom: 0.75rem;
+    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/redundancy/src/dashboard/redundancyActions.js b/skyquake/plugins/redundancy/src/dashboard/redundancyActions.js
new file mode 100644 (file)
index 0000000..e314fe1
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+module.exports = function(Alt) {
+   return Alt.generateActions(
+                                       'handleUpdateInput',
+                                       'handleAddSiteItem',
+                                       'handleRemoveSiteItem',
+                                       'handleUpdateSiteRole',
+                                       'handleUpdateConfigInput',
+                                       'viewSite',
+                                       'editSite',
+                                       'handleCloseSitePanel',
+                                       'handleHideColumns',
+                                       'handleSelectedUser',
+                                       'handleSelectedRole',
+                                       'handleAddUser',
+                                       'handleRemoveUserFromSite',
+                                       'getSitesSuccess',
+                                       'getRedundancySuccess',
+                                       'getSitesNotification',
+                                       'handleDisabledChange',
+                                       'handlePlatformRoleUpdate',
+                                       'handleAddSite',
+                                       'handleCreateSite',
+                                       'handleUpdateSite',
+                                       'handleAddTargetEndpoint',
+                                       'handleRemoveTargetEndpoint',
+                                       'handleAddInstance',
+                                       'handleRemoveInstance',
+                                       'handleFailOverDecisionChange',
+                                       'openRedundancyStateSocketSuccess',
+                                       'updateSiteSuccess',
+                                       'createSiteSuccess',
+                                       'deleteSiteSuccess',
+                                       'updateConfigSuccess'
+                                       );
+}
diff --git a/skyquake/plugins/redundancy/src/dashboard/redundancySource.js b/skyquake/plugins/redundancy/src/dashboard/redundancySource.js
new file mode 100644 (file)
index 0000000..21822c8
--- /dev/null
@@ -0,0 +1,246 @@
+/*
+ * 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;
+}
+
+
+
+module.exports = function(Alt) {
+    return {
+
+        getRedundancy: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `config?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error retrieving the redundancy config.'
+          }),
+          success: Alt.actions.global.getRedundancySuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        openRedundancyStateSocket: {
+          remote: function(state) {
+            return new Promise(function(resolve, reject) {
+              //If socket connection already exists, eat the request.
+              if(state.socket) {
+                console.log('connection already exists')
+                return resolve(false);
+              }
+               $.ajax({
+                url: '/socket-polling',
+                type: 'POST',
+                beforeSend: Utils.addAuthorizationStub,
+                data: {
+                    url: 'redundancy/state?api_server=' + API_SERVER
+                },
+                success: function(data, textStatus, jqXHR) {
+                  Utils.checkAndResolveSocketRequest(data, resolve, reject);
+                }
+              }).fail(function(xhr){
+                //Authentication and the handling of fail states should be wrapped up into a connection class.
+                Utils.checkAuthentication(xhr.status);
+                reject(xhr.responseText || 'An error occurred. Check your logs for more information');
+              });;
+            });
+          },
+          loading: Alt.actions.global.showScreenLoader,
+          success: Alt.actions.global.openRedundancyStateSocketSuccess
+        },
+        getSites: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `site?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.site);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error retrieving the redundancy sites.'
+          }),
+          success: Alt.actions.global.getSitesSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        updateConfig: {
+          remote: function(state, site) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                  url: `config?api_server=${API_SERVER}`,
+                  type: 'PUT',
+                  data: site,
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the redundancy config.'
+          }),
+          success: Alt.actions.global.updateConfigSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        updateSite: {
+          remote: function(state, site) {
+            let data = site;
+            data['rw-instances'].map(function(s) {
+              if(s.hasOwnProperty('isNew')) {
+                delete s.isNew;
+              }
+            })
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                  url: `site/${encodeURIComponent(site['site-name'])}?api_server=${API_SERVER}`,
+                  type: 'PUT',
+                  data: data,
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the site.'
+          }),
+          success: Alt.actions.global.updateSiteSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        deleteSite: {
+          remote: function(state, site) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                url: `site/${encodeURIComponent(site['site-name'])}?api_server=${API_SERVER}`,
+                type: 'DELETE',
+                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);
+                let msg = xhr.responseText;
+                if(xhr.errorMessage) {
+                  msg = xhr.errorMessage
+                }
+                reject(msg);
+              });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error deleting the site.'
+          }),
+          success: Alt.actions.global.deleteSiteSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        createSite: {
+            remote: function(state, site) {
+              let data = site;
+              data['rw-instances'].map(function(s) {
+                if(s.hasOwnProperty('isNew')) {
+                  delete s.isNew;
+                }
+              })
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `site?api_server=${API_SERVER}`,
+                  type: 'POST',
+                  data: data,
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+            },
+            interceptResponse: interceptResponse({
+              'error': 'There was an error creating the site.'
+            }),
+            success: Alt.actions.global.createSiteSuccess,
+            loading: Alt.actions.global.showScreenLoader,
+            error: Alt.actions.global.handleServerReportedError
+        }
+      }
+}
+
+function interceptResponse (responses) {
+  return function(data, action, args) {
+    if(responses.hasOwnProperty(data)) {
+      return {
+        type: data,
+        msg: responses[data]
+      }
+    } else {
+      return data;
+    }
+  }
+}
+
diff --git a/skyquake/plugins/redundancy/src/dashboard/redundancyStore.js b/skyquake/plugins/redundancy/src/dashboard/redundancyStore.js
new file mode 100644 (file)
index 0000000..f2e46d8
--- /dev/null
@@ -0,0 +1,270 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import RedundancyActions from './redundancyActions.js';
+import RedundancySource from './redundancySource.js';
+var Utils = require('utils/utils.js');
+import ROLES from 'utils/roleConstants.js';
+import _ from 'lodash';
+export default class RedundancyStore {
+    constructor() {
+        this.actions = RedundancyActions(this.alt);
+        this.bindActions(this.actions);
+        this.registerAsync(RedundancySource);
+        this.sites = [];
+        this.failoverDecision = "INDIRECT";
+        this.siteData = {
+            'target-endpoint': [],
+            'rw-instances':[{
+                endpoint:[{},{}]
+            }]
+
+        };
+        this.configData = {
+                'polling-config' : {},
+                'revertive-preference': {},
+                'geographic-failover-decision': 'INDIRECT',
+                'user-credentials': {
+                    'username': '',
+                    'password': ''
+                }
+        }
+        this.siteIdPattern = /^((((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\p{N}\p{L}]+)?)|(^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$))|(((([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.)*([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.?)|\.)$/;
+        this.siteIdValidation = [ "^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$"
+        , "((:|[0-9a-fA-F]{0,4}):)([0-9a-fA-F]{0,4}:){0,5}((([0-9a-fA-F]{0,4}:)?(:|[0-9a-fA-F]{0,4}))|(((25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])))(%[\p{N}\p{L}]+)?"
+        , '((([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.)*([a-zA-Z0-9_]([a-zA-Z0-9\-_]){0,61})?[a-zA-Z0-9]\.?)|\.'
+        ];
+        this.failOverDecisionOptions = [{ label: "INDIRECT", value: "INDIRECT" }, { label: "DIRECT", value: "DIRECT" }];
+        this.status = {};
+        this.activeIndex = null;
+        this.isReadOnly = true;
+        this.siteOpen = false;
+        this.hideColumns = false;
+        this.isEdit = false;
+        this.isEditSite = true;
+        this.exportPublicMethods({
+            closeSocket: this.closeSocket
+        })
+    }
+    /**
+     * [handleFieldUpdate description]
+     * @param  {Object} data {
+     *                       [store_property] : [value]
+     * }
+     * @return {[type]}      [description]
+     */
+    handleUpdateInput(data) {
+        this.setState(data);
+    }
+    handleUpdateConfigInput(data) {
+        let configData = this.configData;
+        configData = _.merge(configData, data);
+        this.setState(configData);
+    }
+    handleFailOverDecisionChange(failoverDecision) {
+        let configData = this.configData;
+        configData['geographic-failover-decision'] = failoverDecision
+        delete configData['dns-ip-fqdn'];
+        this.setState({configData});
+    }
+
+    viewSite() {
+        let self = this;
+        let data = arguments[0];
+        let SiteData = data[0];
+        let siteIndex = data[1];
+        let isReadOnly = data[2];
+
+        let state = _.merge({
+            activeIndex: siteIndex,
+            siteOpen: true,
+            isEdit: true,
+            isReadOnly: isReadOnly,
+            isEditSite: isReadOnly,
+            siteData: SiteData
+        });
+        this.setState(state)
+    }
+    editSite(isReadOnly) {
+        this.viewSite([this.sites[this.activeIndex], this.activeIndex, isReadOnly]);
+
+    }
+    handleCloseSitePanel() {
+        this.setState({
+            siteOpen: false,
+            isEdit: false,
+            isReadOnly: true
+        })
+    }
+    handleHideColumns(e) {
+        if(this.siteOpen && e.currentTarget.classList.contains('hideColumns')) {
+            this.setState({
+                hideColumns: true
+            })
+        } else {
+            this.setState({
+                hideColumns: false
+            })
+        }
+    }
+    resetSite() {
+        let name = '';
+        let description = '';
+        return {
+            siteData: {
+                'target-endpoint': [{},{}],
+                'rw-instances':[{
+                    isNew: true,
+                    endpoint:[{},{}]
+                }]
+            }
+        }
+    }
+    handleAddSite() {
+        this.setState(_.merge( this.resetSite() ,
+              {
+                isEdit: false,
+                siteOpen: true,
+                activeIndex: null,
+                isEditSite: true,
+                isReadOnly: false,
+            }
+        ))
+    }
+    handleAddTargetEndpoint() {
+        let newSiteData = this.siteData;
+        if(!newSiteData['target-endpoint']) {
+            newSiteData['target-endpoint'] = [];
+        }
+        newSiteData['target-endpoint'].push({
+            name: '',
+            port: ''
+        })
+        this.setState({siteData: newSiteData})
+    }
+    handleRemoveTargetEndpoint(data) {
+        let newSiteData = this.siteData;
+        newSiteData['target-endpoint'].splice(
+           data[0].index
+            , 1
+        );
+        this.setState({siteData: newSiteData})
+    }
+    handleAddInstance() {
+        let newSiteData = this.siteData;
+        if(!newSiteData['rw-instances']) {
+            newSiteData['rw-instances'] = [];
+        }
+        newSiteData['rw-instances'].push({
+                isNew: true,
+                endpoint:[{},{}]
+            })
+        this.setState({siteData: newSiteData})
+    }
+    handleRemoveInstance(data) {
+        let newSiteData = this.siteData;
+        newSiteData['rw-instances'].splice(
+           data[0].index
+            , 1
+        );
+        this.setState({siteData: newSiteData})
+    }
+    getSitesSuccess(sites) {
+        this.alt.actions.global.hideScreenLoader.defer();
+        this.setState({sites: sites});
+    }
+    getRedundancySuccess(data) {
+        console.log(data)
+        this.alt.actions.global.hideScreenLoader.defer();
+        let sites =  data.site && data.site.map(function(site, i) {
+            return site;
+        });
+        this.setState({
+            sites,
+            configData : {
+                'polling-config' : data['polling-config'],
+                'revertive-preference': data['revertive-preference'],
+                'geographic-failover-decision': data['geographic-failover-decision'],
+                'dns-ip-fqdn': data['dns-ip-fqdn'],
+                'user-credentials': data['user-credentials'] || {
+                    'username': '',
+                    'password': ''
+                }
+            }
+        });
+    }
+    updateConfigSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let self = this;
+        this.setState({
+            isEdit: true,
+            isReadOnly: true
+        });
+    }
+    updateSiteSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let self = this;
+        let sites = this.sites || [];
+        sites[this.activeIndex] = this.siteData
+        this.setState({
+            sites,
+            isEdit: true,
+            isReadOnly: true
+        });
+    }
+    deleteSiteSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let sites = this.sites;
+        sites.splice(this.activeIndex, 1);
+        this.setState({sites, siteOpen: false,isEdit: true,
+            isReadOnly: false,})
+    }
+    createSiteSuccess() {
+        let self = this;
+        this.alt.actions.global.hideScreenLoader.defer();
+        let sites = this.sites || [];
+        sites.push(self.siteData);
+        let newState = {
+            sites,
+            isEdit: true,
+            isReadOnly: true,
+            activeIndex: sites.length - 1
+        };
+        _.merge(newState)
+        this.setState(newState);
+    }
+    openRedundancyStateSocketSuccess(connection) {
+        let self = this;
+        let  ws = window.multiplexer.channel(connection);
+        if (!connection) return;
+        this.setState({
+            socket: ws.ws,
+            channelId: connection
+        });
+        ws.onmessage = (socket) => {
+            try {
+                var data = JSON.parse(socket.data);
+                var newState = {status: data};
+                Utils.checkAuthentication(data.statusCode, function() {
+                    self.closeSocket();
+                });
+
+                self.setState(newState);
+            } catch(error) {
+                console.log('Hit at exception in openRedundancyStateSocketSuccess', error)
+            }
+
+        }
+        ws.onclose = () => {
+            self.closeSocket();
+        }
+    }
+    closeSocket = () => {
+        if (this.socket) {
+            window.multiplexer.channel(this.channelId).close();
+        }
+        this.setState({
+            socket: null
+        })
+    }
+}
diff --git a/skyquake/plugins/redundancy/src/dashboard/sites.jsx b/skyquake/plugins/redundancy/src/dashboard/sites.jsx
new file mode 100644 (file)
index 0000000..414a594
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import RedundancyStore from './redundancyStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import SkyquakeRBAC from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import 'style/layout.scss';
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+import {InputCollection, FormSection} from 'widgets/form_controls/formControls.jsx';
+
+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'
+import _  from 'lodash';
+import ROLES from 'utils/roleConstants.js';
+
+import './redundancy.scss';
+const PLATFORM = ROLES.PLATFORM;
+
+class SiteManagementDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('RedundancyStore') ? this.props.flux.stores.RedundancyStore : this.props.flux.createStore(RedundancyStore, 'RedundancyStore');
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+    }
+    componentDidUpdate() {
+        let self = this;
+        ReactDOM.findDOMNode(this.siteList).addEventListener('transitionend', this.onTransitionEnd, false);
+        setTimeout(function() {
+            let element = self[`site-ref-${self.state.activeIndex}`]
+            element && !isElementInView(element) && element.scrollIntoView({block: 'end', behavior: 'smooth'});
+        })
+    }
+    componentWillMount() {
+        this.state = this.Store.getState();
+        this.Store.getRedundancy();
+        this.Store.listen(this.updateState);
+    }
+    componentWillUnmount() {
+        this.Store.unlisten(this.updateState);
+    }
+    updateState = (state) => {
+        this.setState(state);
+    }
+    updateInput = (key, e) => {
+        let property = key;
+        let siteData = this.state.siteData;
+        siteData[property] = e.target.value;
+        this.actions.handleUpdateInput({
+            siteData
+        })
+    }
+    updateServiceTargetInput = (serviceName, key, e) => {
+        let state = this.state;
+        let siteData = this.state.siteData;
+        let value = e.target.value;
+        let index =  _.findIndex(state.siteData['target-endpoint'], function(o) {
+            return o.name == serviceName
+        });
+        if((index == undefined) || index == -1) {
+            index = siteData['target-endpoint'].push({name: serviceName}) - 1
+        }
+        siteData['target-endpoint'][index]['name'] = serviceName;
+        if(value.trim() == '') {
+            delete siteData['target-endpoint'][index][key];
+        } else {
+            siteData['target-endpoint'][index][key] = value
+        }
+        this.actions.handleUpdateInput({
+            siteData
+        })
+    }
+    updateInstanceInput = (index, key, e) => {
+        let siteData = this.state.siteData;
+        siteData['rw-instances'][index][key] = e.target.value;
+        this.actions.handleUpdateInput({
+            siteData
+        })
+    }
+    updateInstanceInputEndpoint = (index, serviceName, key, e) => {
+        let siteData = this.state.siteData;
+        let state = this.state;
+        let value = e.target.value;
+        let listIndex =  _.findIndex(siteData['rw-instances'][index].endpoint, function(o) {
+         return o.name == serviceName
+        });
+        if(!siteData['rw-instances'][index].endpoint) {
+            siteData['rw-instances'][index].endpoint = []
+        }
+        if(listIndex == undefined || listIndex == -1) {
+            listIndex = siteData['rw-instances'][index].endpoint.push({name: serviceName}) - 1;
+        }
+         if(value.trim() == '') {
+            delete siteData['rw-instances'][index].endpoint[listIndex][key];
+        } else {
+            siteData['rw-instances'][index].endpoint[listIndex][key] = value
+        }
+        siteData['rw-instances'][index].endpoint[listIndex]['name'] = serviceName;
+        this.actions.handleUpdateInput({
+            siteData
+        })
+    }
+    addSite = () => {
+        this.actions.handleAddSite();
+    }
+    viewSite = (un, index) => {
+        this.actions.viewSite(un, index, true);
+    }
+    editSite = () => {
+        this.actions.editSite(false);
+    }
+    cancelEditSite = () => {
+        this.actions.editSite(true)
+    }
+    closePanel = () => {
+        this.actions.handleCloseSitePanel();
+    }
+
+    deleteSite = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (confirm('Are you sure you want to delete this site?')) {
+            this.Store.deleteSite({
+                'site-name': this.state.siteData['site-name']
+            });
+        }
+    }
+    createSite = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let siteData = self.state.siteData;
+        if (this.validateFields(self, siteData)) {
+            this.Store.createSite(siteData);
+        }
+    }
+    updateSite = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let siteData = self.state.siteData;
+        if (this.validateFields(self, siteData)) {
+            this.Store.updateSite(siteData);
+        }
+    }
+    validateFields(self, siteData) {
+        if (!siteData['site-name'] || siteData['site-name'].trim() == '') {
+            self.props.flux.actions.global.showNotification("Please enter a site name");
+            return false;
+        }
+        let instanceInvalid = false;
+        siteData['rw-instances'] && siteData['rw-instances'].map(function(rw) {
+            if (!rw['rwinstance-id'] || rw['rwinstance-id'].trim() == '' ) {
+                instanceInvalid = true;;
+            }
+        });
+        if (instanceInvalid) {
+            self.props.flux.actions.global.showNotification("One or more of your RIFT.WARE Instances is missing it's FQDN/IP Address");
+            return false;
+        }
+        return true;
+    }
+     evaluateSubmit = (e) => {
+        if (e.keyCode == 13) {
+            if (this.props.isEdit) {
+                this.updateSite(e);
+            } else {
+                this.createSite(e);
+            }
+            e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    onTransitionEnd = (e) => {
+        this.actions.handleHideColumns(e);
+        console.log('transition end')
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let passwordSectionHTML = null;
+        let formButtonsHTML = (
+                <ButtonGroup className="buttonGroup">
+                    <Button label="EDIT" type="submit" onClick={this.editSite} />
+                </ButtonGroup>
+        );
+        if(!this.state.isReadOnly) {
+            formButtonsHTML = (
+                                state.isEdit ?
+                                (
+                                    <ButtonGroup className="buttonGroup">
+                                        <Button label="Update" type="submit" onClick={this.updateSite} />
+                                        <Button label="Delete" onClick={this.deleteSite} />
+                                        <Button label="Cancel" onClick={this.cancelEditSite} />
+                                    </ButtonGroup>
+                                )
+                                : (
+                                    <ButtonGroup className="buttonGroup">
+                                        <Button label="Create" type="submit" onClick={this.createSite}  />
+                                    </ButtonGroup>
+                                )
+                            )
+        }
+
+        html = (
+        <PanelWrapper column>
+             <AppHeader nav={[{ name: 'SITES' }, { name: 'CONFIG', onClick: this.context.router.push.bind(this, { pathname: '/config' }) }, { name: 'STATUS', onClick: this.context.router.push.bind(this, { pathname: '/status' }) }]} />
+            <PanelWrapper className={`row siteManagement ${!this.state.siteOpen ? 'siteList-open' : ''}`} style={{'alignContent': 'center', 'flexDirection': 'row'}} >
+
+                <PanelWrapper ref={(div) => { this.siteList = div}} className={`column siteList expanded ${this.state.siteOpen ? 'collapsed ' : ' '} ${this.state.hideColumns ? 'hideColumns ' : ' '}`}>
+                    <Panel title="Sites" style={{marginBottom: 0}} no-corners>
+                        <div className="tableRow tableRow--header">
+                            <div className="siteName">
+                                Site Name
+                            </div>
+                            <div>
+                                # of Instances
+                            </div>
+                        </div>
+                        {state.sites && state.sites.map((u, k) => {
+                            return (
+                                <div onClick={self.viewSite.bind(null, u, k)} ref={(el) => this[`site-ref-${k}`] = el} className={`tableRow tableRow--data ${((self.state.activeIndex == k) && self.state.siteOpen) ? 'tableRow--data-active' : ''}`} key={k}>
+                                    <div
+                                        className={`siteName siteName-header ${((self.state.activeIndex == k) && self.state.siteOpen) ? 'activeSite' : ''}`}
+                                        >
+                                        {u['site-name']}
+                                    </div>
+                                    <div>
+                                        {u['rw-instances'] && u['rw-instances'].length}
+                                    </div>
+
+                                </div>
+                            )
+                        })}
+                    </Panel>
+                    <SkyquakeRBAC className="rbacButtonGroup">
+                        <ButtonGroup  className="buttonGroup">
+                            <Button label="Add Site" onClick={this.addSite} />
+                        </ButtonGroup>
+                    </SkyquakeRBAC>
+                </PanelWrapper>
+                <PanelWrapper onKeyUp={this.evaluateSubmit}
+                    className={`SiteAdmin column`}>
+                    <Panel
+                        title={state.isEdit ? state.siteData['site-name'] : 'Create Site'}
+                        style={{marginBottom: 0}}
+                        hasCloseButton={this.closePanel}
+                        no-corners>
+                        <FormSection title="SITE INFO">
+                            {
+                                (state.isEditSite ||  state.isReadOnly) ?
+                                    <Input readonly={state.isReadOnly || this.state.isEdit} required label="Name" value={state.siteData['site-name']} onChange={this.updateInput.bind(null, 'site-name')} />
+                                    : null
+                            }
+                            <TextInput readonly={state.isReadOnly}
+                                label='FQDN/IP Address'
+                                pattern={state.siteIdPattern}
+                                value={state.siteData['site-id']}
+                                onChange={this.updateInput.bind(null, 'site-id')}
+                            />
+                        </FormSection>
+                        {
+                            <FormSection className="subSection" title="Service Endpoints">
+                                <TextInput type="text" readonly={state.isReadOnly} onChange={self.updateServiceTargetInput.bind(self, 'ui-service', 'port')} label="UI Port" value={state.siteData['target-endpoint'] && state.siteData['target-endpoint'][ _.findIndex(state.siteData['target-endpoint'], function(o) { return o.name == 'ui-service' })] && state.siteData['target-endpoint'][ _.findIndex(state.siteData['target-endpoint'], function(o) { return o.name == 'ui-service' })].port} />
+                                <TextInput type="text" readonly={state.isReadOnly} onChange={self.updateServiceTargetInput.bind(self, 'rest-service', 'port')} label="REST Port" value={state.siteData['target-endpoint'] && state.siteData['target-endpoint'][ _.findIndex(state.siteData['target-endpoint'], function(o) { return o.name == 'rest-service' })] && state.siteData['target-endpoint'][ _.findIndex(state.siteData['target-endpoint'], function(o) { return o.name == 'rest-service' })].port} />
+                            </FormSection>
+                        }
+                        {
+                            <FormSection title="RIFT.WARE INSTANCES">
+                                        {
+                                            state.siteData['rw-instances'] && state.siteData['rw-instances'].map(function(t, i) {
+                                                return <div key={i} className="rwInstance">
+                                                    <h3>
+                                                        <span className="title">INSTANCE</span>
+                                                        {
+                                                            (state.isReadOnly) ? null :
+                                                            <span
+                                                                onClick={self.actions.handleRemoveInstance.bind(null, {index: i})}
+                                                                className="removeInput">
+                                                                    <img
+                                                                        src={imgRemove}
+                                                                        style={{marginBottom: '0px'}}/>
+                                                                    Remove
+                                                            </span>
+                                                        }
+                                                    </h3>
+                                                    <TextInput type="text"
+                                                            label="FQDN/IP Address"
+                                                            required
+                                                            pattern={state.siteIdPattern}
+                                                            readonly={!(t.isNew && (!state.isReadOnly))}
+                                                            onChange={self.updateInstanceInput.bind(self, i, 'rwinstance-id')}
+                                                            value={state.siteData['rw-instances'][i]['rwinstance-id']} />
+                                                    <TextInput type="text"
+                                                        label="Floating IP"
+                                                        readonly={state.isReadOnly}
+                                                        onChange={self.updateInstanceInput.bind(self, i, 'floating-ip')}
+                                                        value={state.siteData['rw-instances'][i]['floating-ip']} />
+                                                    <FormSection className="subSection" title="Service Endpoints">
+                                                        <TextInput type="text"
+                                                            label="UI Port"
+                                                            readonly={state.isReadOnly}
+                                                            onChange={self.updateInstanceInputEndpoint.bind(self, i, 'ui-service', 'port')}
+                                                            value={state.siteData['rw-instances'] && state.siteData['rw-instances'][i] && state.siteData['rw-instances'][i].endpoint && state.siteData['rw-instances'][i].endpoint[ _.findIndex(state.siteData['rw-instances'][i].endpoint, function(o) { return o.name == 'ui-service' })
+                                                            ] && state.siteData['rw-instances'][i].endpoint[ _.findIndex(state.siteData['rw-instances'][i].endpoint, function(o) { return o.name == 'ui-service' })
+                                                            ].port}
+                                                            />
+                                                        <TextInput type="text"
+                                                            label="REST Port"
+                                                            readonly={state.isReadOnly}
+                                                            onChange={self.updateInstanceInputEndpoint.bind(self, i,'rest-service', 'port')}
+                                                            value={state.siteData['rw-instances'] && state.siteData['rw-instances'][i] && state.siteData['rw-instances'][i].endpoint && state.siteData['rw-instances'][i].endpoint[
+                                                             _.findIndex(state.siteData['rw-instances'][i].endpoint, function(o) { return o.name == 'rest-service' })
+                                                            ] && state.siteData['rw-instances'][i].endpoint[ _.findIndex(state.siteData['rw-instances'][i].endpoint, function(o) { return o.name == 'rest-service' })
+                                                            ].port}
+                                                            />
+                                                    </FormSection>
+                                                </div>
+                                            })
+                                        }
+                                        {
+                                                (state.isReadOnly) ? null :
+                                                <span onClick={self.actions.handleAddInstance} className="addInput"  ><img src={imgAdd} />Add Instance</span>
+                                         }
+                            </FormSection>
+                        }
+
+
+                    </Panel>
+                     <SkyquakeRBAC allow={[PLATFORM.SUPER, PLATFORM.ADMIN]} site={this.state.name} className="rbacButtonGroup">
+                        {formButtonsHTML}
+                     </SkyquakeRBAC>
+                </PanelWrapper>
+            </PanelWrapper>
+        </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+SiteManagementDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+SiteManagementDashboard.defaultProps = {
+    siteList: [],
+    selectedSite: {}
+}
+
+export default SkyquakeComponent(SiteManagementDashboard);
+
+
+function isElementInView(el) {
+    var rect = el && el.getBoundingClientRect() || {};
+
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
+    );
+}
+
+
+// isReadOnly={state.isReadOnly} disabled={state.disabled} onChange={this.disableChange}
+
+class isDisabled extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        let props = this.props;
+        return (<div/>)
+    }
+}
+
+function showInput(e){
+  let target = e.target;
+  if(target.parentElement.classList.contains("addInput")) {
+    target = target.parentElement;
+  }
+  target.style.display = 'none';
+  target.parentElement.nextElementSibling.style.display = 'flex';
+  // e.target.parentElement.nextElementSibling.children[1].style.display = 'initial';
+}
+function hideInput(e){
+  let target = e.target;
+  if(target.parentElement.classList.contains("removeInput")) {
+    target = target.parentElement;
+  }
+  target.parentElement.style.display = 'none';
+  target.parentElement.previousElementSibling.children[1].style.display = 'inline';
+  target.previousSibling.value = '';
+}
+
+
diff --git a/skyquake/plugins/redundancy/src/dashboard/status.jsx b/skyquake/plugins/redundancy/src/dashboard/status.jsx
new file mode 100644 (file)
index 0000000..fbbd8bd
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import RedundancyStore from './redundancyStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import SkyquakeRBAC from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+import 'style/layout.scss';
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+import {InputCollection, FormSection} from 'widgets/form_controls/formControls.jsx';
+
+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'
+import _  from 'lodash';
+import ROLES from 'utils/roleConstants.js';
+
+import './redundancy.scss';
+const PROJECT_ROLES = ROLES.PROJECT;
+const PLATFORM = ROLES.PLATFORM;
+
+class StatusDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('RedundancyStore') ? this.props.flux.stores.RedundancyStore : this.props.flux.createStore(RedundancyStore, 'RedundancyStore');
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+    }
+    componentDidUpdate() {
+    }
+    componentWillMount() {
+        this.state = this.Store.getState();
+        this.Store.getRedundancy();
+        this.Store.openRedundancyStateSocket();
+        this.Store.listen(this.updateState);
+    }
+    componentWillUnmount() {
+        this.Store.unlisten(this.updateState);
+        this.Store.closeSocket();
+    }
+    updateState = (state) => {
+        this.setState(state);
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let STATUS = state.status;
+        /*
+
+    {
+        "active-instance": {
+            "active-inst-id": "10.64.210.20",
+            "is-this-instance-active": "true",
+            "site-name": "site20"
+        },
+        "vm-identity": "ebbd6444-d6a7-4eda-ab0d-e23fdbcdeffe",
+        "health-status": [
+            {
+                "state": "RUNNING_AS_ACTIVE",
+                "rwinstance-id": "10.64.210.20",
+                "site-name": "site20"
+            },
+            {
+                "state": "NO_RESPONSE",
+                "rwinstance-id": "10.64.210.33",
+                "site-name": "site33"
+            }
+        ]
+    }
+
+
+         */
+
+        html = (
+                <PanelWrapper onKeyUp={this.evaluateSubmit}
+                    className={`SiteAdmin column`} column>
+                    <AppHeader nav={[{ name: 'SITES', onClick: this.context.router.push.bind(this, { pathname: '/sites' })  }, { name: 'CONFIG', onClick: this.context.router.push.bind(this, { pathname: '/config' })}, { name: 'STATUS' }]} />
+                    <PanelWrapper onKeyUp={this.evaluateSubmit}
+                    className={`SiteAdmin column`} style={{overflow: 'auto'}} column>
+                    <Panel
+                        title="Active Instance"
+                        style={{flex: '0 0 300px'}}
+                        no-corners>
+                        <TextInput type="text" label="VM ID" readonly={true} value={STATUS['vm-identity'] } />
+                        <TextInput type="text" label="active instance ID" readonly={true} value={STATUS['active-instance'] && STATUS['active-instance']['active-inst-id'] } />
+                        <TextInput type="text" label="Site Name" readonly={true} value={STATUS['active-instance'] && STATUS['active-instance']['site-name'] } />
+                    </Panel>
+                    <Panel
+                        title="CONFIGURATION STATE"
+                        style={{flex: '0 0 250px'}}
+                        no-corners>
+                           <div className="tableRow tableRow--header">
+                                <div>
+                                    SITE NAME
+                                </div>
+                                 <div>
+                                    INSTANCE IP
+                                </div>
+                                <div>
+                                    CURRENT STATE
+                                </div>
+                                <div>
+                                    PREVIOUS STATE
+                                </div>
+                                <div>
+                                    CONFIG GENERATION #
+                                </div>
+                                <div>
+                                    LAST PACKAGE UPDATE
+                                </div>
+                            </div>
+                            {
+                                STATUS['config-state'] && STATUS['config-state'].map((u, k) => {
+                                    return (
+                                        <div className={`tableRow tableRow--data`} key={k}>
+                                            <div>
+                                                {u['site-name'] || '--'}
+                                            </div>
+                                            <div>
+                                                {u['rwinstance-ip'] || '--'}
+                                            </div>
+                                            <div>
+                                                {u['current-state'] || '--'}
+                                            </div>
+                                            <div>
+                                                {u['previous-state'] || '--'}
+                                            </div>
+                                            <div>
+                                                {u['config-generation-number'] || '--'}
+                                            </div>
+                                            <div>
+                                                {u['last-package-update'] || '--'}
+                                            </div>
+
+
+                                        </div>
+                                    )
+                                })
+                            }
+
+
+                    </Panel>
+                    <Panel
+                        title="HEALTH STATUS"
+                        style={{flex: '0 0 250px'}}
+                        no-corners>
+                           <div className="tableRow tableRow--header">
+                                <div>
+                                    RW INSTANCE
+                                </div>
+                                <div>
+                                    STATUS
+                                </div>
+                            </div>
+                            {STATUS['health-status'] && STATUS['health-status'].map((u, k) => {
+                            return (
+                                <div className={`tableRow tableRow--data`} key={k}>
+                                    <div>
+                                        {u['rwinstance-id'] || '--'}
+                                    </div>
+                                    <div>
+                                        {u['state'] || '--'}
+                                    </div>
+
+
+                                </div>
+                            )
+                        })}
+
+
+                    </Panel>
+                </PanelWrapper>
+                </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+StatusDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+StatusDashboard.defaultProps = {
+    siteList: [],
+    selectedSite: {}
+}
+
+export default SkyquakeComponent(StatusDashboard);
diff --git a/skyquake/plugins/redundancy/src/dashboard/utils/utils.js b/skyquake/plugins/redundancy/src/dashboard/utils/utils.js
new file mode 100644 (file)
index 0000000..7c13f05
--- /dev/null
@@ -0,0 +1,14 @@
+import Input from 'widgets/form_controls/input.jsx';
+
+var componentMap = {
+    leaf: {
+        component: 'input'
+    }
+}
+
+function buildFormElements (schema, onChange) {
+    var elements = [];
+    schema.map(function(s, i) {
+
+    })
+}
diff --git a/skyquake/plugins/redundancy/src/main.js b/skyquake/plugins/redundancy/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/redundancy/webpack.production.config.js b/skyquake/plugins/redundancy/webpack.production.config.js
new file mode 100644 (file)
index 0000000..23b6a5c
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 CompressionPlugin = require("compression-webpack-plugin");
+
+// Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
+process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
+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: '../' + htmlFilename,
+            template: frameworkPath + '/plugin-index.html'
+        }),
+    ]
+};
+
+if (process.argv.indexOf('--optimize-minimize') !== -1) {
+    // we are going to output a gzip file in the production process
+    config.output.filename = "gzip-" + config.output.filename;
+    config.plugins.push(new webpack.DefinePlugin({ // <-- key to reducing React's size
+      'process.env': {
+        'NODE_ENV': JSON.stringify('production')
+      }
+    }));
+    config.plugins.push(new CompressionPlugin({
+        asset: "[path]", // overwrite js file with gz file
+        algorithm: "gzip",
+        test: /\.(js)$/
+    }));
+}
+
+module.exports = config;
diff --git a/skyquake/plugins/user_management/CMakeLists.txt b/skyquake/plugins/user_management/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5235092
--- /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(
+  user_management
+  DEPENDS skyquake
+  SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}
+  CONFIGURE_COMMAND echo
+  BUILD_COMMAND
+    ${CMAKE_CURRENT_BINARY_DIR}/user_management/user_management-build/scripts/build.sh
+  INSTALL_COMMAND
+    ${CMAKE_CURRENT_SOURCE_DIR}/scripts/install.sh
+    ${CMAKE_CURRENT_BINARY_DIR}/user_management/user_management-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/config.json b/skyquake/plugins/user_management/config.json
new file mode 100644 (file)
index 0000000..495ac54
--- /dev/null
@@ -0,0 +1,33 @@
+{
+    "root": "public",
+    "name": "User Management",
+    "dashboard": "./dashboard/dashboard.jsx",
+    "order": 2,
+    "priority":1,
+    "admin_link": true,
+    "allow": [
+        "rw-rbac-platform:super-admin",
+        "rw-rbac-platform:platform-admin",
+        "rw-rbac-platform:platform-oper"],
+    "routes": [
+    {
+        "label": "User Management Dashboard",
+        "route": "user-management",
+        "component": "./dashboard/dashboard.jsx",
+        "type": "internal",
+        "allow": ["rw-rbac-platform:super-admin", "rw-rbac-platform:platform-admin", "rw-rbac-platform:platform-oper"]
+    },{
+        "label": "Platform Role Management",
+        "route": "platform",
+        "component": "./platformRoleManagement/platformRoleManagement.jsx",
+        "type": "external"
+    },
+      {
+        "label": "User Profile",
+        "route": "user-profile",
+        "component": "./userProfile/userProfile.jsx",
+        "type": "internal",
+        "unique" : true
+    }
+    ]
+}
diff --git a/skyquake/plugins/user_management/package.json b/skyquake/plugins/user_management/package.json
new file mode 100644 (file)
index 0000000..c459845
--- /dev/null
@@ -0,0 +1,55 @@
+{
+  "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",
+    "compression-webpack-plugin": "^0.3.2",
+    "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.10.1"
+  }
+}
diff --git a/skyquake/plugins/user_management/routes.js b/skyquake/plugins/user_management/routes.js
new file mode 100644 (file)
index 0000000..8640a99
--- /dev/null
@@ -0,0 +1,12 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+var app = require('express').Router();
+var cors = require('cors');
+var utils = require('../../framework/core/api_utils/utils.js')
+ // Begin Accounts API
+
+    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..bfc6294
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+# STANDARD_RIFT_IO_COPYRIGHT
+
+PLUGIN_NAME=user_management
+# change to the directory of this script
+PLUGIN_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+source ../../../../skyquake/skyquake-build/scripts/build-plugin.sh
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..2fdad03
--- /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..9bad06b
--- /dev/null
@@ -0,0 +1,509 @@
+/*
+ * 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 SkyquakeRBAC from 'widgets/skyquake_rbac/skyquakeRBAC.jsx';
+
+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';
+import { merge } from 'lodash';
+
+import ROLES from 'utils/roleConstants.js';
+const PLATFORM = ROLES.PLATFORM;
+
+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, 'UserManagementStore');
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+    }
+    componentDidUpdate() {
+        let self = this;
+        ReactDOM.findDOMNode(this.UserList).addEventListener('transitionend', this.onTransitionEnd, false);
+        setTimeout(function () {
+            let element = self[`user-ref-${self.state.activeIndex}`]
+            element && !isElementInView(element) && element.scrollIntoView({ block: 'end', behavior: 'smooth' });
+        })
+    }
+    componentWillMount() {
+        this.Store.listen(this.updateState);
+        this.Store.getUsers();
+    }
+    componentWillUnmount() {
+        this.Store.unlisten(this.updateState);
+    }
+    updateState = (state) => {
+        this.setState(state);
+    }
+    updateInput = (key, e) => {
+        let property = key;
+        this.actions.handleUpdateInput({
+            [property]: e.target.value
+        })
+    }
+    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) => {
+        this.actions.viewUser(un, index);
+    }
+    editUser = () => {
+        this.actions.editUser(false);
+    }
+    cancelEditUser = () => {
+        this.actions.editUser(true)
+    }
+    closePanel = () => {
+        this.actions.handleCloseUserPanel();
+    }
+    // updateUser = (e) => {
+    //     e.preventDefault();
+    //     e.stopPropagation();
+
+    //     this.Store.updateUser();
+    // }
+    deleteUser = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (confirm('Are you sure you want to delete this user?')) {
+            this.Store.deleteUser({
+                'user-name': this.state['user-name'],
+                'user-domain': this.state['user-domain']
+            });
+        }
+
+    }
+    createUser = (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (!this.state['user-name'] || (this.state['user-name'].trim() == "")) {
+            this.props.actions.showNotification('Please enter a valid username');
+            return;
+        }
+
+        if((this.state['user-domain'].toLowerCase() == 'system') && !this.state['new-password'] || (this.state['new-password'].trim == "")) {
+            this.props.actions.showNotification('You must enter a password');
+            return;
+        }
+        if((this.state['user-domain'].toLowerCase() == 'system') && this.state['new-password'] != this.state['confirm-password']) {
+            this.props.actions.showNotification('Passwords do not match');
+            return;
+        } else {
+            let isDisabled = {};
+            if (this.state.disabled == "TRUE") {
+                isDisabled = {
+                    disabled: [null]
+                }
+            }
+            this.Store.createUser(_.merge({
+                'user-name': this.state['user-name'],
+                'user-domain': this.state['user-domain'],
+                'password': this.state['new-password'],
+                // 'confirm-password': this.state['confirm-password']
+            }, isDisabled));
+        }
+    }
+    updateUser = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let validatedPasswords = validatePasswordFields(this.state);
+        if (validatedPasswords) {
+            let isDisabled = {};
+            let password = {};
+            if (self.state.disabled == "TRUE") {
+                isDisabled = {
+                    disabled: [null]
+                }
+            }
+            if (this.state['new-password'] != '') {
+                password = { 'password': this.state['new-password'] }
+            } else {
+                password = {
+                    'password': this.state.currentPassword
+                }
+            }
+            this.Store.updateUser(_.merge({
+                'user-name': this.state['user-name'],
+                'user-domain': this.state['user-domain'],
+                'ui-state': this.context.userProfile.data['ui-state']
+            }, _.merge(isDisabled, password)));
+        }
+        function validatePasswordFields(state) {
+            let newOne = state['new-password'];
+            let confirmOne = state['confirm-password'];
+            if (!newOne && !confirmOne) {
+                // self.props.actions.showNotification('Please fill in all fields.');
+                return true;
+            }
+            if (newOne != confirmOne) {
+                self.props.actions.showNotification('Passwords do not match');
+                return false;
+            }
+            return {
+                'new-password': newOne,
+                'confirm-password': confirmOne
+            }
+        }
+    }
+    evaluateSubmit = (e) => {
+        if (e.keyCode == 13) {
+            if (this.state.isEdit) {
+                this.updateUser(e);
+            } else {
+                this.createUser(e);
+            }
+            e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    onTransitionEnd = (e) => {
+        this.actions.handleHideColumns(e);
+        console.log('transition end')
+    }
+    disableChange = (e) => {
+        let value = e.target.value;
+        value = value.toUpperCase();
+        this.actions.handleDisabledChange(value);
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let passwordSectionHTML = null;
+        let formButtonsHTML = (
+            <ButtonGroup className="buttonGroup">
+                <Button label="EDIT" type="submit" onClick={this.editUser} />
+            </ButtonGroup>
+        );
+        if(!this.state.isReadOnly) {
+            passwordSectionHTML = (state['user-domain'].toLowerCase() == 'system') ?
+                ( this.state.isEdit ?
+                    (
+                        <FormSection title="PASSWORD CHANGE">
+                            <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>
+                    )
+                )
+                : null;
+            formButtonsHTML = (
+                state.isEdit ?
+                    (
+                        <ButtonGroup className="buttonGroup">
+                            <Button label="Delete" onClick={this.deleteUser} />
+                            <Button label="Cancel" onClick={this.cancelEditUser} />
+                            <Button label="Update" type="submit" onClick={this.updateUser} />
+                        </ButtonGroup>
+                    )
+                    : (
+                        <ButtonGroup className="buttonGroup">
+                            <Button label="Cancel" onClick={this.closePanel} />
+                            <Button label="Create" type="submit" onClick={this.createUser} />
+                        </ButtonGroup>
+                    )
+            )
+        }
+        html = (
+            <PanelWrapper column>
+                <SkyquakeRBAC allow={[PLATFORM.SUPER, PLATFORM.ADMIN]} >
+                    <AppHeader nav={[{ name: 'USER MANAGEMENT' }, { name: 'PLATFORM ROLE MANAGEMENT', onClick: this.context.router.push.bind(this, { pathname: '/platform' }) }]} />
+                </SkyquakeRBAC>
+                <PanelWrapper className={`row userManagement ${!this.state.userOpen ? 'userList-open' : ''}`} style={{ '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>
+                                    Status
+                                </div>
+                            </div>
+                            {state.users && state.users.map((u, k) => {
+                                let platformRoles = [];
+                                for (let role in u.platformRoles) {
+                                    platformRoles.push(<div>{`${role}: ${u.platformRoles[role]}`}</div>)
+                                }
+                                return (
+                                    <div ref={(el) => this[`user-ref-${k}`] = el} className={`tableRow tableRow--data ${((self.state.activeIndex == k) && self.state.userOpen) ? 'tableRow--data-active' : ''}`}
+                                        key={k}
+                                        onClick={self.viewUser.bind(null, u, k)}>
+                                        <div
+                                            className={`userName userName-header ${((self.state.activeIndex == k) && self.state.userOpen) ? 'activeUser' : ''}`}
+                                        >
+                                            {u['user-name']}
+                                        </div>
+                                        <div>
+                                            {u['user-domain']}
+                                        </div>
+                                        <div>
+                                            {u['disabled'] ? "DISABLED" : "ENABLED"}
+                                        </div>
+                                    </div>
+                                )
+                            })}
+                        </Panel>
+                        <SkyquakeRBAC allow={[PLATFORM.SUPER, PLATFORM.ADMIN]} className="rbacButtonGroup">
+                            <ButtonGroup className="buttonGroup">
+                                <Button label="Add User" onClick={this.addUser} />
+                            </ButtonGroup>
+                        </SkyquakeRBAC>
+                    </PanelWrapper>
+                    <PanelWrapper onKeyUp={this.evaluateSubmit}
+                        className={`userAdmin column`}>
+                        <Panel
+                            title={state.isEdit ? state['user-name'] : 'Create User'}
+                            style={{ marginBottom: 0 }}
+                            hasCloseButton={this.closePanel}
+                            no-corners>
+                            <FormSection title="USER INFO" className="userInfo">
+                            {
+                                (!state.isEditUser ||  state.isReadOnly) ?
+                                <Input className="userInfo-section" readonly={state.isReadOnly || this.state.isEdit}  label="Username" value={state['user-name']} onChange={this.updateInput.bind(null, 'user-name')} />
+                                : null
+                            }
+                                <Input className="userInfo-section" readonly={state.isReadOnly || this.state.isEdit} label="Domain" value={state['user-domain']}  onChange={this.updateInput.bind(null, 'user-domain')}></Input>
+                                <Input className="userInfo-section"
+                                    type="radiogroup"
+                                    onChange={this.disableChange}
+                                    label="STATUS"
+                                    value={this.state.disabled}
+                                    options={[{ label: "DISABLED", value: "TRUE" }, { label: "ENABLED", value: "FALSE" }]}
+                                    readonly={state.isReadOnly}
+                                    readonlydisplay={this.state.disabled == "TRUE" ? "DISABLED" : "ENABLED"}
+                                />
+                            </FormSection>
+                            <FormSection title="PLATFORM ROLES" style={{ display: 'none' }}>
+                                <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>
+                            {
+                                state.isEdit ?
+                                    <FormSection title="PROJECT ROLES">
+                                        <table className="userProfile-table">
+                                            <thead>
+                                                <tr>
+                                                    <td>Project</td>
+                                                    <td>Role</td>
+                                                </tr>
+                                            </thead>
+                                            <tbody>
+                                                {
+                                                    this.state.projects && this.state.projects.ids && this.state.projects.ids.map((p, i) => {
+                                                        let project = self.state.projects.data[p];
+                                                        let userRoles = [];
+                                                        return (
+                                                            <tr key={i}>
+                                                                <td>
+                                                                    {p}
+                                                                </td>
+                                                                <td>
+                                                                    {
+                                                                        project.map(function (k) {
+                                                                            return <div>{k}</div>
+                                                                        })
+                                                                    }
+                                                                </td>
+                                                            </tr>
+                                                        )
+                                                    })
+                                                }
+                                            </tbody>
+                                        </table>
+                                    </FormSection>
+                                    : null
+                            }
+
+                            {passwordSectionHTML}
+
+                        </Panel>
+                        <SkyquakeRBAC allow={[PLATFORM.SUPER, PLATFORM.ADMIN]} className="rbacButtonGroup">
+                            {formButtonsHTML}
+                        </SkyquakeRBAC>
+                    </PanelWrapper>
+                </PanelWrapper>
+            </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+UserManagementDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+UserManagementDashboard.defaultProps = {
+    userList: [],
+    selectedUser: {}
+}
+
+export default SkyquakeComponent(UserManagementDashboard);
+
+
+function isElementInView(el) {
+    var rect = el && el.getBoundingClientRect() || {};
+
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
+    );
+}
+
+
+// isReadOnly={state.isReadOnly} disabled={state.disabled} onChange={this.disableChange}
+
+class isDisabled extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        let props = this.props;
+        return (<div />)
+    }
+}
+
+/**
+ * AddItemFn:
+ */
+class InputCollection extends React.Component {
+    constructor(props) {
+        super(props);
+        this.collection = props.collection;
+    }
+    buildTextInput(onChange, v, i) {
+        return (
+            <Input
+                readonly={this.props.readonly}
+                style={{ flex: '1 1' }}
+                key={i}
+                value={v}
+                onChange={onChange.bind(null, i)}
+            />
+        )
+    }
+    buildSelectOption(initial, options, onChange, v, i) {
+        return (
+            <SelectOption
+                readonly={this.props.readonly}
+                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)}
+                            {
+                                props.readonly ? null : <span onClick={props.RemoveItemFn.bind(null, i)} className="removeInput"><img src={imgRemove} />Remove</span>}
+                        </div>
+                    )
+                })}
+                {props.readonly ? null : <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..3bf81c4
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+/* If there's time this really needs to be rewritten. Ideally with smooth animations.*/
+@import "style/_colors.scss";
+
+.userManagement {
+        max-width: 900px;
+
+    .skyquakePanel-wrapper {
+        overflow-x: hidden;
+    }
+    .userList {
+
+        -ms-flex: 0 1 200px;
+        -webkit-box-flex: 0;
+                flex: 0 1 200px;
+
+        .activeUser {
+            font-weight:bold;
+        }
+
+        /* transition: all 2s;*/
+        &.expanded {
+            -ms-flex: 1 1 100%;
+            -webkit-box-flex: 1;
+                    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;
+                -webkit-box-flex: 0;
+                        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 {
+            &:not(:first-child) {
+                cursor:pointer;
+            }
+        }
+
+    }
+    .userInfo {
+        &-section {
+            padding: 0.5rem;
+            margin-bottom: 0;
+            &:nth-child(even) {
+                background:$neutral-dark-0;
+            }
+        }
+    }
+    .userAdmin {
+            -ms-flex: 1 1;
+            -webkit-box-flex: 1;
+                    flex: 1 1;
+            width:auto;
+            opacity:1;
+    }
+    &.userList-open {
+        .userAdmin {
+            -ms-flex: 0 1 0px;
+                -webkit-box-flex: 0;
+                    flex: 0 1 0px;
+            opacity:0;
+            /* width: 0px;*/
+            display:none;
+            /* transition: opacity 300ms;*/
+            /* transition: width 600ms;*/
+
+        }
+    }
+    .rbacButtonGroup, .buttonSection {
+        margin: 0 0.5rem 0.5rem;
+        background: #ddd;
+        padding-bottom: 0.5rem;
+        padding: 0.5rem 0;
+    }
+    .buttonGroup {
+        border-top: #d3d3d3 1px solid;
+        padding-top:0.5rem;
+    }
+    table {
+        font-size: 0.8rem;
+        thead {
+            border-bottom:1px solid #d3d3d3;
+            td{
+                font-weight:bold;
+            }
+        }
+        td{
+            padding:0.25rem 0.5rem;
+            vertical-align: middle;
+            .checkbox {
+                -ms-flex-pack:center;
+                    -webkit-box-pack:center;
+                        justify-content:center;
+            }
+        }
+    }
+    .sqCheckBox {
+        width:100%;
+        label input {
+            min-width:auto;
+        }
+    }
+    .FormSection label {
+        -webkit-box-flex: 0;
+        -ms-flex: 0 1;
+        flex: 0 1;
+        margin-right: 1rem;
+    }
+}
+
+
+
+
+.tableRow {
+    display:-ms-flexbox;
+    display:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    padding: 0.25rem;
+    >div {
+        padding:0.25rem 1rem 0.25rem 0;
+        -ms-flex: 1 1 33%;
+            -webkit-box-flex: 1;
+                flex: 1 1 33%;
+    }
+    &--header {
+        font-weight:bold;
+    }
+    &--data {
+        &:nth-child(even) {
+            background:$neutral-dark-0;
+        }
+        &:hover:not(&-active) {
+            background:$neutral-dark-1;
+        }
+        &:hover, .activeUser, &-active{
+            cursor:pointer;
+            color:white;
+        }
+        .activeUser, &-active{
+            background: #00acee !important;
+        }
+    }
+
+
+    .userProfile {
+        &-table {
+            thead{
+                font-weight:bold;
+            }
+            font-size: 1rem;
+            tr {
+                td {
+                    vertical-align:top;
+                }
+            }
+        }
+    }
+}
+
+
+
+
+.addInput, .removeInput {
+    display:-ms-flexbox;
+    display:-webkit-box;
+    display:flex;
+    -ms-flex-align:center;
+    -webkit-box-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..7277ce1
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+module.exports = function(Alt) {
+   return Alt.generateActions(
+                                       'handleUpdateInput',
+                                       'handleAddProjectItem',
+                                       'handleRemoveProjectItem',
+                                       'handleUpdateProjectRole',
+                                       'viewUser',
+                                       'editUser',
+                                       'handleCloseUserPanel',
+                                       'handleHideColumns',
+                                       'getUsersSuccess',
+                                       'getUsersNotification',
+                                       'handleDisabledChange',
+                                       'handlePlatformRoleUpdate',
+                                       'handleAddUser',
+                                       'handleCreateUser',
+                                       'handleUpdateUser',
+                                       'updateUserSuccess',
+                                       'createUserSuccess',
+                                       'deleteUserSuccess',
+                                       'handleDisabledChange'
+                                       );
+}
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..edca56f
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * 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;
+}
+
+
+
+
+module.exports = function(Alt) {
+    return {
+        getUsers: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.user);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          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.handleServerReportedError
+        },
+        updateUser: {
+          remote: function(state, user) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'PUT',
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the user.'
+          }),
+          success: Alt.actions.global.updateUserSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        deleteUser: {
+          remote: function(state, user) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                url: `/user/${encodeURIComponent(user['user-name'])}/${encodeURIComponent(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);
+                let msg = xhr.responseText;
+                if(xhr.errorMessage) {
+                  msg = xhr.errorMessage
+                }
+                reject(msg);
+              });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error deleting the user.'
+          }),
+          success: Alt.actions.global.deleteUserSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        createUser: {
+            remote: function(state, user) {
+
+              return new Promise(function(resolve, reject) {
+                $.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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+            },
+            interceptResponse: interceptResponse({
+              'error': 'There was an error updating the account.'
+            }),
+            success: Alt.actions.global.createUserSuccess,
+            loading: Alt.actions.global.showScreenLoader,
+            error: Alt.actions.global.handleServerReportedError
+        }
+      }
+}
+
+function interceptResponse (responses) {
+  return function(data, action, args) {
+    if(responses.hasOwnProperty(data)) {
+      return {
+        type: data,
+        msg: responses[data]
+      }
+    } else {
+      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..d948b0b
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+ * 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'] = 'system';
+        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.isReadOnly = true;
+        this.userOpen = false;
+        this.hideColumns = false;
+        //There is probably a better way of handling the view/edit/readonly matrix conditions for some of these inputs. Should definitely revist
+        this.isEdit = false;
+        this.isEditUser = 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.hasOwnProperty('disabled').toString().toUpperCase(),
+            projectRoles: user.projectRoles || this.projectRoles,
+            projects: user.projects,
+            currentPassword: user.password
+        }
+        let state = _.merge({
+            activeIndex: userIndex,
+            userOpen: true,
+            isEdit: true,
+            isReadOnly: true
+        }, ActiveUser);
+        this.setState(state)
+    }
+    editUser(isEdit) {
+        this.setState({
+            isEditUser: !isEdit,
+            isReadOnly: isEdit
+        })
+    }
+    handleCloseUserPanel() {
+        this.setState({
+            userOpen: false,
+            isEdit: false,
+            isReadOnly: true
+        })
+    }
+    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 = 'system';
+        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() ,
+                              {
+                                isEditUser: false,
+                                isEdit: false,
+                                userOpen: true,
+                                activeIndex: null,
+                                isReadOnly: false
+                            }
+        ))
+    }
+
+    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,
+            isEdit: true,
+            isReadOnly: true
+        })
+    }
+    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,
+            isReadOnly: true,
+            activeIndex: users.length - 1
+        };
+        _.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/src/platformRoleManagement/platformRoleManagement.jsx b/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagement.jsx
new file mode 100644 (file)
index 0000000..bb8acda
--- /dev/null
@@ -0,0 +1,355 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import PlatformRoleManagementStore from './platformRoleManagementStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import 'style/layout.scss';
+import './platformRoleManagement.scss';
+import {Panel, PanelWrapper} from 'widgets/panel/panel';
+import {InputCollection, FormSection} from 'widgets/form_controls/formControls.jsx';
+
+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'
+import _  from 'lodash';
+
+class PlatformRoleManagement extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('PlatformRoleManagementStore') ? this.props.flux.stores.PlatformRoleManagementStore : this.props.flux.createStore(PlatformRoleManagementStore,'PlatformRoleManagementStore');
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+        this.Store.getPlatform();
+        this.Store.getUsers();
+    }
+    componentDidUpdate() {
+
+    }
+    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);
+    }
+    editProject = () => {
+        this.actions.editPlatform(false);
+    }
+    cancelEditPlatform = () => {
+        this.actions.editPlatform(true)
+    }
+    closePanel = () => {
+        this.actions.handleCloseProjectPanel();
+    }
+    updatePlatform = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let platformUsers = self.state.platformUsers;
+        let cleanUsers = this.cleanUsers(platformUsers);
+        this.Store.updatePlatform({
+                'user': JSON.stringify(cleanUsers)
+            }
+        );
+    }
+     cleanUsers(platformUsers) {
+        let self = this;
+        let cleanUsers = [];
+        //Remove null values from role
+        platformUsers.map((u) => {
+            let cleanRoles = [];
+            u.role && u.role.map((r,i) => {
+                let role = {};
+                //Platform user can not change role of itself.
+                if(r.role){
+                    //removing key for rbac-platform
+                    delete r.keys;
+                    cleanRoles.push(r)
+                }
+            });
+           u.role = cleanRoles;
+           // if (u['user-name'] != self.context.userProfile.userId) {
+                cleanUsers.push(u);
+           // }
+        });
+        return cleanUsers;
+    }
+     evaluateSubmit = (e) => {
+        if (e.keyCode == 13) {
+            if (this.props.isEdit) {
+                this.updatePlatform(e);
+            }
+            e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    updateSelectedUser = (e) => {
+        this.setState({
+            selected
+        })
+    }
+    addUserToProject = (e) => {
+        this.actions.handleAddUser();
+    }
+    removeUserFromProject = (userIndex, e) => {
+        this.actions.handleRemoveUserFromProject(userIndex);
+    }
+    updateUserRoleInProject = (userIndex, roleIndex, e) => {
+        this.actions.handleUpdateUserRoleInProject({
+            userIndex,
+            roleIndex,
+            value: JSON.parse(e.target.value)
+        })
+    }
+    toggleUserRoleInProject = (userIndex, roleIndex, e) => {
+        this.actions.handleToggleUserRoleInProject({
+            userIndex,
+            roleIndex,
+            checked: JSON.parse(e.currentTarget.checked)
+        })
+    }
+    removeRoleFromUserInProject = (userIndex, roleIndex, e) => {
+        this.actions.handleRemoveRoleFromUserInProject({
+            userIndex,
+            roleIndex
+        })
+    }
+    addRoleToUserInProject = (userIndex, e) => {
+        this.actions.addRoleToUserInProject(userIndex);
+    }
+    onTransitionEnd = (e) => {
+        this.actions.handleHideColumns(e);
+        console.log('transition end')
+    }
+    disableChange = (e) => {
+        let value = e.target.value;
+        value = value.toUpperCase();
+        if (value=="TRUE") {
+            value = true;
+        } else {
+            value = false;
+        }
+        console.log(value)
+    }
+    render() {
+        let self = this;
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let passwordSectionHTML = null;
+        let formButtonsHTML = (
+            <ButtonGroup className="buttonGroup">
+                <Button label="EDIT" type="submit" onClick={this.editProject} />
+            </ButtonGroup>
+        );
+        let platformUsers = self.state.platformUsers;
+        let availableDomains = state.domains;
+        let availableUsers = state.users && state.users.filter((u) => {
+            return state.selectedDomain == u['user-domain'] && _.findIndex(platformUsers, (s) => {return (s['user-name'] == u['user-name']) && (u['user-domain'] == s['user-domain'])}) == -1
+        }).map((u) => {
+            return {
+                label: `${u['user-name']}`,
+                value: u
+            }
+        });
+
+
+        if(!this.state.isReadOnly) {
+            formButtonsHTML = (
+                                state.isEdit ?
+                                (
+                                    <ButtonGroup className="buttonGroup">
+                                        <Button label="Update" type="submit" onClick={this.updatePlatform} />
+                                        <Button label="Cancel" onClick={this.cancelEditPlatform} />
+                                    </ButtonGroup>
+                                )
+                                : (
+                                    <ButtonGroup className="buttonGroup">
+                                        <Button label="Edit" type="submit" onClick={this.updatePlatform}  />
+                                    </ButtonGroup>
+                                )
+                            )
+        }
+
+        html = (
+            <PanelWrapper column>
+                <AppHeader nav={[{name: 'USER MANAGEMENT', onClick: this.context.router.push.bind(this, {pathname: '/'})}, {name: 'PLATFORM ROLE MANAGEMENT'}]}/>
+                <PanelWrapper className={`row projectManagement ${false ? 'projectList-open' : ''}`} style={{'alignContent': 'center', 'flexDirection': 'row'}} >
+                    <PanelWrapper onKeyUp={this.evaluateSubmit}
+                        className={`ProjectAdmin column`}>
+                        <Panel
+                            title="Manage Roles"
+                            style={{marginBottom: 0}}
+                            no-corners>
+                            <FormSection title="USER ROLES">
+
+                            <table>
+                                <thead>
+                                    <tr>
+                                        <td>Domain</td>
+                                        <td>User Name</td>
+                                        {
+                                            state.roles.map((r,i) => {
+                                                return <td key={i}>{r}</td>
+                                            })
+                                        }
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    {
+                                state.platformUsers.map((u,i)=> {
+                                    let userRoles = u.role && u.role.map((r) => {
+                                        return r.role;
+                                    }) || [];
+                                    return (
+                                        <tr key={i}>
+                                            <td>
+                                                {u['user-domain']}
+                                            </td>
+                                            <td>
+                                                {u['user-name']}
+                                            </td>
+                                            {
+                                                state.roles.map((r,j) => {
+                                                    return <td key={j}><Input readonly={state.isReadOnly} type="checkbox" onChange={self.toggleUserRoleInProject.bind(self, i, j)} checked={(userRoles.indexOf(r) > -1)} /></td>
+                                                })
+                                            }
+                                             {!state.isReadOnly ? <td><span
+                                                                        className="removeInput"
+                                                                        onClick={self.removeUserFromProject.bind(self, i)}
+                                                                    >
+                                                                        <img src={imgRemove} />
+
+                                                                    </span></td> : null}
+                                        </tr>
+                                    )
+                                })
+                            }
+                                </tbody>
+                            </table>
+                                {
+                                    !state.isReadOnly ?
+                                        <div className="tableRow tableRow--header">
+                                            <div>
+                                                <div className="addUser">
+                                                    {
+                                                        availableDomains.length == 1 ?
+                                                            <SelectOption
+                                                                label="Domain"
+                                                                onChange={this.actions.handleSelectedDomain}
+                                                                defaultValue={state.selectedDomain || availableDomains[0]}
+                                                                initial={false}
+                                                                readonly={true}
+                                                                options={availableDomains}
+                                                                ref={(el) => self.selectUserList = el}
+                                                            /> :
+                                                            <SelectOption
+                                                                label="Domain"
+                                                                onChange={this.actions.handleSelectedDomain}
+                                                                value={state.selectedDomain || availableDomains[0]}
+                                                                initial={false}
+                                                                options={availableDomains}
+                                                                ref={(el) => self.selectUserList = el}
+                                                            />
+                                                    }
+                                                    {
+                                                        availableUsers.length ?
+                                                            <SelectOption
+                                                                label="Username"
+                                                                onChange={this.actions.handleSelectedUser}
+                                                                value={state.selectedUser}
+                                                                initial={true}
+                                                                options={availableUsers}
+                                                                ref={(el) => self.selectUserList = el}
+                                                            /> :
+                                                            <label className="noUsersAvailable">
+                                                            <span>Username</span>
+                                                            <span style={{display: 'block',
+        marginTop: '0.8rem', color: '#666'}}>No Available Users for this Domain</span></label>
+                                                    }
+                                                    {
+                                                        availableUsers.length ?
+                                                            <span className="addInput" onClick={this.addUserToProject}><img src={imgAdd} />
+                                                                Add User
+                                                            </span> :
+                                                            null
+                                                    }
+
+                                                </div>
+                                            </div>
+                                        </div> : null
+                                }
+
+                            </FormSection>
+                        </Panel>
+                        {formButtonsHTML}
+                    </PanelWrapper>
+                </PanelWrapper>
+            </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+PlatformRoleManagement.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+PlatformRoleManagement.defaultProps = {
+    projectList: [],
+    selectedProject: {}
+}
+
+export default SkyquakeComponent(PlatformRoleManagement);
+
+
+function isElementInView(el) {
+    var rect = el && el.getBoundingClientRect() || {};
+
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
+    );
+}
+
+
+// isReadOnly={state.isReadOnly} disabled={state.disabled} onChange={this.disableChange}
+
+class isDisabled extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        let props = this.props;
+        return (<div/>)
+    }
+}
+
+
+
+
diff --git a/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagement.scss b/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagement.scss
new file mode 100644 (file)
index 0000000..314c8b3
--- /dev/null
@@ -0,0 +1,337 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+/* If there's time this really needs to be rewritten. Ideally with smooth animations.*/
+@import "style/_colors.scss";
+
+.projectManagement {
+        max-width: 900px;
+
+    .skyquakePanel-wrapper {
+        overflow-x: hidden;
+    }
+    .projectList {
+
+        -ms-flex: 0 1 200px;
+        -webkit-box-flex: 0;
+                flex: 0 1 200px;
+
+        .activeUser {
+            font-weight:bold;
+        }
+
+        /* transition: all 2s;*/
+        &.expanded {
+            -ms-flex: 1 1 100%;
+            -webkit-box-flex: 1;
+                    flex: 1 1 100%;
+            /* transition: all 300ms;*/
+            .tableRow>div:not(.projectName) {
+                opacity: 1;
+                /* width:auto;*/
+                /* transition: width 600ms;*/
+                /* transition: opacity 300ms;*/
+            }
+            &.collapsed {
+                -ms-flex: 0 1 200px;
+                -webkit-box-flex: 0;
+                        flex: 0 1 200px;
+                /* transition: all 2s;*/
+                .tableRow>div:not(.projectName) {
+                    /* opacity: 0;*/
+                    /* width:0px;*/
+                    display:none;
+                    overflow:hidden;
+                    /* transition: all 600ms;*/
+                }
+            }
+        }
+        &.hideColumns {
+            overflow:hidden;
+            >div {
+                overflow:hidden;
+            }
+            .tableRow>div:not(.projectName) {
+                width: 0px;
+                /* transition: all 600ms;*/
+            }
+            .projectName {
+                &--header {
+                    /* display:none;*/
+             }
+            }
+        }
+        .projectName {
+            cursor:pointer;
+        }
+
+
+    }
+
+    .projectAdmin {
+            -ms-flex: 1 1;
+            -webkit-box-flex: 1;
+                    flex: 1 1;
+            width:auto;
+            opacity:1;
+
+        textarea{
+            height: 100px;
+        }
+    }
+    &.projectList-open {
+        .projectAdmin {
+            -ms-flex: 0 1 0px;
+                -webkit-box-flex: 0;
+                    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;
+    }
+    .addUser {
+        display:-ms-flexbox;
+        display:-webkit-box;
+        display:flex;
+        -ms-flex-direction:row;
+            -webkit-box-orient:horizontal;
+            -webkit-box-direction:normal;
+                flex-direction:row;
+        label {
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                -ms-flex-direction: column;
+                    flex-direction: column;
+            display: -webkit-box;
+            display: -ms-flexbox;
+            display: flex;
+            -ms-flex: 0 1;
+            -webkit-box-flex: 0;
+                flex: 0 1;
+
+            width:150px;
+            span {
+                margin-bottom: 0.5rem;
+            }
+            span:nth-child(2) {
+                -webkit-box-align: center;
+                    -ms-flex-align: center;
+                        align-items: center;
+                -webkit-box-flex: 1;
+                    -ms-flex: 1;
+                        flex: 1;
+                display: -webkit-box;
+                display: -ms-flexbox;
+                display: flex;
+            }
+
+            margin-right: 1rem;
+            select {
+                width:150px;
+            }
+        }
+        .noUsersAvailable {
+            display:-webkit-box;
+            display:-ms-flexbox;
+            display:flex;
+            -webkit-box-align: start;
+                -ms-flex-align: start;
+                    align-items: flex-start;
+            margin-bottom: 0.75rem;
+            -webkit-box-flex: 1;
+                -ms-flex: 1;
+                    flex: 1;
+            div {
+                width:260px;
+                margin-bottom: 0.5rem;
+            }
+        }
+    }
+    .projectUsers {
+        .userName {
+            -ms-flex-pack: start;
+            -webkit-box-pack: start;
+                    justify-content: flex-start;
+            padding-top: 0.75rem;
+        }
+        select {
+            margin-bottom:0.5rem;
+        }
+        .addRole {
+            margin:.25rem 0;
+        }
+        .buttonGroup {
+            display:-ms-flexbox;
+            display:-webkit-box;
+            display:flex;
+        }
+
+    }
+    .projectUsers.tableRow--data:hover {
+            background:none;
+            color: black;
+        }
+
+    table {
+        thead {
+            border-bottom:1px solid #d3d3d3;
+            td{
+                font-weight:bold;
+            }
+        }
+        td{
+            padding:0.25rem 0.5rem;
+            vertical-align: middle;
+            .checkbox {
+                -ms-flex-pack:center;
+                    -webkit-box-pack:center;
+                        justify-content:center;
+            }
+        }
+        tbody tr {
+            &:nth-child(even) {
+                background:$neutral-dark-0;
+            }
+        }
+    }
+}
+
+
+
+.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;
+            -webkit-box-flex: 1;
+                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:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+        flex-wrap: nowrap;
+    -ms-flex-align: center;
+        -webkit-box-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:-webkit-box;
+    display:flex;
+    -ms-flex-wrap: nowrap;
+    flex-wrap: nowrap;
+    padding: 0.25rem;
+    >div {
+        padding: 0.25rem;
+        -ms-flex: 1 1 33%;
+            -webkit-box-flex: 1;
+                flex: 1 1 33%;
+        display: -ms-flexbox;
+        display: -webkit-box;
+        display: flex;
+        -ms-flex-direction: column;
+            -webkit-box-orient: vertical;
+            -webkit-box-direction: normal;
+                flex-direction: column;
+        -ms-flex-pack: center;
+            -webkit-box-pack: center;
+                justify-content: center;
+    }
+    &--header {
+        font-weight:bold;
+    }
+    &--data {
+        &:nth-child(even) {
+            background:$neutral-dark-0;
+        }
+        &:hover:not(&-active) {
+            background:$neutral-dark-1;
+        }
+        &:hover, .activeUser, &-active{
+            cursor:pointer;
+            color:white;
+        }
+        .activeUser, &-active{
+            background: #00acee;
+        }
+    }
+}
+
+.addInput, .removeInput {
+    display:-ms-flexbox;
+    display:-webkit-box;
+    display:flex;
+    -webkit-box-align: end;
+    -ms-flex-align: end;
+    align-items: flex-end;
+    margin-bottom: 0.75rem;
+    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/platformRoleManagement/platformRoleManagementActions.js b/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementActions.js
new file mode 100644 (file)
index 0000000..438bde7
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+module.exports = function(Alt) {
+   return Alt.generateActions(
+                                       'handleUpdateInput',
+                                       'handleAddProjectItem',
+                                       'handleRemoveProjectItem',
+                                       'handleUpdateProjectRole',
+                                       'viewProject',
+                                       'editPlatform',
+                                       'handleCancelEdit',
+                                       'handleCloseProjectPanel',
+                                       'handleHideColumns',
+                                       'handleSelectedUser',
+                                       'handleSelectedDomain',
+                                       'handleSelectedRole',
+                                       'handleAddUser',
+                                       'handleRemoveUserFromProject',
+                                       'getProjectsSuccess',
+                                       'getPlatformSuccess',
+                                       'getPlatformRoleUsersSuccess',
+                                       'getProjectsNotification',
+                                       'handleDisabledChange',
+                                       'handlePlatformRoleUpdate',
+                                       'handleAddProject',
+                                       'handleCreateProject',
+                                       'handleUpdateProject',
+                                       'handleUpdateSelectedUser',
+                                       'handleUpdateUserRoleInProject',
+                                       'handleToggleUserRoleInProject',
+                                       'addRoleToUserInProject',
+                                       'handleRemoveRoleFromUserInProject',
+                                       'updatePlatformSuccess',
+                                       'createProjectSuccess',
+                                       'deleteProjectSuccess'
+                                       );
+}
diff --git a/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementSource.js b/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementSource.js
new file mode 100644 (file)
index 0000000..4938dfa
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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;
+}
+
+
+
+module.exports = function(Alt) {
+    return {
+
+        getUsers: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.user);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error retrieving the resource orchestrator information.'
+          }),
+          success: Alt.actions.global.getPlatformRoleUsersSuccess,
+                    loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        getPlatform: {
+          remote: function() {
+              return new Promise(function(resolve, reject) {
+                $.ajax({
+                  url: `/platform?api_server=${API_SERVER}`,
+                  type: 'GET',
+                  beforeSend: Utils.addAuthorizationStub,
+                  success: function(data, textStatus, jqXHR) {
+                    resolve(data.platform);
+                  }
+                }).fail(function(xhr){
+                  //Authentication and the handling of fail states should be wrapped up into a connection class.
+                  Utils.checkAuthentication(xhr.status);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error retrieving the resource orchestrator information.'
+          }),
+          success: Alt.actions.global.getPlatformSuccess,
+                    loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        updatePlatform: {
+          remote: function(state, project) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                  url: `/platform?api_server=${API_SERVER}`,
+                  type: 'PUT',
+                  data: project,
+                  dataType: 'json',
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the project.'
+          }),
+          success: Alt.actions.global.updatePlatformSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        }
+      }
+}
+
+function interceptResponse (responses) {
+  return function(data, action, args) {
+    if(responses.hasOwnProperty(data)) {
+      return {
+        type: data,
+        msg: responses[data]
+      }
+    } else {
+      return data;
+    }
+  }
+}
+
diff --git a/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementStore.js b/skyquake/plugins/user_management/src/platformRoleManagement/platformRoleManagementStore.js
new file mode 100644 (file)
index 0000000..165c74c
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import PlatformRoleManagementActions from './platformRoleManagementActions.js';
+import PlatformRoleManagementSource from './platformRoleManagementSource.js';
+import _ from 'lodash';
+export default class PlatformRoleManagementStore {
+    constructor() {
+        this.actions = PlatformRoleManagementActions(this.alt);
+        this.bindActions(this.actions);
+        this.registerAsync(PlatformRoleManagementSource);
+        this.projects = [];
+        this['name'] = '';
+        this['description'] = 'Some Description';
+        this.platformUsers = [];
+        this.selectedUser = null;
+        this.selectedRole = null;
+        this.selectedDomain = null;
+        this.roles = ['rw-rbac-platform:super-admin', 'rw-rbac-platform:platform-admin', 'rw-rbac-platform:platform-oper'
+        // 'some_other_role', 'yet_another_role', 'operator_role', 'some_other_role', 'yet_another_role'
+        ];
+        this.users = [];
+        this.domains = [];
+        this.activeIndex = null;
+        this.isReadOnly = true;
+        this.projectOpen = 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
+        });
+    }
+    editPlatform(isReadOnly) {
+        let state = _.merge({
+            isEdit: true,
+            isReadOnly: isReadOnly,
+        }, {
+            'platformUsers': this.cachedUsers
+        });
+        this.setState(state)
+    }
+
+    handleCancelEdit() {
+
+    }
+    handleCloseProjectPanel() {
+        this.setState({
+            projectOpen: false,
+            isEdit: false,
+            isReadOnly: true
+        })
+    }
+    handleHideColumns(e) {
+        if(this.projectOpen && 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
+        })
+    }
+    handleSelectedUser(event) {
+        this.setState({
+            selectedUser: JSON.parse(event.currentTarget.value)
+        })
+    }
+
+    handleSelectedRole(event) {
+        this.setState({
+            selectedRole: JSON.parse(event.currentTarget.value)
+        })
+    }
+    resetProject() {
+        let name = '';
+        let description = '';
+        return {
+            'name' : name,
+            'description' : description
+        }
+    }
+    handleAddProject() {
+        this.setState(_.merge( this.resetProject() ,
+              {
+                isEdit: false,
+                projectOpen: true,
+                activeIndex: null,
+                isReadOnly: false,
+                platformUsers: []
+            }
+        ))
+    }
+
+    handleUpdateSelectedUser(user) {
+        this.setState({
+            selectedUser: JSON.parse(user)
+        });
+    }
+    handleSelectedDomain(event) {
+        let domain = JSON.parse(event.target.value);
+        this.setState({
+            selectedDomain: domain
+        });
+    }
+    handleAddUser() {
+        let u = JSON.parse(this.selectedUser);
+        let r = this.selectedRole;
+        let platformUsers = this.platformUsers;
+        console.log('adding user')
+        platformUsers.push({
+          'user-name': u['user-name'],
+          'user-domain': u['user-domain'],
+          "role":[{
+                      "role": r
+            }
+          ]
+        })
+        this.setState({platformUsers, selectedUser: null})
+    }
+    handleToggleUserRoleInProject(data) {
+        let self = this;
+        let {userIndex, roleIndex, checked} = data;
+        let platformUsers = this.platformUsers;
+        let selectedRole = self.roles[roleIndex];
+        if(checked) {
+            if(!platformUsers[userIndex].role) platformUsers[userIndex].role = [];
+            platformUsers[userIndex].role.push({
+                role: selectedRole
+            })
+        } else {
+            let role = platformUsers[userIndex].role;
+            platformUsers[userIndex].role.splice(_.findIndex(role, function(r) { return r.role == selectedRole; }), 1)
+        }
+       self.setState({platformUsers});
+
+    }
+    handleUpdateUserRoleInProject(data) {
+        let {userIndex, roleIndex, value} = data;
+        let platformUsers = this.platformUsers;
+        platformUsers[userIndex].role[roleIndex].role = value;
+
+    }
+    addRoleToUserInProject(userIndex) {
+        let platformUsers = this.platformUsers;
+        if(!platformUsers[userIndex].role) {
+            platformUsers[userIndex].role = [];
+        }
+        platformUsers[userIndex].role.push({
+              'role': null
+            });
+        this.setState({
+            platformUsers
+        })
+    }
+    handleRemoveRoleFromUserInProject (data) {
+        let {userIndex, roleIndex} = data;
+        let platformUsers = this.platformUsers;
+        platformUsers[userIndex].role.splice(roleIndex, 1);
+        this.setState({
+            platformUsers
+        })
+    }
+    handleRemoveUserFromProject (userIndex) {
+        let platformUsers = this.platformUsers;
+        platformUsers.splice(userIndex, 1);
+        this.setState({
+            platformUsers
+        })
+    }
+    getProjectsSuccess(projects) {
+        this.alt.actions.global.hideScreenLoader.defer();
+        this.setState({projects: projects});
+    }
+    getPlatformSuccess(platform) {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let platformUsers = platform && platform.user || [];
+        let state = _.merge({
+            platform: platform,
+            projectOpen: true,
+            isEdit: true,
+            isReadOnly: true,
+            platformUsers: platformUsers,
+            cachedUsers: platformUsers
+        });
+        this.setState(state)
+    }
+    getPlatformRoleUsersSuccess(users) {
+        console.log(users)
+        this.alt.actions.global.hideScreenLoader.defer();
+        let domains =  users && users.reduce(function(arr, u) {
+            if (arr.indexOf(u['user-domain']) == -1) {
+                arr.push(u['user-domain']);
+                return arr;
+            } else {
+                return arr
+            }
+        }, []);
+        this.setState({users, domains, selectedDomain: domains[0]});
+    }
+    updatePlatformSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let platformUsers = this.platformUsers;
+        this.setState({
+            platformUsers,
+            cachedUsers: platformUsers,
+            isEdit: true,
+            isReadOnly: true
+        })
+    }
+    deleteProjectSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let projects = this.projects;
+        projects.splice(this.activeIndex, 1);
+        this.setState({projects, projectOpen: false})
+    }
+    createProjectSuccess() {
+        this.alt.actions.global.hideScreenLoader.defer();
+        let projects = this.projects || [];
+        projects.push({
+            'name': this['name'],
+            'description': this['description']
+         });
+        let newState = {
+            projects,
+            isEdit: true,
+            isReadOnly: true,
+            activeIndex: projects.length - 1
+        };
+        _.merge(newState)
+        this.setState(newState);
+    }
+}
diff --git a/skyquake/plugins/user_management/src/userProfile/userProfile.jsx b/skyquake/plugins/user_management/src/userProfile/userProfile.jsx
new file mode 100644 (file)
index 0000000..c4f657b
--- /dev/null
@@ -0,0 +1,387 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import AppHeader from 'widgets/header/header.jsx';
+import UserProfileStore from './userProfileStore.js';
+import SkyquakeComponent from 'widgets/skyquake_container/skyquakeComponent.jsx';
+import 'style/layout.scss';
+import '../dashboard/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 UserProfileDashboard extends React.Component {
+    constructor(props) {
+        super(props);
+        this.Store = this.props.flux.stores.hasOwnProperty('UserProfileStore') ? this.props.flux.stores.UserProfileStore : this.props.flux.createStore(UserProfileStore);
+        this.state = this.Store.getState();
+        this.actions = this.state.actions;
+
+    }
+    componentDidUpdate() {
+        let self = this;
+        ReactDOM.findDOMNode(this.UserList).addEventListener('transitionend', this.onTransitionEnd, false);
+        setTimeout(function () {
+            let element = self[`user-ref-${self.state.activeIndex}`]
+            element && !isElementInView(element) && element.scrollIntoView({ block: 'end', behavior: 'smooth' });
+        })
+    }
+    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) => {
+        this.actions.viewUser(un, index);
+    }
+    editUser = () => {
+        this.actions.editUser(false);
+    }
+    cancelEditUser = () => {
+        this.actions.editUser(true)
+    }
+    osePanel = () => {
+        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']
+            });
+        }
+    }
+    updateUser = (e) => {
+        let self = this;
+        e.preventDefault();
+        e.stopPropagation();
+        let validatedPasswords = validatePasswordFields(this.state);
+        if (validatedPasswords) {
+            this.Store.updateUser(_.merge({
+                'user-name': this.context.userProfile.userId,
+                'user-domain': this.state['user-domain'],
+                'password': this.state['new-password'],
+                'ui-state': this.context.userProfile.data['ui-state']
+            }));
+        }
+        function validatePasswordFields(state) {
+            let newOne = state['new-password'];
+            let confirmOne = state['confirm-password'];
+            if (!newOne || !confirmOne) {
+                self.props.actions.showNotification('Please fill in all fields.');
+                return false;
+            }
+            if (newOne != confirmOne) {
+                self.props.actions.showNotification('Passwords do not match');
+                return false;
+            }
+            return {
+                'new-password': newOne,
+                'confirm-password': confirmOne
+            }
+        }
+    }
+    evaluateSubmit = (e) => {
+        if (e.keyCode == 13) {
+            if (this.state.isEdit) {
+                this.updateUser(e);
+            } else {
+                this.createUser(e);
+            }
+            e.preventDefault();
+            e.stopPropagation();
+        }
+    }
+    onTransitionEnd = (e) => {
+        this.actions.handleHideColumns(e);
+        console.log('transition end')
+    }
+    disableChange = (e) => {
+        let value = e.target.value;
+        value = value.toUpperCase();
+        if (value == "TRUE") {
+            value = true;
+        } else {
+            value = false;
+        }
+        console.log(value)
+    }
+    render() {
+
+        let self = this;
+        const User = this.context.userProfile || {};
+        let html;
+        let props = this.props;
+        let state = this.state;
+        let passwordSectionHTML = null;
+        let formButtonsHTML = (
+            <ButtonGroup className="buttonGroup">
+                <Button label="EDIT" type="submit" onClick={this.editUser} />
+            </ButtonGroup>
+        );
+        passwordSectionHTML = (
+            (
+                <FormSection title="PASSWORD CHANGE">
+                    <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>
+            )
+        );
+        formButtonsHTML = (
+            <ButtonGroup className="buttonGroup">
+                <Button label="Update" type="submit" onClick={this.updateUser} />
+            </ButtonGroup>
+        )
+
+        html = (
+            <PanelWrapper column>
+                <PanelWrapper className={`row userManagement ${!this.state.userOpen ? 'userList-open' : ''}`} style={{ 'flexDirection': 'row' }} >
+                    <PanelWrapper ref={(div) => { this.UserList = div }} className={`column userList expanded hideColumns`}>
+                        <Panel title={User.userId} style={{ marginBottom: 0 }} no-corners>
+                            <FormSection title="USER INFO">
+                                <table className="userProfile-table">
+                                    <thead>
+                                        <tr>
+                                            <td>Project</td>
+                                            <td>Role</td>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        {
+                                            User.data && User.data.projectId && User.data.projectId.map((p, i) => {
+                                                let project = User.data.project[p];
+                                                let projectConfig = project && project.data['project-config'];
+                                                let userRoles = [];
+                                                return (
+                                                    <tr key={i}>
+                                                        <td>
+                                                            {p}
+                                                        </td>
+                                                        <td>
+                                                            {
+                                                                project && Object.keys(project.role).map(function (k, i) {
+                                                                    return <div key={i}>{k}</div>
+                                                                })
+                                                            }
+                                                        </td>
+                                                    </tr>
+                                                )
+                                            })
+                                        }
+                                    </tbody>
+                                </table>
+                            </FormSection>
+                            {passwordSectionHTML}
+
+                        </Panel>
+                        <div className="buttonSection">
+                            {formButtonsHTML}
+                        </div>
+                    </PanelWrapper>
+
+                </PanelWrapper>
+            </PanelWrapper>
+        );
+        return html;
+    }
+}
+// onClick={this.Store.update.bind(null, Account)}
+UserProfileDashboard.contextTypes = {
+    router: React.PropTypes.object,
+    userProfile: React.PropTypes.object
+};
+
+UserProfileDashboard.defaultProps = {
+    userList: [],
+    selectedUser: {}
+}
+
+export default SkyquakeComponent(UserProfileDashboard);
+
+
+function isElementInView(el) {
+    var rect = el && el.getBoundingClientRect() || {};
+
+    return (
+        rect.top >= 0 &&
+        rect.left >= 0 &&
+        rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */
+        rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */
+    );
+}
+
+
+// isReadOnly={state.isReadOnly} disabled={state.disabled} onChange={this.disableChange}
+
+class isDisabled extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        let props = this.props;
+        return (<div />)
+    }
+}
+
+/**
+ * AddItemFn:
+ */
+class InputCollection extends React.Component {
+    constructor(props) {
+        super(props);
+        this.collection = props.collection;
+    }
+    buildTextInput(onChange, v, i) {
+        return (
+            <Input
+                readonly={this.props.readonly}
+                style={{ flex: '1 1' }}
+                key={i}
+                value={v}
+                onChange={onChange.bind(null, i)}
+            />
+        )
+    }
+    buildSelectOption(initial, options, onChange, v, i) {
+        return (
+            <SelectOption
+                readonly={this.props.readonly}
+                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)}
+                            {
+                                props.readonly ? null : <span onClick={props.RemoveItemFn.bind(null, i)} className="removeInput"><img src={imgRemove} />Remove</span>}
+                        </div>
+                    )
+                })}
+                {props.readonly ? null : <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/userProfile/userProfileActions.js b/skyquake/plugins/user_management/src/userProfile/userProfileActions.js
new file mode 100644 (file)
index 0000000..128b597
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+module.exports = function(Alt) {
+   return Alt.generateActions(
+                                       'handleUpdateInput',
+                                       'handleAddProjectItem',
+                                       'handleRemoveProjectItem',
+                                       'handleUpdateProjectRole',
+                                       'viewUser',
+                                       'editUser',
+                                       'handleCloseUserPanel',
+                                       'handleHideColumns',
+                                       'getUsersNotification',
+                                       'handleDisabledChange',
+                                       'handlePlatformRoleUpdate',
+                                       'handleUpdateUser',
+                                       'updateUserSuccess'
+                                       );
+}
diff --git a/skyquake/plugins/user_management/src/userProfile/userProfileSource.js b/skyquake/plugins/user_management/src/userProfile/userProfileSource.js
new file mode 100644 (file)
index 0000000..d7559b8
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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;
+}
+
+
+
+
+module.exports = function(Alt) {
+    return {
+        updateUser: {
+          remote: function(state, user) {
+            return new Promise(function(resolve, reject) {
+              $.ajax({
+                  url: `/user?api_server=${API_SERVER}`,
+                  type: 'PUT',
+                  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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+            });
+          },
+          interceptResponse: interceptResponse({
+            'error': 'There was an error updating the user.'
+          }),
+          success: Alt.actions.global.updateUserSuccess,
+          loading: Alt.actions.global.showScreenLoader,
+          error: Alt.actions.global.handleServerReportedError
+        },
+        createUser: {
+            remote: function(state, user) {
+
+              return new Promise(function(resolve, reject) {
+                $.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);
+                  let msg = xhr.responseText;
+                  if(xhr.errorMessage) {
+                    msg = xhr.errorMessage
+                  }
+                  reject(msg);
+                });
+              });
+            },
+            interceptResponse: interceptResponse({
+              'error': 'There was an error updating the account.'
+            }),
+            success: Alt.actions.global.createUserSuccess,
+            loading: Alt.actions.global.showScreenLoader,
+            error: Alt.actions.global.handleServerReportedError
+        }
+      }
+}
+
+function interceptResponse (responses) {
+  return function(data, action, args) {
+    if(responses.hasOwnProperty(data)) {
+      return {
+        type: data,
+        msg: responses[data]
+      }
+    } else {
+      return data;
+    }
+  }
+}
+
diff --git a/skyquake/plugins/user_management/src/userProfile/userProfileStore.js b/skyquake/plugins/user_management/src/userProfile/userProfileStore.js
new file mode 100644 (file)
index 0000000..b8a721b
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * STANDARD_RIFT_IO_COPYRIGHT
+ */
+import UserProfileActions from './userProfileActions.js';
+import UserProfileSource from './userProfileSource.js';
+import _ from 'lodash';
+export default class UserProfileStore {
+    constructor() {
+        this.actions = UserProfileActions(this.alt);
+        this.bindActions(this.actions);
+        this.registerAsync(UserProfileSource);
+        this.users = [];
+        this['user-name'] = '';
+        this['user-domain'] = 'system';
+        this.disabled = false;
+        this.roles = ['rw-project:project-admin', 'rw-project:project-oper', 'rw-project:project-create'
+        ];
+        this.currentPassword = '';
+        this['old-password'] = '';
+        this['new-password'] = '';
+        this['confirm-password'] = '';
+
+        this.activeIndex = null;
+        this.isReadOnly = true;
+        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,
+            isReadOnly: true
+        }, ActiveUser);
+        this.setState(state)
+    }
+    editUser(isEdit) {
+        this.setState({
+            isReadOnly: isEdit
+        })
+    }
+    handleCloseUserPanel() {
+        this.setState({
+            userOpen: false,
+            isEdit: false,
+            isReadOnly: true
+        })
+    }
+    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 = 'system';
+        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,
+                                activeIndex: null,
+                                isReadOnly: false
+                            }
+        ))
+    }
+    handleCreateUser() {
+
+    }
+    handleUpdateUser() {
+
+    }
+    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,
+            isEdit: true,
+            isReadOnly: true
+        })
+    }
+    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,
+            isReadOnly: true,
+            activeIndex: users.length - 1
+        };
+        _.merge(newState, this.resetPassword())
+        this.setState(newState);
+    }
+}
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..23b6a5c
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 CompressionPlugin = require("compression-webpack-plugin");
+
+// Added to overcome node-sass bug https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62
+process.env.UV_THREADPOOL_SIZE=64;
+var htmlFilename = (process.argv.indexOf('--production-debug') !== -1) ? 'debug.html' : 'index.html';
+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: '../' + htmlFilename,
+            template: frameworkPath + '/plugin-index.html'
+        }),
+    ]
+};
+
+if (process.argv.indexOf('--optimize-minimize') !== -1) {
+    // we are going to output a gzip file in the production process
+    config.output.filename = "gzip-" + config.output.filename;
+    config.plugins.push(new webpack.DefinePlugin({ // <-- key to reducing React's size
+      'process.env': {
+        'NODE_ENV': JSON.stringify('production')
+      }
+    }));
+    config.plugins.push(new CompressionPlugin({
+        asset: "[path]", // overwrite js file with gz file
+        algorithm: "gzip",
+        test: /\.(js)$/
+    }));
+}
+
+module.exports = config;
diff --git a/skyquake/scripts/build-dev.sh b/skyquake/scripts/build-dev.sh
new file mode 100755 (executable)
index 0000000..80bf626
--- /dev/null
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# 
+#   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.
+#
+
+
+
+abort()
+{
+    echo >&2 '
+***************
+*** ABORTED ***
+***************
+'
+    echo "An error occurred. Exiting..." >&2
+    exit 1
+}
+
+trap 'abort' 0
+
+set -e
+
+# Add your script below....
+# If an error occurs, the abort() function will be called.
+#----------------------------------------------------------
+# change to the directory of this script
+THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+cd $THIS_DIR
+cd ..
+
+echo "NPM config"
+npm config ls
+echo "clean node_modules"
+rm -fr node_modules
+echo "Building RW.UI framework"
+npm install
+echo "RW.UI framework build... done"
+
+echo "Building RW.UI plugins"
+cd plugins
+for f in *; do
+    if [[ -d $f ]]; then
+        echo 'Building plugin '$f
+        cd $f
+        rm -fr node_modules
+        npm install
+        ./node_modules/.bin/webpack --progress --config webpack.production.config.js --bail
+        cd ..
+        echo 'Building plugin '$f' ... done'
+    fi
+done
+
+echo "Building RW.UI plugins... done"
+# Done!
+trap : 0
+
+echo >&2 '
+************
+*** DONE *** 
+************
+'
+
+
diff --git a/skyquake/scripts/build-plugin.sh b/skyquake/scripts/build-plugin.sh
new file mode 100644 (file)
index 0000000..6c57800
--- /dev/null
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+# 
+#   Copyright 2017 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.
+#
+
+abort()
+{
+    echo >&2 '
+***************
+*** ABORTED ***
+***************
+'
+    echo "An error occurred. Exiting..." >&2
+    exit 1
+}
+
+trap 'abort' 0
+
+set -e
+
+# Add your script below....
+# If an error occurs, the abort() function will be called.
+#----------------------------------------------------------
+CMAKE_BUILD=true
+
+# change to the directory of this script
+cd $PLUGIN_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=$CMAKE_BUILD ./node_modules/.bin/webpack --optimize-minimize --progress --config webpack.production.config.js --bail
+echo 'Packaging '$PLUGIN_NAME' using webpack... done'
+echo 'Packaging debug version of '$PLUGIN_NAME' using webpack'
+ui_plugin_cmake_build=$CMAKE_BUILD ./node_modules/.bin/webpack --progress --config webpack.production.config.js --production-debug --bail
+echo 'Packaging '$PLUGIN_NAME' using webpack... done'
+echo 'Building plugin '$PLUGIN_NAME'... done'
+# Done!
+trap : 0
+
+echo >&2 '
+************
+*** DONE *** 
+************
+'
\ No newline at end of file
index 9d9d635..aef81d8 100755 (executable)
@@ -34,12 +34,8 @@ for f in *; do
     if [[ -d $f ]]; then
         echo 'Building plugin '$f
         cd $f
-        echo 'Fetching third-party node_modules for '$f
-        npm install
-        echo 'Fetching third-party node_modules for '$f'...done'
-        echo 'Packaging '$f' using webpack'
-        ./node_modules/.bin/webpack --optimize-minimize --progress --config webpack.production.config.js
-        echo 'Packaging '$f' using webpack... done'
+        echo 'Run build script for '$f
+        ./scripts/build.sh
         cd ..
         echo 'Building plugin '$f'... done'
     fi
diff --git a/skyquake/scripts/handle_plugin_node_modules b/skyquake/scripts/handle_plugin_node_modules
new file mode 100755 (executable)
index 0000000..75b65c0
--- /dev/null
@@ -0,0 +1,58 @@
+#!/bin/bash
+
+# 
+#   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.
+#
+
+# Unpack tar archives for plugins.  Maintain a timestamp.txt file for each, so
+# that tar archives are only unpacked if they have been changed.
+#
+# This script is needed to unpack the files as cpack cannot handle a lot of files.
+
+usage() {
+    echo "usage: $(basename ${BASH_SOURCE[0]})"
+}
+
+function extract_node_modules() {
+    tar xf node_modules.tar
+    touch timestamp.txt
+}
+
+# change to the directory of this script
+THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+
+cd $THIS_DIR
+echo "Handling plugin node modules"
+
+cd ../plugins
+for dir in */; do
+    echo "Checking plugin "${dir}" for newer node_modules"
+    cd ${dir}
+    if [ ! -f timestamp.txt ]; then
+        echo "timestamp file not found ... node_modules need to be expanded and timestamp needs to be touched"
+        extract_node_modules
+    else
+        echo "Checking if node_modules.tar has a newer timestamp than timestamp.txt"
+        if [[ node_modules.tar -nt timestamp.txt ]]; then
+            echo "node_modules.tar is newer than timestamp ... node modules need to be expanded and timestamp needs to be touched"
+            extract_node_modules
+        else
+            echo "node_modules.tar is older than timestamp ... nothing needs to be done"
+        fi
+    fi
+    cd ..
+    echo "Checking plugin "${dir}" for newer node_modules ...done"
+done
+
index 5a8f175..f26b325 100755 (executable)
@@ -17,7 +17,7 @@
 #
 
 usage() {
-       echo "usage: launch_ui.sh [--enable-https --keyfile-path=<keyfile_path> --certfile-path=<certfile-path>]"
+       echo "usage: launch_ui.sh --launchpad-address=<ip_or_fqdn> --idp-port-number=<port_number> [--enable-https --keyfile-path=<keyfile_path> --certfile-path=<certfile-path>]"
 }
 
 function handle_received_signal() {
@@ -26,53 +26,34 @@ function handle_received_signal() {
     exit
 }
 
+# Gets the current hosts ip/fqdn. If the host is resolvable through dns, it
+# returns the fqdn else returns the ip address.
+get_host_address() {
+  if [[ -z $(hostname -d) ]]; then
+    # not resolvable via dns, use resolvable ip address
+    echo $(hostname --ip-address)
+  else
+    # use the fqdn
+    echo $(hostname --fqdn)
+  fi
+}
 
 start_servers() {
        cd $THIS_DIR
        echo "Stopping any previous instances of Skyquake and API servers started with forever"
        forever stopall
 
+  local launchpad_address=$(get_host_address)
 
-       echo "Running Node.js Skyquake server. HTTPS Enabled: ${ENABLE_HTTPS}"
+       echo "Running Node.js Skyquake server. HTTPS Enabled: ${ENABLE_HTTPS}, Launchpad Address: ${launchpad_address}"
        cd ..
        if [ ! -z "${ENABLE_HTTPS}" ]; then
-               forever start -a -l forever.log -o out.log -e err.log skyquake.js       --enable-https --keyfile-path="${KEYFILE_PATH}" --certfile-path="${CERTFILE_PATH}"
+               forever start -a -l forever.log -o out.log -e err.log skyquake.js --enable-https --keyfile-path="${KEYFILE_PATH}" --certfile-path="${CERTFILE_PATH}" --launchpad-address="${LAUNCHPAD_ADDRESS}" --idp-port-number="${IDP_PORT_NUMBER}" --callback-address="${CALLBACK_ADDRESS}"
        else
-               forever start -a -l forever.log -o out.log -e err.log skyquake.js
+               forever start -a -l forever.log -o out.log -e err.log skyquake.js --launchpad-address="${LAUNCHPAD_ADDRESS}"  --idp-port-number="${IDP_PORT_NUMBER}" --callback-address="${CALLBACK_ADDRESS}"
        fi
 }
 
-function extract_node_modules() {
-    tar xf node_modules.tar
-    touch timestamp.txt
-}
-
-function handle_plugin_node_modules() {
-    cd $THIS_DIR
-    echo "Handling plugin node modules"
-
-    cd ../plugins
-    for dir in */; do
-        echo "Checking plugin "${dir}" for newer node_modules"
-        cd ${dir}
-        if [ ! -f timestamp.txt ]; then
-            echo "timestamp file not found ... node_modules need to be expanded and timestamp needs to be touched"
-            extract_node_modules
-        else
-            echo "Checking if node_modules.tar has a newer timestamp than timestamp.txt"
-            if [[ node_modules.tar -nt timestamp.txt ]]; then
-                echo "node_modules.tar is newer than timestamp ... node modules need to be expanded and timestamp needs to be touched"
-                extract_node_modules
-            else
-                echo "node_modules.tar is older than timestamp ... nothing needs to be done"
-            fi
-        fi
-        cd ..
-        echo "Checking plugin "${dir}" for newer node_modules ...done"
-    done
-}
-
-
 # Begin work
 for i in "$@"
 do
@@ -85,6 +66,18 @@ case $i in
     CERTFILE_PATH="${i#*=}"
     shift # past argument=value
     ;;
+    -l=*|--launchpad-address=*)
+    LAUNCHPAD_ADDRESS="${i#*=}"
+    shift # past argument=value
+    ;;
+    -p=*|--idp-port-number=*)
+    IDP_PORT_NUMBER="${i#*=}"
+    shift # past argument=value
+    ;;
+    -r=*|--callback-address=*)
+    CALLBACK_ADDRESS="${i#*=}"
+    shift # past argument=value
+    ;;
     -h|--help)
     usage
     exit
@@ -110,8 +103,10 @@ fi
 # change to the directory of this script
 THIS_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
 
-# Call function to handle tarred node_modules as cpack+RPM cannot handle a lot of files
-handle_plugin_node_modules
+cd $THIS_DIR
+
+# Call script to handle tarred node_modules as cpack+RPM cannot handle a lot of files
+$THIS_DIR/handle_plugin_node_modules
 
 # Call function to start web and API servers
 start_servers
index 9b52e96..5d25216 100644 (file)
@@ -1,3 +1,5 @@
+
+
 /*
  *
  *   Copyright 2016 RIFT.IO Inc
@@ -31,25 +33,25 @@ var constants = require('./framework/core/api_utils/constants');
 // const Replay  = require('replay');
 var freePorts = [];
 for (var i = 0; i < constants.SOCKET_POOL_LENGTH; i++) {
-       freePorts[i] = constants.SOCKET_BASE_PORT + i;
+    freePorts[i] = constants.SOCKET_BASE_PORT + i;
 };
 
 
 if (cluster.isMaster && clusteredLaunch) {
     console.log(cpu, 'CPUs found');
     for (var i = 0; i < cpu; i ++) {
-       var worker = cluster.fork();
-       worker.on('message', function(msg) {
-               if (msg && msg.getPort) {
-                               worker.send({
-                                       port: freePorts.shift()
-                               });
-                               console.log('freePorts after shift for worker', this.process.pid, ':', freePorts);
-                       } else if (msg && msg.freePort) {
-                               freePorts.unshift(msg.port);
-                               console.log('freePorts after unshift of', msg.port, 'for worker', this.process.pid, ':', freePorts);
-                       }
-           });
+        var worker = cluster.fork();
+        worker.on('message', function(msg) {
+            if (msg && msg.getPort) {
+                worker.send({
+                    port: freePorts.shift()
+                });
+                console.log('freePorts after shift for worker', this.process.pid, ':', freePorts);
+            } else if (msg && msg.freePort) {
+                freePorts.unshift(msg.port);
+                console.log('freePorts after unshift of', msg.port, 'for worker', this.process.pid, ':', freePorts);
+            }
+        });
     }
 
     cluster.on('online', function(worker) {
@@ -59,242 +61,348 @@ if (cluster.isMaster && clusteredLaunch) {
         console.log('worker ' + worker.process.pid + ' stopped');
     });
 } else {
-       // Standard library imports
-       var argv = require('minimist')(process.argv.slice(2));
-       var pid = process.pid;
-       var fs = require('fs');
-       var https = require('https');
-       var http = require('http');
-       var express = require('express');
-       var session = require('express-session');
-       var cors = require('cors');
-       var bodyParser = require('body-parser');
-       var _ = require('lodash');
-       var reload = require('require-reload')(require);
-       var Sockets = require('./framework/core/api_utils/sockets.js');
-
-       require('require-json');
-
-       // SSL related configuration bootstrap
-       var httpServer = null;
-       var secureHttpServer = null;
-
-       var httpsConfigured = false;
-
-       var sslOptions = null;
-
-       var apiServer = argv['api-server'] ? argv['api-server'] : 'localhost';
-       var uploadServer = argv['upload-server'] ? argv['upload-server'] : null;
-
-       try {
-               if (argv['enable-https']) {
-                       var keyFilePath = argv['keyfile-path'];
-                       var certFilePath = argv['certfile-path'];
-
-                       sslOptions = {
-                               key: fs.readFileSync(keyFilePath),
-                       cert: fs.readFileSync(certFilePath)
-                       };
-
-                       httpsConfigured = true;
-               }
-       } catch (e) {
-               console.log('HTTPS enabled but file paths missing/incorrect');
-               process.exit(code = -1);
-       }
-
-       var app = express();
-
-       app.use(session({
-         secret: 'ritio rocks',
-         resave: true,
-         saveUninitialized: true
-       }));
-       app.use(bodyParser.json());
-       app.use(cors());
-       app.use(bodyParser.urlencoded({
-               extended: true
-       }));
-
-       var socketManager = new Sockets();
-       var socketConfig = {
-               httpsConfigured: httpsConfigured
-       };
-
-       if (httpsConfigured) {
-               socketConfig.sslOptions = sslOptions;
-       }
-
-       // Rift framework imports
-       var constants = require('./framework/core/api_utils/constants');
-       var skyquakeEmitter = require('./framework/core/modules/skyquakeEmitter');
-       var navigation_routes = require('./framework/core/modules/routes/navigation');
-       var socket_routes = require('./framework/core/modules/routes/sockets');
-       var restconf_routes = require('./framework/core/modules/routes/restconf');
-       var inactivity_routes = require('./framework/core/modules/routes/inactivity');
-       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');
-       /**
-        * Processing when a plugin is added or modified
-        * @param {string} plugin_name - Name of the plugin
-        */
-       function onPluginAdded(plugin_name) {
-               // Load plugin config
-               var plugin_config = reload('./plugins/' + plugin_name + '/config.json');
-
-               // Load all app's views
-               app.use('/' + plugin_name, express.static('./plugins/' + plugin_name + '/' + plugin_config.root));
-
-               // Load all app's routes
-               app.use('/' + plugin_name, require('./plugins/' + plugin_name + '/routes'));
-
-               // Publish navigation links
-               if (plugin_config.routes && _.isArray(plugin_config.routes)) {
-                       skyquakeEmitter.emit('config_discoverer.navigation_discovered', plugin_name, plugin_config);
-               }
-
-       }
-
-       /**
-        * Start listening on a port
-        * @param {string} port - Port to listen on
-        * @param {object} httpServer - httpServer created with http(s).createServer
-        */
-       function startListening(port, httpServer) {
-               var server = httpServer.listen(port, function () {
-                       var host = server.address().address;
-
-                       var port = server.address().port;
-
-                       console.log('Express server listening on port', port);
-               });
-               return server;
-       }
-
-       /**
-        * Initialize skyquake
-        */
-       function init() {
-               skyquakeEmitter.on('plugin_discoverer.plugin_discovered', onPluginAdded);
-               skyquakeEmitter.on('plugin_discoverer.plugin_updated', onPluginAdded);
-       }
-
-       /**
-        * Configure skyquake
-        */
-       function config() {
-               // Conigure any globals
-               process.env.NODE_TLS_REJECT_UNAUTHORIZED=0;
-
-               // Configure navigation router
-               app.use(navigation_routes);
-
-               // Configure restconf router
-               app.use(restconf_routes);
-
-               //Configure inactivity route(s)
-               app.use(inactivity_routes);
-
-               // Configure global config with ssl enabled/disabled
-               var globalConfig = {
-                       ssl_enabled: httpsConfigured,
-                       api_server: apiServer
-               };
-
-               if (uploadServer) {
-                       globalConfig.upload_server = uploadServer;
-               }
-
-               configurationAPI.globalConfiguration.update(globalConfig);
-
-               // Configure configuration route(s)
-               app.use(configuration_routes);
-
-               //Configure descriptor route(s)
-               app.use(descriptor_routes);
-
-               // app.get('/testme', function(req, res) {
-               //      res.sendFile(__dirname + '/index.html');
-               // });
-
-               // Configure HTTP/HTTPS server and populate socketConfig.
-               if (httpsConfigured) {
-                       console.log('HTTPS configured. Will create 2 servers');
-                       secureHttpServer = https.createServer(sslOptions, app);
-                       // Add redirection on SERVER_PORT
-                       httpServer = http.createServer(function(req, res) {
-                               var host = req.headers['host'];
-                               host = host.replace(/:\d+$/, ":" + constants.SECURE_SERVER_PORT);
-
-                               res.writeHead(301, { "Location": "https://" + host + req.url });
-                       res.end();
-                       });
-
-                       socketConfig.httpServer = secureHttpServer;
-               } else {
-                       httpServer = http.createServer(app);
-                       socketConfig.httpServer = httpServer;
-               }
-
-               // Configure socket manager
-               socketManager.configure(socketConfig);
-
-               // Configure socket router
-               socket_routes.routes(socketManager);
-               app.use(socket_routes.router);
-
-               // Serve multiplex-client
-               app.get('/multiplex-client', function(req, res) {
-                       res.sendFile(__dirname + '/node_modules/websocket-multiplex/multiplex_client.js');
-               });
-
-               // handle requests for gzip'd files
-           app.get('*gzip*', function (req, res, next) {
-                       res.set('Content-Encoding', 'gzip');
-                       next();
-           });
-
-       }
-
-       /**
-        * Run skyquake functionality
-        */
-       function run() {
-
-               // Start plugin_discoverer
-               var navigation_manager = require('./framework/core/modules/navigation_manager');
-               var plugin_discoverer = require('./framework/core/modules/plugin_discoverer');
-
-               // Initialize asynchronous modules
-               navigation_manager.init();
-               plugin_discoverer.init();
-
-               // Configure asynchronous modules
-               navigation_manager.config()
-               plugin_discoverer.config({
-                       plugins_path: './plugins'
-               });
-
-               // Run asynchronous modules
-               navigation_manager.run();
-               plugin_discoverer.run();
+    // Standard library imports
+    require('require-json');
+    var argv = require('minimist')(process.argv.slice(2));
+    var pid = process.pid;
+    var fs = require('fs');
+    var https = require('https');
+    var http = require('http');
+    var express = require('express');
+    var session = require('express-session');
+    var cors = require('cors');
+    var lusca = require('lusca');
+    var bodyParser = require('body-parser');
+    var _ = require('lodash');
+    var reload = require('require-reload')(require);
+    var Sockets = require('./framework/core/api_utils/sockets.js');
+    var AuthorizationManager = require('./framework/core/api_utils/auth.js');
+    var utils = require('./framework/core/api_utils/utils.js');
+    var CSRFManager = require('./framework/core/api_utils/csrf.js');
+
+    // SSL related configuration bootstrap
+    var httpServer = null;
+    var secureHttpServer = null;
+
+    var httpsConfigured = false;
+
+    var sslOptions = null;
+
+    var apiServer = argv['api-server'] ? argv['api-server'] : 'localhost';
+    var apiServerProtocol = argv['api-server-protocol'] ? argv['api-server-protocol'] : 'https';
+    var uploadServer = argv['upload-server'] ? argv['upload-server'] : null;
+    var devDownloadServer = argv['dev-download-server'] ? argv['dev-download-server'] : null;
+
+    var launchpadAddress = argv['launchpad-address'] ? argv['launchpad-address'] : constants.LAUNCHPAD_ADDRESS;
+    var idpServerPortNumber = argv['idp-port-number'] ? argv['idp-port-number'] : constants.IDP_PORT_NUMBER;
+    var idpServerProtocol = argv['idp-server-protocol'] ? argv['idp-server-protocol'] : constants.IDP_SERVER_PROTOCOL;
+    var callbackServerProtocol = argv['callback-server-protocol'] ? argv['callback-server-protocol'] : constants.CALLBACK_SERVER_PROTOCOL;
+    var callbackPortNumber = argv['callback-port-number'] ? argv['callback-port-number'] : constants.CALLBACK_PORT_NUMBER;
+    var callbackAddress = argv['callback-address'] ? argv['callback-address'] : constants.CALLBACK_ADDRESS;
+
+    var devServerAddress = argv['dev-server-address'] ? argv['dev-server-address'] : null;
+
+    try {
+        if (argv['enable-https']) {
+            var keyFilePath = argv['keyfile-path'];
+            var certFilePath = argv['certfile-path'];
+
+            sslOptions = {
+                key: fs.readFileSync(keyFilePath),
+                cert: fs.readFileSync(certFilePath)
+            };
+
+            httpsConfigured = true;
+        }
+    } catch (e) {
+        console.log('HTTPS enabled but file paths missing/incorrect');
+        process.exit(code = -1);
+    }
+
+    var app = express();
+
+    app.set('views', __dirname + '/framework/core/views');
+    app.engine('html', require('ejs').renderFile);
+    app.set('view engine', 'ejs');
+
+    app.use(session({
+      secret: 'riftio rocks',
+      resave: false,
+      saveUninitialized: true
+    }));
+    // clickjack attach suppression
+    app.use(lusca.xframe('SAMEORIGIN')); // for older browsers
+    app.use(lusca.csp({ policy: { 'frame-ancestors': '\'none\'' }}));
+
+    app.use(bodyParser.json());
+    app.use(cors());
+    app.use(bodyParser.urlencoded({
+        extended: true
+    }));
+
+    var csrfTarget = (devServerAddress ? devServerAddress : launchpadAddress)
+
+    var csrfConfig = {
+        target: csrfTarget
+    }
+
+    CSRFManager.configure(csrfConfig);
+
+    var openidConfig = {
+        idpServerProtocol: idpServerProtocol,
+        idpServerAddress: launchpadAddress,
+        idpServerPortNumber: idpServerPortNumber,
+        callbackServerProtocol: callbackServerProtocol,
+        callbackAddress: callbackAddress,
+        callbackPortNumber: callbackPortNumber
+    }
+
+    var authManager = new AuthorizationManager(openidConfig);
+    var authConfig = {
+        app: app
+    };
+
+
+    var socketManager = new Sockets();
+    var socketConfig = {
+        httpsConfigured: httpsConfigured
+    };
+
+    if (httpsConfigured) {
+        socketConfig.sslOptions = sslOptions;
+    };
+
+    var sessionsConfig = {
+        authManager: authManager,
+        api_server: apiServer,
+        api_server_protocol: apiServerProtocol
+    }
 
+    // Rift framework imports
+    var constants = require('./framework/core/api_utils/constants');
+    var skyquakeEmitter = require('./framework/core/modules/skyquakeEmitter');
+    var auth_routes = require('./framework/core/modules/routes/auth');
+    var navigation_routes = require('./framework/core/modules/routes/navigation');
+    var socket_routes = require('./framework/core/modules/routes/sockets');
+    var restconf_routes = require('./framework/core/modules/routes/restconf');
+    var inactivity_routes = require('./framework/core/modules/routes/inactivity');
+    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');
+    var projectManagement_routes = require('./framework/core/modules/routes/projectManagement');
+    var session_routes = require('./framework/core/modules/routes/sessions');
+    var schemaAPI = require('./framework/core/modules/api/schemaAPI');
+    var modelAPI = require('./framework/core/modules/api/modelAPI');
+    var appConfigAPI = require('./framework/core/modules/api/appConfigAPI');
+
+    schemaAPI.init();
+    modelAPI.init();
+    appConfigAPI.init();
+    /**
+     * Processing when a plugin is added or modified
+     * @param {string} plugin_name - Name of the plugin
+     */
+    function onPluginAdded(plugin_name) {
+        // Load plugin config
+        var plugin_config = reload('./plugins/' + plugin_name + '/config.json');
+
+        // Load all app's views
+        app.use('/' + plugin_name, express.static('./plugins/' + plugin_name + '/' + plugin_config.root));
+
+        // Load all app's routes
+        app.use('/' + plugin_name, require('./plugins/' + plugin_name + '/routes'));
+
+        // Publish navigation links
+        if (plugin_config.routes && _.isArray(plugin_config.routes)) {
+            skyquakeEmitter.emit('config_discoverer.navigation_discovered', plugin_name, plugin_config);
+        }
+
+    }
+
+    /**
+     * Serve jquery
+     */
+    app.use('/jquery', express.static('./node_modules/jquery/dist/jquery.min.js'));
+    /**
+     * Serve images
+     */
+    app.use('/img', express.static('./framework/style/img'));
+
+    /**
+     * Start listening on a port
+     * @param {string} port - Port to listen on
+     * @param {object} httpServer - httpServer created with http(s).createServer
+     */
+    function startListening(port, httpServer) {
+        var server = httpServer.listen(port, function () {
+            var host = server.address().address;
+
+            var port = server.address().port;
+
+            console.log('Express server listening on port', port);
+        });
+        return server;
+    }
+
+    /**
+     * Initialize skyquake
+     */
+    function init() {
+        skyquakeEmitter.on('plugin_discoverer.plugin_discovered', onPluginAdded);
+        skyquakeEmitter.on('plugin_discoverer.plugin_updated', onPluginAdded);
+    }
+
+    /**
+     * Configure skyquake
+     */
+    function config() {
+        // Conigure any globals
+        process.env.NODE_TLS_REJECT_UNAUTHORIZED=0;
+
+        // Configure auth manager
+        authManager.configure(authConfig);
+
+        // Configure auth router
+        auth_routes.routes(authManager);
+        app.use(auth_routes.router);
+
+        //Configure session route(s)
+        session_routes.routes(sessionsConfig);
+        app.use(session_routes.router);
+
+        // Configure navigation router
+        app.use(navigation_routes);
+
+        // Configure restconf router
+        app.use(restconf_routes);
+
+        //Configure inactivity route(s)
+        app.use(inactivity_routes);
+
+        // Configure global config with ssl enabled/disabled
+        var globalConfig = {
+            ssl_enabled: httpsConfigured,
+            api_server: apiServer,
+            api_server_protocol: apiServerProtocol,
+            api_server_port_number: constants.LAUNCHPAD_PORT,
+            idp_server_address: launchpadAddress,
+            idp_server_protocol: idpServerProtocol,
+            idp_server_port_number: idpServerPortNumber,
+            csrf_target: csrfTarget
+        };
+
+        if (uploadServer) {
+            globalConfig.upload_server = uploadServer;
+        }
+        if (devDownloadServer) {
+            globalConfig.dev_download_server = devDownloadServer;
+        }
+
+        configurationAPI.globalConfiguration.update(globalConfig);
+
+        // Configure configuration route(s)
+        app.use(configuration_routes);
+
+        // Configure schema api
+        app.use(schemaAPI.getRouter());
+
+        // Configure model api
+        app.use(modelAPI.getRouter());
+
+        // Configure config api
+        app.use(appConfigAPI.getRouter());
+
+        //Configure descriptor route(s)
+        app.use(descriptor_routes);
+
+        //Configure user management route(s)
+        app.use(userManagement_routes);
+
+        //Configure project management route(s)
+        app.use(projectManagement_routes);
+
+        // app.get('/testme', function(req, res) {
+        //  res.sendFile(__dirname + '/index.html');
+        // });
+
+        // Configure HTTP/HTTPS server and populate socketConfig.
+        if (httpsConfigured) {
+            console.log('HTTPS configured. Will create 2 servers');
+            secureHttpServer = https.createServer(sslOptions, app);
+            // Add redirection on SERVER_PORT
+            httpServer = http.createServer(function(req, res) {
+                var host = req.headers['host'];
+                host = host.replace(/:\d+$/, ":" + constants.SECURE_SERVER_PORT);
+
+                res.writeHead(301, { "Location": "https://" + host + req.url });
+                res.end();
+            });
+
+            socketConfig.httpServer = secureHttpServer;
+        } else {
+            httpServer = http.createServer(app);
+            socketConfig.httpServer = httpServer;
+        }
+
+        // Configure socket manager
+        socketManager.configure(socketConfig);
+
+        // Configure socket router
+        socket_routes.routes(socketManager);
+        app.use(socket_routes.router);
+
+        // Serve multiplex-client
+        app.get('/multiplex-client', function(req, res) {
+            res.sendFile(__dirname + '/node_modules/websocket-multiplex/multiplex_client.js');
+        });
+
+        // handle requests for gzip'd files
+        app.get('*gzip*', function (req, res, next) {
+            res.set('Content-Encoding', 'gzip');
+            next();
+        });
+
+    }
+
+    /**
+     * Run skyquake functionality
+     */
+    function run() {
+
+        // Start plugin_discoverer
+        var navigation_manager = require('./framework/core/modules/navigation_manager');
+        var plugin_discoverer = require('./framework/core/modules/plugin_discoverer');
+
+        // Initialize asynchronous modules
+        navigation_manager.init();
+        plugin_discoverer.init();
+
+        // Configure asynchronous modules
+        navigation_manager.config()
+        plugin_discoverer.config({
+            plugins_path: './plugins'
+        });
+
+        // Run asynchronous modules
+        navigation_manager.run();
+        plugin_discoverer.run();
+
+
+        // Server start
+        if (httpsConfigured) {
+            console.log('HTTPS configured. Will start 2 servers');
+            // Start listening on SECURE_SERVER_PORT (8443)
+            var secureServer = startListening(constants.SECURE_SERVER_PORT, secureHttpServer);
+        }
+        // Start listening on SERVER_PORT (8000)
+        var server = startListening(constants.SERVER_PORT, httpServer);
+
+    }
 
-               // Server start
-               if (httpsConfigured) {
-                       console.log('HTTPS configured. Will start 2 servers');
-                       // Start listening on SECURE_SERVER_PORT (8443)
-                       var secureServer = startListening(constants.SECURE_SERVER_PORT, secureHttpServer);
-               }
-               // Start listening on SERVER_PORT (8000)
-               var server = startListening(constants.SERVER_PORT, httpServer);
-
-       }
+    init();
 
-       init();
-
-       config();
+    config();
 
-       run();
+    run();
 }
diff --git a/skyquake/tests/stories/FileManager.js b/skyquake/tests/stories/FileManager.js
new file mode 100644 (file)
index 0000000..f1c62da
--- /dev/null
@@ -0,0 +1,19 @@
+import React from 'react';
+import { storiesOf, action } from '@kadira/storybook';
+import reactToJsx from 'react-to-jsx';
+
+import FileManager from '../../plugins/composer/src/src/components/filemanager/FileManager.jsx';
+
+import '../../node_modules/open-iconic/font/css/open-iconic.css';
+import 'style/base.scss';
+
+
+storiesOf('File Manager', module)
+    .add('File Manager', () => {
+        return (
+                <div style={{backgroundColor: '#e5e5e5',height:'100%', position:'absolute', top: 0, left: 0, right: 0, bottom:0}}>
+            <FileManager/>
+            </div>
+        )
+    })
+