http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-client.js ---------------------------------------------------------------------- diff --git a/odf/odf-web/src/main/webapp/scripts/odf-client.js b/odf/odf-web/src/main/webapp/scripts/odf-client.js new file mode 100755 index 0000000..de64367 --- /dev/null +++ b/odf/odf-web/src/main/webapp/scripts/odf-client.js @@ -0,0 +1,1087 @@ +/** + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +require("bootstrap/dist/css/bootstrap.min.css"); + +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 Nav = ReactBootstrap.Nav; +var NavItem = ReactBootstrap.NavItem; +var Navbar = ReactBootstrap.Navbar; +var NavDropdown = ReactBootstrap.NavDropdown; +var Button = ReactBootstrap.Button; +var Grid = ReactBootstrap.Grid; +var Row = ReactBootstrap.Row; +var Col = ReactBootstrap.Col; +var Table = ReactBootstrap.Table; +var Modal = ReactBootstrap.Modal; +var Alert = ReactBootstrap.Alert; +var Panel = ReactBootstrap.Panel; +var Label = ReactBootstrap.Label; +var Input = ReactBootstrap.Input; +var Jumbotron = ReactBootstrap.Jumbotron; +var Image = ReactBootstrap.Image; +var Dropdown = ReactBootstrap.Dropdown; +var DropdownButton = ReactBootstrap.DropdownButton; +var CustomMenu = ReactBootstrap.CustomMenu; +var MenuItem = ReactBootstrap.MenuItem; +var Tooltip = ReactBootstrap.Tooltip; +var OverlayTrigger = ReactBootstrap.OverlayTrigger; +var Glyphicon = ReactBootstrap.Glyphicon; + +var ODFGlobals = require("./odf-globals.js"); +var OdfAnalysisRequest = require("./odf-analysis-request.js"); +var NewAnalysisRequestButton = OdfAnalysisRequest.NewCreateAnnotationsButton; +var ODFBrowser = require("./odf-metadata-browser.js"); +var Utils = require("./odf-utils.js"); +var AtlasHelper = Utils.AtlasHelper; +var AJAXCleanupMixin = require("./odf-mixins.js"); +var UISpec = require("./odf-ui-spec.js"); + + +var knownAnnotations = { + "Default": [{value : "annotationType", style : "primary", label: "Unknown"}], + "ColumnAnalysisColumnAnnotation" : [{value: "jsonProperties.inferredDataClass.className", style: "danger" , label: "Class name"}, {value: "jsonProperties.inferredDataType.type", style: "info", label :"Datatype"}], + "DataQualityColumnAnnotation": [{style: "warning", value: "jsonProperties.qualityScore" , label: "Data quality score"}], + "MatcherAnnotation": [{style: "success", value: "jsonProperties.termAssignments", label: "Matching terms"}] +}; + +//////////////////////////////////////////////////////////////// +// toplevel navigation bar + +const constants_ODFNavBar = { + odfDataLakePage: "navKeyDataLakePage", + odfTermPage: "navKeyTermPage" +} + +var ODFNavBar = React.createClass({ + render: function() { + return ( + <Navbar inverse> + <Navbar.Header> + <Navbar.Brand> + <b>Shop for Data Application, powered by Open Discovery Framework</b> + </Navbar.Brand> + <Navbar.Toggle /> + </Navbar.Header> + <Navbar.Collapse> + <Nav pullRight activeKey={this.props.activeKey} onSelect={this.props.selectCallback}> + <NavItem eventKey={constants_ODFNavBar.odfDataLakePage} href="#">Data Lake Browser</NavItem> + <NavItem eventKey={constants_ODFNavBar.odfTermPage} href="#">Glossary</NavItem> + </Nav> + </Navbar.Collapse> + </Navbar> + ); + } +}); + +var ODFAnnotationLegend = React.createClass({ + + render : function(){ + var items = []; + $.each(knownAnnotations, function(key, val){ + $.each(val, function(key2, item){ + items.push(<Label key={key + "_" + key2} bsStyle={item.style}>{item.label}</Label>); + }); + }); + + return <div>{items}</div>; + } + +}); + +var ODFAnnotationMarker = React.createClass({ + + render : function(){ + var annotationKey = "Default"; + var annotationLabels = []; + if(this.props.annotation && knownAnnotations[this.props.annotation.annotationType]){ + annotationKey = this.props.annotation.annotationType; + var tooltip = <Tooltip id={this.props.annotation.annotationType}>{this.props.annotation.annotationType}<br/>{this.props.annotation.summary}</Tooltip> + $.each(knownAnnotations[annotationKey], function(key, val){ + var style = val.style; + var value = ODFGlobals.getPathValue(this.props.annotation, val.value); + if (annotationKey === "MatcherAnnotation") { + value = value[0].matchingString; // if no abbreviation matches this will be the term; ideally it should be based on the OMBusinessTerm reference + } + else if(value && !isNaN(value)){ + value = Math.round(value*100) + " %"; + } + annotationLabels.push(<OverlayTrigger key={key} placement="top" overlay={tooltip}><Label style={{margin: "5px"}} bsStyle={style}>{value}</Label></OverlayTrigger>); + }.bind(this)); + }else{ + var tooltip = <Tooltip id={this.props.annotation.annotationType}>{this.props.annotation.annotationType}<br/>{this.props.annotation.summary}</Tooltip> + annotationLabels.push(<OverlayTrigger key="unknownAnnotation" placement="top" overlay={tooltip}><Label style={{margin: "5px"}} bsStyle={knownAnnotations[annotationKey][0].style}>{this.props.annotation.annotationType}</Label></OverlayTrigger>); + } + + return <div style={this.props.style}>{annotationLabels}</div>; + } +}); + + +var AnnotationsColumn = React.createClass({ + mixins : [AJAXCleanupMixin], + + getInitialState : function(){ + return {annotations: []}; + }, + + componentDidMount : function() { + if(this.props.annotations){ + this.setState({loadedAnnotations : this.props.annotations}); + return; + } + + if(this.props.annotationReferences){ + this.loadColumnAnnotations(this.props.annotationReferences); + } + }, + + componentWillReceiveProps : function(nextProps){ + if(!this.isMounted()){ + return; + } + + if(nextProps.annotations){ + this.setState({loadedAnnotations : nextProps.annotations}); + return; + } + }, + + render : function(){ + if(this.state){ + var annotations = this.state.loadedAnnotations; + if(!annotations || annotations.length > 0 && annotations[0].repositoryId){ + return <noscript/>; + } + + var processedTypes = []; + var colAnnotations = []; + $.each(annotations, function(key, val){ + if(processedTypes.indexOf(val.annotationType) == -1){ + processedTypes.push(val.annotationType); + var style = {float: "left"}; + if(key % 6 == 0){ + style = {clear: "both"}; + } + + var summary = (val.summary ? val.summary : ""); + colAnnotations.push(<ODFAnnotationMarker style={style} key={key} annotation={val}/>); + } + }); + + return <div>{colAnnotations}</div>; + } + return <noscript/>; + } + +}); + +var QualityScoreFilter = React.createClass({ + + getInitialState : function(){ + return {key: "All", val : "0", showMenu : false}; + }, + + onSelect : function(obj, key){ + + if(obj.target.tagName != "INPUT"){ + this.setState({key: key}); + var equation = "All"; + if(key != "All"){ + if(this.refs.numberInput.getValue().trim() == ""){ + return; + } + equation = key + this.refs.numberInput.getValue(); + } + this.props.onFilter(equation); + } + }, + + textChange : function(event){ + var equation = "All"; + if(this.state.key != "All"){ + if(this.refs.numberInput.getValue().trim() == ""){ + return; + } + equation = this.state.key + this.refs.numberInput.getValue(); + } + this.props.onFilter(equation); + }, + + render : function(){ + var items = []; + var values = ["<", "<=", "==", ">=", ">", "!=", "All"]; + $.each(values, function(key, val){ + items.push(<MenuItem onSelect={this.onSelect} id={val} key={key} eventKey={val}>{val}</MenuItem>) + }.bind(this)); + + var menu = <div bsRole="menu" className={"dropdown-menu"}> + <h5 style={{float: "left", marginLeft: "15px"}}><Label ref="typeLabel">{this.state.key}</Label></h5> + <Input style={{width: "100px"}} ref="numberInput" onChange={this.textChange} type="number" defaultValue="1"/> + {items} + </div>; + + return <div style={this.props.style} > + <Dropdown id="quality score select" onSelect={this.onSelect} open={this.state.showMenu} onToggle={function(){}}> + <Button bsRole="toggle" onClick={function(){this.setState({showMenu: !this.state.showMenu})}.bind(this)}>Qualityscore filter</Button> + {menu} + </Dropdown> + </div>; + } +}); + +var DataClassFilter = React.createClass({ + + defaultClasses : ["US Zip", "Credit Card"], + + render : function(){ + var items = []; + var classes = (this.props.dataClasses ? this.props.dataClasses.slice() : this.defaultClasses); + classes.push("All"); + $.each(classes, function(key, val){ + items.push(<MenuItem id={val} key={key} eventKey={val}>{val}</MenuItem>) + }); + + return <div style={this.props.style}> + <DropdownButton id="Data class filter" onSelect={function(obj, key){this.props.onFilter(key)}.bind(this)} title="Data Class filter"> + {items} + </DropdownButton> + </div>; + } +}); + + +var FilterMenu = React.createClass({ + + getInitialState : function(){ + return {showMenu : false, dataClassFilter: "All", qualityScoreFilter: "All"}; + }, + + onQualityScoreFilter: function(param){ + this.setState({qualityScoreFilter: param}); + if(this.props.onFilter){ + this.props.onFilter({dataClassFilter: this.state.dataClassFilter, qualityScoreFilter: param}); + } + }, + + onDataClassFilter : function(param){ + this.setState({dataClassFilter: param}); + if(this.props.onFilter){ + this.props.onFilter({dataClassFilter: param, qualityScoreFilter: this.state.qualityScoreFilter}); + } + }, + + render : function(){ + var menu = <div bsRole="menu" className={"dropdown-menu"}> + <QualityScoreFilter onFilter={this.onQualityScoreFilter}/> + <br /> + <DataClassFilter dataClasses={this.props.dataClasses} onFilter={this.onDataClassFilter} /> + </div>; + + return <div style={this.props.style} > + <Dropdown id="filter menu" open={this.state.showMenu} onToggle={function(){}}> + <Button bsRole="toggle" onClick={function(){this.setState({showMenu: !this.state.showMenu})}.bind(this)}>Filter annotations</Button> + {menu} + </Dropdown> + </div>; + } + +}); + + +var SelectCheckbox = React.createClass({ + + getInitialState : function(){ + return {selected : this.props.asset.isSelected}; + }, + + componentWillReceiveProps : function(nextProps){ + if(!this.isMounted()){ + return; + } + if(nextProps.asset.reference.id != this.props.asset.reference.id){ + this.setState({selected : nextProps.asset.isSelected}); + } + }, + + onChange : function(selected){ + if(this.props.onChange){ + this.props.onChange(selected); + } + this.setState({selected : selected}); + }, + + render : function(){ + return <div><Input style={{marginTop: "-6px"}} type="checkbox" label=" " checked={this.state.selected} onChange={function(e){ + this.onChange($(e.target).prop("checked")); + }.bind(this)}/></div>; + } + +}); + +var ODFDataLakePage = React.createClass({ + + columnAnnotations : {}, + + getInitialState : function(){ + return { + ajaxAborts : [], + sourceLoading: false, + columns: [], + dataClasses: [], + qualityScoreFilter: "All", + dataClassFilter: "All", + importFeedback: {msg: null, style: "primary"} + }; + }, + + componentDidMount : function() { + this.loadSources(); + }, + + loadSources : function(){ + this.searchAtlasMetadata("from RelationalDataSet", function(data){ + $.each(data, function(key, source){ + source.isSelected = false; + }); + this.setState({filteredSources: data, sources: data}); + }.bind(this)); + }, + + searchAtlasMetadata : function(query, successCallback, errorCallback) { + var url = ODFGlobals.metadataUrl + "/search?" + $.param({query: query}); + $.ajax({ + url: url, + dataType: 'json', + type: 'GET', + success: function(data) { + successCallback(data); + }, + error: function(xhr, status, err) { + console.error(url, status, err.toString()); + var msg = "Error while loading metadata: " + err.toString(); + if(errorCallback){ + errorCallback(msg); + } + } + }); + }, + + load : function(assetRef){ + $.each(this.state.ajaxAborts, function(key, abort){ + if(abort && abort.call){ + abort.call(); + } + }); + this.setState({ajaxAborts : []}); + + var req = AtlasHelper.loadAtlasAsset(assetRef, function(data){ + var source = data; + var refresh = false; + if(this.state == null || this.state.selectedTable == null || this.state.selectedTable.reference.id != source.reference.id){ + console.log("set state source " + new Date().toLocaleString()); + this.setState({selectedTable: source}); + if(source.annotations == null){ + source.annotations = []; + } + if(source.columns == null){ + source.columns = []; + } + }else{ + source.annotations = this.state.selectedTable.annotations; + refresh = true; + } + + this.loadSourceAnnotations(source, refresh); + this.loadColumns(source, refresh); + }.bind(this), function(){ + + }); + }, + + loadSourceAnnotations : function(source, refresh){ + if(!refresh || !source.loadedAnnotations){ + source.loadedAnnotations = []; + } + var reqs = AtlasHelper.loadMostRecentAnnotations(source.reference, function(annotationList){ + if (refresh) { + var newAnnotations = []; + if(source.loadedAnnotations.length > 0){ + $.each(annotationList, function(key, val){ + if(!this.atlasAssetArrayContains(source.loadedAnnotations, val)){ + newAnnotations.push(val); + } + }.bind(this)); + }else{ + newAnnotations = annotationList; + } + source.loadedAnnotations = newAnnotations; + }else{ + source.loadedAnnotations = annotationList; + } + console.log("set state source anns " + new Date().toLocaleString()); + this.setState({selectedTable: source}); + }.bind(this), function(){ + + }); + + var ajaxAborts = []; + $.each(reqs, function(key, req){ + ajaxAborts.push(req.abort); + }.bind(this)) + this.setState({ajaxAborts : ajaxAborts}); + }, + + atlasAssetArrayContains : function(array, obj){ + for(var no = 0; no < array.length; no++){ + var val = array[no]; + if(val && val.reference && obj && obj.reference && val.reference.id == obj.reference.id){ + return true; + } + } + return false; + }, + + loadColumns : function(dataSet, refresh){ + var columns = []; + if(refresh){ + columns = this.state.columns; + } + var reqs = AtlasHelper.loadRelationalDataSet(dataSet, function(result){ + var foundAnnotations = false; + if(!refresh){ + $.each(result, function(key, col){ + if(col.annotations && col.annotations.length > 0){ + foundAnnotations = true; + } + if(col.isSelected == null || col.isSelected == undefined){ + col.isSelected = false; + } + columns.push(col); + }); + }else{ + //if result size is different, reset completely + if(result.length != columns.length){ + columns = []; + } + //if the old array contains any column that is not in the new columns, reset completely + $.each(columns, function(key, col){ + if(!this.atlasAssetArrayContains(result, col)){ + columns = []; + } + }.bind(this)); + $.each(result, function(key, col){ + //only add new columns + if(!this.atlasAssetArrayContains(columns, col)){ + columns.push(col); + } + if(col.annotations && col.annotations.length > 0){ + for(var no = 0; no < columns.length; no++){ + if(columns[no] == null || columns[no] == undefined){ + col.isSelected = false; + } + if(columns[no].reference.id == col.reference.id){ + columns[no].annotations = col.annotations; + break; + } + } + foundAnnotations = true; + } + }.bind(this)); + } + + if(!foundAnnotations){ + if(!Utils.arraysEqual(this.state.columns, columns)){ + console.log("set state columns " + new Date().toLocaleString()); + this.setState({currentlyLoading : false, columns: columns, filteredColumns: columns}); + }else{ + console.log("columns same, no annotations, dont update"); + } + }else{ + this.loadColumnAnnotations(columns, refresh); + } + }.bind(this), function(){ + + }); + + var ajaxAborts = []; + $.each(reqs, function(key, req){ + ajaxAborts.push(req.abort); + }.bind(this)) + this.setState({ajaxAborts : ajaxAborts}); + }, + + loadColumnAnnotations : function(columns, refresh){ + var annotationRefs = []; + $.each(columns, function(key, col){ + if(!refresh || !col.loadedAnnotations){ + col.loadedAnnotations = []; + } + }); + + var requests = []; + var annotationsChanged = false; + var dataClasses = []; + $.each(columns, function(key, column){ + var req = AtlasHelper.loadMostRecentAnnotations(column.reference, function(annotations){ + $.each(annotations, function(key, annotation){ + if(!this.atlasAssetArrayContains(column.loadedAnnotations, annotation)){ + annotationsChanged = true; + column.loadedAnnotations.push(annotation); + } + if(annotation && + annotation.inferredDataClass && dataClasses.indexOf(annotation.inferredDataClass.className) == -1){ + dataClasses.push(annotation.inferredDataClass.className); + } + }.bind(this)); + }.bind(this)); + requests.push(req); + }.bind(this)); + + $.when.apply(undefined, requests).done(function(){ + if(annotationsChanged){ + console.log("set state column anns " + new Date().toLocaleString()); + this.setState({currentlyLoading : false, columns: columns, filteredColumns: columns, dataClasses: dataClasses}); + }else{ + if(!Utils.arraysEqual(this.state.columns, columns)){ + console.log("set state column anns " + new Date().toLocaleString()); + this.setState({currentlyLoading : false, columns: columns, filteredColumns: columns}); + }else{ + console.log("columns same, annotations same, dont update"); + } + } + }.bind(this)); + + var ajaxAborts = []; + $.each(requests, function(key, req){ + ajaxAborts.push(req.abort); + }.bind(this)); + this.setState({ajaxAborts : ajaxAborts}); + }, + + storeColumnAnnotation : function(columnId, annotation){ + if(!this.columnAnnotations[columnId]){ + this.columnAnnotations[columnId] = []; + } + if(!this.atlasAssetArrayContains(this.columnAnnotations[columnId], annotation)){ + this.columnAnnotations[columnId].push(annotation); + } + }, + + componentWillUnmount : function() { + if(this.refreshInterval){ + clearInterval(this.refreshInterval); + } + }, + + referenceClick : function(asset){ + if(this.state == null || this.state.selectedTable == null || this.state.selectedTable.reference.id != asset.reference.id){ + if(this.refreshInterval){ + clearInterval(this.refreshInterval); + } + this.setState({currentlyLoading : true, selectedTable: null, filteredColumns : [], columns: []}); + this.load(asset.reference); + this.refreshInterval = setInterval(function(){this.load(asset.reference)}.bind(this), 15000); + } + }, + + doFilter : function(params){ + var columns = this.state.columns.slice(); + var filteredColumns = this.filterOnDataQualityScore(columns, params.qualityScoreFilter); + filteredColumns = this.filterOnDataClass(filteredColumns, params.dataClassFilter); + this.setState({filteredColumns: filteredColumns}); + }, + + filterOnDataQualityScore : function(columns, equation){ + if(equation.indexOf("All")>-1){ + return columns; + } + + var columns = columns.slice(); + var matchedColumns = []; + $.each(columns, function(index, col){ + var match = false; + $.each(col.loadedAnnotations, function(k, annotation){ + if(equation && annotation.qualityScore){ + if(eval("annotation.qualityScore" + equation)){ + if(matchedColumns.indexOf(col) == -1){ + matchedColumns.push(col); + } + } + } + }.bind(this)); + }.bind(this)); + + return matchedColumns; + }, + + filterOnDataClass : function(columns, key){ + if(key == "All"){ + return columns; + } + var matchedColumns = []; + $.each(columns, function(index, col){ + var match = false; + $.each(col.loadedAnnotations, function(k, annotation){ + if(annotation.inferredDataClass && + annotation.inferredDataClass.className == key){ + if(matchedColumns.indexOf(col) == -1){ + matchedColumns.push(col); + } + } + }); + }); + + return matchedColumns; + }, + + doImport : function(){ + var params = { + jdbcString : this.refs.jdbcInput.getValue(), + user: this.refs.userInput.getValue(), + password : this.refs.passInput.getValue(), + database :this.refs.dbInput.getValue(), + schema : this.refs.schemaInput.getValue(), + table : this.refs.sourceInput.getValue() + }; + + this.setState({importingTable : true, tableWasImported : true, }); + + $.ajax({ + url: ODFGlobals.importUrl, + contentType: "application/json", + dataType: 'json', + type: 'POST', + data: JSON.stringify(params), + success: function(data) { + this.setState({importFeedback: {msg: "Registration successful!", style: "primary"}, importingTable: false}); + }.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 = "Table could not be registered: " + errorMsg + ", " + err.toString(); + this.setState({importFeedback: {msg: msg, style: "warning"}, importingTable: false}); + } + }.bind(this) + }); + }, + + closeImportingDialog : function(){ + if(this.state.importingTable){ + return; + } + + var newState = {tableWasImported: false, showImportDialog : false, importFeedback: {msg: null}}; + if(this.state.tableWasImported){ + this.loadSources(); + newState.sources = null; + newState.filteredSources = null; + } + this.setState(newState); + }, + + shopData : function(){ + var selectedColumns = []; + var selectedSources = []; + + $.each(this.state.columns, function(key, col){ + if(col.isSelected){ + selectedColumns.push(col); + } + }); + + $.each(this.state.sources, function(key, src){ + if(src.isSelected){ + selectedSources.push(src); + } + }); + + console.log("Do something with the selected columns!") + console.log(selectedColumns); + console.log(selectedSources); + }, + + filterSources : function(e){ + var value = $(e.target).val(); + var filtered = []; + if(value.trim() == ""){ + filtered = this.state.sources; + }else{ + $.each(this.state.sources, function(key, source){ + if(source.name.toUpperCase().indexOf(value.toUpperCase()) > -1){ + filtered.push(source); + } + }); + } + this.setState({filteredSources : filtered}); + }, + + storeImportDialogDefaults: function() { + var defaultValues = { + "jdbcInput": this.refs.jdbcInput.getValue(), + "userInput": this.refs.userInput.getValue(), + "passInput": this.refs.passInput.getValue(), + "dbInput": this.refs.dbInput.getValue(), + "schemaInput": this.refs.schemaInput.getValue(), + "sourceInput": this.refs.sourceInput.getValue(), + }; + localStorage.setItem("odf-client-defaults", JSON.stringify(defaultValues) ); + }, + + render : function(){ + var columnRows = []; + var sourceHead = null; + var sourceList = null; + var columnsGridHeader = <thead><tr><th>Column</th><th>Datatype</th><th>Annotations</th></tr></thead>; + var currentlyLoadingImg = null; + if(this.state){ + var sourceListContent = null; + if(this.state.sources){ + var sourceSpec = { + + attributes: [ + {key: "isSelected", label: "", + func: function(val, asset){ + return <SelectCheckbox onChange={function(selected){ + asset.isSelected = selected; + }.bind(this)} asset={asset} /> + + }}, + {key: "icon", label: "", func: + function(val, asset){ + if(asset && asset.type && UISpec[asset.type] && UISpec[asset.type].icon){ + return UISpec[asset.type].icon; + } + return UISpec["DefaultDocument"].icon; + } + }, + {key: "name", label: "Name"}, + {key: "type", label: "Type"}, + {key: "annotations", label: "Annotations", + func: function(val){ + if(!val){ + return 0; + } + return val.length; + } + } + ]}; + + sourceListContent = <ODFBrowser.ODFPagingTable rowAssets={this.state.filteredSources} onRowClick={this.referenceClick} spec={sourceSpec}/>; + }else{ + sourceListContent = <Image src="img/lg_proc.gif" rounded />; + } + + var sourceImportBtn = <Button style={{float:"right"}} onClick={function(){this.setState({showImportDialog: true});}.bind(this)}>Register new data set</Button>; + var sourceImportingImg = null; + if(this.state.importingTable){ + sourceImportingImg = <Image src="img/lg_proc.gif" rounded />; + } + + var importFeedback = <h3><Label style={{whiteSpace: "normal"}} bsStyle={this.state.importFeedback.style}>{this.state.importFeedback.msg}</Label></h3> + + var storedDefaults = null; + try { + storedDefaults = JSON.parse(localStorage.getItem("odf-client-defaults")); + } catch(e) { + console.log("Couldnt parse defaults from localStorage: " + e); + storedDefaults = {}; + } + if (!storedDefaults) { + storedDefaults = {}; + } + console.log("Stored defaults: " + storedDefaults); + + var sourceImportDialog = <Modal show={this.state.showImportDialog} onHide={this.closeImportingDialog}> + <Modal.Header closeButton> + <Modal.Title>Register new JDBC data set</Modal.Title> + </Modal.Header> + <Modal.Body> + {importFeedback} + <form> + <Input type="text" ref="jdbcInput" defaultValue={storedDefaults.jdbcInput} label="JDBC string" /> + <Input type="text" ref="userInput" defaultValue={storedDefaults.userInput} label="Username" /> + <Input type="password" ref="passInput" defaultValue={storedDefaults.passInput} label="Password" /> + <Input type="text" ref="dbInput" defaultValue={storedDefaults.dbInput} label="Database" /> + <Input type="text" ref="schemaInput" defaultValue={storedDefaults.schemaInput} label="Schema" /> + <Input type="text" ref="sourceInput" defaultValue={storedDefaults.sourceInput} label="Table" /> + </form> + {sourceImportingImg} + </Modal.Body> + <Modal.Footer> + <Button onClick={this.storeImportDialogDefaults}>Store values as defaults</Button> + <Button bsStyle="primary" onClick={this.doImport}>Register</Button> + <Button onClick={this.closeImportingDialog}>Close</Button> + </Modal.Footer> + </Modal>; + sourceList = <Panel style={{float:"left", marginRight: 30, maxWidth:600, minHeight: 550}}> + {sourceImportDialog} + <h3 style={{float: "left", marginTop: "5px"}}> + Data sets + </h3> + {sourceImportBtn}<br style={{clear: "both"}}/> + <Input onChange={this.filterSources} addonBefore={<Glyphicon glyph="search" />} label=" " type="text" placeholder="Filter ..." /> + <br/> + {sourceListContent} + </Panel>; + if(this.state.currentlyLoading){ + currentlyLoadingImg = <Image src="img/lg_proc.gif" rounded />; + } + var panel = <div style={{float: "left"}}>{currentlyLoadingImg}</div>; + + if(this.state.selectedTable){ + var source = this.state.selectedTable; + var sourceAnnotations = []; + if(source.loadedAnnotations){ + //reverse so newest is at front + var sourceAnns = source.loadedAnnotations.slice(); + sourceAnns.reverse(); + var processedTypes = []; + $.each(sourceAnns, function(key, val){ + if(processedTypes.indexOf(val.annotationType) == -1){ + processedTypes.push(val.annotationType); + var summary = (val.summary ? ", " + val.summary : ""); + sourceAnnotations.push(<ODFAnnotationMarker key={key} annotation={val}/>); + } + }); + } + + var hasColumns = (source.columns && source.columns.length > 0 ? true : false); + var columnsString = (hasColumns ? "Columns: " + source.columns.length : null); + var annotationsFilter = (hasColumns ? <FilterMenu onFilter={this.doFilter} dataClasses={this.state.dataClasses} style={{float: "right"}} /> : null); + + sourceHead = <div> + <h3>{source.name} </h3> + <div style={{}}> + <NewAnalysisRequestButton dataSetId={this.state.selectedTable.reference.id} /> + </div> + <br/> + Description: {source.description} + <br/> + {columnsString} + <br/>Annotations:{sourceAnnotations} + <br/> + {annotationsFilter} + </div>; + + panel = <Panel style={{float: "left", width: "50%"}} header={sourceHead}> + {currentlyLoadingImg} + </Panel>; + } + var columnsTable = null; + var filteredColumns = (this.state.filteredColumns ? this.state.filteredColumns : []).slice(); + + if(filteredColumns.length > 0){ + var colSpec = {attributes: [{key: "isSelected", label: "Select", + func: function(val, col){ + return <SelectCheckbox onChange={function(selected){ + col.isSelected = selected; + }.bind(this)} asset={col} /> + + }}, + {key: "name", label: "Name", sort: true}, + {key: "dataType", label: "Datatype"}, + {key: "loadedAnnotations", label: "Annotations", + func: function(annotations, obj){ + return <AnnotationsColumn annotations={annotations} />; + } + }]}; + columnsTable = <div><ODFBrowser.ODFPagingTable ref="columnsTable" rowAssets={filteredColumns} assetType={"columns"} spec={colSpec}/><br/><ODFAnnotationLegend /></div>; + panel = (<Panel style={{float:"left", width: "50%"}} header={sourceHead}> + {columnsTable} + </Panel>); + } + } + + var contentComponent = <Jumbotron> + <div> + <h2>Welcome to your Data Lake</h2> + <Button bsStyle="success" onClick={this.shopData}> + Shop selected data <Glyphicon glyph="shopping-cart" /> + </Button> + <br/> + <br/> + {sourceList} + {panel} + <div style={{clear: "both"}} /> + </div> + </Jumbotron>; + + return <div>{contentComponent}</div>; + } +}); + +var ODFTermPage = React.createClass({ + + getInitialState() { + return {terms: []}; + }, + + loadTerms : function() { + // clear alert + this.props.alertCallback({type: "", message: ""}); + var req = AtlasHelper.searchAtlasMetadata("from BusinessTerm", + + function(data){ + if(!this.isMounted()){ + return; + } + this.setState({terms: data}); + }.bind(this), + + function() { + }.bind(this) + ); + }, + + componentDidMount() { + this.loadTerms(); + }, + + render: function() { + var terms = $.map( + this.state.terms, + function(term) { + return <tr style={{cursor: 'pointer'}} key={term.name} title={term.example} onClick={function(){ + var win = window.open(term.originRef, '_blank'); + win.focus();} + }> + <td> + {term.name} + </td> + <td> + {term.description} + </td> + </tr> + }.bind(this) + ); + + return ( + <div className="jumbotron"> + <h2>Glossary</h2> + <br/> + <br/> + <Panel> + <h3>Terms</h3> + <Table> + <thead> + <tr> + <th>Name</th> + <th>Description</th> + </tr> + </thead> + <tbody> + {terms} + </tbody> + </Table> + </Panel> + </div> + ) + } +}); + +var ODFClient = React.createClass({ + + componentDidMount: function() { + $(window).bind("hashchange", this.parseUrl); + this.parseUrl(); + }, + + parseUrl : function(){ + var target = constants_ODFNavBar.odfDataLakePage; + var navAddition = null; + var hash = document.location.hash; + if(hash && hash.length > 1){ + hash = hash.split("#")[1]; + var split = hash.split("/"); + var navHash = split[0]; + if(split.length > 0){ + navAddition = split.slice(1); + } + if(constants_ODFNavBar[navHash]){ + target = constants_ODFNavBar[navHash]; + } + } + this.setState({ + activeNavBarItem: target, + navAddition: navAddition} + ); + }, + + getInitialState: function() { + return ({ + activeNavBarItem: constants_ODFNavBar.odfDataLakePage, + navAddition: null, + globalAlert: { + type: "", + message: "" + } + }); + }, + + handleNavBarSelection: function(selection) { + $.each(constants_ODFNavBar, function(key, ref){ + if(ref == selection){ + document.location.hash = key; + } + }); + this.setState({ activeNavBarItem: selection }); + }, + + handleAlert: function(alertInfo) { + this.setState({ globalAlert: alertInfo }); + }, + + render: function() { + var alertComp = null; + if (this.state.globalAlert.type != "") { + alertComp = <Alert bsStyle={this.state.globalAlert.type}>{this.state.globalAlert.message}</Alert>; + } + + var contentComponent = <ODFDataLakePage alertCallback={this.handleAlert}/>; + if (this.state.activeNavBarItem == constants_ODFNavBar.odfDataLakePage) { + contentComponent = <ODFDataLakePage alertCallback={this.handleAlert}/>; + } else if (this.state.activeNavBarItem == constants_ODFNavBar.odfTermPage) { + contentComponent = <ODFTermPage alertCallback={this.handleAlert}/>; + } + + var divStyle = { +// marginLeft: "80px", +// marginRight: "80px" + }; + + return ( + <div> + <ODFNavBar activeKey={this.state.activeNavBarItem} selectCallback={this.handleNavBarSelection}></ODFNavBar> + <div style={divStyle}> + {alertComp} + {contentComponent} + </div> + </div> + ); + } +}); + +var div = $("#odf-toplevel-div")[0]; +ReactDOM.render(<ODFClient/>, div);
http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-configuration-store.js ---------------------------------------------------------------------- diff --git a/odf/odf-web/src/main/webapp/scripts/odf-configuration-store.js b/odf/odf-web/src/main/webapp/scripts/odf-configuration-store.js new file mode 100755 index 0000000..cf50075 --- /dev/null +++ b/odf/odf-web/src/main/webapp/scripts/odf-configuration-store.js @@ -0,0 +1,63 @@ +/** + * + * 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 ODFGlobals = require("./odf-globals.js"); + +var ConfigurationStore = { + + // readUserDefinedProperties(successCallback, alertCallback) { + readConfig(successCallback, alertCallback) { + if (alertCallback) { + alertCallback({type: ""}); + } + // clear alert + + return $.ajax({ + url: ODFGlobals.apiPrefix + "settings", + dataType: 'json', + type: 'GET', + success: successCallback, + error: function(xhr, status, err) { + if (alertCallback) { + var msg = "Error while reading user defined properties: " + err.toString(); + alertCallback({type: "danger", message: msg}); + } + } + }).abort; + }, + + updateConfig(config, successCallback, alertCallback) { + if (alertCallback) { + alertCallback({type: ""}); + } + + return $.ajax({ + url: ODFGlobals.apiPrefix + "settings", + contentType: "application/json", + dataType: 'json', + type: 'PUT', + data: JSON.stringify(config), + success: successCallback, + error: function(xhr, status, err) { + if (alertCallback) { + var msg = "Error while reading user defined properties: " + err.toString(); + alertCallback({type: "danger", message: msg}); + } + } + }).abort; + } +} + +module.exports = ConfigurationStore; http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-console.js ---------------------------------------------------------------------- diff --git a/odf/odf-web/src/main/webapp/scripts/odf-console.js b/odf/odf-web/src/main/webapp/scripts/odf-console.js new file mode 100755 index 0000000..aa70808 --- /dev/null +++ b/odf/odf-web/src/main/webapp/scripts/odf-console.js @@ -0,0 +1,967 @@ +/** + * + * 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. + */ +//css imports +require("bootstrap/dist/css/bootstrap.min.css"); +require("bootstrap-material-design/dist/css/bootstrap-material-design.min.css"); +require("bootstrap-material-design/dist/css/ripples.min.css"); +require("roboto-font/css/fonts.css"); + + +//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 ODFStats = require("./odf-statistics.js"); +var ODFSettings = require("./odf-settings.js"); +var ODFServices = require("./odf-services.js"); +var ODFBrowser = require("./odf-metadata-browser.js").ODFMetadataBrowser; +var ODFRequestBrowser = require("./odf-request-browser.js"); +var AJAXCleanupMixin = require("./odf-mixins.js"); +var configurationStore = require("./odf-utils.js").ConfigurationStore; +var servicesStore = require("./odf-utils.js").ServicesStore; +var AtlasHelper = require("./odf-utils.js").AtlasHelper; +var AnnotationStoreHelper = require("./odf-utils.js").AnnotationStoreHelper; +var OdfAnalysisRequest = require("./odf-analysis-request.js"); +var LogViewer = require("./odf-logs.js"); +//var Notifications = require("./odf-notifications.js"); +var NewAnalysisRequestButton = OdfAnalysisRequest.NewAnalysisRequestButton; +var NewAnalysisRequestDialog = OdfAnalysisRequest.NewAnalysisRequestDialog; +var NewCreateAnnotationsButton = OdfAnalysisRequest.NewCreateAnnotationsButton; +var NewCreateAnnotationsDialog = OdfAnalysisRequest.NewCreateAnnotationsDialog; + +var Button = ReactBootstrap.Button; +var Nav = ReactBootstrap.Nav; +var NavItem = ReactBootstrap.NavItem; +var Navbar = ReactBootstrap.Navbar; +var NavDropdown = ReactBootstrap.NavDropdown; +var MenuItem = ReactBootstrap.MenuItem; +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 ProgressBar = ReactBootstrap.ProgressBar; +var Image = ReactBootstrap.Image; +var ListGroup = ReactBootstrap.ListGroup; +var ListGroupItem = ReactBootstrap.ListGroupItem; +var Tabs = ReactBootstrap.Tabs; +var Tab = ReactBootstrap.Tab; +var Glyphicon = ReactBootstrap.Glyphicon; + +var PerServiceStatusGraph = ODFStats.PerServiceStatusGraph; +var TotalAnalysisGraph = ODFStats.TotalAnalysisGraph; +var SystemDiagnostics = ODFStats.SystemDiagnostics; + +//////////////////////////////////////////////////////////////// +// toplevel navigation bar + +const constants_ODFNavBar = { + gettingStarted: "navKeyGettingStarted", + configuration: "navKeyConfiguration", + monitor: "navKeyMonitor", + discoveryServices: "navKeyDiscoveryServices", + data: "navKeyData", + analysis: "navKeyAnalysis" +} + +var ODFNavBar = React.createClass({ + render: function() { + return ( + <Navbar inverse> + <Navbar.Header> + <Navbar.Brand> + <b>Open Discovery Framework</b> + </Navbar.Brand> + <Navbar.Toggle /> + </Navbar.Header> + <Navbar.Collapse> + <Nav pullRight activeKey={this.props.activeKey} onSelect={this.props.selectCallback}> + <NavItem eventKey={constants_ODFNavBar.gettingStarted} href="#">Getting Started</NavItem> + <NavItem eventKey={constants_ODFNavBar.monitor} href="#">System Monitor</NavItem> + <NavItem eventKey={constants_ODFNavBar.configuration} href="#">Settings</NavItem> + <NavItem eventKey={constants_ODFNavBar.discoveryServices} href="#">Services</NavItem> + <NavItem eventKey={constants_ODFNavBar.data} href="#">Data sets</NavItem> + <NavItem eventKey={constants_ODFNavBar.analysis} href="#">Analysis</NavItem> + </Nav> + </Navbar.Collapse> + </Navbar> + ); + } +}); + + + +///////////////////////////////////////////////////////////////////////////////////////// +// Configuration page + +var ConfigurationPage = React.createClass({ + componentWillMount() { + this.props.alertCallback({type: ""}); + }, + + render: function() { + return ( + <div className="jumbotron"> + <Tabs position="left" defaultActiveyKey={1}> + <Tab eventKey={1} title="General"> + <ODFSettings.ODFConfigPage alertCallback={this.props.alertCallback}/> + </Tab> + <Tab eventKey={2} title="Spark settings"> + <ODFSettings.SparkConfigPage alertCallback={this.props.alertCallback}/> + </Tab> + <Tab eventKey={3} title="User-defined"> + <ODFSettings.UserDefinedConfigPage alertCallback={this.props.alertCallback}/> + </Tab> + </Tabs> + </div> + ); + } + +}); + +const GettingStartedPage = React.createClass({ + getInitialState() { + return ({version: "NOTFOUND"}); + }, + + componentWillMount() { + this.props.alertCallback({type: ""}); + $.ajax({ + url: ODFGlobals.engineUrl + "/version", + type: 'GET', + success: function(data) { + this.setState(data); + }.bind(this) + }); + }, + + render: function() { + var divStyle = { + marginLeft: "80px", + marginRight: "80px" + }; + return ( + <Jumbotron> + <div style={divStyle}> + <h2>Welcome to the Open Discovery Framework Console</h2> + <p/>The "Open Discovery Framework" (ODF) is an open metadata-based platform + that strives to be a common home for different analytics technologies + that discover characteristics of data sets and relationships between + them (think "AppStore for discovery algorithms"). + Using ODF, applications can leverage new discovery algorithms and their + results with minimal integration effort. + <p/> + This console lets you administer and configure your ODF system, as well as + run analyses and browse their results. + <p/> + <p><Button target="_blank" href="doc" bsStyle="primary">Open Documentation</Button></p> + <p><Button target="_blank" href="swagger" bsStyle="success">Show API Reference</Button></p> + <p/> + Version: {this.state.version} + </div> + </Jumbotron> + + ) + } + +}); + +///////////////////////////////////////////////////////////////////// +// monitor page +var StatusGraphs = React.createClass({ + + selectTab : function(key){ + this.setState({key}); + }, + + getInitialState() { + return { + key: "system_state" + }; + }, + + render : function() { + var divStyle = { + marginLeft: "20px" + }; + + return ( + <div> + <Tabs position="left" activeKey={this.state.key} onSelect={this.selectTab}> + <Tab eventKey={"system_state"} title="System state"> + <div style={divStyle}> + <TotalAnalysisGraph visible={this.state.key == "system_state"} alertCallback={this.props.alertCallback}/> + <PerServiceStatusGraph visible={this.state.key == "system_state"} alertCallback={this.props.alertCallback}/> + </div> + </Tab> + <Tab eventKey={"diagnostics"} title="Diagnostics"> + <div style={divStyle}> + <SystemDiagnostics visible={this.state.key == "diagnostics"} alertCallback={this.props.alertCallback}/> + </div> + </Tab> + <Tab eventKey={"logs"} title="System logs"> + <div style={divStyle}> + <LogViewer visible={this.state.key == "logs"} alertCallback={this.props.alertCallback}/> + </div> + </Tab> + </Tabs> + </div> + ); + } + + +}); + +var MonitorPage = React.createClass({ + mixins : [AJAXCleanupMixin], + + getInitialState() { + return ( { + monitorStatusVisible: false, + monitorStatusStyle:"success", + monitorStatusMessage: "OK", + monitorWorkInProgress: false + }); + }, + + componentWillMount() { + this.props.alertCallback({type: ""}); + }, + + checkHealth() { + this.setState({monitorWorkInProgress: true, monitorStatusVisible: false}); + var url = ODFGlobals.engineUrl + "/health"; + var req = $.ajax({ + url: url, + dataType: 'json', + type: 'GET', + success: function(data) { + var status = data.status; + var newState = { + monitorStatusVisible: true, + monitorWorkInProgress: false + }; + + if (status == "OK") { + newState.monitorStatusStyle = "success"; + } else if (status == "WARNING") { + newState.monitorStatusStyle = "warning"; + } else if (status == "ERROR") { + newState.monitorStatusStyle = "danger"; + } + // TODO show more than just the first message + newState.monitorStatusMessage = "Status: " + status + ". " + data.messages[0]; + + this.setState(newState); + }.bind(this), + error: function(xhr, status, err) { + if(this.isMounted()){ + this.setState({ + monitorStatusVisible: true, + monitorStatusStyle:"danger", + monitorStatusMessage: "An error occured: " + err.toString(), + monitorWorkInProgress: false}); + }; + }.bind(this) + }); + this.storeAbort(req.abort); + }, + + performRestart : function(){ + $.ajax({ + url: ODFGlobals.engineUrl + "/shutdown", + contentType: "application/json", + type: 'POST', + data: JSON.stringify({restart: "true"}), + success: function(data) { + this.setState({monitorStatusVisible : true, monitorStatusStyle: "info", monitorStatusMessage: "Restart in progress..."}); + }.bind(this), + error: function(xhr, status, err) { + this.setState({monitorStatusVisible : true, monitorStatusStyle: "warning", monitorStatusMessage: "Restart request failed"}); + }.bind(this) + }); + }, + + render() { + var divStyle = { + marginLeft: "20px" + }; + var monitorStatus = null; + if (this.state.monitorStatusVisible) { + monitorStatus = <Alert bsStyle={this.state.monitorStatusStyle}>{this.state.monitorStatusMessage}</Alert>; + } + var progressIndicator = null; + if (this.state.monitorWorkInProgress) { + progressIndicator = <Image src="img/lg_proc.gif" rounded />; + } + return ( + <div className="jumbotron"> + <h3>System health</h3> + <div style={divStyle}> + <Button className="btn-raised" bsStyle="primary" disabled={this.state.monitorWorkInProgress} onClick={this.checkHealth}>Check health</Button> + <Button className="btn-raised" bsStyle="warning" onClick={this.performRestart}>Restart ODF</Button> + {progressIndicator} + {monitorStatus} + <hr/> + <div> + </div> + <StatusGraphs alertCallback={this.props.alertCallback}/> + </div> + </div> + ); + } + +}); + +////////////////////////////////////////////////////// +// discovery services page +var DiscoveryServicesPage = React.createClass({ + mixins : [AJAXCleanupMixin], + + getInitialState() { + return ({discoveryServices: []}); + }, + + loadDiscoveryServices() { + // clear alert + this.props.alertCallback({type: "", message: ""}); + + var req = $.ajax({ + url: ODFGlobals.servicesUrl, + dataType: 'json', + type: 'GET', + success: function(data) { + this.setState({discoveryServices: data}); + }.bind(this), + error: function(xhr, status, err) { + if(this.isMounted()){ + var msg = "Error while reading ODF services: " + err.toString(); + this.props.alertCallback({type: "danger", message: msg}); + } + }.bind(this) + }); + + this.storeAbort(req.abort); + }, + + componentDidMount() { + this.loadDiscoveryServices(); + }, + + render: function() { + var services = $.map( + this.state.discoveryServices, + function(dsreg) { + return <tr key={dsreg.id}> + <td> + <ODFServices.DiscoveryServiceInfo dsreg={dsreg} refreshCallback={this.loadDiscoveryServices} alertCallback={this.props.alertCallback}/> + </td> + </tr> + }.bind(this) + ); + + return ( + <div className="jumbotron"> + <h3>Services</h3> + This page lets you manage the services for this ODF instance. + You can add services manually by clicking the <em>Add Service</em> button or + register remote services (e.g. deployed on Bluemix) you have built with the ODF service developer kit by + clicking the <em>Register remote services</em> link. + <p/> + <ODFServices.AddDiscoveryServiceButton refreshCallback={this.loadDiscoveryServices}/> + <p/> + <Table bordered responsive> + <tbody> + {services} + </tbody> + </Table> + </div> + ); + } + +}); + +////////////////////////////////////////////////////////////// +// Analysis Page +var AnalysisRequestsPage = React.createClass({ + mixins : [AJAXCleanupMixin], + + getInitialState() { + return {recentAnalysisRequests: null, config: {}, services : []}; + }, + + componentWillReceiveProps : function(nextProps){ + var selection = null; + if(nextProps.navAddition && nextProps.navAddition.length > 0 && nextProps.navAddition[0] && nextProps.navAddition[0].length > 0){ + var jsonAddition = {}; + + try{ + jsonAddition = JSON.parse(decodeURIComponent(nextProps.navAddition[0])); + }catch(e){ + + } + + if(jsonAddition.requestId){ + $.each(this.state.recentAnalysisRequests, function(key, tracker){ + var reqId = jsonAddition.requestId; + + if(tracker.request.id == reqId){ + selection = reqId; + } + }.bind(this)); + }else if(jsonAddition.id && jsonAddition.repositoryId){ + selection = jsonAddition; + } + } + + if(selection != this.state.selection){ + this.setState({selection : selection}); + } + }, + + componentDidMount() { + if(!this.refreshInterval){ + this.refreshInterval = window.setInterval(this.refreshAnalysisRequests, 5000); + } + this.initialLoadServices(); + this.initialLoadRecentAnalysisRequests(); + }, + + componentWillUnmount : function() { + if(this.refreshInterval){ + window.clearInterval(this.refreshInterval); + } + }, + + getDiscoveryServiceNameFromId(id) { + var servicesWithSameId = this.state.services.filter( + function(dsreg) { + return dsreg.id == id; + } + ); + if (servicesWithSameId.length > 0) { + return servicesWithSameId[0].name; + } + return null; + }, + + refreshAnalysisRequests : function(){ + var req = configurationStore.readConfig( + function(config) { + this.setState({config: config}); + const url = ODFGlobals.analysisUrl + "?offset=0&limit=20"; + $.ajax({ + url: url, + dataType: 'json', + type: 'GET', + success: function(data) { + $.each(data.analysisRequestTrackers, function(key, tracker){ + //collect service names by id and add to json so that it can be displayed later + $.each(tracker.discoveryServiceRequests, function(key, request){ + var serviceName = this.getDiscoveryServiceNameFromId(request.discoveryServiceId); + request.discoveryServiceName = serviceName; + }.bind(this)); + }.bind(this)); + this.setState({recentAnalysisRequests: data.analysisRequestTrackers}); + }.bind(this), + error: function(xhr, status, err) { + if(status != "abort" ){ + console.error(url, status, err.toString()); + } + if(this.isMounted()){ + var msg = "Error while refreshing recent analysis requests: " + err.toString(); + this.props.alertCallback({type: "danger", message: msg}); + } + }.bind(this) + }); + }.bind(this), + this.props.alertCallback + ); + + this.storeAbort(req.abort); + }, + + initialLoadServices() { + this.setState({services: null}); + + var req = servicesStore.getServices( + function(services) { + this.setState({services: services}); + }.bind(this), + this.props.alertCallback + ); + + this.storeAbort(req.abort); + }, + + initialLoadRecentAnalysisRequests() { + this.setState({recentAnalysisRequests: null}); + + var req = configurationStore.readConfig( + function(config) { + this.setState({config: config}); + const url = ODFGlobals.analysisUrl + "?offset=0&limit=20"; + $.ajax({ + url: url, + dataType: 'json', + type: 'GET', + success: function(data) { + var selection = null; + $.each(data.analysisRequestTrackers, function(key, tracker){ + if(this.props.navAddition && this.props.navAddition.length > 0 && this.props.navAddition[0].length > 0){ + var reqId = ""; + try{ + reqId = JSON.parse(decodeURIComponent(this.props.navAddition[0])).requestId + }catch(e){ + + } + if(tracker.request.id == reqId){ + selection = reqId; + } + } + + //collect service names by id and add to json so that it can be displayed later + $.each(tracker.discoveryServiceRequests, function(key, request){ + var serviceName = this.getDiscoveryServiceNameFromId(request.discoveryServiceId); + request.discoveryServiceName = serviceName; + }.bind(this)); + }.bind(this)); + + var newState = {recentAnalysisRequests: data.analysisRequestTrackers}; + if(selection){ + newState.selection = selection; + } + + this.setState(newState); + }.bind(this), + error: function(xhr, status, err) { + if(status != "abort" ){ + console.error(url, status, err.toString()); + } + if(this.isMounted()){ + var msg = "Error while loading recent analysis requests: " + err.toString(); + this.props.alertCallback({type: "danger", message: msg}); + } + }.bind(this) + }); + }.bind(this), + this.props.alertCallback + ); + + this.storeAbort(req.abort); + }, + + cancelAnalysisRequest(tracker) { + var url = ODFGlobals.analysisUrl + "/" + tracker.request.id + "/cancel"; + + $.ajax({ + url: url, + type: 'POST', + success: function() { + if(this.isMounted()){ + this.refreshAnalysisRequests(); + } + }.bind(this), + error: function(xhr, status, err) { + if(status != "abort" ){ + console.error(url, status, err.toString()); + } + + var errMsg = null; + if(err == "Forbidden"){ + errMsg = "only analyses that have not been started yet can be cancelled!"; + }else if(err == "Bad Request"){ + errMsg = "the requested analysis could not be found!"; + } + if(this.isMounted()){ + var msg = "Analysis could not be cancelled: " + (errMsg ? errMsg : err.toString()); + if(this.props.alertCallback){ + this.props.alertCallback({type: "danger", message: msg}); + } + } + }.bind(this) + }); + }, + + viewResultAnnotations : function(target){ + this.setState({ + resultAnnotations : null, + showAnnotations: true + }); + var req = AnnotationStoreHelper.loadAnnotationsForRequest(target.request.id, + function(data){ + this.setState({ + resultAnnotations : data.annotations + }); + }.bind(this), + function(error){ + console.error('Annotations could not be loaded ' + error); + } + ); + this.storeAbort(req.abort); + }, + + viewInAtlas : function(target){ + var repo = target.request.dataSets[0].repositoryId; + repo = repo.split("atlas:")[1]; + var annotationQueryUrl = repo + "/#!/search?query=from%20ODFAnnotation%20where%20analysisRun%3D'"+ target.request.id + "'"; + var win = window.open(annotationQueryUrl, '_blank'); + }, + + render : function() { + var loadingImg = null; + if(this.state.recentAnalysisRequests == null){ + loadingImg = <Image src="img/lg_proc.gif" rounded />; + } + var requestActions = [ + { + assetType: ["requests"], + actions : [ + { + label: "Cancel analysis", + func: this.cancelAnalysisRequest, + filter: function(obj){ + var val = obj.status; + if (val == "INITIALIZED" || val == "IN_DISCOVERY_SERVICE_QUEUE") { + return true; + } + return false; + } + }, + { + label: "View results", + func: this.viewResultAnnotations + }, + { + label: "View results in atlas", + func: this.viewInAtlas + } + ] + } + ]; + return ( + <div className="jumbotron"> + <h3>Analysis requests</h3> + <div> + Click Refresh to refresh the list of existing analysis requests. + Only the last 20 valid requests are shown. + <p/> + <NewAnalysisRequestButton bsStyle="primary" onClose={this.refreshAnalysisRequests} alertCallback={this.props.alertCallback}/> + <NewCreateAnnotationsButton bsStyle="primary" onClose={this.refreshAnalysisRequests} alertCallback={this.props.alertCallback}/> + + <Button bsStyle="success" onClick={this.refreshAnalysisRequests}>Refresh</Button> + {loadingImg} + <ODFRequestBrowser registeredServices={this.state.config.registeredServices} actions={requestActions} ref="requestBrowser" selection={this.state.selection} assets={this.state.recentAnalysisRequests}/> + </div> + <Modal show={this.state.showAnnotations} onHide={function(){this.setState({showAnnotations : false})}.bind(this)}> + <Modal.Header closeButton> + <Modal.Title>Analysis results for analysis {this.state.resultTarget}</Modal.Title> + </Modal.Header> + <Modal.Body> + <ODFBrowser ref="resultBrowser" type={"annotations"} assets={this.state.resultAnnotations} /> + </Modal.Body> + <Modal.Footer> + <Button onClick={function(){this.setState({showAnnotations : false})}.bind(this)}>Close</Button> + </Modal.Footer> + </Modal> + </div> + ); + } + +}); + +var AnalysisDataSetsPage = React.createClass({ + mixins : [AJAXCleanupMixin], + + componentDidMount() { + this.loadDataFiles(); + this.loadTables(); + this.loadDocuments(); + }, + + getInitialState() { + return ({ showDataFiles: true, + showHideDataFilesIcon: "chevron-up", + showTables: true, + showHideTablesIcon: "chevron-up", + showDocuments: true, + showHideDocumentsIcon: "chevron-up", + config: null}); + }, + + componentWillReceiveProps : function(nextProps){ + if(nextProps.navAddition && nextProps.navAddition.length > 0 && nextProps.navAddition[0]){ + this.setState({selection : nextProps.navAddition[0]}); + }else{ + this.setState({selection : null}); + } + }, + + showHideDataFiles() { + this.setState({showDataFiles: !this.state.showDataFiles, showHideDataFilesIcon: (!this.state.showDataFiles? "chevron-up" : "chevron-down")}); + }, + + showHideTables() { + this.setState({showTables: !this.state.showTables, showHideTablesIcon: (!this.state.showTables? "chevron-up" : "chevron-down")}); + }, + + showHideDocuments() { + this.setState({showDocuments: !this.state.showDocuments, showHideDocumentsIcon: (!this.state.showDocuments ? "chevron-up" : "chevron-down")}); + }, + + createAnnotations : function(target){ + this.setState({showCreateAnnotationsDialog: true, selectedAsset : target.reference.id}); + }, + + startAnalysis : function(target){ + this.setState({showAnalysisRequestDialog: true, selectedAsset : target.reference.id}); + }, + + viewInAtlas : function(target){ + var win = window.open(target.reference.url, '_blank'); + win.focus(); + }, + + loadDataFiles : function(){ + var resultQuery = "from DataFile"; + this.setState({ + dataFileAssets : null + }); + var req = AtlasHelper.searchAtlasMetadata(resultQuery, + function(data){ + this.setState({ + dataFileAssets : data + }); + }.bind(this), + function(error){ + + } + ); + this.storeAbort(req.abort); + }, + + loadTables : function(){ + var resultQuery = "from Table"; + this.setState({ + tableAssets : null + }); + var req = AtlasHelper.searchAtlasMetadata(resultQuery, + function(data){ + this.setState({ + tableAssets : data + }); + }.bind(this), + function(error){ + + } + ); + this.storeAbort(req.abort); + }, + + loadDocuments : function(){ + var resultQuery = "from Document"; + this.setState({ + docAssets : null + }); + var req = AtlasHelper.searchAtlasMetadata(resultQuery, + function(data){ + this.setState({ + docAssets : data + }); + }.bind(this), + function(error){ + + } + ); + this.storeAbort(req.abort); + }, + + render() { + var actions = [ + { + assetType: ["DataFiles", "Tables", "Documents"], + actions : [ + { + label: "Start analysis (annotation types)", + func: this.createAnnotations + } , + { + label: "Start analysis (service sequence)", + func: this.startAnalysis + } , + { + label: "View in atlas", + func: this.viewInAtlas + } + ] + } + ]; + + return ( + <div className="jumbotron"> + <h3>Data sets</h3> + <div> + <NewAnalysisRequestDialog alertCallback={this.props.alertCallback} dataSetId={this.state.selectedAsset} show={this.state.showAnalysisRequestDialog} onClose={function(){this.setState({showAnalysisRequestDialog: false});}.bind(this)} /> + <NewCreateAnnotationsDialog alertCallback={this.props.alertCallback} dataSetId={this.state.selectedAsset} show={this.state.showCreateAnnotationsDialog} onClose={function(){this.setState({showCreateAnnotationsDialog: false});}.bind(this)} /> + Here are all data sets of the metadata repository that are available for analysis. + <p/> + <Panel collapsible expanded={this.state.showDataFiles} header={ + <div style={{textAlign:"right"}}> + <span style={{float: "left"}}>Data Files</span> + <Button bsStyle="primary" onClick={function(){this.loadDataFiles();}.bind(this)}> + Refresh + </Button> + <Button onClick={this.showHideDataFiles}> + <Glyphicon glyph={this.state.showHideDataFilesIcon} /> + </Button> + </div>}> + <ODFBrowser ref="dataFileBrowser" type={"DataFiles"} selection={this.state.selection} actions={actions} assets={this.state.dataFileAssets} /> + </Panel> + <Panel collapsible expanded={this.state.showTables} header={ + <div style={{textAlign:"right"}}> + <span style={{float: "left"}}>Relational Tables</span> + <Button bsStyle="primary" onClick={function(){this.loadTables();}.bind(this)}> + Refresh + </Button> + <Button onClick={this.showHideTables}> + <Glyphicon glyph={this.state.showHideTablesIcon} /> + </Button> + </div>}> + <ODFBrowser ref="tableBrowser" type={"Tables"} actions={actions} assets={this.state.tableAssets} /> + </Panel> + <Panel collapsible expanded={this.state.showDocuments} header={ + <div style={{textAlign:"right"}}> + <span style={{float: "left"}}>Documents</span> + <Button bsStyle="primary" onClick={function(){this.loadDocuments();}.bind(this)}> + Refresh + </Button> + <Button onClick={this.showHideDocuments}> + <Glyphicon glyph={this.state.showHideDocumentsIcon} /> + </Button> + </div>}> + <ODFBrowser ref="docBrowser" type={"Documents"} actions={actions} assets={this.state.docAssets}/> + </Panel> + </div> + </div> + ); + } + +}); + + +//////////////////////////////////////////////////////////////////////// +// main component +var ODFUI = React.createClass({ + + componentDidMount: function() { + $(window).bind("hashchange", this.parseUrl); + this.parseUrl(); + }, + + parseUrl : function(){ + var target = constants_ODFNavBar.gettingStarted; + var navAddition = null; + var hash = document.location.hash; + if(hash && hash.length > 1){ + hash = hash.split("#")[1]; + var split = hash.split("/"); + var navHash = split[0]; + if(split.length > 0){ + navAddition = split.slice(1); + } + if(constants_ODFNavBar[navHash]){ + target = constants_ODFNavBar[navHash]; + } + } + this.setState({ + activeNavBarItem: target, + navAddition: navAddition} + ); + }, + + getInitialState: function() { + return ({ + activeNavBarItem: constants_ODFNavBar.gettingStarted, + navAddition: null, + globalAlert: { + type: "", + message: "" + } + }); + }, + + handleNavBarSelection: function(selection) { + $.each(constants_ODFNavBar, function(key, ref){ + if(ref == selection){ + document.location.hash = key; + } + }); + this.setState({ activeNavBarItem: selection }); + }, + + handleAlert: function(alertInfo) { + this.setState({ globalAlert: alertInfo }); + }, + + render: function() { + var alertComp = null; + if (this.state.globalAlert.type != "") { + alertComp = <Alert bsStyle={this.state.globalAlert.type}>{this.state.globalAlert.message}</Alert>; + } + + var contentComponent = <GettingStartedPage alertCallback={this.handleAlert}/>; + if (this.state.activeNavBarItem == constants_ODFNavBar.configuration) { + contentComponent = <ConfigurationPage alertCallback={this.handleAlert}/>; + } else if (this.state.activeNavBarItem == constants_ODFNavBar.discoveryServices) { + contentComponent = <DiscoveryServicesPage alertCallback={this.handleAlert}/>; + } else if (this.state.activeNavBarItem == constants_ODFNavBar.monitor) { + contentComponent = <MonitorPage alertCallback={this.handleAlert}/>; + } else if (this.state.activeNavBarItem == constants_ODFNavBar.analysis) { + contentComponent = <AnalysisRequestsPage navAddition={this.state.navAddition} alertCallback={this.handleAlert}/>; + } else if (this.state.activeNavBarItem == constants_ODFNavBar.data) { + contentComponent = <AnalysisDataSetsPage navAddition={this.state.navAddition} alertCallback={this.handleAlert}/>; + } + + var divStyle = { + marginLeft: "80px", + marginRight: "80px" + }; + + return ( + <div> + <ODFNavBar activeKey={this.state.activeNavBarItem} selectCallback={this.handleNavBarSelection}></ODFNavBar> + <div style={divStyle}> + {alertComp} + {contentComponent} + </div> + </div> + ); + } +}); + +var div = $("#odf-toplevel-div")[0]; +ReactDOM.render(<ODFUI/>, div); http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-globals.js ---------------------------------------------------------------------- diff --git a/odf/odf-web/src/main/webapp/scripts/odf-globals.js b/odf/odf-web/src/main/webapp/scripts/odf-globals.js new file mode 100755 index 0000000..d67a2d3 --- /dev/null +++ b/odf/odf-web/src/main/webapp/scripts/odf-globals.js @@ -0,0 +1,54 @@ +/** + * + * 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. + */ +const CONTEXT_ROOT = ""; // window.location.origin + "/" + (window.location.pathname.split("/")[1].length > 0 ? window.location.pathname.split("/")[1] + "/" : ""); +const API_PREFIX = CONTEXT_ROOT + API_PATH; +const SERVICES_URL = API_PREFIX + "services"; +const ANALYSIS_URL = API_PREFIX + "analyses"; +const ENGINE_URL = API_PREFIX + "engine"; +const CONFIG_URL = API_PREFIX + "config"; +const METADATA_URL = API_PREFIX + "metadata"; +const IMPORT_URL = API_PREFIX + "import"; +const ANNOTATIONS_URL = API_PREFIX + "annotations"; + +var OdfUrls = { + "contextRoot": CONTEXT_ROOT, + "apiPrefix": API_PREFIX, + "servicesUrl": SERVICES_URL, + "analysisUrl": ANALYSIS_URL, + "engineUrl": ENGINE_URL, + "configUrl": CONFIG_URL, + "metadataUrl": METADATA_URL, + "importUrl": IMPORT_URL, + "annotationsUrl": ANNOTATIONS_URL, + + getPathValue: function(obj, path) { + var value = obj; + $.each(path.split("."), + function(propKey, prop) { + // if value is null, do nothing + if (value) { + if(value[prop] != null && value[prop] != undefined){ + value = value[prop]; + } else { + value = null; + } + } + } + ); + return value; + } +}; + +module.exports = OdfUrls; http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/6d19e129/odf/odf-web/src/main/webapp/scripts/odf-logs.js ---------------------------------------------------------------------- diff --git a/odf/odf-web/src/main/webapp/scripts/odf-logs.js b/odf/odf-web/src/main/webapp/scripts/odf-logs.js new file mode 100755 index 0000000..ecca602 --- /dev/null +++ b/odf/odf-web/src/main/webapp/scripts/odf-logs.js @@ -0,0 +1,83 @@ +/** + * + * 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 Input = ReactBootstrap.Input; + +var REFRESH_DELAY = 5000; + +var ODFLogViewer = React.createClass({ + mixins : [AJAXCleanupMixin], + + getInitialState : function(){ + return {logLevel : "ALL", log : ""}; + }, + + getLogs : function() { + const url = ODFGlobals.engineUrl + "/log?numberOfLogs=50&logLevel=" + this.state.logLevel; + var req = $.ajax({ + url: url, + contentType: "text/plain", + type: 'GET', + success: function(data) { + this.setState({log: data}); + }.bind(this), + error: function(xhr, status, err) { + var msg = "ODF log request failed, " + err.toString(); + this.props.alertCallback({type: "danger", message: msg}); + }.bind(this) + }); + + this.storeAbort(req.abort); + }, + + componentWillMount : function() { + this.getLogs(); + }, + + 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.getLogs, REFRESH_DELAY); + } + }, + render : function(){ + return (<div> + <h4>ODF system logs</h4> + <h5>(This only works for the node this web application is running on, logs from other ODF nodes in a clustered environment will not be displayed)</h5> + <Input label="Log level:" type="select" onChange={(el) => {this.setState({logLevel : el.target.value}); this.getLogs()}} value={this.state.logLevel}> + <option value="ALL">ALL</option> + <option value="FINE">FINE</option> + <option value="INFO">INFO</option> + <option value="WARNING">WARNING</option> + </Input> + <textarea disabled style={{width: '100%', height: '700px'}} value={this.state.log} /></div>); + } +}); + +module.exports = ODFLogViewer;
