You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by ma...@apache.org on 2017/06/28 05:57:15 UTC

[02/25] incubator-atlas git commit: ATLAS-1898: initial commit of ODF

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-metadata-browser.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-metadata-browser.js b/odf/odf-web/src/main/webapp/scripts/odf-metadata-browser.js
new file mode 100755
index 0000000..d7072dd
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-metadata-browser.js
@@ -0,0 +1,661 @@
+/**
+ *
+ * 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 $ = require("jquery");
+var React = require("react");
+var ReactBootstrap = require("react-bootstrap");
+
+var Panel = ReactBootstrap.Panel;
+var Table = ReactBootstrap.Table;
+var Label = ReactBootstrap.Label;
+var Image = ReactBootstrap.Image;
+var Modal = ReactBootstrap.Modal;
+var Button = ReactBootstrap.Button;
+var FormControls = ReactBootstrap.FormControls;
+var ListGroup = ReactBootstrap.ListGroup;
+var ListGroupItem = ReactBootstrap.ListGroupItem;
+
+var ODFGlobals = require("./odf-globals.js");
+var UISpec = require("./odf-ui-spec.js");
+var AJAXCleanupMixin = require("./odf-mixins.js");
+var Utils = require("./odf-utils.js")
+var AtlasHelper = Utils.AtlasHelper;
+var URLHelper = Utils.URLHelper;
+
+var ODFBrowser = {
+
+	//set rowReferences property and pass an array of atlas references {id : ..., repositoryId: ...}, these rows will then be fetched
+	//or set rowAssets property and pass an array of data that is supposed to be displayed as is
+	ODFPagingTable : React.createClass({
+
+		mixins : [AJAXCleanupMixin],
+
+		getInitialState : function(){
+			var pageSize = (this.props.pageSize ? this.props.pageSize : 5);
+			var rowReferences = this.props.rowReferences;
+			var rowAssets = this.props.rowAssets;
+			var max = (rowReferences ? rowReferences.length : (rowAssets ? rowAssets.length : 0));
+			var pageRows = (rowAssets ? rowAssets.slice(0, pageSize) : null);
+
+			return {
+				pageRows : pageRows,
+				max : 0, tablePage : 0,
+				pageSize : pageSize,
+				max: max,
+				tablePage: 0,
+				rowReferenceLoadingAborts : []
+			};
+		},
+
+		componentDidMount : function() {
+			if(this.props.rowReferences){
+				var pagerowReferences = this.props.rowReferences.slice(0, this.state.pageSize);
+				this.loadRows(pagerowReferences);
+			}
+		},
+
+		componentWillReceiveProps : function(nextProps){
+			if(!this.isMounted()){
+				return;
+			}
+			this.setStateFromProps(nextProps);
+		},
+
+		setStateFromProps : function(nextProps){
+			if(nextProps.rowReferences && !Utils.arraysEqual(this.props.rowReferences, nextProps.rowReferences)){
+				this.setState({max: nextProps.rowReferences.length, tablePage: 0});
+				var pagerowReferences = nextProps.rowReferences.slice(0, this.state.pageSize);
+				this.loadRows(pagerowReferences);
+			}else if(nextProps.rowAssets && !Utils.arraysEqual(this.props.rowAssets, nextProps.rowAssets)){
+				var rows = nextProps.rowAssets.slice(0, this.state.pageSize);
+				this.setState({pageRows : rows, max: nextProps.rowAssets.length, tablePage: 0});
+			}
+		},
+
+		getType : function(){
+			if(this.props.assetType){
+				return this.props.assetType;
+			}else if(this.state.pageRows && this.state.pageRows.length > 0 && this.state.pageRows[0].type){
+				return this.state.pageRows[0].type;
+			}
+		},
+
+		getUISpec : function(){
+			if(this.props.spec){
+				return this.props.spec;
+			}
+			return UISpec[this.getType()];
+		},
+
+		loadRows : function(rowReferences){
+			$.each(this.state.rowReferenceLoadingAborts, function(key, abort){
+				if(abort && abort.call){
+					abort.call();
+				}
+			});
+
+			this.setState({pageRows: [], rowReferenceLoadingAborts: []});
+
+			var reqs = AtlasHelper.loadAtlasAssets(rowReferences,
+				function(rowAsset){
+					var rowData = this.state.pageRows;
+					rowData.push(rowAsset);
+					if(this.isMounted()){
+						this.setState({pageRows : rowData});
+					}
+					if(rowReferences && rowData && rowData.length == rowReferences.length && this.props.onLoad){
+						this.props.onLoad(rowData);
+					}
+				}.bind(this),
+				function(err){
+
+				}
+			);
+			var aborts = [];
+			$.each(reqs, function(key, val){
+				var aborts = this.state.rowReferenceLoadingAborts;
+				aborts.push(val.abort);
+				this.setState({rowReferenceLoadingAborts: aborts});
+			}.bind(this));
+
+			this.storeAbort(aborts);
+		},
+
+		previousPage : function(){
+			if(this.state.tablePage > -1){
+				var tablePage = this.state.tablePage - 1;
+				this.setState({tablePage : tablePage});
+				if(this.props.rowAssets){
+					var rows = this.props.rowAssets.slice(tablePage * this.state.pageSize, (tablePage + 1) * this.state.pageSize)
+					this.setState({pageRows : rows});
+				}else if(this.props.rowReferences){
+					var rowRefs = this.props.rowReferences.slice(tablePage * this.state.pageSize, (tablePage + 1) * this.state.pageSize)
+					this.loadRows(rowRefs);
+				}
+			}
+		},
+
+		nextPage : function(){
+			var max = this.state.max;
+			if((this.state.tablePage * this.state.pageSize) < max){
+				var tablePage = this.state.tablePage + 1;
+				this.setState({tablePage : tablePage});
+				if(this.props.rowAssets){
+					var rows = this.props.rowAssets.slice(tablePage * this.state.pageSize, (tablePage + 1) * this.state.pageSize)
+					this.setState({pageRows : rows})
+				}else if(this.props.rowReferences){
+					var rows = this.props.rowReferences.slice(tablePage * this.state.pageSize, (tablePage + 1) * this.state.pageSize)
+					this.loadRows(rows);
+				}
+			}
+		},
+
+		sortAlphabetical : function(a, b){
+			if(this.getUISpec() && this.getUISpec().attributes){
+				var attrs = this.getUISpec().attributes;
+				var sortProp = null;
+				for(var no = 0; no < attrs.length; no++){
+					if(attrs[no].sort == true){
+						sortProp = attrs[no].key;
+						var aProp = a[sortProp].toLowerCase();
+						var bProp = b[sortProp].toLowerCase();
+						return ((aProp < bProp) ? -1 : ((aProp > bProp) ? 1 : 0));
+					}
+				}
+			}
+			return 0;
+		},
+
+		onRowClick : function(rowData){
+			if(this.props.onRowClick){
+				var type = this.getType();
+				if(type){
+					//new type is singular of list type ...
+					type = type.substring(0, type.length - 1);
+				}
+				this.props.onRowClick(rowData, type);
+			}
+		},
+
+		parseValue : function(value){
+		    if (value) {
+		        if(Array.isArray(value)){
+		            return value.length;
+		        }else if(typeof value === "object" && value.id && value.url && value.repositoryId){
+		            return <a href={value.url}>{value.id}</a>;
+		        }else if(typeof value === "object"){
+		            return JSON.stringify(value);
+		        }
+		    }
+			return value;
+		},
+
+		render : function(){
+			var loadingImg = <Image src="img/lg_proc.gif" rounded />;
+			var contentRows = [];
+			var pageIndicator = "(0-0)";
+			if(this.state.pageRows){
+				loadingImg = null;
+				this.state.pageRows.sort(this.sortAlphabetical);
+				$.each(this.state.pageRows, function(key, rowData){
+					var displayProperties = [];
+					var icon = null;
+					if(this.getUISpec()){
+						displayProperties = this.getUISpec().attributes;
+						if(this.getUISpec().icon){
+							icon = this.getUISpec().icon;
+						}
+					}else{
+						$.each(rowData, function(propName, val){
+							var label = propName;
+							if(label && label[0]){
+								label = label[0].toUpperCase() + label.slice(1);
+							}
+							displayProperties.push({key: propName, label: label});
+						});
+					}
+
+					var colCss = {};
+					if(this.props.actions){
+						colCss.paddingTop = "26px";
+					}
+					var columns = [<td style={colCss} key={"iconCol" + key}>{icon}</td>];
+					$.each(displayProperties,
+						function(key, propObj){
+							//properties can be a path such as prop1.prop2
+					        var value = ODFGlobals.getPathValue(rowData, propObj.key);
+
+							if(propObj.func){
+								value = propObj.func(value, rowData);
+							}else{
+								value = this.parseValue(value);
+							}
+
+							var col = <td style={colCss} key={propObj.key}>{value}</td>;
+							columns.push(col);
+						}.bind(this)
+					);
+
+					if(this.props.actions){
+						var btns = [];
+						$.each(this.props.actions, function(key, obj){
+							if(obj.assetType.indexOf(this.getType()) > -1){
+									$.each(obj.actions, function(actKey, action){
+										if((action.filter && action.filter(rowData)) || !action.filter){
+											var btn = <div key={actKey}><Button onClick={function(e){e.stopPropagation(); action.func(rowData);}}>{action.label}</Button><br/></div>;
+											btns.push(btn);
+										}
+									});
+							}
+						}.bind(this));
+						columns.push(<td key={"actionBtns"}>{btns}</td>);
+					}
+
+					var rowCss = {};
+					if(this.props.onRowClick){
+						rowCss.cursor = "pointer";
+					}
+
+					var row = <tr style={rowCss} onClick={function(){this.onRowClick(rowData);}.bind(this)} key={key}>
+								{columns}
+							  </tr>;
+					contentRows.push(row);
+				}.bind(this));
+
+				var max = this.state.max;
+				var min = (max > 0 ? (this.state.tablePage * this.state.pageSize + 1)  : 0);
+				pageIndicator = "(" + min + "-";
+				if((this.state.tablePage + 1) * this.state.pageSize >= max){
+					pageIndicator += max + ")";
+				}else{
+					pageIndicator += (this.state.tablePage + 1) * this.state.pageSize + ")";
+				}
+			}
+
+			var header = [];
+			var lbls = [""];
+
+			if(this.getUISpec()){
+				$.each(this.getUISpec().attributes, function(key, propObj){
+					lbls.push(propObj.label);
+				});
+			}else if(this.state.pageRows && this.state.pageRows.length > 0){
+				$.each(this.state.pageRows[0], function(key, val){
+					lbls.push(key[0].toUpperCase() + key.slice(1));
+				});
+			}
+			if(this.props.actions){
+				lbls.push("Actions");
+			}
+
+			$.each(lbls, function(key, val){
+					var headerCss = null;
+					if(val == "Actions"){
+						headerCss = {paddingLeft: "38px"};
+					}
+					var th = <th style={headerCss} key={key}>{val}</th>;
+					header.push(th);
+				});
+
+			return <div style={this.props.style}>
+					<div style={{minHeight:250}}>
+					<Table responsive>
+						<thead>
+							<tr>
+								{header}
+							</tr>
+						</thead>
+						<tbody>
+							{contentRows}
+						</tbody>
+					</Table>
+					</div>
+					<Button disabled={(this.state.pageRows==null || this.state.tablePage <= 0 )} onClick={this.previousPage}>previous</Button>
+					<span>
+						{pageIndicator}
+					</span>
+					<Button disabled={(this.state.pageRows==null || (this.state.tablePage + 1) * this.state.pageSize >= this.state.max)} onClick={this.nextPage}>next</Button>
+				</div>;
+		}
+	}),
+
+	ODFAssetDetails : React.createClass({
+
+		mixins : [AJAXCleanupMixin],
+
+		onHide : function(){
+			if(this.props.onHide){
+				this.props.onHide();
+			}
+			if(this.isMounted()){
+				this.setState({show: true});
+			}
+		},
+
+		getInitialState : function(){
+			return {
+					show : true
+					};
+		},
+
+		getType : function(){
+			if(this.props.assetType){
+				return this.props.assetType;
+			}else if(this.props.asset && this.props.asset.type){
+				return this.props.asset.type;
+			}
+			return null;
+		},
+
+		getUISpec : function(asset){
+			return UISpec[this.getType()];
+		},
+
+		getPropertiesByType : function(srcObject, uiSpecAttributes){
+			var properties = [];
+			var references = [];
+			var lists = [];
+			var objects = [];
+			if (uiSpecAttributes) {
+	            var label = null;
+	            var func = null;
+	            var key = null;
+	            $.each(uiSpecAttributes, function(index, property){
+	                var value = ODFGlobals.getPathValue(srcObject, property.key);
+	                if (value) {
+	                    if(property.func){
+	                        value = property.func(value, srcObject);
+	                    }
+	                    var obj = property;
+	                    obj.value = value;
+	                    if(value && Array.isArray(value)){
+	                        lists.push(obj);
+	                    }else if(value && value.id && value.repositoryId){
+	                        references.push(obj);
+	                    } /*else if(typeof value === "object"){
+	                        objects.push(obj);
+	                    } */else{
+	                        properties.push(obj);
+	                    }
+	                }
+	            }.bind(this) );
+			}
+	        return {lists: lists, properties: properties, references: references, objects: objects};
+		},
+
+		sortPropsByLabelPosition : function(properties, uiSpecAttributes){
+			if(uiSpecAttributes){
+				properties.sort(function(val1, val2){
+					var index1 = -1;
+					var index2 = -1;
+					for(var no = 0; no < uiSpecAttributes.length; no++){
+						if(uiSpecAttributes[no].label == val1.label){
+							index1 = no;
+						}else if(uiSpecAttributes[no].label == val2.label){
+							index2 = no
+						}
+						if(index1 != -1 && index2 != -1){
+							break;
+						}
+					}
+					if(index1 > index2){
+						return 1;
+					}else if(index1 < index2){
+						return -1;
+					}
+					return 0;
+				});
+			}
+		},
+
+		createPropertiesJSX : function(properties){
+			var props = [];
+			$.each(properties, function(key, val){
+				var value = val.value;
+				if(value){
+					var prop = <FormControls.Static key={key} label={val.label} standalone>{val.value}</FormControls.Static>
+					props.push(prop);
+				}
+			}.bind(this));
+			return props;
+		},
+
+		createReferenceJSX : function(references){
+			var refs = [];
+			$.each(references, function(key, val){
+				var prop = <a key={key} href={val.value.url}>{val.label}</a>
+				refs.push(prop);
+			}.bind(this));
+			return refs;
+		},
+
+		createObjectJSX : function(objects){
+			var objs = [];
+			$.each(objects, function(key, val){
+				var obj = <span key={key}>{JSON.stringify(val.value)}</span>;
+				objs.push(obj);
+			}.bind(this));
+
+			return objs;
+		},
+
+		createTableJSX : function(lists){
+			var tables = [];
+			$.each(lists, function(key, val){
+				var isRemote = false;
+				var first = val.value[0];
+				var rowReferences = null;
+				var rowAssets = null;
+				if(first && first.id && first.repositoryId){
+					rowReferences = val.value;
+				}else{
+					rowAssets = val.value;
+				}
+
+				var spec = null;
+				var label = val.label.toLowerCase();
+				var type = label;
+				if(val.uiSpec){
+					spec = UISpec[val.uiSpec];
+				}else{
+					spec = UISpec[type];
+				}
+
+				var table = <div key={val.label + "_" + key}>
+								<h3>{val.label}</h3>
+								<ODFBrowser.ODFPagingTable rowAssets={rowAssets} assetType={type} rowReferences={rowReferences} onRowClick={this.props.onReferenceClick} spec={spec}/>
+							</div>;
+				tables.push(table);
+			}.bind(this));
+
+			return tables;
+		},
+
+		render : function(){
+			var loadingOverlay = <div style={{position:"absolute", width:"100%", height:"100%", left:"50%", top: "30%"}}><Image src="img/lg_proc.gif" rounded /></div>;
+			if(!this.props.loading){
+				loadingOverlay = null;
+			}
+
+			var tablesPanel = <Panel collapsible defaultExpanded={false} header="References">
+	          </Panel>;
+			var propertiesPanel = <Panel collapsible defaultExpanded={false} header="Properties">
+	          </Panel>;
+
+			if(this.props.asset){
+				var uiSpecAttrs = this.getUISpec(this.props.asset).attributes;
+				if(!uiSpecAttrs){
+					uiSpecAttrs = [];
+					$.each(this.props.asset, function(propName, val){
+						var label = propName;
+						if(label && label[0]){
+							label = label[0].toUpperCase() + label.slice(1);
+						}
+						uiSpecAttrs.push({key: propName, label: label});
+					});
+				}
+				var allProps = this.getPropertiesByType(this.props.asset, uiSpecAttrs);
+
+				var properties = allProps.properties;
+				var references = allProps.references;
+				var objects = allProps.objects;
+				var lists = allProps.lists;
+
+				var props = [];
+				var refs = [];
+				var objs = [];
+				var tables = [];
+
+				this.sortPropsByLabelPosition(properties, uiSpecAttrs);
+				props = this.createPropertiesJSX(properties);
+				refs = this.createReferenceJSX(references);
+				objs = this.createObjectJSX(objects);
+				tables = this.createTableJSX(lists);
+
+				if(props.length > 0 || refs.length > 0 || objs.length > 0){
+					propertiesPanel = <Panel collapsible defaultExpanded={true} header="Properties">
+							     		{props}
+							     		{refs}
+							     		{objs}
+							     	  </Panel>;
+				}
+
+				if(tables.length > 0){
+					tablesPanel = <Panel collapsible defaultExpanded={true} header="References">
+						     		{tables}
+						          </Panel>;
+				}
+			}
+
+			var icon = null;
+			if(this.getUISpec(this.props.asset) && this.getUISpec(this.props.asset).icon){
+				icon = this.getUISpec(this.props.asset).icon;
+			}
+
+		    var title = <span>{icon} Details</span>;
+		    if(this.props.asset && this.props.asset.reference){
+		          title = <div>{title} <a target="_blank" href={this.props.asset.reference.url}>( {this.props.asset.reference.id} )</a></div>;
+		    }
+			return <Modal show={this.props.show} onHide={this.onHide}>
+			        	<Modal.Header closeButton>
+			           <Modal.Title>{title}</Modal.Title>
+				        </Modal.Header>
+				        <Modal.Body>
+					    	{loadingOverlay}
+					        {propertiesPanel}
+					        {tablesPanel}
+						</Modal.Body>
+				       <Modal.Footer>
+				       <Button onClick={function(){this.onHide();}.bind(this)}>Close</Button>
+				       </Modal.Footer>
+					</Modal>
+		}
+
+	}),
+
+	//Atlas Metadata browser: either pass an atlas query in the query property in order to execute the query and display the results
+	ODFMetadataBrowser : React.createClass({
+
+		mixins : [AJAXCleanupMixin],
+
+		getInitialState : function() {
+			return ({
+				assets: null,
+				loadingAssetDetails: false
+				});
+		},
+
+		componentWillMount : function() {
+			if(this.props.selection){
+				this.loadSelectionFromAtlas(this.props.selection);
+			}
+		},
+
+		referenceClick: function(val, type){
+			if(!type || (type && val.type)){
+				type = val.type;
+			}
+			var selectedAsset = {id: val.reference.id, repositoryId: val.reference.repositoryId, type: type};
+			URLHelper.setUrlHash(selectedAsset);
+		},
+
+		loadSelectionFromAtlas : function(selection){
+			if(selection){
+				this.setState({showAssetDetails: true, loadingAssetDetails: true});
+				var sel = selection;
+				if(!sel.id){
+					sel = JSON.parse(decodeURIComponent(sel));
+				}
+
+				var loading = false;
+				if(sel.id && sel.repositoryId){
+					if(!this.state.assetDetails || !this.state.assetDetails.reference || this.state.assetDetails.reference &&
+							(this.state.assetDetails.reference.id != sel.id ||
+							this.state.assetDetails.reference.repositoryId != sel.repositoryId)){
+						loading = true;
+						var req = AtlasHelper.loadAtlasAsset(sel,
+								function(data){
+									if(!data.type && sel.type){
+										data.type = sel.type;
+									}
+									var state = {
+											assetDetails: data,
+											loadingAssetDetails: false};
+									this.setState(state);
+								}.bind(this),
+								function(){
+
+								}
+						);
+						this.storeAbort(req.abort);
+					}
+				}
+				if(!loading && this.state.loadingAssetDetails){
+					this.setState({loadingAssetDetails: false});
+				}
+			}
+		},
+
+		componentWillReceiveProps : function(nextProps){
+			if(!this.isMounted()){
+				return;
+			}
+			var newState = {};
+			if(nextProps.selection && this.props.selection != nextProps.selection){
+				this.loadSelectionFromAtlas(nextProps.selection);
+			}else if(nextProps.selection == null){
+				newState.assetDetails = null;
+				newState.showAssetDetails = false;
+			}
+			this.setState(newState);
+		},
+
+		render : function(){
+			var loadingImg = null;
+			var list = null;
+			if(this.props.assets){
+				list = <ODFBrowser.ODFPagingTable actions={this.props.actions} rowAssets={this.props.assets} onRowClick={this.referenceClick} assetType={this.props.type}/>;
+			}else{
+				loadingImg = <Image src="img/lg_proc.gif" rounded />;
+			}
+
+			return <div>{list}
+						{loadingImg}
+						<ODFBrowser.ODFAssetDetails show={this.state.assetDetails != null || this.state.loadingAssetDetails} loading={this.state.loadingAssetDetails} key={(this.state.assetDetails ? this.state.assetDetails.id : "0")} onReferenceClick={this.referenceClick} asset={this.state.assetDetails} onHide={function(){URLHelper.setUrlHash(); this.setState({assetDetails : null})}.bind(this)} />
+					</div>;
+		}
+	})
+}
+
+module.exports = ODFBrowser;

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-mixins.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-mixins.js b/odf/odf-web/src/main/webapp/scripts/odf-mixins.js
new file mode 100755
index 0000000..40c0aa9
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-mixins.js
@@ -0,0 +1,51 @@
+/**
+ *
+ * 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 $ = require("jquery");
+var React = require("react");
+
+var AJAXCleanupMixin = {
+
+	componentWillMount: function() {
+		this.requestAborts = [];
+	},
+
+	storeAborts : function(aborts) {
+		if(Array.isArray(aborts)){
+			$.each(aborts, function(key, val){
+				this.storeAbort(val);
+			}.bind(this));
+		}
+	},
+
+	storeAbort : function(abort) {
+		if(Array.isArray(abort)){
+			$.each(abort, function(key, val){
+				this.requestAborts.push(val);
+			}.bind(this));
+		}else{
+			this.requestAborts.push(abort);
+		}
+	},
+
+	componentWillUnmount : function() {
+		$.each(this.requestAborts, function(key, val){
+			if(val && val.call){
+				val.call();
+			}
+		});
+	}
+};
+
+module.exports = AJAXCleanupMixin;

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-notifications.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-notifications.js b/odf/odf-web/src/main/webapp/scripts/odf-notifications.js
new file mode 100755
index 0000000..a3b99ce
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-notifications.js
@@ -0,0 +1,171 @@
+/**
+ *
+ * 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 $ = require("jquery");
+var React = require("react");
+var ReactDOM = require("react-dom");
+var d3 = require("d3");
+var ReactBootstrap = require("react-bootstrap");
+var ReactD3 = require("react-d3-components");
+var ODFGlobals = require("./odf-globals.js");
+var AJAXCleanupMixin = require("./odf-mixins.js");
+var ReactD3 = require("react-d3-components");
+var LineChart = ReactD3.LineChart;
+var Input = ReactBootstrap.Input;
+var Image = ReactBootstrap.Image;
+
+var REFRESH_DELAY = 5000;
+
+var CurrentNotificationsGraph = React.createClass({
+
+	tooltipLine : function(label, data) {
+        return "Arrived notifications " + data.y;
+    },
+
+	render : function(){
+		var lineChart = null;
+
+		if(this.props.values){
+			var data = [
+			        {
+			        	label: 'Asset notifications',
+			            values: [ ]
+			         }
+			    ];
+
+			for(var no = 0; no < this.props.values.length; no++){
+				data[0].values.push({x : no + 1, y : this.props.values[no]});
+			};
+
+			lineChart = (<LineChart
+			                data={data}
+							width={400}
+			                height={400}
+			                margin={{top: 10, bottom: 50, left: 50, right: 10}}
+			                tooltipContained
+		                    tooltipHtml={this.tooltipLine}
+			                shapeColor={"red"}
+			 				xAxis={{tickValues: []}}
+							/>);
+		}
+
+		return (
+				<div>
+					<h4>Number of received notifications</h4>
+					<h5>(This only works for the node this web application is running on.  In a clustered environment, notifications could be processed on another node and therefore not be visible here)</h5>
+
+					{lineChart}
+				</div>);
+	}
+
+
+});
+
+var ODFNotificationsGraph = React.createClass({
+	mixins : [AJAXCleanupMixin],
+
+	getInitialState : function(){
+		return {notifications : [], notificationCount : [0]};
+	},
+
+	getNotifications : function(){
+		const url = ODFGlobals.metadataUrl + "/notifications?numberOfNotifications=50";
+        var req = $.ajax({
+            url: url,
+            contentType: "application/json",
+            type: 'GET',
+            success: function(data) {
+            	this.setState({notifications: data.notifications});
+            }.bind(this),
+            error: function(xhr, status, err) {
+              var msg = "ODF notification request failed, " + err.toString();
+              this.props.alertCallback({type: "danger", message: msg});
+            }.bind(this)
+        });
+
+        this.storeAbort(req.abort);
+	},
+
+	getNotificationCount : function() {
+		const url = ODFGlobals.metadataUrl + "/notifications/count";
+        var req = $.ajax({
+            url: url,
+            contentType: "application/json",
+            type: 'GET',
+            success: function(data) {
+            	var current = this.state.notificationCount;
+            	if(!current){
+            		current = [];
+            	}else if(current.length > 1 && current[current.length - 1] != current[current.length - 2]){
+            		this.getNotifications();
+            	}
+            	if(current.length == 10){
+            		current.splice(0, 1);
+            	}
+            	current.push(data.notificationCount);
+               this.setState({notificationCount: current});
+            }.bind(this),
+            error: function(xhr, status, err) {
+              var msg = "ODF notification count request failed, " + err.toString();
+              this.props.alertCallback({type: "danger", message: msg});
+            }.bind(this)
+        });
+
+        this.storeAbort(req.abort);
+	},
+
+	componentWillMount : function() {
+		this.getNotifications();
+		this.getNotificationCount();
+	},
+
+	componentWillUnmount () {
+	    this.refreshInterval && clearInterval(this.refreshInterval);
+	    this.refreshInterval = false;
+	},
+
+	componentWillReceiveProps: function(nextProps){
+		if(!nextProps.visible){
+			 this.refreshInterval && clearInterval(this.refreshInterval);
+			 this.refreshInterval = false;
+		}else if(!this.refreshInterval){
+			this.refreshInterval = window.setInterval(this.getNotificationCount, REFRESH_DELAY);
+		}
+	},
+	render : function(){
+		var progressIndicator = <Image src="img/lg_proc.gif" rounded />;
+
+		var notificationGraph = null;
+		if(this.state){
+			progressIndicator = null;
+			notificationGraph = <CurrentNotificationsGraph values={this.state.notificationCount} />;
+		}
+
+		var notificationsValue = "";
+		$.each(this.state.notifications, function(key, val){
+			notificationsValue +="\n";
+			notificationsValue += val.type + " , " + val.asset.repositoryId + " -- " + val.asset.id;
+		});
+
+		return (
+				<div>
+					{progressIndicator}
+					{notificationGraph}
+					<textarea disabled style={{width: '100%', height: '300px'}} value={notificationsValue} />
+				</div>);
+
+	}
+});
+
+module.exports = ODFNotificationsGraph;

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-request-browser.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-request-browser.js b/odf/odf-web/src/main/webapp/scripts/odf-request-browser.js
new file mode 100755
index 0000000..55c053b
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-request-browser.js
@@ -0,0 +1,154 @@
+/**
+ *
+ * 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 $ = require("jquery");
+var React = require("react");
+var ReactBootstrap = require("react-bootstrap");
+
+var ODFAssetDetails = require("./odf-metadata-browser.js").ODFAssetDetails;
+var ODFPagingTable = require("./odf-metadata-browser.js").ODFPagingTable;
+var ODFGlobals = require("./odf-globals.js");
+var AtlasHelper = require("./odf-utils.js").AtlasHelper;
+var URLHelper = require("./odf-utils.js").URLHelper;
+var AJAXCleanupMixin = require("./odf-mixins.js");
+
+var Image = ReactBootstrap.Image;
+
+var ODFRequestBrowser = React.createClass({
+
+	mixins : [AJAXCleanupMixin],
+
+	getInitialState : function(){
+		return {assetDetails : null, loadingAssetDetails: false};
+	},
+
+	getDiscoveryServiceNameFromId(id) {
+		if(!this.props.registeredServices){
+			return id;
+		}
+		var servicesWithSameId = this.props.registeredServices.filter(
+	         function(dsreg) {
+	             return dsreg.id == id;
+	         }
+		);
+		if (servicesWithSameId.length > 0) {
+			return servicesWithSameId[0].name;
+		}
+		return id;
+	},
+
+	loadSelectedRequestStatus: function(requestId){
+		if(requestId){
+			this.setState({showAssetDetails: true, loadingAssetDetails: true});
+
+			var req = $.ajax({
+	            url: ODFGlobals.analysisUrl + "/" + requestId,
+	            contentType: "application/json",
+	            dataType: 'json',
+	            type: 'GET',
+	            success: function(data) {
+	            	$.each(data.serviceRequests, function(key, request){
+	            		var serviceName = this.getDiscoveryServiceNameFromId(request.discoveryServiceId);
+	            		request.discoveryServiceName = serviceName;
+            		}.bind(this));
+
+	               this.setState({assetType: "request", assetDetails: data, loadingAssetDetails: false});
+	            }.bind(this),
+	            error: function(data){
+	            	this.setState({loadingAssetDetails: false});
+	            }.bind(this)
+	        });
+		    this.storeAbort(req.abort);
+
+			if(this.state.loadingAssetDetails){
+				this.setState({loadingAssetDetails: false});
+			}
+		}
+	},
+
+	loadSelectionFromAtlas : function(selection){
+		if(selection){
+			this.setState({showAssetDetails: true, loadingAssetDetails: true});
+			var sel = selection;
+			if(!sel.id){
+				sel = JSON.parse(decodeURIComponent(sel));
+			}
+
+			var loading = false;
+			if(sel.id && sel.repositoryId){
+				if(!this.state.assetDetails || !this.state.assetDetails.reference || this.state.assetDetails.reference &&
+						(this.state.assetDetails.reference.id != sel.id ||
+						this.state.assetDetails.reference.repositoryId != sel.repositoryId)){
+						loading = true;
+						var req = AtlasHelper.loadAtlasAsset(sel,
+							function(data){
+								if(!data.type && sel.type){
+									data.type = sel.type;
+								}
+								var state = {
+										assetDetails: data, assetType: data.type, loadingAssetDetails: false};
+								this.setState(state);
+							}.bind(this),
+							function(){
+
+							}
+						);
+					    this.storeAbort(req.abort);
+				}
+			}
+
+			if(!loading && this.state.loadingAssetDetails){
+				this.setState({loadingAssetDetails: false});
+			}
+		}
+	},
+
+	componentWillReceiveProps : function(nextProps){
+		if(!this.isMounted()){
+			return;
+		}
+		var newState = {};
+		if((nextProps.selection && this.props.selection && this.props.selection.id != nextProps.selection.id) || (nextProps.selection && this.props.selection == null)){
+			if(nextProps.selection.id && nextProps.selection.repositoryId){
+				this.loadSelectionFromAtlas(nextProps.selection);
+			}else{
+				this.loadSelectedRequestStatus(nextProps.selection);
+			}
+		}else if(nextProps.selection == null){
+			newState.assetDetails = null;
+		}
+		this.setState(newState);
+	},
+
+	rowClick : function(val, type){
+		if(!type || (type && val.type)){
+			type = val.type;
+		}
+		if(val && val.reference && val.reference.id){
+			var selectedAsset = {id: val.reference.id, repositoryId: val.reference.repositoryId, type: type};
+			URLHelper.setUrlHash(JSON.stringify(selectedAsset));
+		}else if(val && val.request && val.request.id){
+			URLHelper.setUrlHash(JSON.stringify({requestId : val.request.id}));
+		}
+	},
+
+	render : function(){
+		return <div>
+					<ODFPagingTable actions={this.props.actions} rowAssets={this.props.assets} onRowClick={this.rowClick} assetType="requests"/>
+					<ODFAssetDetails show={this.state.assetDetails != null || this.state.loadingAssetDetails} loading={this.state.loadingAssetDetails} key={(this.state.assetDetails ? this.state.assetDetails.id : "0")} onReferenceClick={this.rowClick} asset={this.state.assetDetails} assetType={this.state.assetType} onHide={function(){URLHelper.setUrlHash(); this.setState({showAssetDetails : false})}.bind(this)} />
+				</div>;
+	}
+});
+
+module.exports = ODFRequestBrowser;

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-services.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-services.js b/odf/odf-web/src/main/webapp/scripts/odf-services.js
new file mode 100755
index 0000000..cf5314b
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-services.js
@@ -0,0 +1,251 @@
+/**
+ *
+ * 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.
+ */
+//js imports
+var $ = require("jquery");
+var bootstrap = require("bootstrap");
+
+var React = require("react");
+var ReactDOM = require("react-dom");
+var LinkedStateMixin = require("react-addons-linked-state-mixin");
+var ReactBootstrap = require("react-bootstrap");
+
+var ODFGlobals = require("./odf-globals.js");
+var AJAXCleanupMixin = require("./odf-mixins.js");
+var configurationStore = require("./odf-utils.js").ConfigurationStore;
+var servicesStore = require("./odf-utils.js").ServicesStore;
+
+var Button = ReactBootstrap.Button;
+var Jumbotron = ReactBootstrap.Jumbotron;
+var Grid = ReactBootstrap.Grid;
+var Row = ReactBootstrap.Row;
+var Col = ReactBootstrap.Col;
+var Table = ReactBootstrap.Table;
+var Modal = ReactBootstrap.Modal;
+var Input = ReactBootstrap.Input;
+var Alert = ReactBootstrap.Alert;
+var Panel = ReactBootstrap.Panel;
+var Label = ReactBootstrap.Label;
+var Input = ReactBootstrap.Input;
+var Image = ReactBootstrap.Image;
+
+var DiscoveryServiceInfo = React.createClass({
+	mixins : [AJAXCleanupMixin],
+
+    testService() {
+        const url = ODFGlobals.servicesUrl + "/" + this.props.dsreg.id;
+        var req = $.ajax({
+            url: url,
+            contentType: "application/json",
+            dataType: 'json',
+            type: 'GET',
+            success: function(data) {
+                var type = "success";
+                if (data.status != "OK") {
+                    type = "danger";
+                }
+                var msg = "Status of ODF service '" + this.props.dsreg.name + "' is "+ data.status +" (" + data.message + ")";
+                this.props.alertCallback({type: type, message: msg});
+            }.bind(this),
+            error: function(xhr, status, err) {
+            	if(status != "abort" ){
+            		console.error(url, status, err.toString());
+            	}
+         	    if(this.isMounted()){
+         	    	var msg = "Service test failed: " + status + ", " + err.toString();
+         	    	this.props.alertCallback({type: "danger", message: msg});
+         	    }
+            }.bind(this)
+        });
+
+        this.storeAbort(req.abort);
+    },
+
+    deleteService() {
+        const url = ODFGlobals.servicesUrl + "/" + this.props.dsreg.id;
+        $.ajax({
+            url: url,
+            type: 'DELETE',
+            success: function(data) {
+            	if(this.isMounted()){
+            		this.props.refreshCallback();
+            	}
+            }.bind(this),
+            error: function(xhr, status, err) {
+            	if(status != "abort" ){
+            		console.error(url, status, err.toString());
+            	}
+            	if(this.isMounted()){
+	              var msg = "Service could not be deleted: " + status + ", " + err.toString();
+	              this.props.alertCallback({type: "danger", message: msg});
+			  }
+			}.bind(this)
+        });
+    },
+
+	render() {
+    	var icon = "";
+    	var imgUrl = this.props.dsreg.iconUrl;
+    	//urls will be used directly.
+    	if(imgUrl != null && (imgUrl.trim().startsWith("http://") || imgUrl.trim().startsWith("https://"))){
+    		icon = imgUrl;
+    	}else{
+    		icon = ODFGlobals.servicesUrl + "/" + encodeURIComponent(this.props.dsreg.id) + "/image";
+    	}
+
+    	var endpointInfo = <span>No additional information</span>;
+    	if (this.props.dsreg.endpoint.type == "Java") {
+            endpointInfo = <span><em>Java class name</em>: {this.props.dsreg.endpoint.className}</span>;
+    	}
+		return (
+				<Grid>
+				  <Row className="show-grid">
+				    <Col sm={1}>
+		             <div >
+		               <Image src={icon} rounded/>
+	   	             </div>
+		            </Col>
+		            <Col sm={4}>
+	             	  <b>{this.props.dsreg.name}</b>
+	             	  <br/>
+	             	  {this.props.dsreg.description}
+	             	  <br/>
+	             	  <a href={this.props.dsreg.link} target="_blank">More</a>
+		              </Col>
+	             	<Col sm={5}>
+	             	  <em>Type</em>: {this.props.dsreg.endpoint.type}
+	       	          <br/>
+	       	          {endpointInfo}
+	       	          <br/>
+	       	          <em>ID</em>: {this.props.dsreg.id}
+	       	          <br/>
+	       	          <em>Protocol</em>: {this.props.dsreg.protocol}
+	             	</Col>
+	             	<Col sm={2}>
+	             	  <Button bsStyle="primary" onClick={this.testService}>Test</Button>
+	             	  <br/>
+	             	 <Button bsStyle="warning" onClick={this.deleteService}>Delete</Button>
+	             	</Col>
+		         </Row>
+		      </Grid>
+		);
+	}
+});
+
+var AddDiscoveryServiceButton = React.createClass({
+  mixins: [LinkedStateMixin, AJAXCleanupMixin],
+
+  getInitialState() {
+	  return({showModal: false, serviceEndpointType: "Spark", parallelismCount: 2, serviceInterfaceType: "DataFrame"});
+  },
+
+  open() {
+    this.setState({showModal: true, errorMessage: null});
+  },
+
+  close() {
+    this.setState({showModal: false});
+  },
+
+  addService() {
+
+	var newService = JSON.parse(JSON.stringify(this.state));
+	delete newService.showModal;
+	delete newService.errorMessage
+
+	 var sparkEndpoint = {
+        jar: newService.serviceApplication,
+        className: newService.serviceClassName,
+        inputMethod: newService.serviceInterfaceType,
+        runtimeName: newService.serviceEndpointType
+	 };
+
+	newService.endpoint = sparkEndpoint;
+
+    delete newService.serviceEndpointType;
+    delete newService.serviceType;
+    delete newService.serviceApplication;
+    delete newService.serviceClassName;
+    delete newService.serviceInterfaceType;
+
+    $.ajax({
+      url: ODFGlobals.servicesUrl,
+      contentType: "application/json",
+      type: 'POST',
+      data: JSON.stringify(newService),
+      success: function(data) {
+		  if(this.isMounted()){
+			  this.close();
+			  this.props.refreshCallback();
+		  }
+      }.bind(this),
+      error: function(xhr, status, err) {
+		  if(this.isMounted()){
+			var errorMsg = status;
+			if(xhr.responseJSON && xhr.responseJSON.error){
+    				errorMsg = xhr.responseJSON.error;
+    		  	}
+		    	var msg = "Service could not be added: " + errorMsg + ", " + err.toString();
+	    	  	this.setState({errorMessage: msg});
+		  }
+      }.bind(this)
+    });
+  },
+
+  render() {
+    var alert = null;
+    if (this.state.errorMessage) {
+       alert = <Alert bsStyle="danger">{this.state.errorMessage}</Alert>;
+    }
+
+  	var endpointInput = null;
+	endpointInput = <div>
+						<Input type="text" valueLink={this.linkState("serviceApplication")} label="Application jar (or zip) file"/>
+						<Input type="text" valueLink={this.linkState("serviceClassName")} label="Class name"/>
+			            <Input type="select" valueLink={this.linkState("serviceInterfaceType")} label="Service interface type" placeholder="DataFrame">
+			                <option value="DataFrame">DataFrame</option>
+			                <option value="Generic">Generic</option>
+		                </Input>
+	            	</div>;
+
+	  return(
+				<span>
+				<Button bsStyle="primary" bsSize="large" onClick={this.open}>Add ODF Service</Button>
+				  <Modal show={this.state.showModal} onHide={this.close}>
+						<Modal.Header closeButton>
+						 	<Modal.Title>Add ODF Service</Modal.Title>
+						</Modal.Header>
+						<Modal.Body>
+						{alert}
+						  <Input type="text" ref="serviceName"  valueLink={this.linkState("name")} label="Name"/>
+							<Input type="text" valueLink={this.linkState("description")} label="Description"/>
+							<Input type="text" valueLink={this.linkState("id")} label="ID"/>
+							<Input type="number" valueLink={this.linkState("parallelismCount")} label="Allowed parallel requests"/>
+							<Input type="select" valueLink={this.linkState("serviceEndpointType")} label="Type" placeholder="Spark">
+			                	<option value="Spark">Spark</option>
+			                </Input>
+							{endpointInput}
+							<Input type="text" valueLink={this.linkState("iconUrl")} label="Icon (Optional)"/>
+							<Input type="text" valueLink={this.linkState("link")} label="Link (Optional)"/>
+					  </Modal.Body>
+				    <Modal.Footer>
+				    <Button bsStyle="primary" onClick={this.addService}>Add</Button>
+				    <Button onClick={this.close}>Cancel</Button>
+				    </Modal.Footer>
+			     </Modal>
+				</span>
+			);
+		}
+});
+module.exports = {DiscoveryServiceInfo: DiscoveryServiceInfo, AddDiscoveryServiceButton: AddDiscoveryServiceButton};

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-settings.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-settings.js b/odf/odf-web/src/main/webapp/scripts/odf-settings.js
new file mode 100755
index 0000000..32802c7
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-settings.js
@@ -0,0 +1,552 @@
+/**
+ *
+ * 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.
+ */
+
+//js imports
+var $ = require("jquery");
+var bootstrap = require("bootstrap");
+var React = require("react");
+var ReactDOM = require("react-dom");
+var LinkedStateMixin = require("react-addons-linked-state-mixin");
+var ReactBootstrap = require("react-bootstrap");
+
+var ODFGlobals = require("./odf-globals.js");
+var AJAXCleanupMixin = require("./odf-mixins.js");
+var configurationStore = require("./odf-utils.js").ConfigurationStore;
+var metadataStore = require("./odf-utils.js").MetadataStore;
+
+var Button = ReactBootstrap.Button;
+var Table = ReactBootstrap.Table;
+var Modal = ReactBootstrap.Modal;
+var Input = ReactBootstrap.Input;
+var Alert = ReactBootstrap.Alert;
+var Panel = ReactBootstrap.Panel;
+var Label = ReactBootstrap.Label;
+var Input = ReactBootstrap.Input;
+var Image = ReactBootstrap.Image;
+var Tabs = ReactBootstrap.Tabs;
+var Tab = ReactBootstrap.Tab;
+
+var ODFConfigPage = React.createClass({
+  mixins: [LinkedStateMixin, AJAXCleanupMixin],
+
+  getInitialState() {
+      return ({odfconfig: { odf: {} }, showDeleteConfirmationDialog: false});
+  },
+
+  componentWillMount() {
+    this.loadODFConfig();
+  },
+
+  componentWillUnmount() {
+	  this.props.alertCallback({type: ""});
+  },
+
+  // all the properties we display under the "odf" path
+  relevantODFPropList: ["instanceId", "odfUrl", "odfUser", "odfPassword", "consumeMessageHubEvents", "atlasMessagehubVcap", "runAnalysisOnImport", "runNewServicesOnRegistration"],
+
+  loadODFConfig() {
+     var req = configurationStore.readConfig(
+    	       function(data) {
+    	    	 // only "fish out" the properties we display and add them as
+    	    	 // toplevel properties to the state.
+
+    	    	 // if we have to make more complex updates this will no longer work
+    	    	 var newStateObj = {};
+    	    	 for (var i=0; i<this.relevantODFPropList.length; i++) {
+    	    		 var prop = this.relevantODFPropList[i];
+    	    		 if (data[prop]) {
+    	    			 newStateObj[prop] = data[prop];
+    	    		 }
+    	    	 }
+    	         this.setState( newStateObj );
+    	       }.bind(this),
+    	       this.props.alertCallback
+    	     );
+	 metadataStore.getProperties(
+			 function(data) {
+			     this.setState({repositoryId: data.STORE_PROPERTY_ID});
+			 }.bind(this)
+	 );
+     this.storeAbort(req.abort);
+  },
+
+  saveODFConfig() {
+	  var newConfigObj = {};
+	  for (var i=0; i<this.relevantODFPropList.length; i++) {
+		 var prop = this.relevantODFPropList[i];
+		 if (this.state[prop] != null) {
+			 newConfigObj[prop] = this.state[prop];
+		 }
+	  }
+	  var req = configurationStore.updateConfig(newConfigObj,
+			  () => {
+					  if(this.isMounted()){
+						  this.props.alertCallback({type: "success", message: "Settings saved successfully."})
+					  }
+				  },
+			  this.props.alertCallback );
+	  this.storeAbort(req.abort);
+  },
+
+  createAtlasSampleData() {
+    this.refs.sampleDataButton.disabled = true;
+    $.ajax({
+       url: ODFGlobals.metadataUrl + "/sampledata",
+       type: 'GET',
+       success: function(data) {
+    	   if(this.isMounted()){
+	    	   this.refs.sampleDataButton.disabled = false;
+	    	   this.props.alertCallback({type: "success", message: "Sample data created successfully."});
+    	   }
+	   }.bind(this),
+       error: function(xhr, status, err) {
+    	   if(this.isMounted()){
+    		   var msg = "Sample data creation failed failed: " + err.toString();
+    		   this.props.alertCallback({type: "danger", message: msg});
+    		   this.refs.sampleDataButton.disabled = false;
+    	   }
+	   }.bind(this)
+     });
+  },
+
+  deleteAllAtlasData() {
+	 this.refs.deleteAllDataButton.disabled = true;
+	 $.ajax({
+	       url: ODFGlobals.metadataUrl + "/resetalldata",
+	       type: 'POST',
+	       success: function(data) {
+	    	   if(this.isMounted()){
+		    	   this.refs.deleteAllDataButton.disabled = false;
+		    	   this.props.alertCallback({type: "success", message: "All data removed!"});
+		    	   this.closeDeleteConfirmationDialog();
+	    	   }
+		   }.bind(this),
+	       error: function(xhr, status, err) {
+	    	   if(this.isMounted()){
+	    		   var msg = "Data deletion failed: " + err.toString();
+	    		   this.props.alertCallback({type: "danger", message: msg});
+	    		   this.refs.deleteAllDataButton.disabled = false;
+		    	   this.closeDeleteConfirmationDialog();
+	    	   }
+		   }.bind(this)
+	     });
+  },
+
+  openDeleteConfirmationDialog() {
+	  this.setState( { showDeleteConfirmationDialog: true} );
+  },
+
+  closeDeleteConfirmationDialog() {
+	  this.setState( { showDeleteConfirmationDialog: false} );
+  },
+
+
+  testAtlasConnection() {
+//	  this.props.alertCallback({type: "warning", message: "Test connection not implemented yet."});
+    $.ajax({
+       url: ODFGlobals.metadataUrl + "/connectiontest",
+       type: 'GET',
+       success: function(data) {
+    	   if(this.isMounted()){
+	    	   this.props.alertCallback({type: "success", message: "Connection test successful."});
+    	   }
+	   }.bind(this),
+       error: function(xhr, status, err) {
+    	   if(this.isMounted()){
+    		   var msg = "Connection test failed: " + err.toString();
+    		   this.props.alertCallback({type: "danger", message: msg});
+    	   }
+	   }.bind(this)
+     });
+  },
+
+  notificationValue() {
+      if (this.state.runAnalysisOnImport) {
+          return "create";
+      }
+      return "none";
+  },
+
+  notificationsChanged() {
+      var newValue = this.refs.runAnalysisInput.getValue();
+      var val = (newValue != "none");
+      this.setState({runAnalysisOnImport: val});
+  },
+
+  render() {
+    var divStyle = {
+      marginLeft: "20px"
+    };
+    return (
+      <div>
+      	<form>
+	      <fieldset className="form-group  label-floating">
+		        <legend>General Settings</legend>
+		        <br/>
+		        <h4>Instance</h4>
+		          <div style={divStyle}>
+		            <Input type="text" label="ODF Instance ID" valueLink={this.linkState("instanceId")} disabled/>
+		            <Input type="text" label="ODF URL" valueLink={this.linkState("odfUrl")}/>
+		            <Input type="text" label="ODF User ID" valueLink={this.linkState("odfUser")}/>
+		            <Input type="password" label="ODF Password" valueLink={this.linkState("odfPassword")}/>
+		          </div>
+		        <hr/>
+		        <h4>Metadata store</h4>
+		        <div style={divStyle}>
+		          <Input type="text" label="Repository ID" valueLink={this.linkState("repositoryId")} disabled/>
+
+		          <div style={divStyle} className="checkbox">
+		           <label>
+	                <input ref="consumeMessageHubEvents" type="checkbox" checkedLink={this.linkState("consumeMessageHubEvents")} />
+	                <span className="checkbox-material">
+	                	<span className="check"></span>
+	                </span>
+	                &nbsp;&nbsp;Consume events from Messagehub instead of a Kafka instance
+	              </label>
+	            </div>
+		          <Input type="text" label="Atlas Messagehub VCAP" disabled={(this.state && !this.state["consumeMessageHubEvents"])} valueLink={this.linkState("atlasMessagehubVcap")}/>
+		          <Button bsStyle="primary" onClick={this.testAtlasConnection}>Test connection</Button>
+		          <Button bsStyle="success" ref="sampleDataButton" onClick={this.createAtlasSampleData}>Create Atlas sample data</Button>
+		          <Button bsStyle="danger" ref="deleteAllDataButton" onClick={this.openDeleteConfirmationDialog}>Delete all Atlas data</Button>
+		          <Modal show={this.state.showDeleteConfirmationDialog} onHide={this.closeDeleteConfirmationDialog}>
+		            <Modal.Header closeButton>
+		              <Modal.Title>Confirm deletion</Modal.Title>
+		            </Modal.Header>
+		            <Modal.Body>
+		              <h4>Are you sure you want to delete all data from the metadata repository?</h4>
+		            </Modal.Body>
+		              <Modal.Footer>
+		              <Button onClick={this.deleteAllAtlasData}>Delete all Data</Button>
+		              <Button onClick={this.closeDeleteConfirmationDialog}>Close</Button>
+		            </Modal.Footer>
+		          </Modal>
+		        </div>
+		        <hr/>
+		        <h4>Notifications</h4>
+		        <div style={divStyle}>
+		          <Input type="select" label="Run analysis automatically" ref="runAnalysisInput" onChange={this.notificationsChanged} value={this.notificationValue()}>
+		             <option value="none">Never</option>
+                     <option value="create">On create</option>
+		          </Input>
+		        </div>
+		        <div style={divStyle} className="checkbox">
+		           <label>
+	                <input ref="runServicesOnRegInput" type="checkbox" checkedLink={this.linkState("runNewServicesOnRegistration")} />
+	                <span className="checkbox-material">
+	                	<span className="check"></span>
+	                </span>
+	                &nbsp;&nbsp;Automatically run all newly registered services in order to keep asset metadata up-to-date
+	              </label>
+	            </div>
+		        <hr/>
+		        <Button className="btn-raised" bsStyle="primary" onClick={this.saveODFConfig}>Save Settings</Button>
+		        <Button onClick={this.loadODFConfig}>Reload</Button>
+	        </fieldset>
+        </form>
+      </div>);
+  }
+
+});
+
+
+var SparkConfigPage = React.createClass({
+  mixins: [LinkedStateMixin, AJAXCleanupMixin],
+
+  getInitialState() {
+      return ({"clusterMasterUrl": ""});
+  },
+
+  componentWillMount() {
+    this.loadODFConfig();
+  },
+
+  componentWillUnmount() {
+	  this.props.alertCallback({type: ""});
+  },
+
+   loadODFConfig() {
+     var req = configurationStore.readConfig(
+    	       function(data) {
+    	    	 var sparkConfig = {};
+    	    	 if(data.sparkConfig != null){
+    	    		 sparkConfig = data.sparkConfig;
+    	    	 }
+    	    	 this.setState(sparkConfig);
+    	       }.bind(this),
+    	       this.props.alertCallback
+    	     );
+     this.storeAbort(req.abort);
+  },
+
+  saveODFConfig() {
+	  var sparkConfig = {clusterMasterUrl: this.state.clusterMasterUrl};
+	  var req = configurationStore.updateConfig({"sparkConfig" : sparkConfig},
+			  () => {
+					  if(this.isMounted()){
+						  this.props.alertCallback({type: "success", message: "Spark config saved successfully."})
+					  }
+				  },
+			  this.props.alertCallback );
+	  this.storeAbort(req.abort);
+  },
+
+  render() {
+    var divStyle = {
+      marginLeft: "20px"
+    };
+
+	var sparkSettings =  <div>
+						<h4>Local spark cluster</h4>
+		    			<div style={divStyle}>
+						  <Input type="text" label="Cluster master url" valueLink={this.linkState("clusterMasterUrl")}/>
+						</div>
+					 </div>;
+
+
+    return (
+      <div>
+      	<form>
+	      <fieldset className="form-group  label-floating">
+		        <legend>Spark configuration</legend>
+		        	{sparkSettings}
+		        <Button className="btn-raised" bsStyle="primary" onClick={this.saveODFConfig}>Save Settings</Button>
+		        <Button onClick={this.loadODFConfig}>Reload</Button>
+	        </fieldset>
+        </form>
+      </div>);
+  }
+
+});
+
+
+var PropertyAddButton = React.createClass({
+  getInitialState() {
+     return ({showModal: false});
+  },
+
+  close() {
+    this.setState({ showModal: false });
+  },
+
+  save() {
+    var newPropObj = {};
+    newPropObj[this.state.name] = this.state.value;
+    var updateConfig = { userDefined: newPropObj };
+    configurationStore.updateConfig(updateConfig,
+    		() => { this.props.successCallback();
+	                this.props.alertCallback({type: "success", message: "User-defined property added successfully."})
+    		},
+    		this.props.alertCallback
+    );
+  },
+
+  saveAndClose() {
+    this.save();
+    this.close();
+  },
+
+  open() {
+    this.setState({ showModal: true });
+  },
+
+  handleTextChange() {
+    this.setState({
+          name: this.refs.inputName.getValue(),
+          value: this.refs.inputValue.getValue()
+        });
+  },
+
+  handleClick() {
+      this.open();
+  },
+
+  render: function() {
+    return (<span>
+    <Button bsStyle="primary" className="btn-raised" onClick={this.handleClick}>Add</Button>
+      <Modal show={this.state.showModal} onHide={this.close}>
+          <Modal.Header closeButton>
+             <Modal.Title>Add Property</Modal.Title>
+          </Modal.Header>
+          <Modal.Body>
+             <Input type="text" ref="inputName" label="Name" onChange={this.handleTextChange}></Input>
+             <Input type="text" ref="inputValue" label="Value" onChange={this.handleTextChange}></Input>
+         </Modal.Body>
+         <Modal.Footer>
+         <Button bsStyle="primary" onClick={this.saveAndClose}>Save</Button>
+         <Button onClick={this.close}>Cancel</Button>
+         </Modal.Footer>
+			</Modal>
+      </span>);
+  }
+
+});
+
+
+var PropertyRemoveButton = React.createClass({
+
+   handleClick() {
+      var newPropObj = {};
+      newPropObj[this.props.name] = null;
+      var updateConfig = { userDefined: newPropObj };
+      configurationStore.updateConfig(updateConfig,
+    		  () => { this.props.successCallback();
+		              this.props.alertCallback({type: "success", message: "User-defined property removed successfully."});
+    		  },
+    		  this.props.alertCallback
+      );
+   },
+
+   render() {
+     return (
+    		 <Button onClick={this.handleClick}>Remove</Button>
+    );
+   }
+
+});
+var PropertyEditButton = React.createClass({
+
+   getInitialState() {
+      return ({showModal: false});
+   },
+
+   close() {
+     this.setState({ showModal: false });
+   },
+
+   save() {
+     var newPropObj = {};
+     newPropObj[this.props.name] = this.state.value;
+     var updateConfig = { userDefined: newPropObj };
+     configurationStore.updateConfig(updateConfig,
+    		 () => { this.props.successCallback();
+    		         this.props.alertCallback({type: "success", message: "User-defined property saved successfully."})
+    		 }, this.props.alertCallback
+     );
+   },
+
+   saveAndClose() {
+     this.save();
+     this.close();
+   },
+
+   open() {
+     this.setState({ showModal: true });
+   },
+
+   handleTextChange() {
+     this.setState({
+           value: this.refs.input.getValue()
+         });
+   },
+
+   handleClick() {
+       this.open();
+   },
+
+   render: function() {
+     return (
+       <span>
+        <Button bsStyle="primary" onClick={this.handleClick}>Edit</Button>
+        <Modal show={this.state.showModal} onHide={this.close}>
+            <Modal.Header closeButton>
+               <Modal.Title>Edit Property</Modal.Title>
+            </Modal.Header>
+            <Modal.Body>
+               <h4>Enter new value for property ''{this.props.name}''</h4>
+               <Input type="text" ref="input" onChange={this.handleTextChange} defaultValue={this.props.value}></Input>
+           </Modal.Body>
+           <Modal.Footer>
+           <Button bsStyle="primary" onClick={this.saveAndClose}>Save</Button>
+           <Button onClick={this.close}>Cancel</Button>
+           </Modal.Footer>
+        </Modal>
+       </span>);
+   }
+});
+
+var UserDefinedConfigPage = React.createClass({
+   mixins : [AJAXCleanupMixin],
+
+   getInitialState: function() {
+      return {odfconfig: { userDefined: {}}};
+   },
+
+   loadUserDefConfig: function() {
+     var req = configurationStore.readConfig(
+       function(data) {
+         this.setState( {odfconfig: data} );
+       }.bind(this),
+       this.props.alertCallback
+     );
+
+     this.storeAbort(req.abort);
+   },
+
+   componentDidMount: function() {
+     this.loadUserDefConfig();
+   },
+
+   componentWillUnmount : function() {
+	  this.props.alertCallback({type: ""});
+   },
+
+   render: function() {
+     var tableContents = $.map(
+           this.state.odfconfig.userDefined,
+           function(value, name) {
+        	   if (value) {
+        		 var tdBtnFixStyle = { paddingTop : "26px"};
+
+                 return <tr key={name}>
+                          <td style={tdBtnFixStyle}>{name}</td>
+                          <td style={tdBtnFixStyle}>{value}</td>
+                          <td><PropertyEditButton name={name} value={value} successCallback={this.loadUserDefConfig} alertCallback={this.props.alertCallback}/>
+                              <PropertyRemoveButton name={name} successCallback={this.loadUserDefConfig} alertCallback={this.props.alertCallback}/>
+                          </td>
+                        </tr>;
+        	   }
+        	   // empty element
+        	   return null;
+           }.bind(this));
+     return (
+       <div>
+	       <form>
+		      <fieldset className="form-group  label-floating">
+		      <legend>
+			       User-defined properties
+		       </legend>
+		       <Table responsive>
+		          <thead>
+		            <tr>
+		              <th>Name</th>
+		              <th>Value</th>
+		              <th></th>
+		            </tr>
+		          </thead>
+		          <tbody>
+		             {tableContents}
+		          </tbody>
+		       </Table>
+		       <PropertyAddButton successCallback={this.loadUserDefConfig} alertCallback={this.props.alertCallback}/>
+	       </fieldset>
+	       </form>
+       </div>);
+   }
+
+});
+
+
+
+module.exports = {ODFConfigPage : ODFConfigPage, SparkConfigPage : SparkConfigPage, UserDefinedConfigPage: UserDefinedConfigPage} ;

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-statistics.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-statistics.js b/odf/odf-web/src/main/webapp/scripts/odf-statistics.js
new file mode 100755
index 0000000..ea0e151
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-statistics.js
@@ -0,0 +1,413 @@
+/**
+ *
+ * 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 $ = require("jquery");
+var React = require("react");
+var d3 = require("d3");
+var ReactBootstrap = require("react-bootstrap");
+var ReactD3 = require("react-d3-components");
+
+var AJAXCleanupMixin = require("./odf-mixins.js");
+
+var Image = ReactBootstrap.Image;
+var Panel = ReactBootstrap.Panel;
+var BarChart = ReactD3.BarChart;
+var PieChart = ReactD3.PieChart;
+var LineChart = ReactD3.LineChart;
+
+var ODFGlobals = require("./odf-globals.js");
+
+const GRAPH_REFRESH_DELAY_MS = 4000;
+
+var ODFStats = {
+	CurrentThreadGraph : React.createClass({
+
+		tooltipLine : function(label, data) {
+	        return "Running threads " + data.y;
+	    },
+
+	    xScale : function() {
+	    	return "";
+	    },
+
+		render : function(){
+			var lineChart = null;
+
+			if(this.props.threadValues){
+
+				var data = [
+				        {
+				        	label: 'Thread count',
+				            values: [ ]
+				         }
+				    ];
+
+				for(var no = 0; no < this.props.threadValues.length; no++){
+					data[0].values.push({x : no + 1, y : this.props.threadValues[no]});
+				};
+
+				lineChart = <LineChart
+				                data={data}
+								width={400}
+				                height={400}
+				                margin={{top: 10, bottom: 50, left: 50, right: 10}}
+				                tooltipContained
+			                    tooltipHtml={this.tooltipLine}
+				                shapeColor={"red"}
+				 				xAxis={{tickValues: []}}
+								/>;
+			}
+
+			return (
+					<div>
+						<h4>Currently running threads in ODF</h4>
+						{lineChart}
+					</div>);
+		}
+
+
+	}),
+
+	SystemDiagnostics : React.createClass({
+		mixins : [AJAXCleanupMixin],
+
+		getODFStatus : function(){
+			var currentState = this.state;
+
+			const url = ODFGlobals.engineUrl + "/status";
+	        var req = $.ajax({
+	            url: url,
+	            contentType: "application/json",
+	            dataType: 'json',
+	            type: 'GET',
+	            success: function(data) {
+	            	if(currentState == null){
+	            		currentState = { threadValues : [0]};
+	            	}
+	            	currentState.threadValues.push(data.threadManagerStatus.length);
+	                if(currentState.threadValues.length > 5){
+	                	currentState.threadValues.splice(0, 1);
+	                }
+
+	            	this.setState(currentState);
+	            }.bind(this),
+	            error: function(xhr, status, err) {
+	              var msg = "ODF status request failed, " + err.toString();
+	              this.props.alertCallback({type: "danger", message: msg});
+	            }.bind(this)
+	        });
+
+	        this.storeAbort(req.abort);
+		},
+
+		componentWillMount : function() {
+			this.getODFStatus();
+		},
+
+		componentWillUnmount () {
+		    this.refreshInterval && clearInterval(this.refreshInterval);
+		    this.refreshInterval = false;
+		},
+
+		componentWillReceiveProps: function(nextProps){
+			if(!nextProps.visible){
+				 this.refreshInterval && clearInterval(this.refreshInterval);
+				 this.refreshInterval = false;
+			}else if(!this.refreshInterval){
+				this.refreshInterval = window.setInterval(this.getODFStatus, GRAPH_REFRESH_DELAY_MS);
+			}
+		},
+
+		tooltipLine : function(label, data) {
+	        return "Running threads " + data.y;
+	    },
+
+	    xScale : function() {
+	    	return "";
+	    },
+
+		render : function(){
+			var progressIndicator = <Image src="img/lg_proc.gif" rounded />;
+
+			var threadGraph = null;
+			if(this.state){
+				progressIndicator = null;
+				threadGraph = <ODFStats.CurrentThreadGraph threadValues={this.state.threadValues} />;
+			}
+
+			return (
+					<div>
+						{progressIndicator}
+						{threadGraph}
+					</div> );
+		}
+	}),
+
+	TotalAnalysisGraph : React.createClass({
+		mixins : [AJAXCleanupMixin],
+
+		getAnalysisStats : function() {
+			const url = ODFGlobals.analysisUrl + "/stats";
+	        var req = $.ajax({
+	            url: url,
+	            contentType: "application/json",
+	            dataType: 'json',
+	            type: 'GET',
+	            success: function(data) {
+	               this.setState(data);
+	            }.bind(this),
+	            error: function(xhr, status, err) {
+	              var msg = "Analysis stats request failed, " + err.toString();
+	              this.props.alertCallback({type: "danger", message: msg});
+	            }.bind(this)
+	        });
+
+	        this.storeAbort(req.abort);
+		},
+
+		componentWillMount : function() {
+			this.getAnalysisStats();
+		},
+
+		componentWillUnmount () {
+		    this.refreshInterval && clearInterval(this.refreshInterval);
+		    this.refreshInterval = false;
+		},
+
+		componentWillReceiveProps: function(nextProps){
+			if(!nextProps.visible){
+				 this.refreshInterval && clearInterval(this.refreshInterval);
+				 this.refreshInterval = false;
+			}else if(!this.refreshInterval){
+				this.refreshInterval = window.setInterval(this.getAnalysisStats, GRAPH_REFRESH_DELAY_MS);
+			}
+		},
+
+		tooltipPie : function(x, y) {
+		    return y.toString() + " absolute";
+		},
+
+		render : function() {
+			var progressIndicator = <Image src="img/lg_proc.gif" rounded />;
+			var pieChart = null;
+
+			if(this.state){
+				progressIndicator = null;
+				var succ = (this.state.success ? this.state.success : (this.state.failure ? 0 : 100));
+				var fail = (this.state.failure ? this.state.failure : 0);
+				var onePercent = (succ + fail) / 100;
+
+				var succVal = (onePercent == 0 ? 100 : (succ / onePercent)).toFixed(2);
+				var failVal = (onePercent == 0 ? 0 : (fail / onePercent)).toFixed(2);
+
+				var pieData = {label: "Total success and failure",
+								values : [{x: "Finished requests (" + succVal + " %)", y: succ},
+								          {x: "Failed requests (" + failVal + " %)", y: fail}
+									]
+								};
+
+				var colorScale = d3.scale.ordinal().range(["lightgreen", "#F44336"]);
+
+				var pieStyle = {opacity : "1 !important"};
+				pieChart = (<PieChart
+	                    data={pieData}
+	                    width={800}
+	                    height={400}
+	                    margin={{top: 10, bottom: 10, left: 200, right: 200}}
+	                    tooltipHtml={this.tooltipPie}
+	                    tooltipOffset={{top: 175, left: 200}}
+						tooltipMode={"fixed"}
+						style={pieStyle}
+   				      	colorScale={colorScale}
+	                    />);
+			}
+			return (
+					<div>
+						<h4>Total analysis requests and failures</h4>
+			           	{progressIndicator}
+						{pieChart}
+						<hr />
+					</div>);
+		}
+	}),
+
+	PerServiceStatusGraph : React.createClass({
+		mixins : [AJAXCleanupMixin],
+
+		getServiceStatus : function() {
+			const url = ODFGlobals.servicesUrl + "/status";
+	        var req = $.ajax({
+	            url: url,
+	            contentType: "application/json",
+	            dataType: 'json',
+	            type: 'GET',
+	            success: function(data) {
+	               this.setState(data);
+	            }.bind(this),
+	            error: function(xhr, status, err) {
+	              var msg = "Analysis stats request failed, " + err.toString();
+	              this.props.alertCallback({type: "danger", message: msg});
+	            }.bind(this)
+	        });
+
+	        this.storeAbort(req.abort);
+		},
+
+		componentWillMount : function() {
+			this.getServiceStatus();
+		},
+
+		componentWillUnmount () {
+		    this.refreshInterval && clearInterval(this.refreshInterval);
+		    this.refreshInterval = false;
+		},
+
+		componentWillReceiveProps: function(nextProps){
+			if(!nextProps.visible){
+				 this.refreshInterval && clearInterval(this.refreshInterval);
+				 this.refreshInterval = false;
+			}else if(!this.refreshInterval){
+				this.refreshInterval = window.setInterval(this.getServiceStatus, GRAPH_REFRESH_DELAY_MS);
+			}
+		},
+
+		tooltip : function(x, y0, y, total) {
+			var barData = this.getBarData();
+			var text = y;
+			var name = null;
+			if(barData && barData.length > 0){
+				$.map(barData, function(res){
+					var bData = barData;
+					$.map(res.values, function(val){
+						if(val.x == x && val.y == y){
+							name = val.fullName;
+						}
+					});
+				});
+			}
+
+			var tooltipStyle = {top : "-20px", position: "absolute", left: "-100px", "minWidth" : "350px"};
+
+			if(name == null){
+				tooltipStyle.left = 0;
+			}
+
+		    return (
+		    		<div style={tooltipStyle}>
+		    			<span>{name}, {text}</span>
+		    		</div>
+		    		);
+		},
+
+		getBarData : function(){
+			if(this.state && !$.isEmptyObject(this.state)){
+				var currentState = this.state;
+				var statusMap = {};
+				$.map(currentState, function(res){
+					var states = res.statusCountMap;
+					$.each(states, function(state, count){
+						var currentArr = statusMap[state];
+						if(currentArr === undefined){
+							currentArr = [];
+						}
+
+						var lbl = (res.name ? res.name : res.id);
+						//only shorten names if more than 1 bar is displayed
+						if(currentState && Object.keys(currentState) && Object.keys(currentState).length > 1 && lbl && lbl.length > 17){
+							lbl = lbl.substring(0, 17) + "..";
+						}
+
+						currentArr.push({"x" : lbl, "y": count, "fullName" : res.name});
+						statusMap[state] = currentArr;
+					});
+				});
+
+				var barData = [];
+
+				$.each(statusMap, function(key, val){
+					barData.push({"label" : key, "values" : val});
+				});
+
+				barData = barData.reverse();
+				return barData;
+			}else{
+				return [ { "label" : "No data available", "values" : [{"x" : "No data availbale", "y" : 0}]}];
+			}
+		},
+
+		getLegend : function(barData, colors){
+			var lbls = [];
+			for(var no = 0; no < barData.length; no++){
+				lbls.push(<div key={no} ><span style={{color: colors[no]}}>{barData[no].label}</span><br/></div>);
+			};
+
+			return (
+					<div style={{float:"right"}}>
+						{lbls}
+					</div>
+				);
+		},
+
+		render : function() {
+			var progressIndicator = <Image src="img/lg_proc.gif" rounded />;
+			var barChart = null;
+
+			if(this.state){
+				progressIndicator = null;
+				var barData = this.getBarData();
+
+				var barStyle = {marginTop: "50px"};
+
+				var barChart = (<BarChart
+				  width={400}
+		          height={400}
+		          margin={{top: 70, bottom: 50, left: 50, right: 10}}
+				  tooltipHtml={this.tooltip}
+			      tooltipMode={"element"}/>);
+
+				//cancelled, initialized, error, running, in queue, finished
+				var colors = ["black", "#F44336", "lightgreen", "blue", "lightblue", "grey"];
+				var colorScale = d3.scale.ordinal().range(colors);
+
+				if(barData != null){
+					var barWidth = (Object.keys(this.state).length >= 2 ? Object.keys(this.state).length * 200 : 400);
+
+					barChart = (
+								<div style={barStyle}>
+									{this.getLegend(barData, colors)}
+									<BarChart
+									  data={barData}
+									  width={barWidth}
+							          height={400}
+								      colorScale={colorScale}
+							          margin={{top: 30, bottom: 50, left: 50, right: 10}}
+									  tooltipHtml={this.tooltip}
+								      tooltipMode={"element"}
+									/>
+								</div>
+							);
+				}
+			}
+
+			return (
+					<div>
+						<h4>Analysis runs per service</h4>
+			           	{progressIndicator}
+						{barChart}
+					</div>);
+			}
+		})
+}
+
+module.exports = ODFStats;

http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-ui-spec.js
----------------------------------------------------------------------
diff --git a/odf/odf-web/src/main/webapp/scripts/odf-ui-spec.js b/odf/odf-web/src/main/webapp/scripts/odf-ui-spec.js
new file mode 100755
index 0000000..a6bc381
--- /dev/null
+++ b/odf/odf-web/src/main/webapp/scripts/odf-ui-spec.js
@@ -0,0 +1,316 @@
+/**
+ *
+ * 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 $ = require("jquery");
+var React = require("react");
+var ReactBootstrap = require("react-bootstrap");
+
+var Label = ReactBootstrap.Label;
+var ListGroup = ReactBootstrap.ListGroup;
+var ListGroupItem = ReactBootstrap.ListGroupItem;
+var Glyphicon = ReactBootstrap.Glyphicon;
+
+/*
+ * for every data type a UI specification can be created.
+ * A UI specification is an array of objects.
+ *
+ * Normally, the key of a property will be used to find a matching ui spec.
+ * This can be overwritten by defining the uiSpec attribute on a property object.
+ *
+ * Each object requires a key to identify the property and a label that will be displayed
+ * In order to manipulate the value or how it is displayed, a property object can pass a function to the func attribute.
+ * This function will be called with the property value and the object as parameters.
+ *
+ * Properties with an array as their value will automatically be displayed in a grid.
+ * A UI specification that is used for a grid can have a property object with the attribute sort:true, causing the tablet to be sorted alphabetically on this property
+*/
+
+var UISpec = {
+
+		DefaultDocument : {
+			attributes: [
+			            {key: "name", label: "Name"},
+			            {key: "description", label: "Description"},
+			            {key: "type", label: "Type"}],
+		    icon: <Glyphicon glyph="question-sign" />
+		},
+
+		DefaultDocuments : {
+			attributes: [
+			            {key: "name", label: "Name"},
+			            {key: "description", label: "Description"},
+			            {key: "type", label: "Type"}],
+		    icon: <Glyphicon glyph="question-sign" />
+		},
+
+		Document : {
+			attributes: [{key: "reference.id", label: "ID"},
+		            {key: "name", label: "Name"},
+		            {key: "type", label: "Type"}],
+		    icon: <Glyphicon glyph="file" />
+		},
+
+		Documents : {
+			attributes: [{key: "name", label: "Name"},
+		            {key: "description", label: "Description"},
+		            {key: "columns", label: "Columns"} ,
+		            {key: "annotations", label: "Annotations",
+		            	 func: function(val){
+		            		 if(!val){
+		            			 return 0;
+		            		 }
+		            		 return val.length;
+		            	 }
+		             }],
+		   icon: <Glyphicon glyph="file" />
+		},
+
+		DataFile : {
+			attributes: [{key: "name", label: "Name"},
+			          {key: "description", label: "Description"},
+			          {key: "columns", label: "Columns"} ,
+			          {key: "annotations", label: "Annotations"}],
+			icon: <Glyphicon glyph="list-alt" />
+		},
+
+		DataFiles: {
+			attributes: [{key: "name", label: "Name"},
+		             {key: "columns", label: "Columns"} ,
+		             {key: "annotations", label: "Annotations",
+		            	 func: function(val){
+		            		 if(!val){
+		            			 return 0;
+		            		 }
+		            		 return val.length;
+		            	 }
+		             }],
+		    icon: <Glyphicon glyph="list-alt" />
+		},
+
+		Table : {
+			attributes: [{key: "schema", label: "Schema"},
+		         {key: "name", label: "Name"},
+		         {key:"description", label: "Description"},
+		         {key:"columns", label: "Columns"} ,
+		         {key: "annotations", label: "Annotations"}],
+     	    icon: <Glyphicon glyph="th" />
+		},
+
+		Tables : {
+			attributes: [ // {key: "schema", label: "Schema"},
+	          {key: "name", label: "Name"},
+		          {key: "columns", label: "Columns"} ,
+		          {key: "annotations", label: "Annotations",
+		        	  func: function(val){
+		        		  if(!val){
+		        			  return 0;
+		        			  }
+		        		  return val.length;
+		        		}
+		          }],
+     	    icon: <Glyphicon glyph="th" />
+		},
+
+		column : {
+			attributes: [{key: "name", label: "Name"},
+		          {key: "dataType", label: "Datatype"},
+		          {key: "annotations", label: "Annotations"}],
+
+		    icon: <Glyphicon glyph="th-list" />
+		},
+
+		columns : {
+			attributes: [{key: "name", label: "Name", sort: true},
+		           {key: "dataType", label: "Datatype"},
+		           {key: "annotations", label: "Annotations",
+			        	  func: function(val){
+			        		  if(!val){
+			        			  return 0;
+			        		  }
+			        		  return val.length;
+			        		}
+			          }],
+		    icon: <Glyphicon glyph="th-list" />
+		},
+
+		//InferredDataClass	AnalysisRun	AnnotationType	JavaClass	Annotations	AnnotatedObject	Reference	JsonProperties
+		annotation : {
+			attributes: [{key: "annotationType", label: "Annotation type"},
+
+		              // see Infosphere DQ service: ColumnAnalysisTableAnnotation
+                      {key: "dataClassDistribution", label: "Data Class Distribution",
+		                  func: function(val) {
+		                      if (val) {
+		                          return <span>{JSON.stringify(val)}</span>;
+		                      }
+		                  }
+                      },
+                      // see Infosphere DQ service: ColumnAnalysisColumnAnnotation
+		              {key: "inferredDataClass", label: "Data Class",
+		                func: function(val) {
+		                     if (val) {
+                                if(val.className){
+                                    var confidence = "";
+                                    if (val.confidenceThreshold) {
+                                        confidence = " ("+val.confidenceThreshold+")";
+                                    }
+                                    return <span>{val.className}{confidence}</span>;
+                                }
+                                return <span>{JSON.stringify(val)}</span>;
+		                     }
+                          }
+		              },
+                      {key: "qualityScore", label: "Data Quality Score"},
+
+		              // see alchemy taxonomy service: TaxonomyDiscoveryService.TaxonomyAnnotation
+		              {key: "label", label: "Category"},
+                      {key: "score", label: "Score"},
+
+		              {key: "analysisRun", label:"Analysis"},
+		              {key: "jsonProperties", label: "Properties"}
+		              ],
+  		    icon: <Glyphicon glyph="tag" />
+		},
+
+		annotations : {
+			attributes: [{key: "annotationType", label: "Annotation type"},
+			             {key: "analysisRun", label:"Analysis"}],
+		  	icon: <Glyphicon glyph="tag" />
+		},
+
+	    request : {
+	    	attributes:[
+	               {key: "request.id", label: "Request ID"},
+	               {key: "state", label: "Status",
+	            	   func: function(val){
+	            		   var btnCss = {};
+	                       var statusLabel = <Label bsStyle="warning">Unknown</Label>;
+	                       if (val == "ACTIVE") {
+	                          statusLabel = <Label bsStyle="info">Active</Label>;
+	                       } else if (val== "QUEUED") {
+	                          statusLabel = <Label bsStyle="info">Queued</Label>;
+	                       } else if (val== "CANCELLED") {
+	                          statusLabel = <Label bsStyle="warning">Cancelled</Label>;
+	                       } else if (val== "FINISHED") {
+	                          statusLabel = <Label bsStyle="success">Finished</Label>;
+	                       } else if (val== "ERROR") {
+	                          statusLabel = <Label bsStyle="danger">Error</Label>;
+	                       }
+	                       return statusLabel;
+	            	   }},
+            	   {key: "request.dataSets", label: "Data sets", uiSpec: "DefaultDocuments"},
+            	   {key: "totalTimeOnQueues", label: "Total time on queues", func: function(val){
+            		   if(val){
+            			   	var x = val / 1000;
+            			    var seconds = Math.floor(x % 60);
+            			    x /= 60;
+            			    var minutes = Math.floor(x % 60);
+            			    x /= 60;
+            			    var hours = Math.floor(x % 24);
+
+            			    return hours + "h " + minutes + "m " + seconds + " s";
+            		   }
+            		   return "";
+            	   }},
+            	   {key: "totalTimeProcessing", label: "Total time processing", func: function(val){
+            		   if(val){
+            			   	var x = val / 1000;
+            			    var seconds = Math.floor(x % 60);
+            			    x /= 60;
+            			    var minutes = Math.floor(x % 60);
+            			    x /= 60;
+            			    var hours = Math.floor(x % 24);
+
+            			    return hours + "h " + minutes + "m " + seconds + " s";
+            		   }
+            		   return "";
+            	   }},
+            	   {key: "totalTimeStoringAnnotations", label: "Total time storing results", func: function(val){
+            		   if(val){
+            			   	var x = val / 1000;
+            			    var seconds = Math.floor(x % 60);
+            			    x /= 60;
+            			    var minutes = Math.floor(x % 60);
+            			    x /= 60;
+            			    var hours = Math.floor(x % 24);
+
+            			    return hours + "h " + minutes + "m " + seconds + " s";
+            		   }
+            		   return "";
+            	   }},
+	               {key: "serviceRequests", label: "Service Sequence", func: function(val, obj){
+	            	   var serviceNames = [];
+	            	   var services = [];
+	            	   for (var i=0; i<val.length; i++) {
+	                       var dsreq = val[i];
+	                       var dsName = dsreq.discoveryServiceName;
+	                       if(serviceNames.indexOf(dsName) == -1){
+	                    	   serviceNames.push(dsName);
+	                    	   services.push(<span key={dsName}>{dsName}<br/></span>);
+	                       }
+	                   }
+
+	                   return <em>{services}</em>;
+	               	}
+	               },
+	               {key: "details", label: "Status Details"}
+	               ],
+	   		  	icon: <Glyphicon glyph="play-circle" />
+	    },
+
+	    requests : {
+	    	attributes: [
+		               {key: "request.id", label: "Request ID"},
+		               {key: "status", label: "Status",
+		            	   func: function(val){
+		                       var statusLabel = <Label bsStyle="warning">Unknown</Label>;
+		                       if (val == "INITIALIZED") {
+		                          statusLabel = <Label bsStyle="info">Initialized</Label>;
+		                       } else if (val== "IN_DISCOVERY_SERVICE_QUEUE") {
+		                          statusLabel = <Label bsStyle="info">Queued</Label>;
+		                       } else if (val== "DISCOVERY_SERVICE_RUNNING") {
+		                           statusLabel = <Label bsStyle="info">Running</Label>;
+		                       } else if (val== "CANCELLED") {
+		                          statusLabel = <Label bsStyle="warning">Cancelled</Label>;
+		                       } else if (val== "FINISHED") {
+		                          statusLabel = <Label bsStyle="success">Finished</Label>;
+		                       } else if (val== "ERROR") {
+		                          statusLabel = <Label bsStyle="danger">Error</Label>;
+		                       }
+		                       return statusLabel;
+		            	   }},
+		               {key: "lastModified", label: "Last modified", func: function(val){
+		            	   return new Date(val).toLocaleString();
+		               }},
+		               {key: "discoveryServiceRequests", label: "Service sequence", func: function(val, obj){
+		            	   var serviceNames = [];
+		            	   var services = [];
+		            	   for (var i=0; i<val.length; i++) {
+		                       var dsreq = val[i];
+		                       var dsName = dsreq.discoveryServiceName;
+		                       if(serviceNames.indexOf(dsName) == -1){
+		                    	   serviceNames.push(dsName);
+		                    	   services.push(<span key={dsName}>{dsName}<br/></span>);
+		                       }
+		                   }
+
+		                   return <ListGroup>{services}</ListGroup>;
+		               }},
+		               {key: "statusDetails", label: "Status Details"}
+		               ],
+	   	icon: <Glyphicon glyph="play-circle" />
+	    }
+};
+
+module.exports = UISpec;