update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
Signed-off-by: Jeremy Mordkoff <Jeremy.Mordkoff@riftio.com>
Change-Id: Ib11aa03a2eff5a53c808342508a5d7bee7b202b8
diff --git a/skyquake/framework/widgets/button/button.scss b/skyquake/framework/widgets/button/button.scss
index c972e14..043a769 100644
--- a/skyquake/framework/widgets/button/button.scss
+++ b/skyquake/framework/widgets/button/button.scss
@@ -1,6 +1,6 @@
/*
- *
+ *
* Copyright 2016 RIFT.IO Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -62,15 +62,18 @@
############################################################################ */
.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 @@
/* 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 @@
fill: $primaryForeground;
}
}
-
-
}
-
+.sqButtonGroup {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-pack: center;
+ justify-content: center;
+}
diff --git a/skyquake/framework/widgets/button/sq-button.jsx b/skyquake/framework/widgets/button/sq-button.jsx
index ae93128..8d0ec77 100644
--- a/skyquake/framework/widgets/button/sq-button.jsx
+++ b/skyquake/framework/widgets/button/sq-button.jsx
@@ -36,17 +36,35 @@
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,
diff --git a/skyquake/framework/widgets/components.js b/skyquake/framework/widgets/components.js
index f38719a..461a783 100644
--- a/skyquake/framework/widgets/components.js
+++ b/skyquake/framework/widgets/components.js
@@ -26,355 +26,7 @@
// 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
index 0000000..e50fb7e
--- /dev/null
+++ b/skyquake/framework/widgets/form_controls/formControls.jsx
@@ -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}`)
+ }
+}
diff --git a/skyquake/framework/widgets/form_controls/formControls.scss b/skyquake/framework/widgets/form_controls/formControls.scss
index 4a88435..ad95add 100644
--- a/skyquake/framework/widgets/form_controls/formControls.scss
+++ b/skyquake/framework/widgets/form_controls/formControls.scss
@@ -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;
@@ -57,4 +58,107 @@
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
index 0000000..ca31c13
--- /dev/null
+++ b/skyquake/framework/widgets/form_controls/input.jsx
@@ -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: ''
+
+}
+
diff --git a/skyquake/framework/widgets/form_controls/selectOption.jsx b/skyquake/framework/widgets/form_controls/selectOption.jsx
index 41a8b13..067d1d5 100644
--- a/skyquake/framework/widgets/form_controls/selectOption.jsx
+++ b/skyquake/framework/widgets/form_controls/selectOption.jsx
@@ -1,5 +1,5 @@
/*
- *
+ *
* Copyright 2016 RIFT.IO Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,32 +28,65 @@
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
}
diff --git a/skyquake/framework/widgets/form_controls/textInput.jsx b/skyquake/framework/widgets/form_controls/textInput.jsx
index 03dfa9c..000dcf7 100644
--- a/skyquake/framework/widgets/form_controls/textInput.jsx
+++ b/skyquake/framework/widgets/form_controls/textInput.jsx
@@ -1,5 +1,5 @@
/*
- *
+ *
* Copyright 2016 RIFT.IO Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,58 +18,12 @@
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;
diff --git a/skyquake/framework/widgets/header/header.scss b/skyquake/framework/widgets/header/header.scss
index 5e1e717..6a2e56c 100644
--- a/skyquake/framework/widgets/header/header.scss
+++ b/skyquake/framework/widgets/header/header.scss
@@ -1,5 +1,5 @@
/*
- *
+ *
* Copyright 2016 RIFT.IO Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,14 +17,20 @@
*/
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 @@
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 @@
}
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 @@
}
}
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;
}
}
diff --git a/skyquake/framework/widgets/operational-status/launchpadOperationalStatus.jsx b/skyquake/framework/widgets/operational-status/launchpadOperationalStatus.jsx
index ba77a79..0e12b6e 100644
--- a/skyquake/framework/widgets/operational-status/launchpadOperationalStatus.jsx
+++ b/skyquake/framework/widgets/operational-status/launchpadOperationalStatus.jsx
@@ -1,6 +1,6 @@
/*
- *
+ *
* Copyright 2016 RIFT.IO Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -101,7 +101,7 @@
}
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>)
}
diff --git a/skyquake/framework/widgets/panel/panel.jsx b/skyquake/framework/widgets/panel/panel.jsx
index 4ef7c89..03877b0 100644
--- a/skyquake/framework/widgets/panel/panel.jsx
+++ b/skyquake/framework/widgets/panel/panel.jsx
@@ -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 @@
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 @@
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;
diff --git a/skyquake/framework/widgets/panel/panel.scss b/skyquake/framework/widgets/panel/panel.scss
index 2a31b1c..75dc8ac 100644
--- a/skyquake/framework/widgets/panel/panel.scss
+++ b/skyquake/framework/widgets/panel/panel.scss
@@ -53,9 +53,22 @@
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;
}
diff --git a/skyquake/framework/widgets/skyquake_container/eventCenter.jsx b/skyquake/framework/widgets/skyquake_container/eventCenter.jsx
index 7df4e3e..471efd1 100644
--- a/skyquake/framework/widgets/skyquake_container/eventCenter.jsx
+++ b/skyquake/framework/widgets/skyquake_container/eventCenter.jsx
@@ -16,12 +16,12 @@
*
*/
- /**
- * 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 _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 @@
componentWillReceiveProps(props) {
let stateObject = {};
- let notificationList = sessionStorage.getItem('notificationList');
let latestNotification = sessionStorage.getItem('latestNotification');
if (props.newNotificationEvent && props.newNotificationMsg) {
@@ -67,21 +68,16 @@
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 @@
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 @@
);
}
- this.state.notifications && this.state.notifications.map((notification, notifIndex) => {
- let notificationFields = {};
+ this.state.notifications &&
+ _isArray(this.state.notifications) &&
+ this.state.notifications.map((notification, notifIndex) => {
+ let notificationFields = {};
- notificationFields = this.getNotificationFields(notification);
+ 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>
- );
- });
+ 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
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeApp.scss b/skyquake/framework/widgets/skyquake_container/skyquakeApp.scss
index 2869560..b6f688f 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeApp.scss
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeApp.scss
@@ -7,15 +7,18 @@
.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 @@
}
.skyquakeNav {
display: -ms-flexbox;
+ display: -webkit-box;
display: flex;
color:white;
background:black;
@@ -36,11 +40,14 @@
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 @@
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 @@
display:none;
z-index:2;
width: 100%;
+ &.project {
+ a {
+ text-transform:none;
+ }
+ }
}
&:first-child{
h2 {
@@ -102,12 +116,13 @@
&: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 @@
text-align:left;
position:relative;
-ms-flex: 1 0 auto;
- flex: 1 0 auto;
+ -webkit-box-flex: 1;
+ flex: 1 0 auto;
}
}
.corner-accent {
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeComponent.jsx b/skyquake/framework/widgets/skyquake_container/skyquakeComponent.jsx
index 893a4c1..cd489ac 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeComponent.jsx
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeComponent.jsx
@@ -7,7 +7,7 @@
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 = {
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx b/skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx
index d83c836..5c4c97e 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeContainer.jsx
@@ -18,17 +18,23 @@
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 @@
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 @@
}
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 @@
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 @@
return html;
}
}
+skyquakeContainer.childContextTypes = {
+ userProfile: React.PropTypes.object
+};
skyquakeContainer.contextTypes = {
router: React.PropTypes.object
};
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeContainerActions.js b/skyquake/framework/widgets/skyquake_container/skyquakeContainerActions.js
index 773978b..1124658 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeContainerActions.js
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeContainerActions.js
@@ -1,5 +1,5 @@
/*
- *
+ *
* Copyright 2016 RIFT.IO Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,9 +25,13 @@
'getEventStreamsSuccess',
'getEventStreamsError',
//Notifications
+ 'handleServerReportedError',
'showNotification',
'hideNotification',
//Screen Loader
'showScreenLoader',
- 'hideScreenLoader'
+ 'hideScreenLoader',
+ 'openProjectSocketSuccess',
+ 'getUserProfileSuccess',
+ 'selectActiveProjectSuccess'
);
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeContainerSource.js b/skyquake/framework/widgets/skyquake_container/skyquakeContainerSource.js
index da7080f..f02e7ba 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeContainerSource.js
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeContainerSource.js
@@ -79,7 +79,7 @@
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 @@
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
+ }
}
}
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeContainerStore.js b/skyquake/framework/widgets/skyquake_container/skyquakeContainerStore.js
index 56ebdda..e9ba78e 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeContainerStore.js
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeContainerStore.js
@@ -20,19 +20,25 @@
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 @@
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 @@
// 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 @@
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 @@
})
}
+ 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);
+ }
+ };
+ }
+ 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
- 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;
- }
- }
- this.setState(state);
+ 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 @@
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
index 8814a18..0000000
--- a/skyquake/framework/widgets/skyquake_container/skyquakeNav.jsx
+++ /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;
-}
diff --git a/skyquake/framework/widgets/skyquake_container/skyquakeRouter.jsx b/skyquake/framework/widgets/skyquake_container/skyquakeRouter.jsx
index fc3231d..7d8daa5 100644
--- a/skyquake/framework/widgets/skyquake_container/skyquakeRouter.jsx
+++ b/skyquake/framework/widgets/skyquake_container/skyquakeRouter.jsx
@@ -22,10 +22,24 @@
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 @@
},
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
index 0000000..7986d5e
--- /dev/null
+++ b/skyquake/framework/widgets/skyquake_nav/skyquakeNav.jsx
@@ -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
index 0000000..8a312ff
--- /dev/null
+++ b/skyquake/framework/widgets/skyquake_nav/skyquakeNav.scss
@@ -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
index 0000000..cde7b5d
--- /dev/null
+++ b/skyquake/framework/widgets/skyquake_notification/netConfErrors.js
@@ -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
index 0000000..c8ce157
--- /dev/null
+++ b/skyquake/framework/widgets/skyquake_notification/skyquakeNotification.jsx
@@ -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
index 0000000..9097b4e
--- /dev/null
+++ b/skyquake/framework/widgets/skyquake_rbac/skyquakeRBAC.jsx
@@ -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;
+// }