X-Git-Url: https://gerrit.onap.org/r/gitweb?p=aai%2Fsparky-fe.git;a=blobdiff_plain;f=src%2Fapp%2Fmodel%2Fhistory%2FHistory.jsx;fp=src%2Fapp%2Fmodel%2Fhistory%2FHistory.jsx;h=61cb1af3ad3b21cbd72bade614a54a560d0165d0;hp=0000000000000000000000000000000000000000;hb=5ee7367a101143715c2869d72ea4a6fbf55f5af6;hpb=ddc05d4ea0254b427fea6ec80e2b03950eeca4ce 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 ( +
+
+

Network Element History

+

+ 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.' : ''} +

+
+ + + +
+
+

{this.pageTitle}

+
+ +
+ 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} +
+
+ { (this.state.isState && this.state.showSlider && this.state.showTicks) && ( +
+ + + +

{this.state.selectedHistoryStateFormatted}

+ + + +
+
)} + { (this.state.isState && this.state.showSlider && !this.state.showTicks) && ( +
+ + + +

{this.state.selectedHistoryStateFormatted}

+ + +
+
)} +
+
+ + + + {this.state.showState && !this.historyErrMsg && ()} + +
+ 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} +
+
+
+ + {this.state.showState && ()} + + + { !this.currentErrMsg && ()} + +
+ 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} +
+
+
+
+ {this.resultsMessage} + +
+
+
+
+ Tip: Click any attribute to view more details +
+
+ +
+ +
+ 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} +
+ {this.state.showLifeCycle && !this.state.lifecycleErrMsg && ( + +
+
+

All Updates on {this.state.nodeDisplay}

+

All Updates on {this.state.nodeDisplay} Since {this.state.selectedHistoryStateFormatted}

+
+
+

Tip: Click any update to view the state of the node at that point in time

+
+
Total Results: {this.state.totalResults}
+
+
+
+
+ +
+
+
+
+
+ +
+ +
+
+
+
+ )} +
+
+
+ ); + }else{ + return(

History Not Enabled for this instance, please check config.

) + } + } +} + +export default History;