diff options
Diffstat (limited to 'src/app/model/history')
-rw-r--r-- | src/app/model/history/History.jsx | 716 | ||||
-rw-r--r-- | src/app/model/history/HistoryActions.js | 19 | ||||
-rw-r--r-- | src/app/model/history/HistoryConstants.js | 25 | ||||
-rw-r--r-- | src/app/model/history/HistoryQuery.jsx | 814 | ||||
-rw-r--r-- | src/app/model/history/HistoryReducer.js | 43 | ||||
-rw-r--r-- | src/app/model/history/components/AnimationControls.jsx | 126 | ||||
-rw-r--r-- | src/app/model/history/components/HistoryCard.jsx | 101 | ||||
-rw-r--r-- | src/app/model/history/components/HistoryEntry.jsx | 55 | ||||
-rw-r--r-- | src/app/model/history/components/HistoryGallery.jsx | 65 | ||||
-rw-r--r-- | src/app/model/history/components/NodeDiffCard.jsx | 106 | ||||
-rw-r--r-- | src/app/model/history/components/TopologyDiffCard.jsx | 151 |
11 files changed, 2221 insertions, 0 deletions
diff --git a/src/app/model/history/History.jsx b/src/app/model/history/History.jsx new file mode 100644 index 0000000..61cb1af --- /dev/null +++ b/src/app/model/history/History.jsx @@ -0,0 +1,716 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import commonApi from 'utils/CommonAPIService.js'; +import deepDiffMapper from 'utils/DiffUtil.js'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; +import Spinner from 'utils/SpinnerContainer.jsx'; + +import HistoryGallery from './components/HistoryGallery.jsx'; +import HistoryCard from './components/HistoryCard.jsx'; +import NodeDiffCard from './components/NodeDiffCard.jsx'; +import AnimationControls from './components/AnimationControls.jsx'; +import moment from "moment"; +import Grid from 'react-bootstrap/lib/Grid'; +import Row from 'react-bootstrap/lib/Row'; +import Col from 'react-bootstrap/lib/Col'; +import Button from 'react-bootstrap/lib/Button'; +import Modal from 'react-bootstrap/lib/Modal'; +import Pagination from 'react-js-pagination'; +import { HistoryConstants } from './HistoryConstants'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Tooltip from 'react-bootstrap/lib/Tooltip'; +import ReactBootstrapSlider from 'react-bootstrap-slider'; + +let INVLIST = GlobalExtConstants.INVLIST; +/** + * This class is used to handle any url interactions for models. + * When a user selects a inventory item in browse or special search, + * this model class should be used to handle the url + params and query + * the proxy server. + */ + +class History extends Component { + + elements = []; + pageTitle = ''; + nodeType = ''; + nodeResults = ''; + payload = {start : atob(this.props.match.params.nodeUriEnc)}; + + constructor(props) { + console.log(props); + super(props); + this.state = { + activePage: 1, + totalResults: 0, + enableBusyFeedback: true, + enableBusyRecentFeedback: true, + enableBusyHistoryStateFeedback: true, + enableBusyDiffFeedback: true, + data: [], + nodes: [], + nodeCurrentState: null, + nodeHistoryState: null, + showHistoryModal: false, + splitScreenCard: false, + entries: [], + filteredEntries: [], + isLifeCycle: false, + isState: false, + currentStateHistoryValue: parseInt(this.props.match.params.epochTime), + stepEpochStateTime: 1000, + maxEpochStartTime: Math.round(new Date().getTime()), + minEpochStartTime: parseInt(this.props.match.params.epochTime) - 259200000, + nodeDiff: null, + showSlider: false, + sliderTickArray: null, + showTicks: INVLIST.showTicks, + selectedHistoryStateFormatted: moment(parseInt(this.props.match.params.epochTime)).format('dddd, MMMM Do, YYYY h:mm:ss A'), + nodeDisplay: (this.props.match.params.nodeType).toUpperCase(),// + ' : ' + (atob(this.props.match.params.nodeUriEnc)).split(this.props.match.params.nodeType + '\/').pop(), + nodeName: (this.props.match.params.nodeType).toUpperCase(), + historyErrMsg: null, + changesErrMsg: null, + lifecycleErrMsg: null, + currentErrMsg: null + }; + console.log('minEpochStateTime: '+this.state.minEpochStateTime); + console.log('maxEpochStartTime: '+this.state.maxEpochStartTime); + console.log('stepEpochStateTime: '+this.state.stepEpochStateTime); + console.log('currentStateHistoryValue: '+this.state.currentStateHistoryValue); + + } + resultsMessage = ''; + componentDidMount = () => { + console.log('[History.jsx] componentDidMount props available are', JSON.stringify(this.props)); + if(INVLIST.isHistoryEnabled){ + this.beforefetchInventoryData(); + } + }; + componentWillUnmount = () => { + console.log('[History.jsx] componentWillUnMount'); + } + getUnixSecondsFromMs = (ms) => { + return ms/1000; + } + beforefetchInventoryData = (param) => { + if (param) { + this.setState( + { enableBusyFeedback: true, activePage: 1, totalResults: 0}, + function () { this.fetchInventoryData(param); }.bind(this) + ); + } else { + this.fetchInventoryData(); + } + }; + initState = () =>{ + this.setState({ + activePage: 1, + totalResults: 0, + enableBusyFeedback: true, + enableBusyRecentFeedback: true, + enableBusyHistoryStateFeedback: true, + enableBusyDiffFeedback: true, + data: [], + nodes: [], + nodeCurrentState: null, + nodeHistoryState: null, + showHistoryModal: false, + splitScreenCard: false, + entries: [], + filteredEntries: [], + isLifeCycle: false, + isState: false, + stepEpochStateTime: 1000, + maxEpochStartTime: Math.round(new Date().getTime()), + minEpochStartTime: parseInt(this.props.match.params.epochTime) - 259200000, + nodeDiff: null, + showSlider: false, + sliderTickArray: null, + showTicks: INVLIST.showTicks, + nodeDisplay: (this.props.match.params.nodeType).toUpperCase(),// + ' : ' + (atob(this.props.match.params.nodeUriEnc)).split(this.props.match.params.nodeType + '\/').pop(), + nodeName: (this.props.match.params.nodeType).toUpperCase(), + historyErrMsg: null, + changesErrMsg: null, + lifecycleErrMsg: null, + currentErrMsg: null + }); + } + getSettings = () => { + const settings = { + 'NODESERVER': INVLIST.NODESERVER, + 'PROXY': INVLIST.PROXY, + 'PREFIX': INVLIST.PREFIX, + 'VERSION': INVLIST.VERSION, + 'USESTUBS': INVLIST.useStubs + }; + return settings; + } + fetchInventoryData = (param) => { + console.log('fetchInventoryData', param); + this.resultsMessage = ''; + let settings = this.getSettings(); + const inventory = INVLIST.INVENTORYLIST; + let url = ''; + console.log('[History.jsx] fetchInventoryData nodeUriEnc= ', atob(this.props.match.params.nodeUriEnc)); + let pageName = "History"; + this.nodeResults = ''; + switch(this.props.match.params.type){ + case('nodeState'): + this.setState({splitScreenCard: true, isState: true, showState: true}); + this.getCurrentStateCall(settings); + this.pageTitle = "State of "+ this.state.nodeDisplay +" at "+ moment(parseInt(this.props.match.params.epochTime)).format('dddd, MMMM Do, YYYY h:mm:ss A'); + break; + case('nodeLifeCycleSince'): + this.setState({splitScreenCard: false, isLifeCycle: true, showLifeCycle: true}); + this.getCurrentStateCall(settings); + this.commonApiServiceCall(settings, null); + this.pageTitle = "Network element state(s) of "+ this.state.nodeDisplay; + break; + case('nodeLifeCycle'): + this.setState({splitScreenCard: false, isLifeCycle: true, showLifeCycle: true}); + this.getCurrentStateCall(settings); + this.commonApiServiceCall(settings, null); + this.pageTitle = "Network element state(s) of "+ this.state.nodeDisplay; + break; + default: + this.pageTitle = "History"; + this.setState({splitScreenCard: false, isLifeCycle: true, showLifeCycle: true}); + this.getCurrentStateCall(settings); + this.commonApiServiceCall(settings, null); + } + console.log('[History.jsx] active page', this.state.activePage); + }; + generateEntries = (properties, relationships, actions) =>{ + let tempEntries = []; + if(properties){ + for (var i = 0; i < properties.length; i++) { + properties[i].displayTimestamp = moment(properties[i].timestamp).format('dddd, MMMM Do, YYYY h:mm:ss A'); + properties[i].timeRank = properties[i].timestamp; + properties[i].type = "attribute"; + properties[i].header = "Attribute: " + properties[i].key; + if(properties[i].value !== null && properties[i].value !== 'null'){ + properties[i].action = "Updated"; + properties[i].body = "Updated to value: " + properties[i].value; + }else{ + properties[i].action = "Deleted"; + properties[i].body = "Removed"; + } + properties[i]['tx-id'] = (properties[i]['tx-id']) ? properties[i]['tx-id'] : 'N/A'; + tempEntries.push(properties[i]); + } + } + if(actions){ + for (var k = 0; k < actions.length; k++) { + actions[k].displayTimestamp = moment(actions[k].timestamp).format('dddd, MMMM Do, YYYY h:mm:ss A'); + actions[k].timeRank = actions[k].timestamp; + actions[k].type = "action"; + actions[k].header = "Action: " + actions[k].action; + if(actions[k].action === 'CREATED'){ + actions[k].action = "Created"; + actions[k].body = "Network Element Created"; + }else if(actions[k].action === 'DELETED'){ + actions[k].action = "Deleted"; + actions[k].body = "Network Element Removed"; + } + actions[k]['tx-id'] = (actions[k]['tx-id']) ? actions[k]['tx-id'] : 'N/A'; + tempEntries.push(actions[k]); + } + } + if(relationships){ + for (var j = 0; j < relationships.length; j++) { + if(relationships[j].timestamp){ + relationships[j].dbStartTime = relationships[j].timestamp; + relationships[j].displayTimestamp = moment(relationships[j].dbStartTime).format('dddd, MMMM Do, YYYY h:mm:ss A'); + relationships[j].timeRank = relationships[j].dbStartTime; + relationships[j].type = "relationship"; + relationships[j].header = "Relationship added"; + relationships[j].body = "The relationship with label " + relationships[j]['relationship-label'] + " was added from this node to the "+ relationships[j]['node-type'] +" at " + relationships[j].url; + relationships[j].action = "Added"; + relationships[j]['tx-id'] = (relationships[j]['tx-id']) ? relationships[j]['tx-id'] : 'N/A'; + let additions = JSON.parse(JSON.stringify(relationships[j])); + tempEntries.push(additions); + } + if(relationships[j]['end-timestamp']){ + relationships[j].dbEndTime = relationships[j]['end-timestamp']; + relationships[j].sot = relationships[j]['end-sot']; + relationships[j].displayTimestamp = moment(relationships[j].dbEndTime).format('dddd, MMMM Do, YYYY h:mm:ss A'); + relationships[j].timeRank = relationships[j].dbEndTime; + relationships[j].header = "Relationship removed"; + relationships[j].body = "The " + relationships[j]['node-type'] +" : " + relationships[j].url + " relationship with label " + relationships[j]['relationship-label'] + " was removed from this node."; + relationships[j].type = "relationship"; + relationships[j].action = "Deleted"; + relationships[j]['tx-id'] = (relationships[j]['tx-id']) ? relationships[j]['tx-id'] : 'N/A'; + let deletions = JSON.parse(JSON.stringify(relationships[j])); + tempEntries.push(deletions); + } + } + } + + let tempEntriesSorted = tempEntries.sort(function(a, b) { + var compareA = a.timeRank; + var compareB = b.timeRank; + if(compareA > compareB) return -1; + if(compareA < compareB) return 1; + return 0; + }); + + this.setState({ + totalResults : tempEntriesSorted.length, + entries : tempEntriesSorted, + filteredEntries: tempEntriesSorted + }); + + } + triggerHistoryStateCall = (nodeUri, epochTime) =>{ + //get url for historical call + let settings = this.getSettings(); + window.scrollTo(0, 400); + this.setState({ + enableBusyHistoryStateFeedback : true, + enableBusyDiffFeedback: true + }); + this.getHistoryStateCall(settings, null, null, epochTime); + } + generateDiffArray = (arr) => { + let tempArray = {}; + tempArray['properties'] = []; + tempArray['related-to'] = []; + + for (var i = 0; i < arr.properties.length; i++ ){ + tempArray['properties'][arr.properties[i].key] = arr.properties[i]; + } + for (var j = 0; j < arr['related-to'].length; j++ ){ + //TODO use id if it is coming + tempArray['related-to'][arr['related-to'][j].url] = arr['related-to'][j]; + } + return tempArray; + } + getCurrentStateCall = (settings,url,param) =>{ + this.setState({currentErrMsg:null}); + commonApi(settings, "query?format=state", 'PUT', this.payload, 'currentNodeState', null, 'history-traversal') + .then(res => { + let node = atob(this.props.match.params.nodeUriEnc).split(res.data.results[0]['node-type'] + '\/').pop(); + res.data.results[0].primaryHeader = 'Current state of ' + this.state.nodeName + ' - ' + node; + res.data.results[0].secondaryHeader = atob(this.props.match.params.nodeUriEnc); + this.setState( + { + nodeCurrentState : res.data.results[0], + enableBusyRecentFeedback: false, + nodeDisplay :node + }); + if(this.props.match.params.type === 'nodeState'){ + this.getHistoryStateCall(settings, null); + } + console.log('After recent node service call ......',this.state); + console.log('[History.jsx] recent node results : ', res.data.results[0]); + }, error=>{ + this.triggerError(error, "current"); + }).catch(error => { + this.triggerError(error, 'current'); + }); + }; + getHistoryStateCall = (settings, url, param, timeStamp) =>{ + let ts = this.state.currentStateHistoryValue; + if(timeStamp){ + ts = parseInt(timeStamp); + } + if(this.state.showTicks){ + this.setState({changesErrMsg:null}); + commonApi(settings, "query?format=changes", 'PUT', this.payload, 'historicalNodeStateChanges', null, 'history-traversal') + .then(res => { + let tickTempArray = []; + for(var j = 0; j < res.data.results.length; j++ ){ + for(var k = 0; k < res.data.results[j].changes.length; k++){ + if(!tickTempArray.includes(res.data.results[j].changes[k])){ + tickTempArray.push(res.data.results[j].changes[k]); + } + } + } + let tickArray = tickTempArray.sort(function(a, b) { + var compareA = a; + var compareB = b; + if(compareA < compareB) return -1; + if(compareA > compareB) return 1; + return 0; + }); + console.log("tick array: " + tickArray); + this.setState({showSlider:true, sliderTickArray: tickArray}); + }, error=>{ + this.triggerError(error, "changes"); + }).catch(error => { + this.triggerError(error, 'changes'); + }); + }else{ + this.setState({showSlider:true}); + } + this.setState({historyErrMsg:null}); + commonApi(settings, "query?format=state&startTs=" + ts + "&endTs=" + ts, 'PUT', this.payload, 'historicalNodeState', null, 'history-traversal') + .then(res => { + let node = atob(this.props.match.params.nodeUriEnc).split(res.data.results[0]['node-type'] + '\/').pop(); + res.data.results[0].primaryHeader = 'Historical state of '+ this.state.nodeName + ' - ' + node + ' as of ' + moment(parseInt(this.props.match.params.epochTime)).format('dddd, MMMM Do, YYYY h:mm:ss A'); + res.data.results[0].secondaryHeader = atob(this.props.match.params.nodeUriEnc); + this.setState( + { + showState: true, + splitScreenCard: true, + nodeHistoryState : res.data.results[0], + enableBusyHistoryStateFeedback: false + }); + console.log('After historical node state service call ......',this.state); + console.log('[History.jsx] historical node state results : ', res.data.results[0]); + if(this.state.nodeHistoryState != null && this.state.nodeCurrentState != null){ + let nodeDiffHistoryArr = this.generateDiffArray(this.state.nodeHistoryState); + let nodeDiffCurrentArr = this.generateDiffArray(this.state.nodeCurrentState); + var result = deepDiffMapper.map(nodeDiffHistoryArr, nodeDiffCurrentArr); + console.log("diff map" + result); + this.setState({ nodeDiff: result, enableBusyDiffFeedback: false }); + }else{ + this.setState({ enableBusyDiffFeedback: false }); + } + }, error=>{ + this.triggerError(error, "historic"); + }).catch(error => { + this.triggerError(error, 'historic'); + }); + + }; + + commonApiServiceCall = (settings,url,param) =>{ + let path = "query?format=lifecycle"; + let stubPath = "nodeLifeCycle"; + if(this.props.match.params.type === "nodeLifeCycleSince"){ + path += "&startTs=" + parseInt(this.props.match.params.epochTime); + stubPath += "Since"; + } + this.setState({lifecycleErrMsg:null}); + commonApi(settings, path, 'PUT', this.payload, stubPath, null, 'history-traversal') + .then(res => { + // Call dispatcher to update state + console.log('once before service call ......',this.state); + let resp = res.data.results[0]; + this.resultsMessage = ''; + let totalResults = 0; + if(resp && resp.properties.length + resp['related-to'].length > 0){ + if(this.props.match.params.type === "nodeState"){ + totalResults = 1; + }else{ + //wait to generate entries to set this + totalResults = 0; + } + } + this.setState( + { + totalResults : totalResults, + enableBusyFeedback:false, + }); + if(resp){ + this.generateEntries(resp.properties, resp['related-to'], resp['node-actions']); + } + console.log('After service call ......',this.state); + console.log('[History.jsx] results : ', resp); + }, error=>{ + this.triggerError(error, 'lifecycle'); + }).catch(error => { + this.triggerError(error, 'lifecycle'); + }); + }; + triggerError = (error, type) => { + console.error('[History.jsx] error : ', JSON.stringify(error)); + let errMsg = ''; + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.log(error.response.data); + console.log(error.response.status); + console.log(error.response.headers); + if(error.response.status){ + errMsg += " Code: " + error.response.status; + } + if(error.response.data){ + errMsg += " - " + JSON.stringify(error.response.data); + } + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + errMsg += " - Request was made but no response received"; + } else { + // Something happened in setting up the request that triggered an Error + console.log('Error', error.message); + errMsg += " - Unknown error occurred " + error.message; + } + //Suppress 404's because that is just no results + if(error.response && error.response.status === 404){ + errMsg = ''; + } + if(type === 'lifecycle'){ + this.setState({ + lifecycleErrMsg: errMsg, + enableBusyFeedback: false, + totalResults: 0 + }); + }else if (type === 'changes'){ + this.setState({ + showSlider:true, + changesErrMsg: errMsg + }); + }else if (type === 'historic'){ + console.log('[History.jsx] historical node state error : ', error); + this.setState( + { + showState: true, + splitScreenCard: true, + nodeHistoryState : null, + enableBusyHistoryStateFeedback:false, + enableBusyDiffFeedback:false, + historyErrMsg: errMsg + }); + }else if (type === 'current'){ + this.setState( + { + nodeCurrentState : null, + currentErrMsg: errMsg, + enableBusyRecentFeedback:false + }); + }else{ + console.log('[History.jsx] triggerError method called without a type.' ); + } + } + + componentWillReceiveProps(nextProps) { + console.log('[History.jsx] componentWillReceiveProps'); + console.log('[History.jsx] next nodeUri:', atob(nextProps.match.params.nodeUriEnc)); + console.log('[History.jsx] this nodeUri:', atob(this.props.match.params.nodeUriEnc)); + + if (nextProps.match.params.nodeUriEnc + && nextProps.match.params.type + && nextProps.match.params.epochTime + && ((nextProps.match.params.nodeUriEnc !== this.props.match.params.nodeUriEnc) || + (nextProps.match.params.type !== this.props.match.params.type) || + (nextProps.match.params.epochTime !== this.props.match.params.epochTime)) + ) { + this.initState(); + this.props = nextProps; + this.beforefetchInventoryData(); + } + }; + + handlePageChange = (pageNumber) => { + console.log('[History.jsx] HandelPageChange active page is', pageNumber); + this.setState( + { activePage: pageNumber, enableBusyFeedback: true }, + function () { this.beforefetchInventoryData(); }.bind(this) + ); + }; + + // HELPER FUNCTIONS + isContaining = (nameKey, listArray) => { + let found = false; + listArray.map((lists) => { + if (lists.id === nameKey) { + found = true; + } + }); + return found; + }; + + stateHistoryFormat = (event) =>{ + this.setState({ currentStateHistoryValue: event.target.value, selectedHistoryStateFormatted: moment(event.target.value).format('dddd, MMMM Do, YYYY h:mm:ss A')}); + }; + changeHistoryState = () =>{ + console.log('minEpochStateTime: ' + this.state.minEpochStateTime); + console.log('maxEpochStartTime: ' + this.state.maxEpochStartTime); + console.log('stepEpochStateTime: ' + this.state.stepEpochStateTime); + console.log('currentStateHistoryValue: ' + this.state.currentStateHistoryValue); + console.log("Calling the route again with a new timestamp"); + this.props.history.push('/history/' + this.props.match.params.type + '/' + this.props.match.params.nodeType + '/' + this.props.match.params.nodeUriEnc + '/' + this.state.currentStateHistoryValue); + } + filterList = (event) =>{ + var updatedList = this.state.entries; + updatedList = updatedList.filter((entry) =>{ + return JSON.stringify(entry).toLowerCase().search( + event.target.value.toLowerCase()) !== -1; + }); + this.setState({filteredEntries: updatedList, totalResults: updatedList.length}); + } + + setHistoricStateValues = (currValue) =>{ + this.setState({currentStateHistoryValue: currValue, selectedHistoryStateFormatted: moment(currValue).format('dddd, MMMM Do, YYYY h:mm:ss A')}); + } + + navigateHistory = (time) =>{ + this.props.history.push('/history/' + this.props.match.params.type + '/' + this.props.match.params.nodeType + '/' + this.props.match.params.nodeUriEnc + '/' + time); + } + + setStateValue = (key, value) =>{ + this.setState((state) => { key : value }); + } + getStateValue = (stateVar) => { + return this.state[stateVar]; + } + + render() { + console.log('[History Props] render: ', JSON.stringify(this.props) + 'elements : ', this.elements); + console.log('[History nodeUri] render: ', atob(this.props.match.params.nodeUriEnc)); + if(INVLIST.isHistoryEnabled){ + return ( + <div> + <header className='addPadding jumbotron my-4'> + <h1 className='display-2'>Network Element History</h1> + <p className='lead'> + On this page you have the ability to view a network element in its current and historic state. + The attributes are clickable to view extended information about who and when they were last updated. + {this.props.match.params.type === "nodeLifeCycle" || this.props.match.params.type === "nodeLifeCycleSince" + ? 'The table at the bottom of the page shows a list of all updates made on the network element (it is filterable).\n' + + 'Click an update in that table to rebuild the historic state at the time of the update.\n' + + 'A difference will be displayed between the current state and the historic state in the center.' : ''} + </p> + </header> + <Grid fluid={true} className='addPadding'> + <Row> + <Col className='col-lg-12'> + <div className='card d3-model-card'> + <div className='card-header model-card-header'> + <h2>{this.pageTitle}</h2> + </div> + <Row className={this.state.changesErrMsg ? 'show' : 'hidden'} > + <div className='addPaddingTop alert alert-danger' role="alert"> + An error occurred while trying to get the state changes, please try again later. If this issue persists, please contact the system administrator. {this.state.changesErrMsg} + </div> + </Row> + { (this.state.isState && this.state.showSlider && this.state.showTicks) && ( + <div className='card-header'> + <Row className='show-grid'> + <Col md={3}> + <ReactBootstrapSlider + value={this.state.currentStateHistoryValue} + change={this.stateHistoryFormat} + slideStop={this.stateHistoryFormat} + step={ 1 } + ticks={ this.state.sliderTickArray } + ticks_snap_bounds={ 10000 } + orientation="horizontal" /> + <p>{this.state.selectedHistoryStateFormatted}</p> + <button type='button' className='btn btn-outline-primary' onClick={this.changeHistoryState}>Refresh</button> + </Col> + <AnimationControls playControlsDisabled={true} get={this.getStateValue} set={this.setStateValue} tickArray={this.state.sliderTickArray} currentValue={this.state.currentStateHistoryValue} setValueState={this.setHistoricStateValues} setNavigate={this.navigateHistory} /> + </Row> + </div>)} + { (this.state.isState && this.state.showSlider && !this.state.showTicks) && ( + <div className='card-header'> + <Row className='show-grid'> + <Col md={12}> + <ReactBootstrapSlider + value={this.state.currentStateHistoryValue} + change={this.stateHistoryFormat} + slideStop={this.stateHistoryFormat} + step={this.state.stepEpochStateTime} + max={this.state.maxEpochStartTime} + min={this.state.minEpochStartTime} + orientation="horizontal" /> + <p>{this.state.selectedHistoryStateFormatted}</p> + <button type='button' className='btn btn-outline-primary' onClick={this.changeHistoryState}>Refresh</button> + </Col> + </Row> + </div>)} + <div className='card-content model-card-content'> + <div> + <Row className='show-grid'> + <Col md={12}> + <Spinner loading={this.state.enableBusyHistoryStateFeedback && this.state.showState}> + {this.state.showState && !this.historyErrMsg && (<HistoryCard split={this.state.splitScreenCard} node={this.state.nodeHistoryState}/>)} + <Row className={this.state.historyErrMsg ? 'show' : 'hidden'} > + <div className='addPaddingTop alert alert-danger' role="alert"> + An error occurred while trying to get the historic state, please try again later. If this issue persists, please contact the system administrator. {this.state.historyErrMsg} + </div> + </Row> + </Spinner> + <Spinner loading={this.state.showState && this.state.enableBusyDiffFeedback}> + {this.state.showState && (<NodeDiffCard diff={this.state.nodeDiff}/>)} + </Spinner> + <Spinner loading={this.state.enableBusyRecentFeedback}> + { !this.currentErrMsg && (<HistoryCard split={this.state.splitScreenCard} node={this.state.nodeCurrentState}/>)} + <Row className={this.state.currentErrMsg ? 'show' : 'hidden'} > + <div className='addPaddingTop alert alert-danger' role="alert"> + An error occurred while trying to get the current state, please try again later. If this issue persists, please contact the system administrator. {this.state.currentErrMsg} + </div> + </Row> + </Spinner> + <hr /> + <span className='resultMessage'>{this.resultsMessage}</span> + </Col> + </Row> + </div> + </div> + <div className='card-footer'> + <strong>Tip:</strong> <em>Click any attribute to view more details</em> + </div> + </div> + </Col> + </Row> + <Row> + <div className={'addPaddingTop alert alert-danger ' + (this.state.lifecycleErrMsg ? 'show' : 'hidden')} role="alert"> + An error occurred while trying to get the list of updates on the current node, please try again later. If this issue persists, please contact the system administrator. {this.state.lifecycleErrMsg} + </div> + {this.state.showLifeCycle && !this.state.lifecycleErrMsg && ( + <Col className='col-lg-12'> + <div className='card d3-model-card'> + <div className='card-header model-card-header'> + <h2 className={this.props.match.params.type === "nodeLifeCycle" ? 'show' : 'hidden'}>All Updates on {this.state.nodeDisplay}</h2> + <h2 className={this.props.match.params.type === "nodeLifeCycleSince" ? 'show' : 'hidden'}>All Updates on {this.state.nodeDisplay} Since {this.state.selectedHistoryStateFormatted}</h2> + </div> + <div className='card-header'> + <p><strong>Tip:</strong> <em>Click any update to view the state of the node at that point in time</em></p> + <div> + <h5>Total Results: <strong>{this.state.totalResults}</strong></h5> + </div> + <div> + <form> + <fieldset className="form-group"> + <input type="text" className="form-control form-control-lg" placeholder="Search" onChange={this.filterList}/> + </fieldset> + </form> + </div> + </div> + <div className='card-content model-card-content'> + <Spinner loading={this.state.enableBusyFeedback}> + <div> + <HistoryGallery nodeId={atob(this.props.match.params.nodeUriEnc)} entries={this.state.filteredEntries} triggerState={this.triggerHistoryStateCall}/> + </div> + </Spinner> + </div> + </div> + </Col> )} + </Row> + </Grid> + </div> + ); + }else{ + return(<p>History Not Enabled for this instance, please check config.</p>) + } + } +} + +export default History; diff --git a/src/app/model/history/HistoryActions.js b/src/app/model/history/HistoryActions.js new file mode 100644 index 0000000..4060806 --- /dev/null +++ b/src/app/model/history/HistoryActions.js @@ -0,0 +1,19 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ diff --git a/src/app/model/history/HistoryConstants.js b/src/app/model/history/HistoryConstants.js new file mode 100644 index 0000000..2d4108e --- /dev/null +++ b/src/app/model/history/HistoryConstants.js @@ -0,0 +1,25 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +export const HistoryConstants = { + UPDATE_INVENTORY_DATA : 'UPDATE_INVENTORY_DATA', + RESULTS_PER_PAGE : 50 , + PAGE_RANGE_DISPLAY : 10 +}; diff --git a/src/app/model/history/HistoryQuery.jsx b/src/app/model/history/HistoryQuery.jsx new file mode 100644 index 0000000..f001f97 --- /dev/null +++ b/src/app/model/history/HistoryQuery.jsx @@ -0,0 +1,814 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; + +import commonApi from 'utils/CommonAPIService.js'; +import deepDiffMapper from 'utils/DiffUtil.js'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; + +import moment from "moment"; +import Grid from 'react-bootstrap/lib/Grid'; +import Row from 'react-bootstrap/lib/Row'; +import Col from 'react-bootstrap/lib/Col'; +import Button from 'react-bootstrap/lib/Button'; +import Modal from 'react-bootstrap/lib/Modal'; +import * as d3 from "d3"; +import 'd3-selection-multi'; +import ReactBootstrapSlider from 'react-bootstrap-slider'; +import HistoryCard from './components/HistoryCard.jsx'; +import NodeDiffCard from './components/NodeDiffCard.jsx'; +import AnimationControls from './components/AnimationControls.jsx'; +import TopologyDiffCard from './components/TopologyDiffCard.jsx'; +import OutputVisualization, {Visualization} from 'generic-components/OutputVisualization.jsx'; + + + +let INVLIST = GlobalExtConstants.INVLIST; + +/** + * This class is to show visualizations of queries and show a historical state + */ + +class HistoryQuery extends Component { + + svgWidth = window.outerWidth * 0.8; + elements = []; + pageTitle = ''; + nodeType = ''; + nodeResults = ''; + + constructor(props) { + console.log(props); + super(props); + this.state = { + totalResults: 0, + enableBusyRecentFeedback: true, + enableBusyHistoryStateFeedback: true, + enableBusyDiffFeedback: true, + data: [], + nodes: [], + payload: ((this.props.match.params.type == 'cq') ? atob(this.props.match.params.payloadEnc) : { "dsl" : atob(this.props.match.params.payloadEnc)}), + topologyCurrentState: null, + topologyHistoryState: null, + currentStateHistoryValue: parseInt(this.props.match.params.epochTime), + stepEpochStateTime: 1000, + maxEpochStartTime: Math.round(new Date().getTime()), + minEpochStartTime: parseInt(this.props.match.params.epochTime) - 259200000, + nodeDiff: null, + selectedHistoryStateFormatted: moment(parseInt(this.props.match.params.epochTime)).format('dddd, MMMM Do, YYYY h:mm:ss A'), + queryDisplay: ((this.props.match.params.type == 'cq') ? JSON.parse(atob(this.props.match.params.payloadEnc)).query : atob(this.props.match.params.payloadEnc)), + currentGraphNodes: [], + currentGraphLinks: [], + historicGraphNodes: [], + historicGraphLinks: [], + selectedNodeHistoryState: null, + selectedNodeCurrentState: null, + showNodeModal: false, + showTopologyDiffModal: false, + nodeDisplay: '', + totalDiff: null, + showSlider: false, + sliderTickArray: null, + showTicks: INVLIST.showTicks, + rawMappedCurrentState: null, + rawMappedHistoricState: null, + isPlaying: false, + isPaused: false, + isStopped: false, + intervalId: null, + diffOverlayOn: false, + currentErrMsg: null, + historicErrMsg: null + + }; + console.log('minEpochStateTime: '+this.state.minEpochStateTime); + console.log('maxEpochStartTime: '+this.state.maxEpochStartTime); + console.log('stepEpochStateTime: '+this.state.stepEpochStateTime); + console.log('currentStateHistoryValue: '+this.state.currentStateHistoryValue); + } + resultsMessage = ''; + componentDidMount = () => { + console.log('[HistoryQuery.jsx] componentDidMount props available are', JSON.stringify(this.props)); + if(INVLIST.isHistoryEnabled){ + this.getCurrentStateCall(this.getSettings()); + } + }; + + componentWillUnmount = () => { + console.log('[History.jsx] componentWillUnMount'); + } + beforefetchInventoryData = (param) => { + if (param) { + this.setState({}); + } else { + this.fetchInventoryData(); + } + }; + initState = () =>{ + this.setState({ + totalResults: 0, + enableBusyRecentFeedback: true, + enableBusyHistoryStateFeedback: true, + enableBusyDiffFeedback: true, + data: [], + nodes: [], + payload : ((this.props.match.params.type == 'cq') ? atob(this.props.match.params.payloadEnc) : { "dsl" : atob(this.props.match.params.payloadEnc)}), + topologyHistoryState: null, + topologyCurrentState: null, + stepEpochStateTime: 1000, + maxEpochStartTime: Math.round(new Date().getTime()), + minEpochStartTime: parseInt(this.props.match.params.epochTime) - 259200000, + nodeDiff: null, + queryDisplay: ((this.props.match.params.type == 'cq') ? JSON.parse(atob(this.props.match.params.payloadEnc)).query : atob(this.props.match.params.payloadEnc)), + currentGraphNodes: [], + currentGraphLinks: [], + historicGraphNodes: [], + historicGraphLinks: [], + selectedNodeHistoryState: null, + selectedNodeCurrentState: null, + showNodeModal: false, + showTopologyDiffModal: false, + nodeDisplay: '', + totalDiff: null, + showSlider: false, + sliderTickArray: null, + rawMappedCurrentState: null, + rawMappedHistoricState: null, + showTicks: INVLIST.showTicks, + diffOverlayOn: false, + currentErrMsg: null, + historicErrMsg: null + }); + } + getSettings = () => { + const settings = { + 'NODESERVER': INVLIST.NODESERVER, + 'PROXY': INVLIST.PROXY, + 'PREFIX': INVLIST.PREFIX, + 'VERSION': INVLIST.VERSION, + 'USESTUBS': INVLIST.useStubs + }; + return settings; + } + fetchInventoryData = (param) => { + console.log('fetchInventoryData', param); + }; + + generateDiffArray = (arr) => { + let tempArray = {}; + tempArray['properties'] = []; + tempArray['related-to'] = []; + + for (var i = 0; i < arr.properties.length; i++ ){ + if(arr.properties[i].key !== "length"){ + tempArray['properties'][arr.properties[i].key] = arr.properties[i]; + }else{ + tempArray['properties']["LENGTH"] = arr.properties[i]; + } + } + for (var j = 0; j < arr['related-to'].length; j++ ){ + tempArray['related-to'][arr['related-to'][j].url] = arr['related-to'][j]; + } + return tempArray; + } + + getCurrentStateCall = (settings,url,param) =>{ + let path = ""; + let stubPath = ""; + if(this.props.match.params.type == 'cq'){ + path = 'query'; + stubPath = 'currentCQState'; + }else{ + path = 'dsl'; + stubPath = "currentBYOQState"; + } + this.setState({currentErrMsg:null}); + commonApi(settings, path + '?format=state', 'PUT', this.state.payload, stubPath, null, 'history-traversal') + .then(res => { + this.getHistoryStateCall(this.getSettings()); + Visualization.chart('currentState', this.state.currentGraphNodes, this.state.currentGraphLinks, res.data, this); + this.setState( + { + topologyCurrentState : res.data, + enableBusyRecentFeedback: false + }); + console.log('After recent node service call ......',this.state); + console.log('[HistoryQuery.jsx] recent node results : ', res.data); + }, error=>{ + this.triggerError(error, "current"); + }).catch(error => { + this.triggerError(error, "current"); + }); + }; + triggerError = (error, type) => { + console.error('[HistoryQuery.jsx] error : ', JSON.stringify(error)); + let errMsg = ''; + if (error.response) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx + console.log(error.response.data); + console.log(error.response.status); + console.log(error.response.headers); + if(error.response.status){ + errMsg += " Code: " + error.response.status; + } + if(error.response.data){ + errMsg += " - " + JSON.stringify(error.response.data); + } + } else if (error.request) { + // The request was made but no response was received + // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // http.ClientRequest in node.js + console.log(error.request); + errMsg += " - Request was made but no response received"; + } else { + // Something happened in setting up the request that triggered an Error + console.log('Error', error.message); + errMsg += " - Unknown error occurred " + error.message; + } + //Suppress 404's because that is just no results + if(error.response && error.response.status === 404){ + errMsg = ''; + } + if(type === 'current'){ + this.setState({ + topologyCurrentState : null, + currentErrMsg: errMsg, + enableBusyRecentFeedback:false + }); + }else if (type === 'historic'){ + if(this.state.isPlaying){ + this.setState({ + isPlaying: false, + isStopped: true, + isPaused: false }); + }else{ + this.setState({ + topologyHistoryState : null, + nodeDiff: null, + enableBusyDiffFeedback: false, + enableBusyHistoryStateFeedback:false, + historicErrMsg: errMsg + }); + } + }else{ + console.log('[HistoryQuery.jsx] tiggerError method called without a type.' ); + } + } + getHistoryStateCall = (settings, url, param, timeStamp) =>{ + let path = ""; + let stubPath = ""; + let stubChangesPath = ""; + if(this.props.match.params.type == 'cq'){ + path = 'query'; + stubPath = 'historicalCQState'; + stubChangesPath = 'historicalCQChanges'; + }else{ + path = 'dsl'; + stubPath = 'historicalBYOQState'; + stubChangesPath = 'historicalBYOQChanges'; + } + let ts = this.state.currentStateHistoryValue; + if(timeStamp){ + ts = parseInt(timeStamp); + } + this.setState({historicErrMsg:null}); + commonApi(settings, path + "?format=state" + "&startTs=" + ts + "&endTs=" + ts, 'PUT', this.state.payload, stubPath, null, 'history-traversal') + .then(res => { + Visualization.chart('historicState', this.state.historicGraphNodes, this.state.historicGraphLinks, res.data, this); + this.setState( + { + topologyHistoryState : res.data, + enableBusyHistoryStateFeedback: false + }); + console.log('After historical node state service call ......',this.state); + console.log('[HistoryQuery.jsx] historical node state results : ', res.data); + if(this.state.topologyHistoryState != null && this.state.topologyCurrentState != null){ + let topologyDiffHistoryArr = []; + let topologyDiffCurrentArr = []; + let tempNodeCurrentState = []; + let tempNodeHistoricState = []; + for( var i = 0; i < this.state.topologyHistoryState.results.length; i++ ){ + topologyDiffHistoryArr[this.state.topologyHistoryState.results[i].url] = this.generateDiffArray(this.state.topologyHistoryState.results[i]); + tempNodeHistoricState[this.state.topologyHistoryState.results[i].url] = this.state.topologyHistoryState.results[i]; + } + for( var j = 0; j < this.state.topologyCurrentState.results.length; j++ ){ + topologyDiffCurrentArr[this.state.topologyCurrentState.results[j].url] = this.generateDiffArray(this.state.topologyCurrentState.results[j]); + tempNodeCurrentState[this.state.topologyCurrentState.results[j].url] = this.state.topologyCurrentState.results[j]; + } + var result = deepDiffMapper.map(topologyDiffHistoryArr, topologyDiffCurrentArr); + console.log("diff map" + result); + this.setState({ totalDiff: result, enableBusyDiffFeedback: false, rawMappedCurrentState: tempNodeCurrentState, rawMappedHistoricState: tempNodeHistoricState}); + }else{ + this.setState({ enableBusyDiffFeedback: false }); + } + }, error=>{ + this.triggerError(error, "historic"); + }).catch(error => { + this.triggerError(error, "historic"); + }); + if(this.state.showTicks){ + commonApi(settings, path + "?format=changes", 'PUT', this.state.payload, stubChangesPath, null, 'history-traversal') + .then(res => { + let tickTempArray = []; + for(var j = 0; j < res.data.results.length; j++ ){ + for(var k = 0; k < res.data.results[j].changes.length; k++){ + if(!tickTempArray.includes(res.data.results[j].changes[k])){ + tickTempArray.push(res.data.results[j].changes[k]); + } + } + } + let tickArray = tickTempArray.sort(function(a, b) { + var compareA = a; + var compareB = b; + if(compareA < compareB) return -1; + if(compareA > compareB) return 1; + return 0; + }); + console.log("tick array: " + tickArray); + this.setState({showSlider:true, sliderTickArray: tickArray}); + }).catch(error => { + console.log('[HistoryQuery.jsx] historical node changes error : ', error); + this.setState({showSlider:false}); + }); + }else{ + this.setState({showSlider:true}); + } + }; + +componentWillReceiveProps(nextProps) { + console.log('[History.jsx] componentWillReceiveProps'); + console.log('[History.jsx] next payloadEnc:', atob(nextProps.match.params.payloadEnc)); + console.log('[History.jsx] this payloadEnc:', atob(this.props.match.params.payloadEnc)); + + if (nextProps.match.params.payloadEnc + && nextProps.match.params.type + && nextProps.match.params.epochTime + && ((nextProps.match.params.payloadEnc !== this.props.match.params.payloadEnc) || + (nextProps.match.params.type !== this.props.match.params.type) || + (nextProps.match.params.epochTime !== this.props.match.params.epochTime)) + ) { + this.props = nextProps; + this.initState(); + this.getCurrentStateCall(this.getSettings()); + } + }; + + handlePageChange = (pageNumber) => { + console.log('[History.jsx] HandelPageChange active page is', pageNumber); + this.setState({}); + }; + + stateHistoryFormat = (event) =>{ + this.setState({ currentStateHistoryValue: event.target.value, selectedHistoryStateFormatted: moment(event.target.value).format('dddd, MMMM Do, YYYY h:mm:ss A')}); + }; + + changeHistoryState = () =>{ + console.log('minEpochStateTime: ' + this.state.minEpochStateTime); + console.log('maxEpochStartTime: ' + this.state.maxEpochStartTime); + console.log('stepEpochStateTime: ' + this.state.stepEpochStateTime); + console.log('currentStateHistoryValue: ' + this.state.currentStateHistoryValue); + console.log("Calling the route again with a new timestamp"); + this.props.history.push('/historyQuery/' + this.props.match.params.type + '/' + this.props.match.params.payloadEnc + '/' + this.state.currentStateHistoryValue); + } + + viewTopologyComp = () =>{ + this.setState({ + showTopologyDiffModal: true + }); + } + + resetGraph = () =>{ + Visualization.chart('currentState', this.state.currentGraphNodes, this.state.currentGraphLinks, this.state.topologyCurrentState, this); + Visualization.chart('historicState', this.state.historicGraphNodes, this.state.historicGraphLinks, this.state.topologyHistoryState, this); + } + + addOverlay = (elementType, key, modificationType) =>{ + let chartToOverlay = 'currentState'; + let color = ''; + let modIcon = ''; + if (modificationType === "deleted"){ + chartToOverlay = "historicState"; + } + switch (modificationType){ + case 'deleted': + color = 'red'; + modIcon = '-'; + break; + case 'created': + color = 'green'; + modIcon = '+'; + break; + case 'modified': + color = 'orange'; + modIcon = '*'; + break; + default: + console.log("hit default " + modificationType); + } + if(key){ + key = (((decodeURIComponent(key)).replace(new RegExp('\/', 'g'),'-')).replace(new RegExp(':', 'g'),'-')).replace(new RegExp('\\.', 'g'),'-'); + }else{ + key=''; + } + console.log("adding overlay item for - element type: " + elementType + " key: " + key + " modificationType: " + modificationType ); + let elementKey = elementType + chartToOverlay + key; + let element = d3.select("#" + elementKey); + + if(elementType === "line"){ + element.attrs({ 'stroke': color, 'stroke-opacity': .6, 'stroke-width': '3px'}); + } + if(elementType === "nodeIcon"){ + element.classed("nodeIcon-" + modificationType, true); + let elementKeyNode = 'node' + chartToOverlay + key; + let elementNode = d3.select("#" + elementKeyNode); + elementNode.append("text") + .attr("dy", 10) + .attr("dx", 35) + .attr('font-size', 25) + .text(modIcon); + } + //Need to also add to historicGraph for modifications in addition to current + if(modificationType === 'modified'){ + let elementKeyMod = elementType + 'historicState' + key; + let elementMod = d3.select("#" + elementKeyMod); + elementMod.classed("nodeIcon-" + modificationType, true); + + let elementKeyModNode = 'nodehistoricState' + key; + let elementModNode = d3.select("#" + elementKeyModNode); + elementModNode.append("text") + .attr("dy", 10) + .attr("dx", 35) + .attr('font-size', 25) + .text(modIcon); + } + } + + viewTopologyCompVisual = () =>{ + if(this.state.diffOverlayOn){ + this.setState({ diffOverlayOn: false}); + this.resetGraph(); + }else if(this.state.totalDiff){ + this.setState({ diffOverlayOn: true}); + const properties = Object.entries(this.state.totalDiff).forEach((prop) => { + if (prop){ + let propWorkaround = prop; + if(prop.data){ + propWorkaround = prop.data; + } + let tempProp = propWorkaround[1]; + let attributeProperties = ''; + let relationships = ''; + let topLevelType = tempProp.type; + let alreadyHighlighted = false; + if(topLevelType){ + this.addOverlay('nodeIcon', propWorkaround[0], topLevelType); + //set this to not mark as modified when it is added new + alreadyHighlighted = true; + } + if(!topLevelType && tempProp.properties){ + for (var key in tempProp.properties) { + if (tempProp.properties.hasOwnProperty(key)) { + let property = tempProp.properties[key]; + if(property && ((property.value && property.value.type !== 'unchanged') || property.type)){ + this.addOverlay('nodeIcon', propWorkaround[0], 'modified'); + break; + } + } + } + } + if(tempProp['related-to'] || tempProp.data['related-to']){ + let rel = null; + let topLevelType = null; + if(tempProp['related-to']){ + rel = tempProp['related-to']; + }else if (tempProp.data['related-to']) { + rel = tempProp.data['related-to']; + topLevelType = tempProp.type; + } + relationships = Object.entries(rel).forEach((property) => { + let relationProp = property[1]; + if(relationProp && relationProp.type && relationProp.type.type && relationProp.type.type !== "unchanged"){ + //do nothing since we only care about additions and deletions on relationships, should not be considered modified + console.log("relationship considered modified: id: " + relationProp.id + " type: "+ relationProp.type + " mod type: " + relationProp.type.type) + }else if(relationProp && relationProp.type && !relationProp.type.type && relationProp.url && relationProp.url.data){ + if(!alreadyHighlighted){ + this.addOverlay('nodeIcon', propWorkaround[0], 'modified'); + } + this.addOverlay('line', relationProp.id.data, relationProp.type); + }else if (relationProp && relationProp.type && relationProp.data){ + if(!alreadyHighlighted){ + this.addOverlay('nodeIcon', propWorkaround[0], 'modified'); + } + this.addOverlay('line', relationProp.data.id, relationProp.type); + }else if (topLevelType){ + if(!alreadyHighlighted){ + this.addOverlay('nodeIcon', propWorkaround[0], 'modified'); + } + this.addOverlay('line', relationProp.id, topLevelType); + } + }); + } + }else{ + //No changes, do nothing + } + }); + + }else{ + // do nothing if no diff + } + } + + closeTopologyDiffModal = () => { + this.setState({ + showTopologyDiffModal: false + }); + } + openNodeModal(nodeDisplay, nodeUri, nodeType){ // open modal + console.log('history >> showModal'); + this.setState({ nodeDiff: this.state.totalDiff[nodeUri]}); + nodeDisplay = "State Comparison of " + nodeUri; + if(!this.state.rawMappedHistoricState[nodeUri]){ + this.state.rawMappedHistoricState[nodeUri] = {}; + } + if(!this.state.rawMappedCurrentState[nodeUri]){ + this.state.rawMappedCurrentState[nodeUri] = {}; + } + this.state.rawMappedHistoricState[nodeUri].primaryHeader = "Historic State of " + nodeDisplay; + this.state.rawMappedCurrentState[nodeUri].primaryHeader = "Current State of " + nodeDisplay; + if(nodeDisplay){ + this.setState({ + nodeDisplay: nodeDisplay, + selectedNodeHistoryState: this.state.rawMappedHistoricState[nodeUri], + selectedNodeCurrentState: this.state.rawMappedCurrentState[nodeUri], + focusedNodeUri: nodeUri, + focusedNodeType: nodeType, + showNodeModal:true + }); + }else{ + this.setState({ + showNodeModal:true + }); + } + } + closeNodeModal = () => { + this.setState({ + showNodeModal: false + }); + } + + getStateIndex = () =>{ + return this.state.sliderTickArray.indexOf(this.state.currentStateHistoryValue); + } + + navigateAnimation = (index, command) => { + if(!command){ + this.setState({isPlaying:false, isStopped: false, isPaused: true, currentStateHistoryValue: this.state.sliderTickArray[index], selectedHistoryStateFormatted: moment(this.state.sliderTickArray[index]).format('dddd, MMMM Do, YYYY h:mm:ss A')}); + }else if (command === 'play'){ + this.setState({currentStateHistoryValue: this.state.sliderTickArray[index], selectedHistoryStateFormatted: moment(this.state.sliderTickArray[index]).format('dddd, MMMM Do, YYYY h:mm:ss A')}); + } + this.props.history.push('/historyQuery/' + this.props.match.params.type + '/' + this.props.match.params.payloadEnc + '/' + this.state.sliderTickArray[index]); + } + + play = () =>{ + if(this.state.isPlaying){ + if(!this.state.enableBusyHistoryStateFeedback){ + var index = Math.min(this.state.sliderTickArray.length - 1, this.getStateIndex() + 1); + if(this.state.sliderTickArray.length > this.getStateIndex() + 1){ + this.navigateAnimation(this.getStateIndex() + 1, 'play'); + + }else{ + this.setState({isPlaying:false, isStopped: true, isPaused: false}); + } + } + }else{ + clearInterval(this.state.intervalId); + } + } + + animationControl = (controlType) => { + console.log("Control was hit: " + controlType); + switch(controlType){ + case 'play': + if(!this.state.isPlaying){ + this.setState({isPlaying:true, isStopped: false, isPaused: false, intervalId: setInterval(this.play, INVLIST.animationIntervalMs)}); + } + break; + case 'pause': + if(this.state.isPlaying){ + clearInterval(this.state.intervalId); + this.setState({isPlaying:false, isPaused: true}); + } + break; + case 'stop': + if(this.state.isPlaying || this.state.isPaused){ + clearInterval(this.state.intervalId); + this.setState({isPlaying:false, isStopped: true, isPaused: false}); + } + break; + case 'skipForwardStep': + var index = Math.min(this.state.sliderTickArray.length - 1, this.getStateIndex() + 1); + this.navigateAnimation(index); + break; + case 'skipBackwardStep': + var index = Math.max(0, this.getStateIndex() - 1); + this.navigateAnimation(index); + break; + case 'skipForwardLast': + this.navigateAnimation(this.state.sliderTickArray.length - 1); + break; + case 'skipBackwardEpoch': + this.navigateAnimation(0); + break; + default: + this.setState({isPlaying:false, isStopped: false, isPaused: false}); + break; + } + } + // START: These functions are for the animation controls + setHistoricStateValues = (currValue) =>{ + this.setState({currentStateHistoryValue: currValue, selectedHistoryStateFormatted: moment(currValue).format('dddd, MMMM Do, YYYY h:mm:ss A')}); + } + + navigateHistory = (time) =>{ + this.props.history.push('/historyQuery/' + this.props.match.params.type + '/' + this.props.match.params.payloadEnc + '/' + time); + } + + setStateValue = (key, value) =>{ + this.setState((state) => { key : value }); + } + getStateValue = (stateVar) => { + return this.state[stateVar]; + } + clear = (interval) =>{ + clearInterval(interval); + } + // END + + render() { + if(INVLIST.isHistoryEnabled){ + return ( + <div> + <header className='addPadding jumbotron my-4'> + <h1 className='display-2'>History Visualization</h1> + <p className='lead'> + On this page you have the ability to view a series of network elements in their current and historic state. + Zooming and panning are enabled in the graphs. You can use the scrollwheel to zoom and click+drag to pan. + If a node is single clicked you can drag the elements to reposition them. Double-clicking a node will + show the comparison between the two nodes in a pop-up modal. Animation controls are provided to seemlessly transition between states. + You can view the graph difference in a visual form by clicking View Visual Comparison. + </p> + </header> + <Grid fluid={true} className="addPadding"> + <Row className={this.state.currentErrMsg ? 'show' : 'hidden'} > + <div className='addPaddingTop alert alert-danger' role="alert"> + An error occurred while trying to get current information, please try again later. If this issue persists, please contact the system administrator. {this.state.currentErrMsg} + </div> + </Row> + <Row className={!this.state.currentErrMsg ? 'show' : 'hidden'} > + <Col className='col-lg-12'> + <div className='card d3-history-query-card'> + <div className='card-header history-query-card-header'> + <h2><strong>Current State</strong> of <em>{this.state.queryDisplay}</em></h2> + </div> + <div className={'card-header ' + (this.state.topologyHistoryState ? '' : 'hidden')}> + <button type='button' className='btn btn-outline-primary' onClick={this.viewTopologyCompVisual}>Toggle Visual Comparison</button> + </div> + <div className='history-query-card-content'> + {!this.state.topologyCurrentState || (this.state.topologyCurrentState.results && this.state.topologyCurrentState.results.length === 0) && (<div className='addPaddingTop'><p>No current state for this query</p></div>)} + <OutputVisualization identifier="currentState" width={this.svgWidth} height="600" overflow="scroll"/> + </div> + <div className='card-footer'> + <strong>Tip:</strong> <em>Click and drag network elements to reposition them, double-click nodes to see the node detail comparison. In addition: The graph supports pinch zoom or scrollwheel for zooming. Panning can be done by single-click and drag.</em> + </div> + </div> + </Col> + </Row> + <Row className={this.state.historicErrMsg ? 'show' : 'hidden'} > + <div className='addPaddingTop alert alert-danger' role="alert"> + An error occurred, while trying to get historic information, please try again later. If this issue persists, please contact the system administrator. {this.state.historicErrMsg} + </div> + </Row> + <Row className={!this.state.historicErrMsg ? 'show' : 'hidden'}> + <Col className='col-lg-12'> + <div className='card d3-history-query-card'> + <div className='card-header history-query-card-header'> + <h2><strong>Historical State</strong> of <em>{this.state.queryDisplay}</em> at {moment(parseInt(this.props.match.params.epochTime)).format('dddd, MMMM Do, YYYY h:mm:ss A')}</h2> + </div> + <div className='card-header'> + { (this.state.showSlider && this.state.showTicks) && (<Row className='show-grid'> + <Col md={3}> + <ReactBootstrapSlider + value={this.state.currentStateHistoryValue} + change={this.stateHistoryFormat} + slideStop={this.stateHistoryFormat} + step={ 1 } + ticks={ this.state.sliderTickArray } + ticks_snap_bounds={ 10000 } + orientation="horizontal" /> + </Col> + <Col md={8}> + <i className='icon-controls-skipbackstartover animationControlIcon' onClick={() => this.animationControl('skipBackwardEpoch')} role="img"></i> + <i className='icon-controls-rewind animationControlIcon' onClick={() => this.animationControl('skipBackwardStep')} role="img"></i> + <i className={'icon-controls-pointer ' + (this.state.isPlaying ? 'animationPlayingIcon' : 'animationControlIcon')} onClick={() => this.animationControl('play')} role="img"></i> + <i className={'icon-controls-pause ' + (this.state.isPaused ? 'animationPausedIcon' : 'animationControlIcon')} onClick={() => this.animationControl('pause')} role="img"></i> + <i className={'icon-controls-stop ' + (this.state.isStopped ? 'animationStoppedIcon' : 'animationControlIcon')} onClick={() => this.animationControl('stop')} role="img"></i> + <i className='icon-controls-fastforward animationControlIcon' onClick={() => this.animationControl('skipForwardStep')} role="img"></i> + <i className='icon-controls-skipforward animationControlIcon' onClick={() => this.animationControl('skipForwardLast')} role="img"></i> + </Col> + </Row> + )} + { (this.state.showSlider && !this.state.showTicks) && (<ReactBootstrapSlider + value={this.state.currentStateHistoryValue} + change={this.stateHistoryFormat} + slideStop={this.stateHistoryFormat} + step={this.state.stepEpochStateTime} + max={this.state.maxEpochStartTime} + min={this.state.minEpochStartTime} + orientation="horizontal" />)} + + <p>{this.state.selectedHistoryStateFormatted}</p> + <button type='button' className='btn btn-outline-primary' onClick={this.changeHistoryState}>Refresh</button> + </div> + <div className='history-query-card-content' > + {!this.state.topologyHistoryState || (this.state.topologyHistoryState.results && this.state.topologyHistoryState.results.length === 0) && (<div className="addPaddingTop"><p>No state during this time period</p></div>)} + <OutputVisualization identifier="historicState" width={this.svgWidth} height="600" overflow="scroll"/> + </div> + <div className={'card-footer ' + (this.state.topologyHistoryState ? '' : 'hidden')}> + <strong>Tip:</strong> <em>Click and drag network elements to reposition them, double-click nodes to see the node detail comparison. In addition: The graph supports pinch zoom or scrollwheel for zooming. Panning can be done by single-click and drag.</em> + </div> + </div> + + </Col> + </Row> + </Grid> + <div className='static-modal'> + <Modal show={this.state.showNodeModal} onHide={this.closeNodeModal} dialogClassName="modal-override"> + <Modal.Header> + <Modal.Title>Retrieve {this.state.nodeDisplay} History</Modal.Title> + </Modal.Header> + <Modal.Body> + <Grid fluid={true}> + <Row className='show-grid'> + <Col md={4}> + <HistoryCard node={this.state.selectedNodeHistoryState}/> + </Col> + <NodeDiffCard diff={this.state.nodeDiff}/> + <Col md={4}> + <HistoryCard node={this.state.selectedNodeCurrentState}/> + </Col> + </Row> + </Grid> + </Modal.Body> + <Modal.Footer> + <Button onClick={this.closeNodeModal}>Close</Button> + </Modal.Footer> + </Modal> + </div> + <div className='static-modal'> + <Modal show={this.state.showTopologyDiffModal} onHide={this.closeTopologyDiffModal}> + <Modal.Header> + <Modal.Title>Retrieve Topology History</Modal.Title> + </Modal.Header> + <Modal.Body> + <Grid fluid={true}> + <Row className='show-grid'> + <Col md={12}> + <TopologyDiffCard node={this.state.totalDiff}/> + </Col> + </Row> + </Grid> + </Modal.Body> + <Modal.Footer> + <Button onClick={this.closeTopologyDiffModal}>Close</Button> + </Modal.Footer> + </Modal> + </div> + </div> + ); + }else{ + return(<p>History Not Enabled for this instance, please check config.</p>) + } + } +} + +export default HistoryQuery; diff --git a/src/app/model/history/HistoryReducer.js b/src/app/model/history/HistoryReducer.js new file mode 100644 index 0000000..27456ec --- /dev/null +++ b/src/app/model/history/HistoryReducer.js @@ -0,0 +1,43 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import {HistoryConstants} from './HistoryConstants'; + +const initalState = { + inventoryData: [] +}; + +export default (state = initalState, action) => { + switch (action.type) { + case(HistoryConstants.UPDATE_INVENTORY_DATA): + console.log('[HistoryReducer] action.data:', action.data); + return { + ...state, + inventoryData: action.data + }; + default: + return { + ...state + }; + } + +}; + + diff --git a/src/app/model/history/components/AnimationControls.jsx b/src/app/model/history/components/AnimationControls.jsx new file mode 100644 index 0000000..3f1eb92 --- /dev/null +++ b/src/app/model/history/components/AnimationControls.jsx @@ -0,0 +1,126 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; +import Col from 'react-bootstrap/lib/Col'; + +class AnimationControls extends Component { + constructor(props){ + console.log(props); + super(props); + this.props = props; + } + + getIndex = () =>{ + return (this.props.get('sliderTickArray')).indexOf(this.props.get('currentStateHistoryValue')); + } + + navigateAnimation = (index, command) => { + if(!command){ + this.props.set('isPlaying',false); + this.props.set('isStopped', false); + this.props.set('isPaused', true); + this.props.setValueState((this.props.get('sliderTickArray'))[index]); + }else if (command === 'play'){ + this.props.setValueState((this.props.get('sliderTickArray'))[index]); + } + this.props.setNavigate((this.props.get('sliderTickArray'))[index]); + } + + play = () =>{ + if(this.props.get('isPlaying')){ + var index = Math.min((this.props.get('sliderTickArray')).length - 1, this.getIndex() + 1); + this.navigateAnimation(this.getIndex() + 1, 'play'); + if(index === (this.props.get('sliderTickArray')).length - 1){ + this.props.set('isPlaying', false); + this.props.set('isStopped', true); + this.props.set('isPaused', false); + } + }else{ + this.props.clear(this.props.get('intervalId')); + } + } + + animationControl = (controlType) => { + console.log("Control was hit: " + controlType); + switch(controlType){ + case 'play': + if(!this.props.get('isPlaying')){ + if(!this.props.get('intervalId')){ + this.props.set('intervalId', setInterval(this.play, 10000)); + } + this.props.set('isPlaying', true); + this.props.set('isStopped', false); + this.props.set('isPaused', false); + } + break; + case 'pause': + if(this.props.get('isPlaying')){ + this.props.clear(this.props.get('intervalId')); + this.props.set('isPlaying', false); + this.props.set('isPaused', true); + } + break; + case 'stop': + if(this.props.get('isPlaying') || this.props.get('isPaused')){ + this.props.clear(this.props.get('intervalId')); + this.props.set('isPlaying', false); + this.props.set('isStopped', true); + this.props.set('isPaused', false); + } + break; + case 'skipForwardStep': + var index = Math.min((this.props.get('sliderTickArray')).length - 1, this.getIndex() + 1); + this.navigateAnimation(index); + break; + case 'skipBackwardStep': + var index = Math.max(0, this.getIndex() - 1); + this.navigateAnimation(index); + break; + case 'skipForwardLast': + this.navigateAnimation((this.props.get('sliderTickArray')).length - 1); + break; + case 'skipBackwardEpoch': + this.navigateAnimation(0); + break; + default: + this.props.set('isPlaying', false); + this.props.set('isStopped', false); + this.props.set('isPaused', false); + break; + } + } + render(){ + return ( + <Col md={8}> + <i className='icon-controls-skipbackstartover animationControlIcon' onClick={() => this.animationControl('skipBackwardEpoch')} role="img"></i> + <i className='icon-controls-rewind animationControlIcon' onClick={() => this.animationControl('skipBackwardStep')} role="img"></i> + { !this.props.playControlsDisabled && (<span><i className={'icon-controls-pointer ' + (this.props.get('isPlaying') ? 'animationPlayingIcon' : 'animationControlIcon')} onClick={() => this.animationControl('play')} role="img"></i> + <i className={'icon-controls-pause ' + (this.props.get('isPaused') ? 'animationPausedIcon' : 'animationControlIcon')} onClick={() => this.animationControl('pause')} role="img"></i> + <i className={'icon-controls-stop ' + (this.props.get('isStopped') ? 'animationStoppedIcon' : 'animationControlIcon')} onClick={() => this.animationControl('stop')} role="img"></i></span>)} + <i className='icon-controls-fastforward animationControlIcon' onClick={() => this.animationControl('skipForwardStep')} role="img"></i> + <i className='icon-controls-skipforward animationControlIcon' onClick={() => this.animationControl('skipForwardLast')} role="img"></i> + </Col> + ); + } +}; + +export default AnimationControls; diff --git a/src/app/model/history/components/HistoryCard.jsx b/src/app/model/history/components/HistoryCard.jsx new file mode 100644 index 0000000..39d7f57 --- /dev/null +++ b/src/app/model/history/components/HistoryCard.jsx @@ -0,0 +1,101 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React from 'react'; +import moment from "moment"; +import Col from 'react-bootstrap/lib/Col'; +import Panel from 'react-bootstrap/lib/Panel'; + +const historyCard = (props) => { + if(props && props.node && props.node.properties){ + const properties = (props.node.properties).map((prop, idx) => { + return ( + <Panel> + <Panel.Heading className="custom-accordion"> + <Panel.Title toggle> <strong>{prop.key}</strong> : {'' + prop.value}</Panel.Title> + </Panel.Heading> + <Panel.Collapse> + <Panel.Body className='cardwrap'> + <p><strong>Last Updated By:</strong> {prop.sot}</p> + <p><strong>Last Updated (time):</strong> {moment(prop.timestamp).format('dddd, MMMM Do, YYYY h:mm:ss A')}</p> + <p><strong>Transaction Id:</strong> {(prop['tx-id']) ? prop['tx-id'] : 'N/A'}</p> + </Panel.Body> + </Panel.Collapse> + </Panel> + ); + }); + + //TODO handle no relationships and no attributes + + const relationships = (props.node['related-to']).map((prop, idx) => { + return ( + <p key={idx}><strong>{prop['node-type']}:</strong> {prop.url} {prop['relationship-label']} (added by {prop.sot} on {moment(prop.timestamp).format('dddd, MMMM Do, YYYY h:mm:ss A')})</p> + ); + }); + + return ( + <Col className={""+(props.split ? 'col-lg-4' : 'col-lg-12')}> + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>{props.node.primaryHeader}</h4> + </div> + <div className='card-header'> + {props.node.secondaryHeader} + </div> + <div className='card-content model-card-content'> + {properties} + </div> + <div className='card-footer'> + <Panel> + <Panel.Heading> + <Panel.Toggle> + <button type='button' className='btn btn-outline-primary'> + Relationships + </button> + </Panel.Toggle> + </Panel.Heading> + <Panel.Collapse> + <Panel.Body className='cardwrap'> + {relationships} + </Panel.Body> + </Panel.Collapse> + </Panel> + </div> + </div> + </Col> + ); + }else{ + return( + <Col className={""+(props.split ? 'col-lg-4' : 'col-lg-12')}> + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>No State Found</h4> + </div> + <div className='card-content model-card-content'> + No State was found at the provided timestamp. Please try another timestamp. + </div> + </div> + </Col> + ); + } +}; + +export default historyCard; + diff --git a/src/app/model/history/components/HistoryEntry.jsx b/src/app/model/history/components/HistoryEntry.jsx new file mode 100644 index 0000000..7b1a297 --- /dev/null +++ b/src/app/model/history/components/HistoryEntry.jsx @@ -0,0 +1,55 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React, { Component } from 'react'; +import { Link } from 'react-router-dom'; +import Col from 'react-bootstrap/lib/Col'; + +class HistoryEntry extends Component { + constructor(props){ + console.log(props); + super(props); + this.props = props; + } + + render(){ + this.triggerState = () => { + this.props.triggerState(this.props.entryNodeId, this.props.entryEpoch); + } + return ( + <a className={"list-group-item list-group-item-action flex-column align-items-start "+ (this.props.entryAction === 'Deleted' ? 'group-item-danger' : '')} + onClick={this.triggerState}> + <div className='d-flex w-100 justify-content-between'> + <h3 className="mb-1">{this.props.entryHeader}</h3> + <h3 className="mb-1">{this.props.entryBody}</h3> + <small>{this.props.entryDate}</small> + </div> + <div> + <small>Modified by {this.props.entrySOT}</small> + </div> + <div> + <small>Transaction Id : {this.props.entryTransId}</small> + </div> + </a> + ); + } +}; + +export default HistoryEntry; diff --git a/src/app/model/history/components/HistoryGallery.jsx b/src/app/model/history/components/HistoryGallery.jsx new file mode 100644 index 0000000..0e4c011 --- /dev/null +++ b/src/app/model/history/components/HistoryGallery.jsx @@ -0,0 +1,65 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React from 'react'; +import Grid from 'react-bootstrap/lib/Grid'; +import Row from 'react-bootstrap/lib/Row'; +import HistoryEntry from './HistoryEntry.jsx'; +/** + * This function will take all of the node objects and turn them into + * a ui grid of HistoryCard components. This function is essentially a container + * for the HistoryCards + * @param props + * @returns {*} + */ +const HistoryGallery = (props) => { + + let entries = null; + if (props.entries && props.entries.length > 0) { + entries = props.entries.map((entry, idx) => { + return ( + <HistoryEntry + key={idx} + triggerState={props.triggerState} + entryKey={entry.key} + entryType={entry.type} + entryValue={entry.value} + entryBody= {entry.body} + entryHeader= {entry.header} + entrySOT={entry.sot} + entryAction= {entry.action} + entryEpoch= {entry.timeRank} + entryNodeId= {props.nodeId} + entryDate={entry.displayTimestamp} + entryTransId = {entry['tx-id']}/> + ); + }); + }else{ + return (<p>No History</p>); + } + + return ( + <div className="list-group"> + {entries} + </div> + ); +}; + +export default HistoryGallery; diff --git a/src/app/model/history/components/NodeDiffCard.jsx b/src/app/model/history/components/NodeDiffCard.jsx new file mode 100644 index 0000000..2bc2a82 --- /dev/null +++ b/src/app/model/history/components/NodeDiffCard.jsx @@ -0,0 +1,106 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React from 'react'; +import moment from "moment"; +import Col from 'react-bootstrap/lib/Col'; +import Panel from 'react-bootstrap/lib/Panel'; + +const nodeDiffCard = (props) => { + let showNoPropsMessage = true; + let showNoRelationshipsMessage = true; + if(props && props.diff && props.diff.properties){ + const properties = Object.entries(props.diff.properties).map((property, idx) => { + let prop = property[1]; + if(prop && prop.value && prop.value.type !== 'unchanged'){ + showNoPropsMessage = false; + return ( + <div> + <p><strong>Attribute:</strong> {prop.key.data} ({prop.value.type})</p> + </div> + ); + }else if (prop && prop.type){ + showNoPropsMessage = false; + return ( + <div> + <p><strong>Attribute:</strong> {prop.data.key} ({prop.type})</p> + </div> + ); + } + }); + + //TODO handle no relationships and no attributes + + const relationships = Object.entries(props.diff['related-to']).map((property, idx) => { + let prop = property[1]; + if(prop && prop.type && prop.type.type){ + return (''); + }else if(prop && prop.type && !prop.data){ + showNoRelationshipsMessage = false; + return ( + <div> + <p><strong>Relationship</strong>: {prop['relationship-label'].data} {prop['node-type'].data} {prop.url.data} ({prop.type})</p> + </div> + ); + }else if (prop && prop.type && prop.data){ + showNoRelationshipsMessage = false; + return ( + <div> + <p><strong>Relationship</strong>: {prop.data['relationship-label']} {prop.data['node-type']} {prop.data.url} ({prop.type})</p> + </div> + ); + } + }); + + return ( + <Col className='col-lg-4'> + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>Changes from Historic to Current State</h4> + </div> + <div className='card-header'></div> + <div className='card-content model-card-content'> + {properties} + <div className={showNoPropsMessage ? 'show' : 'hidden'}><p><strong>No Attribute differences, current.</strong></p></div> + {relationships} + <div className={showNoRelationshipsMessage ? 'show' : 'hidden'}><p><strong>No Relationship differences, current.</strong></p></div> + </div> + </div> + </Col> + ); + }else{ + return( + <Col className='col-lg-4'> + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>Unable to pull diff</h4> + </div> + <div className='card-header'></div> + <div className='card-content model-card-content'> + Diff unable to be calculated currently, choose a different timeframe. + </div> + </div> + </Col> + ); + } +}; + +export default nodeDiffCard; + diff --git a/src/app/model/history/components/TopologyDiffCard.jsx b/src/app/model/history/components/TopologyDiffCard.jsx new file mode 100644 index 0000000..9d49b17 --- /dev/null +++ b/src/app/model/history/components/TopologyDiffCard.jsx @@ -0,0 +1,151 @@ +/* + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2021 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +import React from 'react'; +import moment from "moment"; +import Col from 'react-bootstrap/lib/Col'; +import Panel from 'react-bootstrap/lib/Panel'; + +const topologyDiffCard = (props) => { + let showNoNodesMessage = true; + if(props && props.node){ + const properties = Object.entries(props.node).map((prop, idx) => { + if (prop){ + showNoNodesMessage = false; + let showNoRelationshipsMessage = true; + let showNoAttributesMessage = true; + let propWorkaround = prop; + if(prop.data){ + propWorkaround = prop.data; + } + let tempProp = propWorkaround[1]; + let attributeProperties = ''; + let relationships = ''; + if(tempProp.properties){ + attributeProperties = Object.entries(tempProp.properties).map((property, idx) => { + let attrProp = property[1]; + if(attrProp && attrProp.value && attrProp.value.type !== 'unchanged'){ + showNoAttributesMessage = false; + return ( + <div> + <p><strong>Attribute:</strong> {attrProp.key.data} ({attrProp.value.type})</p> + </div> + ); + }else if (attrProp && attrProp.type){ + showNoAttributesMessage = false; + return ( + <div> + <p><strong>Attribute:</strong> {attrProp.data.key} ({attrProp.type})</p> + </div> + ); + } + }); + } + if(tempProp['related-to'] || tempProp.data['related-to']){ + let rel = null; + let topLevelType = null; + if(tempProp['related-to']){ + rel = tempProp['related-to']; + }else if (tempProp.data['related-to']) { + rel = tempProp.data['related-to']; + topLevelType = tempProp.type; + } + relationships = Object.entries(rel).map((property, idx) => { + let relationProp = property[1]; + if(relationProp && relationProp.type && relationProp.type.type && relationProp.type.type !== "unchanged"){ + return (''); + }else if(relationProp && relationProp.type && !relationProp.type.type && relationProp.url && relationProp.url.data){ + showNoRelationshipsMessage = false; + return ( + <div> + <p><strong>Relationship</strong>: {relationProp['relationship-label'].data} {relationProp['node-type'].data} {relationProp.url.data} ({relationProp.type})</p> + </div> + ); + }else if (relationProp && relationProp.type && relationProp.data){ + showNoRelationshipsMessage = false; + return ( + <div> + <p><strong>Relationship</strong>: {relationProp.data['relationship-label']} {relationProp.data['node-type']} {relationProp.data.url} ({relationProp.type})</p> + </div> + ); + }else if (topLevelType){ + showNoRelationshipsMessage = false; + return ( + <div> + <p><strong>Relationship</strong>: {relationProp['relationship-label']} {relationProp['node-type']} {relationProp.url} ({topLevelType})</p> + </div> + ); + } + }); + } + return ( + <Panel> + <Panel.Heading className="custom-accordion"> + <Panel.Title toggle><strong>Node:</strong> {prop[0]} <p className={tempProp.type ? 'show' : 'hidden'}>({tempProp.type})</p></Panel.Title> + </Panel.Heading> + <Panel.Collapse> + <Panel.Body className='cardwrap'> + {attributeProperties} + <div className={showNoAttributesMessage ? 'show' : 'hidden'}><p><strong>No Attribute differences, current.</strong></p></div> + {relationships} + <div className={showNoRelationshipsMessage ? 'show' : 'hidden'}><p><strong>No Relationship differences, current.</strong></p></div> + </Panel.Body> + </Panel.Collapse> + </Panel> + ); + }else{ + <div> + <p><strong>Node changes in the topology states</strong></p> + </div> + } + }); + + return ( + <Col className='col-lg-12'> + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>Changes from Historic to Current State</h4> + </div> + <div className='card-header'></div> + <div className='card-content model-card-content'> + {properties} + </div> + </div> + </Col> + ); + }else{ + return( + <Col className='col-lg-12'> + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>Unable to pull diff</h4> + </div> + <div className='card-header'></div> + <div className='card-content model-card-content'> + Diff unable to be calculated currently, choose a different timeframe. + </div> + </div> + </Col> + ); + } +}; + +export default topologyDiffCard; + |