diff options
Diffstat (limited to 'src/app/model')
26 files changed, 4761 insertions, 0 deletions
diff --git a/src/app/model/CustomQueryMultiNode.json b/src/app/model/CustomQueryMultiNode.json new file mode 100644 index 0000000..598192c --- /dev/null +++ b/src/app/model/CustomQueryMultiNode.json @@ -0,0 +1,83 @@ +{ + "results":[ + { + "id":"2519120", + "node-type":"generic-vnf", + "url":"/aai/v12/network/generic-vnfs/generic-vnf/d5b749b2-3b0f-46c4-bf0a-fb55e76866fb", + "properties":{ + "vnf-id":"d5b749b2-3b0f-46c4-bf0a-fb55e76866fb", + "vnf-name":"USSTFMBMIIL0101UVHN01", + "vnf-type":"HN", + "prov-status":"prov", + "is-closed-loop-disabled":false, + "resource-version":"1492705097080" + }, + "related-to":[ + { + "id":"28274856", + "relationship-label":"org.onap.relationships.inventory.ComposedOf", + "node-type":"service-instance", + "url":"/aai/v12/business/customers/customer/StateFarmSAARTID/service-subscriptions/service-subscription/vHNFaaS/service-instances/service-instance/3f433278-94d6-4203-8bf2-8bd01b82a848" + }, + { + "id":"13705400", + "relationship-label":"org.onap.relationships.inventory.BelongsTo", + "node-type":"l-interface", + "url":"/aai/v12/network/generic-vnfs/generic-vnf/d5b749b2-3b0f-46c4-bf0a-fb55e76866fb/l-interfaces/l-interface/ge4" + }, + { + "id":"26591400", + "relationship-label":"org.onap.relationships.inventory.BelongsTo", + "node-type":"l-interface", + "url":"/aai/v12/network/generic-vnfs/generic-vnf/d5b749b2-3b0f-46c4-bf0a-fb55e76866fb/l-interfaces/l-interface/ge5" + }, + { + "id":"5664936", + "relationship-label":"tosca.relationships.HostedOn", + "node-type":"vserver", + "url":"/aai/v12/cloud-infrastructure/cloud-regions/cloud-region/att-aic/AAIAIC25/tenants/tenant/USSTFMBMIIL0101UJZZ01%3A%3AuCPE-VMS/vservers/vserver/5fbeb08c-bc6f-4bd1-9129-36852c014f72" + } + ] + }, + { + "id":"23769160", + "node-type":"pserver", + "url":"/aai/v12/cloud-infrastructure/pservers/pserver/USSTFMBMIIL0101UJZZ01", + "properties":{ + "hostname":"USSTFMBMIIL0101UJZZ01", + "ptnii-equip-name":"USSTFMBMIIL0101UJZZ01", + "equip-type":"JUNIPER-UCPE", + "equip-vendor":"JUNIPER", + "equip-model":"ATT-U210", + "serial-number":"JUNIPER-UCPE", + "resource-version":"1492639826832" + }, + "related-to":[ + { + "id":"5664936", + "relationship-label":"tosca.relationships.HostedOn", + "node-type":"vserver", + "url":"/aai/v12/cloud-infrastructure/cloud-regions/cloud-region/att-aic/AAIAIC25/tenants/tenant/USSTFMBMIIL0101UJZZ01%3A%3AuCPE-VMS/vservers/vserver/5fbeb08c-bc6f-4bd1-9129-36852c014f72" + }, + { + "id":"22016072", + "relationship-label":"tosca.relationships.HostedOn", + "node-type":"vserver", + "url":"/aai/v12/cloud-infrastructure/cloud-regions/cloud-region/att-aic/AAIAIC25/tenants/tenant/USSTFMBMIIL0101UJZZ01%3A%3AuCPE-VMS/vservers/vserver/8ed14c24-f002-4a2c-8216-0303e79488d0" + }, + { + "id":"2093224", + "relationship-label":"tosca.relationships.network.BindsTo", + "node-type":"p-interface", + "url":"/aai/v12/cloud-infrastructure/pservers/pserver/USSTFMBMIIL0101UJZZ01/p-interfaces/p-interface/ge-0%2F0%2F9" + }, + { + "id":"19787800", + "relationship-label":"tosca.relationships.network.BindsTo", + "node-type":"p-interface", + "url":"/aai/v12/cloud-infrastructure/pservers/pserver/USSTFMBMIIL0101UJZZ01/p-interfaces/p-interface/ge-0%2F0%2F8" + } + ] + } + ] +} diff --git a/src/app/model/ModelDisplay.jsx b/src/app/model/ModelDisplay.jsx new file mode 100644 index 0000000..59dc05a --- /dev/null +++ b/src/app/model/ModelDisplay.jsx @@ -0,0 +1,35 @@ +/* + * ============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 ModelGallery from 'app/modelSearch/components/ModelGallery.jsx'; +import ModelBreadcrumb from 'app/modelSearch/components/ModelBreadcrumb.jsx'; +//import modelCard from 'app/modelSearch/components/ModelCard.jsx'; +//import modelRelationships from 'app/modelSearch/components/ModelRelationships.jsx'; + + + + +//export const ModelGallery = modelGallery; +//export const ModelCard = modelCard; +//export const ModelRelationships = modelRelationships; + +export default ModelGallery; +//export default modelCard; +//export default modelRelationships; 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; + diff --git a/src/app/model/modelSearch/Model.jsx b/src/app/model/modelSearch/Model.jsx new file mode 100644 index 0000000..9a49be9 --- /dev/null +++ b/src/app/model/modelSearch/Model.jsx @@ -0,0 +1,846 @@ +/* + * ============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 Filter from 'generic-components/filter/Filter.jsx'; +import OutputToggle from 'generic-components/OutputToggle.jsx'; +import {ExportExcel} from 'utils/ExportExcel.js'; +import commonApi from 'utils/CommonAPIService.js'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; +import Spinner from 'utils/SpinnerContainer.jsx'; +import ModelGallery from './components/ModelGallery.jsx'; +import DatePicker from 'react-datepicker'; +import moment from "moment"; +import ModelBreadcrumb from './components/ModelBreadcrumb.jsx'; +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 { ModelConstants } from './ModelConstants'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Tooltip from 'react-bootstrap/lib/Tooltip'; +import DownloadRangeModel from 'generic-components/DownloadRangeModel.jsx'; +import BootstrapSwitchButton from 'bootstrap-switch-button-react'; + +let INVLIST = GlobalExtConstants.INVLIST; +let DOWNLOAD_ALL = GlobalExtConstants.DOWNLOAD_ALL; +let generateExcels = ExportExcel.generateExcels; +let buildAttrList = ExportExcel.buildAttrList; +let DOWNLOAD_TOOLTIP = GlobalExtConstants.DOWNLOAD_TOOLTIP; +let ENVIRONMENT = GlobalExtConstants.ENVIRONMENT; +let APERTURE_SERVICE = JSON.parse(sessionStorage.getItem(ENVIRONMENT + 'APERTURE_SERVICE')); +let filterTypeList = GlobalExtConstants.FILTER_TYPES; +let TABULAR_FILTER_TYPE = GlobalExtConstants.TABULAR_FILTER_TYPE; +let URI_DELIMITCHAR = GlobalExtConstants.URI_DELIMITCHAR; +/*const mapStateToProps = ({extensibility: {extModelReducer} }) => { + let {inventoryData} = extModelReducer; + return { + inventoryData + }; +};*/ + + const mapStateToProps = (state) => { + return { + currState: state.modelReducer + }; +}; + +const mapActionToProps = (dispatch) => { + return { + onInventoryDataReceived: (data) => { + dispatch({ type: 'UPDATE_INVENTORY_DATA', data: data }); + } + }; +}; + +/** + * 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. + */ + +export class model extends Component { + + elements = []; + filterQuery = ''; + pageTitle = ''; + nodeType = ''; + historyStackString = ''; + typeOfCall = true; + nodeResults = ''; + downloadTooltip = DOWNLOAD_TOOLTIP; + downloadAllTooltip = 'Downloads First ' + DOWNLOAD_ALL + ' Results'; + downloadRangeTooltip= 'Downloads Results By Custom Range Selection'; + initialFilterSelectedList = []; + initialFilterMessage = []; + initialNonDisplay = true; + constructor(props) { + console.log(props); + APERTURE_SERVICE = JSON.parse(sessionStorage.getItem(ENVIRONMENT + 'APERTURE_SERVICE')); + super(props); + var filterSelectedList = []; + var seletedFilter = []; + var filterMessage = []; + var nonDisplay = true; + var enableToggle= JSON.parse(sessionStorage.getItem(ENVIRONMENT + 'ENABLE_ANALYSIS')); + TABULAR_FILTER_TYPE=(APERTURE_SERVICE && enableToggle)?'CONTAINS':'='; + if (this.props.match.params.type) { + seletedFilter = this.props.match.params.type.split(';'); + seletedFilter.map((param) => { + console.log('param', param); + console.log('param.indexOf(=)', param.indexOf('=')); + if (param.indexOf(URI_DELIMITCHAR+'='+URI_DELIMITCHAR) !== -1) { + let id = param.split(URI_DELIMITCHAR+'='+URI_DELIMITCHAR)[0]; + let value = param.split(URI_DELIMITCHAR+'='+URI_DELIMITCHAR)[1]; + filterSelectedList.push({ 'id': id, 'value': value, 'type':'='}); + filterMessage.push(id+'='+value); + enableToggle=false; + }else{ + for(var x in filterTypeList){ + if (param.indexOf(URI_DELIMITCHAR+filterTypeList[x]+URI_DELIMITCHAR) !== -1) { + let paramArray=param.split(URI_DELIMITCHAR+filterTypeList[x]+URI_DELIMITCHAR); + let id = paramArray[0]; + let value = paramArray[1]; + filterSelectedList.push({ 'id': id, 'value': value, 'type': filterTypeList[x]}); + filterMessage.push(id+filterTypeList[x]+value); + enableToggle=true; + } + } + } + }); + } + if (this.props.match.params.nodeId) { + nonDisplay = false; + } + if(this.props.match.params.page==='false'){ + enableToggle=false; + } + if(!this.props.location.historyStackString){ + this.props.location.historyStackString = this.props.location.pathname + ',,Origin||'; + }else{ + this.historyStackString = this.props.location.historyStackString; + } + this.state = { + activePage: 1, + totalResults: 0, + enableBusyFeedback: true, + data: [], + filterList: [], + nodes: [], + filterSelected: (this.props.match.params.type) ? this.props.match.params.type.split(';')[0] : '', + filterDisplay: 'Select Filter', + filterTypeDisplay: 'Filter Type', + filterMessage: filterMessage, + filterSelectedList: filterSelectedList, + isRunEnable: false, + isFilterEnable: nonDisplay, + isPaginationEnable: nonDisplay, + showHistoryModal: false, + nodeDisplay: 'test', + startDate: moment(), + historyType: 'nodeState', + enableCalendar: true, + focusedNodeUri: 0, + viewName: localStorage.getItem(GlobalExtConstants.ENVIRONMENT + '_' + sessionStorage.getItem(GlobalExtConstants.ENVIRONMENT + 'userId') + '_viewPreference') || 'CardLayout', + errorResults: false, + errorMessage: '', + showResults: false, + resetColumnFilters: true, + defaultViewName: localStorage.getItem(GlobalExtConstants.ENVIRONMENT + '_' + sessionStorage.getItem(GlobalExtConstants.ENVIRONMENT + 'userId') + '_viewPreference') || 'CardLayout', + isPageNumberChange: false, + totalPages: 0, + pageRange: 1, + showDownloadResultsModal: false, + errorDownloadResults:false, + downloadErrorMsg: '', + enableModelBusyFeedback:false, + downloadCount:DOWNLOAD_ALL, + enableRealTime: enableToggle + }; + this.baseState=this.state; + } + resultsMessage = ''; + componentDidMount = () => { + console.log('[Model.jsx] componentDidMount props available are', JSON.stringify(this.props)); + if (this.state.isFilterEnable) { + this.populateFilteringOptions(); + } + this.initialFilterSelectedList = this.state.filterSelectedList; + this.initialFilterMessage = this.state.filterMessage; + this.initialNonDisplay = this.state.isFilterEnable; + this.beforefetchInventoryData(); + }; + handleDateChange = (newDate) =>{ + this.setState({ startDate: moment(+newDate) }); + console.log('[Model.jsx] handleDateChange date is ', this.state.startDate); + console.log('[Model.jsx] handleDateChange date is in millis ', +this.state.startDate); + } + openHistory = (nodeDisplay, nodeUri, nodeType) => { // open modal + console.log('history >> showModal'); + let historyNodeUri = (nodeUri)?nodeUri.replace('/aperture/','/'):nodeUri;//replace always first occurence + if(nodeDisplay){ + this.setState({ + nodeDisplay: nodeDisplay, + focusedNodeUri: historyNodeUri, + focusedNodeType: nodeType, + showHistoryModal:true + }); + }else{ + this.setState({ + showHistoryModal:true + }); + } + } + closeHistory = () => { + this.setState({ + showHistoryModal: false + }); + } + submitHistory = () => { + //do some logic in history + console.log("submitting history"); + let epochStartTime = (this.state.startDate).unix(); + this.props.history.push('/history/' + this.state.historyType+'/' + this.nodeType + '/' + btoa(this.state.focusedNodeUri) + '/' + epochStartTime * 1000); + } + setHistoryType(event) { + console.log(event.target.value); + let enableCalendar = false; + if(event.target.value === 'nodeLifeCycle'){ + enableCalendar = false; + }else{ + enableCalendar = true; + } + this.setState({ + historyType: event.target.value, + enableCalendar: enableCalendar + }); + console.log(this.state.enableCalendar); + } + setViewName(event) { + console.log(event.currentTarget.value); + this.setState({ + viewName: event.currentTarget.value + }); + } + setDefaultViewName = (event) =>{ + let ENVIRONMENT = GlobalExtConstants.ENVIRONMENT; + let layout = event.target.value; + + if(sessionStorage.getItem(ENVIRONMENT + 'userId')) { + if (event.target.checked) { + localStorage.setItem(ENVIRONMENT + '_' + sessionStorage.getItem(ENVIRONMENT + 'userId') + '_viewPreference', layout); + } else { + localStorage.removeItem(ENVIRONMENT + '_' + sessionStorage.getItem(ENVIRONMENT + 'userId') + '_viewPreference'); + } + } + + this.setState({ + defaultViewName: event.target.value + }); + this.baseState.viewName=event.target.value; + this.baseState.defaultViewName=event.target.value; + } + componentWillUnmount = () => { + console.log('[Model.jsx] componentWillUnMount'); + this.props.onInventoryDataReceived([]); + } + beforefetchInventoryData = (param) => { + this.typeOfCall = true; + if (param) { + this.props.onInventoryDataReceived([]); + this.formFilterQuery(param.filterMessage); + this.setState( + { enableBusyFeedback: true, activePage: 1, totalResults: 0, totalPages: 0,filterMessage: param.filterMessage, filterSelectedList: param.filterSelectedList}, + function () { this.fetchInventoryData(param); }.bind(this) + ); + } else { + this.formFilterQuery(this.state.filterMessage); + this.fetchInventoryData(); + } + }; + + formFilterQuery = (filterMessage) => { + let filterQuery = filterMessage.join('&'); + this.filterQuery = (filterMessage.length > 0) ? '&' + filterQuery : ''; + }; + + fetchInventoryData = (param) => { + console.log('fetchInventoryData', param); + this.resultsMessage = ''; + const inventory = INVLIST.INVENTORYLIST; + let url = ''; + console.log('[Model.jsx] fetchInventoryData nodeId= ', this.props.match.params.nodeId); + if (this.props.match.params.type !== undefined && this.props.match.params.type !== null) { + this.nodeType = this.props.match.params.type; + } + console.log('[Model.jsx] nodeType: ' + this.nodeType); + let pageName = this.nodeType.split(';')[0].replace(/\s/g, '').toUpperCase(); + console.log('[Model.jsx] pageName: ' + pageName); + if (inventory[pageName] && inventory[pageName].display) { + this.pageTitle = inventory[pageName].display; + } else { + this.pageTitle = pageName; + } + var nonRelationshipState = false; + if (this.props.match.params.nodeId) { + this.setUri(this.props.location.uri); + this.setBreadcrumb(this.props.location.uri); + url = sessionStorage.getItem(ENVIRONMENT + 'URI'); + if(this.state.enableRealTime){ + let versionPattern="^"+INVLIST.VERSION+"\\/"; + var versionRegularExp = new RegExp(versionPattern, 'g'); + let matchVersion = url.match(versionRegularExp,'g'); + if(!matchVersion){ + url= INVLIST.VERSION+'/'+url; + } + } + } else { + url = inventory[pageName].apiPath; + this.setBreadcrumb(url); + nonRelationshipState=true; + } + console.log('[Model.jsx] active page', this.state.activePage); + console.log('this.state.filterSelectedList', this.state.filterSelectedList); + console.log('filterQuery', this.filterQuery); + //Aperture with Diff Filter types Operater + this.nodeResults = ''; + var method = 'GET'; + var payload = {}; + const settings = { + 'NODESERVER': INVLIST.NODESERVER, + 'PROXY': INVLIST.PROXY, + 'PREFIX': INVLIST.PREFIX, + 'VERSION': INVLIST.VERSION, + 'USESTUBS': INVLIST.useStubs, + 'APERTURE': INVLIST.APERTURE, + 'APERTURE_SERVICENAME':INVLIST.APERTURE_SERVICENAME + }; + if(this.state.enableRealTime){ + settings['ISAPERTURE'] = (nonRelationshipState)? true : false; + }else{ + if(!(this.state.enableRealTime && nonRelationshipState)){ + url = (url)?url.replace(INVLIST.VERSION+'/',''):url; + } + } + if(this.state.enableRealTime && nonRelationshipState){ + var filterList=this.state.filterSelectedList; + var filters = []; + for(var k in filterList){ + if(filterList.hasOwnProperty(k)){ + let filter ={} + filter['filter']= filterList[k].type; + filter['key'] = filterList[k].id; + filter['value'] = filterList[k].value; + filters.push(filter); + } + } + method= 'POST'; + payload['node-type'] = url.split('/')[1]; + payload['filter-version'] = 'v1'; + payload['filters'] = filters; + }else{ + payload = null; + } + + var path = '?format=simple&resultIndex=' + this.state.activePage + '&resultSize='; + if(this.typeOfCall){ + path = path + ModelConstants.RESULTS_PER_PAGE; + url=(this.state.enableRealTime && nonRelationshipState)? path: url + path + this.filterQuery; + this.commonApiServiceCall(settings,url,param,method,payload); + }else{ + let pagerange=this.state.pageRange.toString(); + pagerange=pagerange.split('-'); + if(pagerange.length > 1){ + path = '?format=simple&resultIndex=' + parseInt(pagerange[0]) + '&resultSize='+ ModelConstants.RESULTS_PER_PAGE + '&resultRangeEnd=' + parseInt(pagerange[1]); + }else{ + path = '?format=simple&resultIndex=' + 1 + '&resultSize=' + parseInt(pagerange); + } + url=(this.state.enableRealTime && nonRelationshipState)? path: url + path + this.filterQuery; + this.commonApiServiceCallForAllData(settings,url,method,payload); + } + }; + commonApiServiceCall = (settings,url,param,method,payload) =>{ + + commonApi(settings, url, method, payload, 'modelDefault') + .then(res => { + // Call dispatcher to update state + console.log('once before service call ......',this.state); + this.resultsMessage = ''; + var totalResults = parseInt(res.headers['total-results']); + let downloadCount = DOWNLOAD_ALL; + if(totalResults > DOWNLOAD_ALL){ + this.downloadAllTooltip = DOWNLOAD_ALL + ' results out of '+ totalResults +' will be downloaded, please filter results further to obtain full report'; + }else{ + this.downloadAllTooltip = (totalResults === 1) ? 'Downloads ' + totalResults + ' Results' : 'Downloads all ' + totalResults + ' Results' + downloadCount= totalResults; + } + this.setState( + { + nodes : res.data.results, + totalResults : res.headers['total-results'], + totalPages: res.headers['total-results'], + enableBusyFeedback:false, + showResults: true, + errorResults: false, + downloadCount: downloadCount, + filterSelectedList:(param)?param.filterSelectedList:this.state.filterSelectedList + },function(){this.props.onInventoryDataReceived(res.data.results);}); + + console.log('After service call ......',this.state); + console.log('[Model.jsx] results : ', res); + }, error=>{ + this.triggerError(error); + }).catch(error => { + this.triggerError(error); + }); + }; + commonApiServiceCallForAllData = (settings,url,method,payload) => { + + commonApi(settings, url,method, payload, 'modelDefault') + .then(res => { + // Call dispatcher to update state + console.log('once before service call ......',this.state); + this.resultsMessage = ''; + this.nodeResults = res.data.results; + let totalResults = parseInt(res.headers['total-results']); + let totalPages = parseInt(res.headers['total-pages']); + this.setState({totalPages:totalPages,errorDownloadResults:false,downloadErrorMsg:''},() => {this.getAllExcels()}); + console.log('[Model.jsx] results : ', res); + }).catch(error => { + console.log('[Model.jsx] error : ', error); + this.nodeResults = ''; + let errMsg = this.renderErrorMsg(error); + this.setState({ enableBusyFeedback: false,errorDownloadResults:true,downloadErrorMsg:errMsg,enableModelBusyFeedback:false}); + }); + }; + + triggerError = (error) => { + console.error('[Model.jsx] error : ', JSON.stringify(error)); + this.props.onInventoryDataReceived([]); + this.resultsMessage = 'No Results Found'; + this.downloadAllTooltip = 'Downloads First ' + DOWNLOAD_ALL + ' Results'; + this.nodeResults = ''; + this.setState({ + enableBusyFeedback: false, + totalResults: 0, + totalPages: 0, + showResults: false, + errorResults: true + }); + let errMsg = this.renderErrorMsg(error); + //Suppress Error Message when 404 results not found occur + if(error.response && error.response.status === 404){ + this.setState({errorMessage:'', errorResults:false}); + }else{ + this.setState({errorMessage:errMsg}); + } + }; + renderErrorMsg = (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; + } + + console.log(error.config); + return errMsg; + } + + componentWillReceiveProps(nextProps) { + console.log('[Model.jsx] componentWillReceiveProps'); + console.log('[Model.jsx] next nodeId:', nextProps.match.params.nodeId); + console.log('[Model.jsx] this nodeId:', this.props.match.params.nodeId); + + if(nextProps.match.params.nodeId !== undefined && this.props.match.params.nodeId !== undefined){ + if (nextProps.match.params.nodeId && nextProps.match.params.nodeId + !== this.props.match.params.nodeId) { + this.props = nextProps; + this.beforefetchInventoryData(); + } + } + }; + + setUri(uri) { + let delimiter = '\/'; + let start = 3; + let tokens = uri.split(delimiter).slice(start); + let result = tokens.join(delimiter); + sessionStorage.setItem(ENVIRONMENT + 'URI', result); + }; + + setBreadcrumb(uri){ + var nodeType = this.nodeType.split(';')[0]; + var display = nodeType + ': ' + (uri).split(nodeType+'\/').pop(); + var hsEntry = this.props.location.pathname + ',' + uri + ','+ display + '||'; + console.log("History Stack String: " + this.props.location.historyStackString); + if(this.historyStackString.indexOf(hsEntry) > -1){ + var tempHistoryStack = this.historyStackString.split(hsEntry)[0]; + var tempHistoryStackFormat = tempHistoryStack.replace(hsEntry, ""); + this.historyStackString = tempHistoryStackFormat; + } + this.historyStackString += hsEntry; + this.props.location.historyStackString = this.historyStackString; + console.log('[Model.jsx] historyStack in model' + this.historyStackString); + } + + handlePageChange = (pageNumber) => { + console.log('[Model.jsx] HandelPageChange active page is', pageNumber); + this.props.onInventoryDataReceived([]); + this.setState( + { activePage: pageNumber, enableBusyFeedback: true, resetColumnFilters: false, isPageNumberChange: true}, + function () { this.beforefetchInventoryData(); }.bind(this) + ); + }; + openDownloadRange = () =>{ + this.setState({ + showDownloadResultsModal: true, + errorDownloadResults: false, + downloadErrorMsg:''}); + } + closeDownloadResults = () =>{ + this.setState({ + showDownloadResultsModal: false, + enableModelBusyFeedback: false + }); + } + getAllExcels = (pageRange,rangeState) =>{ + console.log('getAllExcels>>>>>>>>>>>*',pageRange); + if(pageRange){ + this.typeOfCall=false; + let rangeModelState=(rangeState)? rangeState: false; + this.setState( + { pageRange: pageRange,enableBusyFeedback: true, enableModelBusyFeedback:true,showDownloadResultsModal:rangeModelState}, + function () { this.fetchInventoryData(); }.bind(this) + ); + }else{ + this.setState( + {errorDownloadResults: false, showDownloadResultsModal: false, downloadErrorMsg:'', enableBusyFeedback: false, enableModelBusyFeedback:false}, + function () { generateExcels(this.nodeResults);this.nodeResults='';this.typeOfCall = true;}.bind(this) + ); + } + }; + + populateFilteringOptions = () => { + let tempState = this.state; + tempState.filterList = buildAttrList(this.state.filterSelected, tempState.filterList); + this.setState(tempState); + }; + + // HELPER FUNCTIONS + isContaining = (nameKey, listArray) => { + let found = false; + listArray.map((lists) => { + if (lists.id === nameKey) { + found = true; + } + }); + return found; + }; + isTableFilterApply = (columnFilterList,nodeType,columnsList) => { + console.log('Model js ....columnFilterList:',columnFilterList); + console.log('Model js .... nodeType:',nodeType); + var columnFilter = columnFilterList[nodeType][0]; + console.log('model js columnFilter:',columnFilter); + var columns = columnsList[nodeType]; + var applyState = false; + var filterSelectedList = []; + var filterMessage = []; + + for(var i=0;i<columnFilter.length;i++){ + var colFilterValue = columnFilter[i][columns[i].value]; + if(colFilterValue != ""){ + filterSelectedList.push({ 'id': columns[i].value, 'value': colFilterValue, 'type': TABULAR_FILTER_TYPE}); + let filterMsg = columns[i].value + TABULAR_FILTER_TYPE + colFilterValue; + filterMessage.push(filterMsg); + applyState = true; + } + } + console.log('isTableFilterApply filterSelectedList>>>>>',filterSelectedList); + console.log('isTableFilterApply filterMessage>>>>>>>>>>',filterMessage); + + if(applyState){ + var tempState = this.state; + var state = true; + var stateFilterSelectedList = tempState.filterSelectedList; + var id = 'id'; + var value = 'value'; + if(stateFilterSelectedList.length > 0){ + for(var j in filterSelectedList){ + state = true; + for(var k in stateFilterSelectedList){ + if(stateFilterSelectedList[k][id] === filterSelectedList[j][id]){ + state =false; + tempState.filterSelectedList[k] = { 'id':filterSelectedList[j][id], 'value': filterSelectedList[j][value], 'type': TABULAR_FILTER_TYPE}; + } + } + if(state){ + tempState.filterSelectedList.push({ 'id':filterSelectedList[j][id], 'value': filterSelectedList[j][value], 'type': TABULAR_FILTER_TYPE}); + } + } + stateFilterSelectedList = tempState.filterSelectedList; + for(var k in stateFilterSelectedList){ + tempState.filterMessage[k] = stateFilterSelectedList[k][id] + TABULAR_FILTER_TYPE + stateFilterSelectedList[k][value] + } + }else{ + tempState.filterSelectedList = filterSelectedList; + tempState.filterMessage = filterMessage; + } + + console.log('isTableFilterApply tempState:',tempState); + this.beforefetchInventoryData(tempState); + } + }; + prepareModelGalleryElement = () =>{ + let modelGalleryElement=''; + if(this.state.isFilterEnable){ + modelGalleryElement = <ModelGallery nodes={this.props.currState.inventoryData} + viewName={this.state.viewName} + historyStackString={this.props.location.historyStackString} + openHistoryModal={this.openHistory} + isPageNumberChange={this.state.isPageNumberChange} + resetColumnInd={this.state.resetColumnFilters} + isTableFilterApply={this.isTableFilterApply} + enableRealTime={this.state.enableRealTime} + />; + }else{ + modelGalleryElement = <ModelGallery nodes={this.props.currState.inventoryData} + viewName={this.state.viewName} + historyStackString={this.props.location.historyStackString} + openHistoryModal={this.openHistory} + isPageNumberChange={this.state.isPageNumberChange} + resetColumnInd={this.state.resetColumnFilters} + enableRealTime={this.state.enableRealTime} + />; + } + return modelGalleryElement; + } + prepareDownloadRangeModel = () =>{ + + let downloadRangeModel =(this.state.showDownloadResultsModal)? <DownloadRangeModel + showDownloadResultsModal={this.state.showDownloadResultsModal} + totalPages={this.state.totalPages} + totalResults={this.state.totalResults} + triggerDownload={this.getAllExcels} + errorDownloadResults={this.state.errorDownloadResults} + downloadErrorMsg={this.state.downloadErrorMsg} + triggerClose={this.closeDownloadResults} + enableModelBusyFeedback={this.state.enableModelBusyFeedback} + /> : ''; + return downloadRangeModel; + } + toggleRealTimeAnalysisCallback=(checked)=>{ + console.log('toggleRealTimeAnalysisCallback>>>>',checked); + sessionStorage.setItem(ENVIRONMENT + 'ENABLE_ANALYSIS', !checked); + TABULAR_FILTER_TYPE=(APERTURE_SERVICE && !checked)?'CONTAINS':'='; + this.baseState.enableRealTime = !checked; + this.baseState.filterMessage = []; + this.baseState.filterSelectedList = []; + this.setState({...this.baseState},()=>{this.beforefetchInventoryData(this.state)}); + } + render() { + console.log('[Model Props] render: ', JSON.stringify(this.props) + 'elements : ', this.elements); + console.log('[Model nodeId] render: ', this.props.match.params.nodeId); + console.log('[Model nodeId] render this.state: ', this.state); + var toggelRealtimeAnalysis = ''; + if(APERTURE_SERVICE){ + toggelRealtimeAnalysis = <div className='toggleSwitch'><BootstrapSwitchButton + checked={!this.state.enableRealTime} + onlabel='Real Time' + onstyle='danger' + offlabel='Analysis' + offstyle='success' + style='w-100 mx-3' + onChange={(checked) => { + this.toggleRealTimeAnalysisCallback(checked); + }} + /></div> + } + const modelGalleryElement = this.prepareModelGalleryElement(); + let downloadRangeModel = this.prepareDownloadRangeModel(); + return ( + <div> + {toggelRealtimeAnalysis} + <Grid fluid={true} className='model-container'> + <Row className='show-grid'> + <Col md={12}> + <h1>{this.pageTitle}</h1> + <Filter key='browseSearch' + nodeType={this.state.filterSelected} + filterList={this.state.filterList} + filterDisplay={this.state.filterDisplay} + filterTypeDisplay={this.state.filterTypeDisplay} + isRunEnable={this.state.isRunEnable} + filterMessage={this.state.filterMessage} + loadInventory={this.beforefetchInventoryData} + filterSelectedList={this.state.filterSelectedList} + isFilterEnable={this.state.isFilterEnable} + enableRealTime={this.state.enableRealTime}/> + </Col> + </Row> + <Spinner loading={this.state.enableBusyFeedback}> + <Row className='show-grid'> + <Col md={8} className={this.state.isPaginationEnable && this.state.showResults ? 'show' : 'hidden'}> + <Pagination + activePage={this.state.activePage} + itemsCountPerPage={ModelConstants.RESULTS_PER_PAGE} + totalItemsCount={this.state.totalResults} + pageRangeDisplayed={ModelConstants.PAGE_RANGE_DISPLAY} + onChange={this.handlePageChange} /> + </Col> + <Col md={2} className={this.state.isPaginationEnable && this.state.showResults ? 'text-right' : 'text-left'}> + <OverlayTrigger placement='top' overlay={<Tooltip id='tooltip-top'>{this.downloadAllTooltip}</Tooltip>}> + <span className='d-inline-block' style={{display: 'inline-block'}}> + <Button bsSize='small' onClick={() => {this.getAllExcels(this.state.downloadCount)}}> + Download XLSX <i className='icon-documents-downloadablefile'></i> + </Button> + </span> + </OverlayTrigger> + </Col> + <Col md={2} className={this.state.isPaginationEnable && this.state.showResults ? 'text-right' : 'text-left'}> + <OverlayTrigger placement='top' overlay={<Tooltip id='tooltip-top'>{this.downloadRangeTooltip}</Tooltip>}> + <span className='d-inline-block' style={{display: 'inline-block'}}> + <Button bsSize='small' onClick={this.openDownloadRange}> + Download XLSX (Range)<i className='icon-documents-downloadablefile'></i> + </Button> + </span> + </OverlayTrigger> + </Col> + </Row> + <Row className='show-grid'> + <ModelBreadcrumb historyStackString={this.props.location.historyStackString}/> + </Row> + <Row> + <div className={'addPaddingTop alert alert-danger ' +(this.state.errorResults ? 'show' : 'hidden')} role="alert"> + An error occurred, please try again later. If this issue persists, please contact the system administrator. {this.state.errorMessage} + </div> + </Row> + <Row className='show-grid'> + { this.state.showResults && <div className='addPaddingTop'> + <OutputToggle scope={this} visualDisabled={true}/> + </div> + } + </Row> + <Row className={'show-grid ' + this.state.showResults ? 'show' : 'hidden'}> + <Col md={12}> + <hr /> + <h5>Total Results: <strong>{this.state.totalResults}</strong></h5> + <span className='resultMessage'>{this.resultsMessage}</span> + </Col> + </Row> + <Row className='show-grid'> + { + modelGalleryElement + } + </Row> + <Row className='show-grid'> + <Col md={12} className={this.state.isPaginationEnable && this.state.showResults ? 'show' : 'hidden'}> + <Pagination + activePage={this.state.activePage} + itemsCountPerPage={ModelConstants.RESULTS_PER_PAGE} + totalItemsCount={this.state.totalResults} + pageRangeDisplayed={ModelConstants.PAGE_RANGE_DISPLAY} + onChange={this.handlePageChange} /> + </Col> + </Row> + <div className='static-modal'> + <Modal show={this.state.showHistoryModal} onHide={this.closeHistory}> + <Modal.Header> + <Modal.Title>Retrieve {this.state.nodeDisplay} History</Modal.Title> + </Modal.Header> + <Modal.Body> + <form> + <div className="radio"> + <label> + <input type="radio" value="nodeState" + checked={this.state.historyType === 'nodeState'} + onChange={(e) => this.setHistoryType(e)} /> + View state at + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="nodeLifeCycleSince" + checked={this.state.historyType === 'nodeLifeCycleSince'} + onChange={(e) => this.setHistoryType(e)} /> + View updates since + </label> + </div> + <div className="radio"> + <label> + <input type="radio" value="nodeLifeCycle" + checked={this.state.historyType === 'nodeLifeCycle'} + onChange={(e) => this.setHistoryType(e)} /> + View all updates + </label> + </div> + </form> + <div className={this.state.enableCalendar ? 'show' : 'hidden'}> + <DatePicker + inline + selected={this.state.startDate} + onChange={(newDate) => this.handleDateChange(newDate)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="MMMM D, YYYY h:mm a" + timeCaption="time" + /> + </div> + </Modal.Body> + <Modal.Footer> + <Button onClick={this.closeHistory}>Close</Button> + <Button onClick={this.submitHistory}>Submit</Button> + </Modal.Footer> + </Modal> + </div> + </Spinner> + <Spinner loading={this.state.enableModelBusyFeedback}> + {downloadRangeModel} + </Spinner> + </Grid> + </div> + ); + } +} + +export default connect(mapStateToProps, mapActionToProps)(model); diff --git a/src/app/model/modelSearch/ModelActions.js b/src/app/model/modelSearch/ModelActions.js new file mode 100644 index 0000000..e5faa35 --- /dev/null +++ b/src/app/model/modelSearch/ModelActions.js @@ -0,0 +1,20 @@ +/* + * ============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/modelSearch/ModelConstants.js b/src/app/model/modelSearch/ModelConstants.js new file mode 100644 index 0000000..a9758c6 --- /dev/null +++ b/src/app/model/modelSearch/ModelConstants.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 ModelConstants = { + UPDATE_INVENTORY_DATA : 'UPDATE_INVENTORY_DATA', + RESULTS_PER_PAGE : 50 , + PAGE_RANGE_DISPLAY : 10 +}; diff --git a/src/app/model/modelSearch/ModelReducer.js b/src/app/model/modelSearch/ModelReducer.js new file mode 100644 index 0000000..0ea0c08 --- /dev/null +++ b/src/app/model/modelSearch/ModelReducer.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 {ModelConstants} from './ModelConstants'; + +const initalState = { + inventoryData: [] +}; + +export default (state = initalState, action) => { + switch (action.type) { + case(ModelConstants.UPDATE_INVENTORY_DATA): + console.log('[ModelReducer] action.data:', action.data); + return { + ...state, + inventoryData: action.data + }; + default: + return { + ...state + }; + } + +}; + + diff --git a/src/app/model/modelSearch/components/ModelBreadcrumb.jsx b/src/app/model/modelSearch/components/ModelBreadcrumb.jsx new file mode 100644 index 0000000..ffcddb1 --- /dev/null +++ b/src/app/model/modelSearch/components/ModelBreadcrumb.jsx @@ -0,0 +1,72 @@ +/* + * ============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 Col from 'react-bootstrap/lib/Col'; +import Label from 'react-bootstrap/lib/Label'; +import { Link } from 'react-router-dom'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; + +const modelBreadcrumb = (props) => { + + let links = null; + let historyStackArr = []; + var setURIInSession = function(uri){ + sessionStorage.setItem(GlobalExtConstants.ENVIRONMENT + 'URI', uri); + } + if (props.historyStackString) { + historyStackArr = props.historyStackString.split('||'); + for(var i = 0; i < historyStackArr.length; i++){ + historyStackArr[i] = historyStackArr[i].split(','); + console.log('[ModelBreadcrumb.jsx] previous url ' + historyStackArr[i][0] + ' previous api call '+ historyStackArr[i][1]); + } + links = historyStackArr.map((link, idx) => { + let breadCrumbTxt=decodeURI(link[2]).replace(/%2F/g,'/'); + return ( + <div className='customBreadCrumb'> + {idx === historyStackArr.length - 2 ? ( + <b id={'breadcrumbStatic' + idx} style={{'float' : 'left'}}>{breadCrumbTxt}</b> + ) : idx !== historyStackArr.length - 1 ? ( + <div id={'breadcrumbLink' + idx}> + <div style={{'float' : 'left'}}> + <Link + key={idx} + to={{ + pathname: link[0], + uri: link[1], + historyStackString: (breadCrumbTxt==='Origin')?'':props.historyStackString + }} onClick={() => setURIInSession(link[1])}>{breadCrumbTxt} + </Link> + </div> + <div style={{'float' : 'left'}}> >> </div> + </div> + ):(<div></div>)} + </div> + ); + }); + } + return ( + <Col md={12} className='addPaddingTop'> + {links} + </Col> + ); +}; + +export default modelBreadcrumb; diff --git a/src/app/model/modelSearch/components/ModelCard.jsx b/src/app/model/modelSearch/components/ModelCard.jsx new file mode 100644 index 0000000..2890b52 --- /dev/null +++ b/src/app/model/modelSearch/components/ModelCard.jsx @@ -0,0 +1,75 @@ +/* + * ============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 ModelRelationships from './ModelRelationships.jsx'; +import { Link } from 'react-router-dom'; +import {ExportExcel} from 'utils/ExportExcel.js'; +let buildAttrList = ExportExcel.buildAttrList; + +const modelCard = (props) => { + + var propKey = ''; + var navigateQueryBuilder = ''; + var editModalIcon = ''; + var requiredParams = buildAttrList(props.nodeType,[],'mandatory'); + const properties = Object.keys(props.nodeProps).map((prop, idx) => { + let description=''; + for(var a in requiredParams){ + if(requiredParams[a].value === prop){ + description=requiredParams[a].description; + if(propKey === ''){ + propKey = prop + ':' + btoa('<pre>' + props.nodeProps[prop].toString() + '</pre>'); + }else{ + propKey = propKey + ';' + prop + ':' + btoa('<pre>' + props.nodeProps[prop].toString() + '</pre>'); + } + } + } + return ( + <p className='pre-wrap-text' key={idx}><strong title={description}>{prop}:</strong> {props.nodeProps[prop].toString()}</p> + ); + }); + let pathNameStr = '/customDslBuilder/' + props.nodeType + '/' + propKey; + editModalIcon = <a className={props.isWriteAllowed ? 'show' : 'hidden'} onClick={e => {props.openEditNodeModal(props.nodeUrl)}}><i style={{cursor: 'pointer'}} className="pull-right fa fa-pencil-square-o" aria-hidden="true"></i></a>; + navigateQueryBuilder = <Link + to={{ + pathname: pathNameStr + }}> + <i className={'icon-misc-operationsL pull-right'} role="img"></i> + </Link>; + return ( + <div className='card model-card'> + <div className='card-header'> + <h4 className='card-title'>{props['nodeType']}{editModalIcon}{navigateQueryBuilder}</h4> + </div> + <div className='card-header'> + {props.nodeUrl} + </div> + <div className='card-content model-card-content'> + {properties} + </div> + <div className='card-footer'> + <ModelRelationships historyStackString={props.historyStackString} relatives={props} openHistoryModal={props.openHistoryModal} /> + </div> + </div> + ); +}; + +export default modelCard; diff --git a/src/app/model/modelSearch/components/ModelGallery.jsx b/src/app/model/modelSearch/components/ModelGallery.jsx new file mode 100644 index 0000000..d192708 --- /dev/null +++ b/src/app/model/modelSearch/components/ModelGallery.jsx @@ -0,0 +1,672 @@ +/* + * ============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 Grid from 'react-bootstrap/lib/Grid'; +import Row from 'react-bootstrap/lib/Row'; +import Col from 'react-bootstrap/lib/Col'; +import ModelCard from './ModelCard.jsx'; +import Modal from 'react-bootstrap/lib/Modal'; +import Button from 'react-bootstrap/lib/Button'; +import BootstrapTable from 'react-bootstrap-table-next'; +import {ExportExcel} from 'utils/ExportExcel.js'; +import filterFactory, { textFilter, customFilter } from 'react-bootstrap-table2-filter'; +//import overlayFactory from 'react-bootstrap-table2-overlay'; +import OutputVisualization from 'generic-components/OutputVisualization.jsx'; +import RelationshipList from './ModelTabularView.jsx'; +import PropTypes from 'prop-types'; +import Tabs from 'react-bootstrap/lib/Tabs'; +import Tab from 'react-bootstrap/lib/Tab'; +import commonApi from 'utils/CommonAPIService.js'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; +import {GeneralCommonFunctions} from 'utils/GeneralCommonFunctions.js'; +import Spinner from 'utils/SpinnerContainer.jsx'; + +let INVLIST = GlobalExtConstants.INVLIST; +let ENVIRONMENT = GlobalExtConstants.ENVIRONMENT; + +/** + * This function will take all of the node objects and turn them into + * a ui grid of ModelCard components. This function is essentially a container + * for the ModelCards + * @param props + * @returns {*} + */ +class AttributeFilter extends Component { + + constructor(props) { + super(props); + this.filter = this.filter.bind(this); + this.getValue = this.getValue.bind(this); + this.props = props; + this.state = { + filterText: '', + isPageChange: this.props.isPageChange, + columnValue : '' + }; + } + getValue = () => { + return this.input.value; + } + setPlaceHolder = () => { + let filterText = ''; + if(filterText === ''){ + filterText = 'Enter ' + this.props.column.text; + } + return filterText; + } + setFilterValue = () =>{ + let filterText = ''; + var columnFilter = this.props.columnFilterList[this.props.nodeType][0]; + for(var i=0;i<columnFilter.length;i++){ + if(columnFilter[i][this.props.column.text] != undefined){ + filterText = columnFilter[i][this.props.column.text]; + } + } + return filterText; + } + filter = () => { + let txt=this.props.column.text; + let obj = {}; + obj[txt] = this.getValue(); + var columnFilterList = this.props.columnFilterList; + var columnFilter = columnFilterList[this.props.nodeType][0]; + for(var i=0;i<columnFilter.length;i++){ + if(columnFilter[i][txt] != undefined){ + columnFilter[i][txt] = this.getValue(); + columnFilterList[this.props.nodeType] = []; + columnFilterList[this.props.nodeType].push(columnFilter); + this.props.handleOnFilter(columnFilterList,this.props.nodeType,this.props.columns,this.getValue(),this.props.aliasColumnList); + this.props.onFilter(this.getValue()); + } + } + } + render() { + return ( + <div> + + <input + key="input" + ref={ node => this.input = node } + type="text" + placeholder={this.setPlaceHolder()} + // value={this.setFilterValue} + onChange={this.filter} + /> + + </div> + ) + } +} + +class ModelGallery extends Component { + constructor(props){ + super(props); + this.props = props; + this.state = { + rerender: false, + expanded: [], + columnFilterList : {}, + columnsList: {}, + aliasColumnList: {}, + nodeType: '', + disableFilter: true, + showEditNodeModal: false, + focusedNode: null, + isEditSuccess: false, + isWriteAllowed: sessionStorage.getItem(ENVIRONMENT + 'roles') && sessionStorage.getItem(ENVIRONMENT + 'roles').indexOf('ui_write') > -1, + editInputFields: [] + } + } + componentWillMount() { + console.log('Model gallery component will mount****'); + } + componentWillUnmount() { + console.log('Model Gallery component will unmount****'); + } + handleOnExpand = (row, isExpand, rowIndex, e) => { + console.log('handleOnExpand single Row...',row.id); + if (isExpand) { + this.setState(() => ({ + expanded: [...this.state.expanded,row.id] + })); + } else { + this.setState(() => ({ + expanded: this.state.expanded.filter(x => x !== row.id) + }),function () { this.forceUpdate(); }.bind(this)); + + } + } + handleOnExpandAll = (isExpand, rows, e) => { + console.log('handleOnExpandAll to expand all rows'); + var expandArr = []; + if (isExpand) { + for(var r=0; r < rows.length; r++){ + expandArr.push(rows[r].id); + } + } + this.setState(() => ({ + expanded: expandArr + }),function () { this.forceUpdate(); }.bind(this)); + + } + handleOnFilter = (colFilterList,nodeType,columns,value,aliasColumnList) =>{ + console.log('handleOnFilter to Re-render',colFilterList); + var applyState = true; + if(value === ''){ + Object.keys(colFilterList).forEach(function(pkey){ + var filterList = colFilterList[pkey][0]; + for(var j in filterList){ + Object.keys(filterList[j]).forEach(function(key){ + if(filterList[j][key] !== ''){ + applyState = false; + } + }); + } + }); + }else{ + applyState = false; + } + this.setState({columnFilterList : colFilterList,rerender:true,columnsList : columns,nodeType: nodeType, disableFilter: applyState,aliasColumnList: aliasColumnList}); + } + generateRegexForDsl= (nodeType) =>{ + var nodePatternwithProp = nodeType+"\\*\\{.*?\\}\\(.*?\\)[\\,|\\>|\\]|\\)]|"+nodeType+"\\*\\(.*?\\)\\{.*?\\}[\\,|\\>|\\]|\\)]|"+nodeType+"\\{.*?\\}\\(.*?\\)[\\,|\\>|\\]|\\)]|"+nodeType+"\\(.*?\\)\\{.*?\\}[\\,|\\>|\\]|\\)]|"+nodeType+"\\{.*?\\}[\\,|\\>|\\]|\\)]|"+nodeType+"\\*\\{.*?\\}[\\,|\\>|\\]|\\)]"; + return nodePatternwithProp; + } + /* Start Edit Node Modal Functions */ + closeEditNodeModal = () =>{ + this.setState({editErrMsg: null, editInfoMsg: null, showEditNodeModal:false}); + } + submitEditNodeModal = () =>{ + var payload = {"operations": []}; + const settings = { + 'NODESERVER': INVLIST.NODESERVER, + 'PROXY': INVLIST.PROXY, + 'PREFIX': INVLIST.PREFIX, + 'VERSION': INVLIST.VERSION, + 'USESTUBS': INVLIST.useStubs, + 'APERTURE': INVLIST.APERTURE, + 'APERTURE_SERVICENAME':INVLIST.APERTURE_SERVICENAME + }; + let delimiter = '\/'; + let start = 3; + if((this.state.focusedNode.url).indexOf("/aperture/v") > -1){ + start = 4; + } + let tokens = (this.state.focusedNode.url).split(delimiter).slice(start); + let patchURL = tokens.join(delimiter); + var entry = { + "action": "patch", + "uri": patchURL, + "body": {} + }; + let path = "bulk/single-transaction"; + this.setState({editErrMsg: null, isPatchLoading: true}); + for(var key in this.state.editInputFields){ + if(this.state.editInputFields[key].isEdited){ + if(this.state.editInputFields[key].newValue !== ""){ + entry.body[key] = encodeURI(this.state.editInputFields[key].newValue); + }else{ + entry.body[key] = null; + } + } + } + payload.operations.push(entry); + console.log('ModelGallery: settings:' + JSON.stringify(settings)); + console.log('ModelGallery: path:' + path); + console.log('ModelGallery: payload:' + JSON.stringify(payload)); + commonApi(settings, path, 'POST', payload, 'SingleTransactionEdit', null, null, null, true) + .then(res => { + console.log('ModelGallery: Response', Object.keys(res.data)); + if(res.status === 201 || res.status === 200){ + if(res.data["operation-responses"] && res.data["operation-responses"][0] && res.data["operation-responses"][0]["response-status-code"] === 200 ){ + this.setState({isEditSuccess: true, isPatchLoading: false, showEditNodeModal:false}); + GeneralCommonFunctions.scrollTo("editSuccessMessage"); + }else{ + this.triggerError(res.data); + } + }else{ + this.triggerError(res.data); + } + }, error=>{ + this.triggerError(error); + }).catch(error => { + this.triggerError(error); + }); + } + triggerError = (error) => { + console.error('[ModelGallery.jsx] error : ', JSON.stringify(error)); + let errMsg = this.renderErrorMsg(error); + this.setState({ + isPatchLoading: false, + isEditSuccess: false, + editErrMsg: errMsg + }); + }; + renderErrorMsg = (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('[ModeGallery.jsx] error :', error.response); + if(error.response.status){ + errMsg += " Code: " + error.response.status; + } + if(error.response.data){ + errMsg += " - " + JSON.stringify(error.response.data); + } + } else if (error["requestError"]){ + errMsg += JSON.stringify(error["requestError"]); + } 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; + } + return errMsg; + } + openEditNodeModal = (nodeKey) => { + console.log("ModelGallery :: openEditNodeModal called with " + nodeKey); + var focusedNode = null; + for (var i = 0; i < this.props.nodes.length && !focusedNode; i++){ + if(nodeKey === this.props.nodes[i].url){ + focusedNode = this.props.nodes[i]; + break; + } + } + var editInputFields = []; + if(focusedNode){ + var nodeType = focusedNode['node-type']; + focusedNode.allowedEditProps = []; + //call to check what props can be modified in oxm here; + focusedNode.allowedEditProps = GeneralCommonFunctions.getEditableAttributes(nodeType); + if(focusedNode.allowedEditProps.length > 0){ + for (var key in focusedNode.allowedEditProps){ + var attr = focusedNode.allowedEditProps[key]; + editInputFields[attr] = {}; + editInputFields[attr].isEdited = false; + if(focusedNode.properties[attr]){ + editInputFields[attr].oldValue = focusedNode.properties[attr]; + editInputFields[attr].newValue = focusedNode.properties[attr]; + }else{ + editInputFields[attr].oldValue = ""; + editInputFields[attr].newValue = ""; + } + } + }else{ + this.setState({editInfoMsg: "This element cannot be edited, please contact an administrator if you need the ability to edit the attributes on this element."}); + } + }else{ + //could not find the node, this shouldn't happen + console.log("ModelGallery :: openEditNodeModal could not find " + nodeKey + " in this.props.nodes. This shouldn't happen."); + } + + this.setState({showEditNodeModal:true, isEditSuccess: false, focusedNode: focusedNode, editInputFields: editInputFields}); + } + handleInputChange(event) { + const target = event.target; + const value = target.type === 'checkbox' ? target.checked : target.value; + const name = target.name; + var editInputFields = this.state.editInputFields; + if(target.type === 'text'){ + editInputFields[name].newValue = value; + }else if(target.type === 'checkbox'){ + if(!value){ + editInputFields[name].newValue = editInputFields[name].oldValue, + editInputFields[name].isEdited = value + }else{ + editInputFields[name].isEdited = value + } + } + this.setState({ + editInputFields: editInputFields + }); + } + /* End Edit Node Modal Functions */ + render(){ + let cards = null; + let tableColumns = []; + let tableValues = []; + let rowIndexValue = 0; + let svgWidth = window.outerWidth * 0.8; + let nodesList=[]; + let tableColumnsList={}; + let tableDataList={}; + let columnFilter = []; + let columnFilterList = (this.state.rerender) ? this.state.columnFilterList : {}; + let aliasColumnList=(this.state.rerender) ? this.state.aliasColumnList: {}; + let aliasRegex=/\'(\s)as(\s)\'|\'as\'/ig; + console.log('Model gallery this.props>>>',this.props); + console.log('columnFilterList while rendering:',columnFilterList); + this.onTableFilterClick = (nodeType) => { + this.setState({rerender:false},function(){this.props.isTableFilterApply(this.state.columnFilterList,nodeType,this.state.columnsList,this.state.aliasColumnList);}.bind(this)) + } + const expandRows = { + parentClassName: 'parent-expand-bar', + onlyOneExpanding: true, + renderer: (row, rowIndex) => ( + <div> + <RelationshipList node={this.props.nodes[parseInt(row.id.split('_')[0])]} + key={this.props.nodes[parseInt(row.id.split('_')[0])].id} + nodeId={this.props.nodes[parseInt(row.id.split('_')[0])].id} + nodeType={this.props.nodes[parseInt(row.id.split('_')[0])]['node-type']} + nodeProps={this.props.nodes[parseInt(row.id.split('_')[0])].properties} + nodeRelatives={this.props.nodes[parseInt(row.id.split('_')[0])]['related-to']} + nodeUrl={this.props.nodes[parseInt(row.id.split('_')[0])].url} + historyStackString={this.props.historyStackString} + openHistoryModal={this.props.openHistoryModal} + openEditNodeModal={this.openEditNodeModal} + isWriteAllowed={this.state.isWriteAllowed} + rowIndex={parseInt(row.id.split('_')[0])} + enableRealTime={this.props.enableRealTime} + aliasColumnList={this.props.tableFilterAliasColumns} + /> + </div> + ), + showExpandColumn: true, + expandByColumnOnly: true, + onExpandAll: this.handleOnExpandAll, + expanded: this.state.expanded, + onExpand: this.handleOnExpand + + }; + const rowEvents = { + onClick: (e, row, rowIndex) => { + //row index is usefull when single node type exist, for multiple node type use row id + rowIndexValue = parseInt(row.id.split('_')[0]); + }, + onMouseEnter: (e, row, rowIndex) => { + //row index is usefull when single node type exist, for multiple node type use row id + rowIndexValue = parseInt(row.id.split('_')[0]); + } + }; + let aliasColumns=[] + + if(this.props.nodes && this.props.nodes[0] && this.props.nodes[0]['node-type'] && this.props.viewName === "CellLayout" ){ + for(var n=0; n<this.props.nodes.length; n++){ + let nodeType = this.props.nodes[n]['node-type']; + let nodeTypeProperties =[]; + let aliasProperties=[]; + let plainNodes =''; + let dslQuery = this.props.dslQuery + ','; + if(this.props.dslQuery){ + var nodePatternwithProp = this.generateRegexForDsl(nodeType); + var nodeRegularExp = new RegExp(nodePatternwithProp, 'g'); + plainNodes = dslQuery.match(nodeRegularExp); + console.log('plainNodes model Gallery>>>>>*',plainNodes); + if(plainNodes){ + let propertiesPattern ="\\{.*?\\}"; + var propRegularExp = new RegExp(propertiesPattern, 'g'); + let nodeTypeProp = plainNodes[0].match(propRegularExp); + nodeTypeProp = nodeTypeProp[0].slice(1,-1).split(',');//.replace(/\'/g,'').toLowerCase().split(','); + for(var s=0;s<nodeTypeProp.length;s++){ + let nodeTypePropes=nodeTypeProp[s].match(aliasRegex); + let alias=''; + let nprop=''; + if(nodeTypePropes){ + let nodeTypeSplit=nodeTypeProp[s].split(aliasRegex); + nprop=nodeTypeSplit[0].replace(/\'/g,''); + alias=nodeTypeSplit[nodeTypeSplit.length-1].replace(/\'/g,''); + }else{ + nprop=nodeTypeProp[s].replace(/\'/g,'').toLowerCase(); + } + aliasProperties.push(alias); + nodeTypeProperties.push(nprop); + } + } + } + if(nodesList.indexOf(nodeType) === -1){ + tableColumns=[]; + tableValues=[]; + nodesList.push(nodeType); + let tableColumnsBuilt = ExportExcel.buildAttrList(nodeType,[],'required'); + if(this.props.dslQuery && plainNodes){ + for(var z=0;z<tableColumnsBuilt.length;z++){ + let index= nodeTypeProperties.indexOf(tableColumnsBuilt[z].value.toLowerCase()); + if(index !== -1){ + if(aliasProperties[index] !==''){ + let objAlias = {}; + objAlias[aliasProperties[index]]=nodeTypeProperties[index]; + aliasColumns.push(objAlias); + tableColumnsBuilt[z].value=aliasProperties[index]; + } + tableColumns.push(tableColumnsBuilt[z]); + } + } + }else{ + tableColumns=tableColumnsBuilt; + } + console.log('after condition table columns>>>>',tableColumns); + tableColumns.push({value:'id'}); + if(!columnFilterList[nodeType]){ + columnFilterList[nodeType] = []; + columnFilter = []; + for(var j = 0; j < tableColumns.length; j++){ + let txt = tableColumns[j].value; + //if(!this.state.reRender && (!columnFilter[j] || (columnFilter[j] && columnFilter[j][txt] === undefined))){ + let obj = {}; + obj[txt] = ''; + obj['description'] = tableColumns[j].description; + columnFilter.push(obj); + //} + } + columnFilterList[nodeType].push(columnFilter); + } + if(!aliasColumnList[nodeType]){ + aliasColumnList[nodeType]=[]; + aliasColumnList[nodeType].push(aliasColumns); + } + for(var j = 0; j < tableColumns.length; j++){ + if(j === tableColumns.length-1){ + tableColumns[j].dataField = 'id'; + tableColumns[j].hidden = true; + tableColumns[j].text = tableColumns[j].value; + }else{ + tableColumns[j].dataField = tableColumns[j].value; + tableColumns[j].text = tableColumns[j].value; + tableColumns[j].headerAttrs= { title:tableColumns[j].description}; + tableColumns[j].ref=tableColumns[j].value; + tableColumns[j].filter = customFilter(); + tableColumns[j].filterRenderer = (onFilter, column) => <AttributeFilter handleOnFilter= {this.handleOnFilter} onFilter={ onFilter } column={ column } isPageChange={this.props.isPageNumberChange} nodeType={nodeType} columnFilterList={columnFilterList} columns={tableColumnsList} aliasColumnList={aliasColumnList}/>; + } + } + tableColumnsList[nodeType] = tableColumns; + tableDataList[nodeType] = []; + for(var m=0; m<this.props.nodes.length; m++){ + let nodeTypeForData = this.props.nodes[m]['node-type']; + if(nodeTypeForData === nodeType){ + let propertiesOfNode = this.props.nodes[m].properties; + propertiesOfNode.id = m + '_' + nodeType + '_id'; + tableValues.push(propertiesOfNode); + tableDataList[nodeType].push(tableValues); + } + } + } + } + }else{ + cards = this.props.nodes.map(node => { + return ( + <Col key={node.id} lg={3} md={3} sm={6} xs={12}> + <ModelCard + key={node.id} + nodeId={node.id} + nodeType={node['node-type']} + nodeProps={node.properties} + nodeRelatives={node['related-to']} + nodeUrl={node.url} + historyStackString={this.props.historyStackString} + openHistoryModal={this.props.openHistoryModal} + openEditNodeModal={this.openEditNodeModal} + isWriteAllowed={this.state.isWriteAllowed} + enableRealTime={this.props.enableRealTime} + aliasColumnList={this.props.tableFilterAliasColumns}/> + </Col> + ); + }); + } + let tabs=nodesList.map((nodeType,index) => { + return( + <Tab eventKey={nodeType} title={nodeType} key={nodeType}> + <BootstrapTable + id={nodeType} + keyField='id' + data={tableDataList[nodeType][0]} + columns={tableColumnsList[nodeType]} + filter={filterFactory()} + bordered={true} + columnFilter={true} + headerClasses='table-header-view' + expandRow={expandRows} + rowEvents={rowEvents} + bootstrap4 striped hover condensed + /> + </Tab> + ) + }); + return ( + <div> + <div className={'addPaddingTop alert alert-success ' +(this.state.isEditSuccess ? 'show' : 'hidden')} id="editSuccessMessage" role="alert"> + Update made successfully to {this.state.focusedNode ? this.state.focusedNode.url : ""}. If you wish, you may check your update using a real-time mode query, it may take some time to reflect in analysis mode. + </div> + <div className='static-modal'> + <Modal show={this.state.showEditNodeModal} onHide={this.closeEditNodeModal}> + <Modal.Header> + <Modal.Title>Edit Element</Modal.Title> + </Modal.Header> + <Modal.Body> + <Spinner loading={this.state.isPatchLoading}> + <div className={'addPaddingTop alert alert-danger ' +(this.state.editErrMsg && this.state.editErrMsg !== '' ? 'show' : 'hidden')} id="editErrorMessage" role="alert"> + An error occurred in editing the element. Please see details {this.state.editErrMsg} + </div> + <div className={'addPaddingTop alert alert-info ' +(this.state.editInfoMsg && this.state.editInfoMsg !== '' ? 'show' : 'hidden')} id="editNotAllowedMessage" role="alert"> + {this.state.editInfoMsg} + </div> + <form> + {this.state.focusedNode && Object.keys(this.state.editInputFields).length > 0 && (this.state.focusedNode.allowedEditProps).sort().map((attr) => { + return <div class="form-group row"> + <div className="col-sm-3"> + <label for={attr} class="col-form-label">{attr}</label> + </div> + <div class="col-sm-1"> + <div className="checkbox"> + <input type="checkbox" name={attr} checked={this.state.editInputFields[attr].isEdited} onChange={this.handleInputChange.bind(this)} /> + </div> + </div> + <div class="col-sm-8"> + <input type="text" class="form-control" id={attr} name={attr} disabled={!this.state.editInputFields[attr].isEdited} onChange={this.handleInputChange.bind(this)} value={this.state.editInputFields[attr].newValue}/> + </div> + </div>; + + }) + } + </form> + </Spinner> + </Modal.Body> + <Modal.Footer> + <Button onClick={this.closeEditNodeModal}>Close</Button> + <Button className={this.state.editInfoMsg && this.state.editInfoMsg !== '' ? 'hidden' : ''} onClick={this.submitEditNodeModal}>Submit</Button> + </Modal.Footer> + </Modal> + </div> + {(() => { + if (this.props.viewName === "CellLayout" && tableValues.length > 0) { + if(nodesList.length > 1){ + if(this.props.isTableFilterApply){ + return ( + <div className="addPaddingSide"> + <button type='button' className={(this.state.disableFilter)? 'btn btn-outline-secondary' : 'btn btn-primary'} disabled={this.state.disableFilter} onClick={() => {this.onTableFilterClick(nodesList)}} style={{float: 'right', margin: '2px'}}>Apply Filters (All)</button> + <Tabs defaultActiveKey={nodesList[0]} id="multipleTabularView"> + {tabs} + </Tabs> + </div> + ) + }else{ + return ( + <div className="addPaddingSide"> + <Tabs defaultActiveKey={nodesList[0]} id="multipleTabularView"> + {tabs} + </Tabs> + </div> + ) + } + }else{ + if(this.props.isTableFilterApply){ + return( + <div className="addPaddingSide"> + <button type='button' className={(this.state.disableFilter)? 'btn btn-outline-secondary' : 'btn btn-primary'} disabled={this.state.disableFilter} onClick={() => {this.onTableFilterClick(this.state.nodeType)}} style={{float: 'right', margin: '10px'}}>Apply Filters (All)</button> + <BootstrapTable + id='modelGallery' + keyField='id' + data={ tableValues } + columns={ tableColumns } + filter={ filterFactory() } + bordered={ true } + columnFilter={ true } + headerClasses='table-header-view' + expandRow={ expandRows } + rowEvents={ rowEvents } + bootstrap4 striped hover condensed + /> + </div> + ) + }else{ + return ( + <div className="addPaddingSide"> + <BootstrapTable + id='modelGallery' + keyField='id' + data={ tableValues } + columns={ tableColumns } + filter={ filterFactory() } + bordered={ true } + columnFilter={ true } + headerClasses='table-header-view' + expandRow={ expandRows } + rowEvents={ rowEvents } + bootstrap4 striped hover condensed + /> + </div> + ) + } + } + } else if (this.props.viewName === "CardLayout") { + return ( + <Grid fluid={true}> + <Row className='show-grid'> + {cards} + </Row> + </Grid> + ) + } + })()} + <div className={this.props.viewName === "VisualLayout" ? 'show' : 'hidden'}> + <OutputVisualization identifier="currentState" width={svgWidth} height="1200" overflow="scroll"/> + </div> + </div> + ); + } +}; + +export default ModelGallery; diff --git a/src/app/model/modelSearch/components/ModelNodeCard.jsx b/src/app/model/modelSearch/components/ModelNodeCard.jsx new file mode 100644 index 0000000..0caf831 --- /dev/null +++ b/src/app/model/modelSearch/components/ModelNodeCard.jsx @@ -0,0 +1,51 @@ +/* + * ============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 ModelRelationships from './ModelRelationships.jsx'; +import Col from 'react-bootstrap/lib/Col'; + +const modelNodeCard = (props) => { + + console.log('[Model Node Card] props : ', props); + const properties = Object.keys(props.nodeProps).map( (prop, idx) => { + return ( + <p key={idx}><strong> {prop} : </strong> { props.nodeProps[prop].toString()} </p> + ); + }); + + return ( + <Col lg={8} md={8} sm={10}> + <div className='card model-card'> + <div className='card-header'> + Node {props.nodeId} + </div> + <div className='card-content model-card-content'> + {properties} + </div> + </div> + <ModelRelationships relatives={props}/> + </Col> + ); +}; + +export default modelNodeCard; + + diff --git a/src/app/model/modelSearch/components/ModelNodeGallery.jsx b/src/app/model/modelSearch/components/ModelNodeGallery.jsx new file mode 100644 index 0000000..ef6947e --- /dev/null +++ b/src/app/model/modelSearch/components/ModelNodeGallery.jsx @@ -0,0 +1,46 @@ +/* + * ============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 ModelNodeCard from './ModelNodeCard.jsx'; + +const modelNodeGallery = (props) => { + + const cards = props.nodes.map(node => { + console.log('[Model Node Gallery] : ', node); + return ( + <ModelNodeCard + key={node.id} + nodeId={node.id} + nodeType={node['node-type']} + nodeProps={node.properties} + nodeRelatives={node['related-to']} + nodeUrl={node.url}/> + ); + }); + + return ( + <div> + {cards} + </div> + ); +}; + +export default modelNodeGallery; diff --git a/src/app/model/modelSearch/components/ModelRelationships.jsx b/src/app/model/modelSearch/components/ModelRelationships.jsx new file mode 100644 index 0000000..8dec154 --- /dev/null +++ b/src/app/model/modelSearch/components/ModelRelationships.jsx @@ -0,0 +1,137 @@ +/* + * ============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 Label from 'react-bootstrap/lib/Label'; +import Panel from 'react-bootstrap/lib/Panel'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; +import {ExportExcel} from 'utils/ExportExcel.js'; +let buildAttrList = ExportExcel.buildAttrList; + +let INVLIST = GlobalExtConstants.INVLIST; + +class ModelRelationships extends Component { + constructor(props){ + console.log(props); + super(props); + this.props = props; + } + + render(){ + console.log('[ModelRelationships.jsx] props : ', this.props); + let relationships = null; + let navigateByoq = null; + let relativesArray = []; + this.nodeDisplay = this.props.relatives.nodeType + ' : ' + (this.props.relatives.nodeUrl).split(this.props.relatives.nodeType + '\/').pop(); + this.historyClick = () => { + this.props.openHistoryModal(this.nodeDisplay, this.props.relatives.nodeUrl,this.props.relatives.nodeType); + } + + if (this.props.relatives.nodeRelatives && this.props.relatives.nodeRelatives.length > 0) { + relationships = this.props.relatives.nodeRelatives.sort(function(a, b) { + var compareA = (a['node-type'] + (a.url).split(a['node-type']+'\/').pop()).toLowerCase(); + var compareB = (b['node-type'] + (b.url).split(a['node-type']+'\/').pop()).toLowerCase(); + if(compareA < compareB) return -1; + if(compareA > compareB) return 1; + return 0; + }).map((relative, idx) => { + if (relativesArray.includes(relative['node-type']) === false) relativesArray.push(relative['node-type']); + return ( + <div> + <Link + key={idx} + to={{ + pathname: '/model/' + relative['node-type'] + '/' + relative.id + '/'+ this.props.relatives.enableRealTime, + uri: relative.url, + historyStackString: this.props.historyStackString + }}> + {relative['node-type']}: {(decodeURI((relative.url).split(relative['node-type']+'\/').pop())).replace(/%2F/g,'/')}</Link> <Label bsStyle='default'>{relative['relationship-label'].slice(33)}</Label> + </div> + ); + }); + } + relativesArray = relativesArray.join('&'); + var propKey = ''; + var requiredParams = buildAttrList(this.props.relatives.nodeType,[],'mandatory'); + var aliasColumnFilters = (this.props.relatives.aliasColumnList && this.props.relatives.aliasColumnList[this.props.relatives.nodeType])?this.props.relatives.aliasColumnList[this.props.relatives.nodeType][0]:[]; + console.log('requiredParams>>>>>>>>>>>>',requiredParams); + Object.keys(this.props.relatives.nodeProps).map((prop, idx) => { + for(var a in requiredParams){ + let alias=''; + if(aliasColumnFilters && aliasColumnFilters[requiredParams[a].value]){ + alias=requiredParams[a].value; + requiredParams[a].value=aliasColumnFilters[requiredParams[a].value]; + } + if(requiredParams[a].value === prop){ + let tag= (alias!='')? alias: prop; + if(propKey === ''){ + propKey = tag + ':' + btoa('<pre>' + this.props.relatives.nodeProps[prop].toString() + '</pre>'); + }else{ + propKey = propKey + ';' + tag + ':' + btoa('<pre>' + this.props.relatives.nodeProps[prop].toString() + '</pre>'); + } + } + } + }); + let pathNameStr = (relativesArray.length>0) ? '/customDsl/' + this.props.relatives.nodeType + '/' + propKey + '/' + relativesArray : '/customDsl/' + this.props.relatives.nodeType + '/' + propKey; + navigateByoq = <Link + to={{ + pathname: pathNameStr + }}> + <button type='button' className='btn btn-primary pull-right'>>>BYOQ</button> + </Link>; + if (this.props.relatives.nodeRelatives && this.props.relatives.nodeRelatives.length > 0) { + return ( + <Panel> + <Panel.Heading> + <Panel.Toggle> + <button type='button' className='btn btn-outline-primary'> + Relationships + </button> + </Panel.Toggle> + { INVLIST.isHistoryEnabled && (<button type='button' className='btn btn-outline-primary' onClick={this.historyClick}> + History + </button>)} + {navigateByoq} + </Panel.Heading> + <Panel.Collapse> + <Panel.Body className='cardwrap'> + {relationships} + </Panel.Body> + </Panel.Collapse> + </Panel> + ); + } else { + return ( + <div> + <button type='button' className='btn btn-outline-disabled'> + No relationships + </button> + { INVLIST.isHistoryEnabled && (<button type='button' className='btn btn-outline-primary' onClick={this.historyClick}> + History + </button>)} + {navigateByoq} + </div> + ); + } +} +}; + +export default ModelRelationships; diff --git a/src/app/model/modelSearch/components/ModelTabularView.jsx b/src/app/model/modelSearch/components/ModelTabularView.jsx new file mode 100644 index 0000000..3b5a94a --- /dev/null +++ b/src/app/model/modelSearch/components/ModelTabularView.jsx @@ -0,0 +1,197 @@ +/* + * ============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 BootstrapTable from 'react-bootstrap-table-next'; +import filterFactory, { textFilter, customFilter } from 'react-bootstrap-table2-filter'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import Label from 'react-bootstrap/lib/Label'; +import {GlobalExtConstants} from 'utils/GlobalExtConstants.js'; +import {ExportExcel} from 'utils/ExportExcel.js'; +let buildAttrList = ExportExcel.buildAttrList; + +let INVLIST = GlobalExtConstants.INVLIST; + + class RelationshipList extends Component { + + + constructor(props) { + super(props); + this.props = props; + this.relationships = null; + this.relativesArray = []; + this.state = { + filteron:false + + } + } + render() { + + + let navigateByoq = null; + this.nodeDisplay = this.props.nodeType + ' : ' + (this.props.nodeUrl).split(this.props.nodeType + '\/').pop(); + this.historyClick = () => { + this.props.openHistoryModal(this.nodeDisplay, this.props.nodeUrl,this.props.nodeType); + } + this.filter = (e) =>{ + let filterValue = e.target.value; + this.returnFilterList(filterValue); + this.setState({filteron:true}); + } + this.returnFilterList = (filterValue) =>{ + if (this.props.nodeRelatives && this.props.nodeRelatives.length > 0) { + this.relationships = null; + this.relationships = this.props.nodeRelatives.sort(function(a, b) { + var compareA = (a['node-type'] + (a.url).split(a['node-type']+'\/').pop()).toLowerCase(); + var compareB = (b['node-type'] + (b.url).split(a['node-type']+'\/').pop()).toLowerCase(); + if(compareA < compareB) return -1; + if(compareA > compareB) return 1; + return 0; + }).map((relative, idx) => { + if (this.relativesArray.includes(relative['node-type']) === false) this.relativesArray.push(relative['node-type']); + if(filterValue === '' || filterValue === ':' || relative['node-type'].toLowerCase().search(filterValue.toLowerCase()) != -1 || (decodeURI((relative.url).split(relative['node-type']+'\/').pop())).replace(/%2F/g,'/').toLowerCase().search(filterValue.toLowerCase()) != -1){ + return ( + <li key={idx + '' +relative.id}> + <Link + key={idx} + to={{ + pathname: '/model/' + relative['node-type'] + '/' + relative.id + '/'+ this.props.enableRealTime, + uri: relative.url, + historyStackString: this.props.historyStackString + }}> + {relative['node-type']}: {(decodeURI((relative.url).split(relative['node-type']+'\/').pop())).replace(/%2F/g,'/')}</Link> <Label bsStyle='default'>{relative['relationship-label'].slice(33)}</Label> + </li> + ); + } + }); + } + } + if(!this.state.filteron){ + this.returnFilterList(''); + } + let relativesArray = (this.relativesArray.length > 0) ? this.relativesArray.join('&') : this.relativesArray; + var propKey = ''; + var requiredParams = buildAttrList(this.props.nodeType,[],'mandatory'); + var aliasColumnFilters = (this.props.aliasColumnList && this.props.aliasColumnList[this.props.nodeType])?this.props.aliasColumnList[this.props.nodeType][0]:[]; + Object.keys(this.props.nodeProps).map((prop, idx) => { + for(var a in requiredParams){ + let alias=''; + if(aliasColumnFilters && aliasColumnFilters[requiredParams[a].value]){ + alias=requiredParams[a].value; + requiredParams[a].value=aliasColumnFilters[requiredParams[a].value]; + } + if(requiredParams[a].value === prop){ + let tag= (alias!='')? alias: prop; + if(propKey === ''){ + propKey = tag + ':' + btoa(this.props.nodeProps[prop].toString()); + }else{ + propKey = propKey + ';' + tag + ':' + btoa(this.props.nodeProps[prop].toString()); + } + } + } + }); + let editModalIcon = <a className={this.props.isWriteAllowed ? 'show' : 'hidden'} onClick={e => {this.props.openEditNodeModal(this.props.nodeUrl)}}><i style={{cursor: 'pointer'}} className="pull-right fa fa-pencil-square-o" aria-hidden="true"></i></a>; + let pathNameStr = (relativesArray.length>0) ? '/customDsl/' + this.props.nodeType + '/' + propKey + '/' + relativesArray : '/customDsl/' + this.props.nodeType + '/' + propKey; + navigateByoq = <Link + to={{ + pathname: pathNameStr + }}> + <button type='button' className='btn btn-primary pull-right'>>>BYOQ</button> + </Link>; + let relationships = []; + if(this.relationships){ + for(var n=0 ; n < this.relationships.length ; n++){ + if(this.relationships[n]){ + relationships.push(this.relationships[n]); + } + } + } + if (this.props.nodeRelatives && this.props.nodeRelatives.length > 0) { + return ( + <div> + <div style={{float: 'left'}}> + <table className='relationshipTable table-striped table-hover table-bordered table-sm'> + <thead> + <tr className='table-header-view'> + <th titlename='Relationships'> + Relationships + <div> + <input + key='input' + type='text' + placeholder='Enter Relationship...' + onChange={(e) => this.filter(e)} + /> + </div> + </th> + </tr> + </thead> + <tbody> + <tr> + <td> + <ul> + {relationships} + </ul> + </td> + </tr> + </tbody> + </table> + </div> + <div style={{float: 'left', margin: '10px'}}> + { INVLIST.isHistoryEnabled && (<button type='button' className='btn btn-primary pull-right' onClick={this.historyClick}> + History + </button>)} + </div> + <div style={{float: 'left',margin: '10px'}}> + {navigateByoq} + </div> + <div style={{float: 'left',margin: '10px'}}> + {editModalIcon} + </div> + </div> + ) + } else { + return ( + <div> + <div style={{float: 'left', margin: '10px'}}> + <button type='button' className='btn btn-outline-disabled'> + No relationships + </button> + </div> + <div style={{float: 'left', margin: '10px'}}> + { INVLIST.isHistoryEnabled && (<button type='button' className='btn btn-primary' onClick={this.historyClick}> + History + </button>)} + </div> + <div style={{float: 'left', margin: '10px'}}> + {navigateByoq} + </div> + <div style={{float: 'left',margin: '10px'}}> + {editModalIcon} + </div> + </div> + ); + } + } + } + +export default RelationshipList; +//export default AttributeFilter; diff --git a/src/app/model/test.jsx b/src/app/model/test.jsx new file mode 100644 index 0000000..69ca45e --- /dev/null +++ b/src/app/model/test.jsx @@ -0,0 +1,147 @@ +/* + * ============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========================================================= + */ + +const languages = [ + { + name: 'C', + year: 1972 + }, + { + name: 'C#', + year: 2000 + }, + { + name: 'C++', + year: 1983 + }, + { + name: 'Clojure', + year: 2007 + }, + { + name: 'Elm', + year: 2012 + }, + { + name: 'Go', + year: 2009 + }, + { + name: 'Haskell', + year: 1990 + }, + { + name: 'Java', + year: 1995 + }, + { + name: 'Javascript', + year: 1995 + }, + { + name: 'Perl', + year: 1987 + }, + { + name: 'PHP', + year: 1995 + }, + { + name: 'Python', + year: 1991 + }, + { + name: 'Ruby', + year: 1995 + }, + { + name: 'Scala', + year: 2003 + } +]; + +// https://developer.mozilla.org/en/docs/Web/JavaScript/Guide/Regular_Expressions#Using_Special_Characters +function escapeRegexCharacters(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function getSuggestions(value) { + const escapedValue = escapeRegexCharacters(value.trim()); + + const regex = new RegExp('^' + escapedValue, 'i'); + + return languages.filter(language => regex.test(language.name)); +} + +function getSuggestionValue(suggestion) { + return suggestion.name; +} + +function renderSuggestion(suggestion) { + return ( + <span>{suggestion.name}</span> + ); +} + +class Test extends React.Component { + constructor() { + super(); + + this.state = { + value: '', + suggestions: getSuggestions('') + }; + + this.onChange = this.onChange.bind(this); + this.onSuggestionsUpdateRequested = this.onSuggestionsUpdateRequested.bind(this); + } + + onChange(event, { newValue, method }) { + this.setState({ + value: newValue + }); + } + + onSuggestionsUpdateRequested({ value }) { + this.setState({ + suggestions: getSuggestions(value) + }); + } + + render() { + const { value, suggestions } = this.state; + const inputProps = { + placeholder: "Type 'c'", + value, + onChange: this.onChange + }; + + return ( + <Autosuggest suggestions={suggestions} + onSuggestionsUpdateRequested={this.onSuggestionsUpdateRequested} + shouldRenderSuggestions={() => true} + getSuggestionValue={getSuggestionValue} + renderSuggestion={renderSuggestion} + inputProps={inputProps} /> + ); + } +} + +ReactDOM.render(<App />, document.getElementById('app')); diff --git a/src/app/model/testGallery.jsx b/src/app/model/testGallery.jsx new file mode 100644 index 0000000..8d73388 --- /dev/null +++ b/src/app/model/testGallery.jsx @@ -0,0 +1,91 @@ +/* + * ============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 ModelGallery from 'app/modelSearch/components/ModelGallery.jsx'; +import testData from 'app/CustomQueryMultiNode'; + +class testGallery extends Component { + + state = { + nodes: [], + multipleNodes: '', + isLoading: 'show' + } + + componentWillMount () { + this.processData(testData); + } + + + + processData = (data) => { + console.log('Response data' + JSON.stringify(data)); + if (data && data.results) { + //this.nodes = data.results; + this.setState({ + nodes: data.results + }); + + this.multipleNodes = this.state.nodes.length > 1; + } + if (data && data.headers && data.headers.get('total-results')) { + this.modelService.setTotalResults(data.headers.get('total-results')); + } + this.setState({ + isLoading: 'hidden' + }); + } + + render() { + + let nodes = ''; + if ( this.state.nodes.length > 0 ) { + + console.log('nodes exist'); + + nodes = + (<div className='model-container'> + <ModelGallery + nodes={this.state.nodes}/> + </div>); + } + + return( + <div className='multipleNodes'> + <div className={!this.state.isLoading}> + <div className='col align-self-cemter'> + <fa name='cog' className='fa-5x fa-spin'></fa> + </div> + </div> + <div className={!this.state.isLoading}> + <h2>{this.header}</h2> + <div className='nodes container-fluid'> + <div className='row-dsl'> + {nodes} + </div> + </div> + </div> + </div> + ); + } +} + +export default testGallery; |