summaryrefslogtreecommitdiffstats
path: root/src/app/model
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/model')
-rw-r--r--src/app/model/CustomQueryMultiNode.json83
-rw-r--r--src/app/model/ModelDisplay.jsx35
-rw-r--r--src/app/model/history/History.jsx716
-rw-r--r--src/app/model/history/HistoryActions.js19
-rw-r--r--src/app/model/history/HistoryConstants.js25
-rw-r--r--src/app/model/history/HistoryQuery.jsx814
-rw-r--r--src/app/model/history/HistoryReducer.js43
-rw-r--r--src/app/model/history/components/AnimationControls.jsx126
-rw-r--r--src/app/model/history/components/HistoryCard.jsx101
-rw-r--r--src/app/model/history/components/HistoryEntry.jsx55
-rw-r--r--src/app/model/history/components/HistoryGallery.jsx65
-rw-r--r--src/app/model/history/components/NodeDiffCard.jsx106
-rw-r--r--src/app/model/history/components/TopologyDiffCard.jsx151
-rw-r--r--src/app/model/modelSearch/Model.jsx846
-rw-r--r--src/app/model/modelSearch/ModelActions.js20
-rw-r--r--src/app/model/modelSearch/ModelConstants.js25
-rw-r--r--src/app/model/modelSearch/ModelReducer.js43
-rw-r--r--src/app/model/modelSearch/components/ModelBreadcrumb.jsx72
-rw-r--r--src/app/model/modelSearch/components/ModelCard.jsx75
-rw-r--r--src/app/model/modelSearch/components/ModelGallery.jsx672
-rw-r--r--src/app/model/modelSearch/components/ModelNodeCard.jsx51
-rw-r--r--src/app/model/modelSearch/components/ModelNodeGallery.jsx46
-rw-r--r--src/app/model/modelSearch/components/ModelRelationships.jsx137
-rw-r--r--src/app/model/modelSearch/components/ModelTabularView.jsx197
-rw-r--r--src/app/model/test.jsx147
-rw-r--r--src/app/model/testGallery.jsx91
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'}}>&nbsp;&nbsp;&#x3E;&#x3E;&nbsp;&nbsp;</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;