From ad6e3220624db8894ebcec5ac1e46cc8bfca5868 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sat, 8 Aug 2020 14:51:21 -0700 Subject: [PATCH 01/47] config-ip-heartbeat: Server now sends periodic heartbeat to any connected websocket clients. --- build/app/unisys/common-defs.js | 2 ++ build/app/unisys/server-network.js | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/build/app/unisys/common-defs.js b/build/app/unisys/common-defs.js index b4b6563f..c446df95 100644 --- a/build/app/unisys/common-defs.js +++ b/build/app/unisys/common-defs.js @@ -10,6 +10,8 @@ const DBG = true; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var DEFS = {}; +DEFS.SERVER_HEARTBEAT_INTERVAL = 5000; + /// EXPORT MODULE DEFINITION ////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - module.exports = DEFS; diff --git a/build/app/unisys/server-network.js b/build/app/unisys/server-network.js index b9f3eeb6..86834701 100644 --- a/build/app/unisys/server-network.js +++ b/build/app/unisys/server-network.js @@ -21,6 +21,7 @@ var FSE = require('fs-extra'); var NetMessage = require('../unisys/common-netmessage-class'); const LOGGER = require('../unisys/server-logger'); var DB = require('../unisys/server-database'); +var DEFS = require('./common-defs'); /// CONSTANTS ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -46,7 +47,8 @@ var mu_sid_counter = 0; // for generating unique socket ids var m_server_handlers = new Map(); // message map storing sets of functions var m_message_map = new Map(); // message map storing other handlers var m_socket_msgs_list = new Map(); // message map by uaddr - +// heartbeat +var m_heartbeat_interval; /// API MEHTHODS ////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -177,7 +179,24 @@ const SERVER_UADDR = NetMessage.DefaultServerUADDR(); // is 'SVR_01' socket.on('close', () => { m_SocketDelete(socket); }); + + // start heartbeat + m_StartHeartbeat(); + } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ When a new socket is connected, we send a periodic heartbeat to let them + know we're still here, and so client can detect when network is lost. +/*/ function m_StartHeartbeat() { + if (DBG) console.log(PR, 'starting heartbeat'); + if (m_heartbeat_interval) return; // already started + m_heartbeat_interval = setInterval(function sendHeartbeat() { + mu_sockets.forEach((socket, key, map) => { + if (DBG) console.log(PR, 'sending heartbeat to', socket.UADDR); + socket.send('heartbeat'); + }); + }, DEFS.SERVER_HEARTBEAT_INTERVAL); } + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ When a new socket connection happens, send back the special registration packet (WIP) From ee36b7d5b7abb6756bded3c6f45808da51f4f30a Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sat, 8 Aug 2020 14:52:48 -0700 Subject: [PATCH 02/47] config-ip-heartbeat: client will now show "Server Disconnected" if a heartbeat is not received within hearbeta timer window. --- build/app/unisys/client-network.js | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/build/app/unisys/client-network.js b/build/app/unisys/client-network.js index 1d569172..16c5d3d0 100644 --- a/build/app/unisys/client-network.js +++ b/build/app/unisys/client-network.js @@ -12,7 +12,9 @@ const DBG = { connect: true, handle: false }; /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - const SETTINGS = require("settings"); const NetMessage = require("unisys/common-netmessage-class"); +const DEFS = require("./common-defs"); const PROMPTS = require("system/util/prompts"); + const PR = PROMPTS.Pad("NETWORK"); const WARN = PROMPTS.Pad("!!!"); const ERR_NM_REQ = "arg1 must be NetMessage instance"; @@ -37,6 +39,9 @@ const M_NOCONNECT = 6; var m_status = M0_INIT; var m_options = {}; +// hearbeat +var m_hearbeat_timer; + /// API METHODS /////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var NETWORK = {}; @@ -149,9 +154,38 @@ function m_HandleRegistrationMessage(msgEvent) { m_status = M4_READY; // (4) network is initialized if (typeof m_options.success === "function") m_options.success(); + // (5) initialize heartbeat timer + m_ResetHearbeatTimer(); +} +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ If a 'hearbeat' message is not received from the server every 5 seconds + we assume the network connection has gone down. The timeout should be + greater than the server heartbeat interval set in + server-network.js:m_StartHeartbeat() + + The UNISYSDisconnect handler only goes down when the server closes the + connection. In order to detect the internet connection going down + (e.g. wifi) we need to check to see if we are peridically receiving + a heartbeat message from the server. +/*/ +function m_ResetHearbeatTimer() { + clearTimeout(m_hearbeat_timer); + m_hearbeat_timer = setTimeout(function heartbeatStopped() { + if (DBG.handle) console.log(PR, 'heartbeat not received before time ran out -- YOURE DEAD!'); + NetMessage.GlobalOfflineMode(); + }, DEFS.SERVER_HEARTBEAT_INTERVAL + 1000); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - function m_HandleMessage(msgEvent) { + + // Check Hearbeat + if (msgEvent.data === 'heartbeat') { + if (DBG.handle) console.log(PR, '...got hearbeat! Reset timer'); + m_ResetHearbeatTimer(); + return; + } + + // Handle Regular Message let pkt = new NetMessage(msgEvent.data); let msg = pkt.Message(); if (pkt.IsOwnResponse()) { From de4de0071f58c73b7b81660c4e8a2b1eeade108e Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 15:37:03 -0700 Subject: [PATCH 03/47] config-ip-filter: Lint --- build/app/view/netcreate/components/d3-simplenetgraph.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/build/app/view/netcreate/components/d3-simplenetgraph.js b/build/app/view/netcreate/components/d3-simplenetgraph.js index 4474f5b8..f133d60c 100644 --- a/build/app/view/netcreate/components/d3-simplenetgraph.js +++ b/build/app/view/netcreate/components/d3-simplenetgraph.js @@ -571,7 +571,7 @@ displayUpdated(nodeEdge) Used by linElements.enter() and linkElements.merge() and mouseover events. /*/ -_UpdateLinkStrokeWidth (edge) { +_UpdateLinkStrokeWidth(edge) { if (edge.selected || (edge.source.id === mouseoverNodeId) || (edge.target.id === mouseoverNodeId) || @@ -583,12 +583,10 @@ _UpdateLinkStrokeWidth (edge) { } } -_UpdateLinkStrokeColor(edge){ +_UpdateLinkStrokeColor(edge) { let COLORMAP = UDATA.AppState('NODECOLORMAP'); let color = COLORMAP[edge.attributes.Relationship] return color; - - } From bdb4ab74053a908e8586512f54050f29f4967352 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 15:37:35 -0700 Subject: [PATCH 04/47] config-ip-filter: d3 set opacity to 0 if `isFilteredOut` --- .../netcreate/components/d3-simplenetgraph.js | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/build/app/view/netcreate/components/d3-simplenetgraph.js b/build/app/view/netcreate/components/d3-simplenetgraph.js index f133d60c..62c90229 100644 --- a/build/app/view/netcreate/components/d3-simplenetgraph.js +++ b/build/app/view/netcreate/components/d3-simplenetgraph.js @@ -303,27 +303,30 @@ class D3NetGraph { // enter node: also append 'circle' element of a calculated size elementG .append("circle") - // "r" has to be set here or circles don't draw. - .attr("r", (d) => { - let radius = this.data.edges.reduce((acc,ed)=>{ - return (ed.source===d.id || ed.target===d.id) ? acc+1 : acc; - },1); - - d.weight = radius - d.size = radius // save the calculated size - return this.defaultSize + (this.defaultSize * d.weight / 2) - }) - // .attr("r", (d) => { return this.defaultSize }) // d.size ? d.size/10 : this.defaultSize; }) - .attr("fill", (d) => { - // REVIEW: Using label match. Should we use id instead? - // The advantage of using the label is backward compatibility with - // Google Fusion table data as well as exportability. - // If we save the type as an id, the data format will be - // less human-readable. - // The problem with this approach though is that any changes - // to the label text will result in a failed color lookup! - return COLORMAP[d.attributes["Node_Type"]]; - }); + // "r" has to be set here or circles don't draw. + .attr("r", (d) => { + let radius = this.data.edges.reduce((acc, ed) => { + return (ed.source === d.id || ed.target === d.id) ? acc + 1 : acc; + }, 1); + + d.weight = radius + d.size = radius // save the calculated size + return this.defaultSize + (this.defaultSize * d.weight / 2) + }) + // .attr("r", (d) => { return this.defaultSize }) // d.size ? d.size/10 : this.defaultSize; }) + .attr("fill", (d) => { + // REVIEW: Using label match. Should we use id instead? + // The advantage of using the label is backward compatibility with + // Google Fusion table data as well as exportability. + // If we save the type as an id, the data format will be + // less human-readable. + // The problem with this approach though is that any changes + // to the label text will result in a failed color lookup! + return COLORMAP[d.attributes["Node_Type"]]; + }) + .style("opacity", d => { + return d.isFilteredOut ? 0 : 1.0 + }); // enter node: also append 'text' element elementG @@ -332,7 +335,10 @@ class D3NetGraph { .attr("font-size", 10) .attr("dx", (d=>{return this.defaultSize + 5})) // 8) .attr("dy", "0.35em") // ".15em") - .text((d) => { return d.label }); + .text((d) => { return d.label }) + .style("opacity", d => { + return d.isFilteredOut ? 0 : 1.0 + }); // enter node: also append a 'title' tag // we should move this to our tooltip functions, but it works for now @@ -404,6 +410,10 @@ class D3NetGraph { d.weight = radius d.size = radius // save the calculated size return this.defaultSize + (this.defaultSize * d.weight / 2) + }) + .style("opacity", d => { + // console.log(d); + return d.isFilteredOut ? 0 : 1.0 }); // UPDATE text in each node for all nodes @@ -420,7 +430,10 @@ class D3NetGraph { if (d.selected) return 'bold'; return undefined; // don't set font weight }) - .text((d) => { return d.label }); // in case text is updated + .text((d) => { return d.label }) // in case text is updated + .style("opacity", d => { + return d.isFilteredOut ? 0 : 1.0 + }); nodeElements.merge(nodeElements) .selectAll("title") // node tooltip @@ -446,12 +459,18 @@ class D3NetGraph { // if (DBG) console.log('clicked on',d.label,d.id) // this.edgeClickFn( d ) // }) + .style("opacity", d => { + return d.isFilteredOut ? 0 : 1.0 + }); // .merge() updates the visuals whenever the data is updated. linkElements.merge(linkElements) .classed("selected", (d) => { return d.selected }) .style('stroke', this._UpdateLinkStrokeColor) .style('stroke-width', this._UpdateLinkStrokeWidth) + .style("opacity", d => { + return d.isFilteredOut ? 0 : 1.0 + }); linkElements.exit().remove() From 65ebf82659f9bc58034cae1e7f1416ee507366ae Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 15:49:25 -0700 Subject: [PATCH 05/47] config-ip-filter: Add filter functions to nc-logic. --- build/app/view/netcreate/nc-logic.js | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/build/app/view/netcreate/nc-logic.js b/build/app/view/netcreate/nc-logic.js index 80b76e31..24c3293b 100644 --- a/build/app/view/netcreate/nc-logic.js +++ b/build/app/view/netcreate/nc-logic.js @@ -668,6 +668,14 @@ MOD.Hook("INITIALIZE", () => { UDATA.HandleMessage("AUTOCOMPLETE_SELECT", function(data) { m_HandleAutoCompleteSelect(data); }); + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ FILTER is called by FiltersPanel when user has updated filter. + /*/ + UDATA.HandleMessage("FILTER", function(data) { + m_HandleFilter(data); + }); + }); // end UNISYS_INIT function m_HandleAutoCompleteSelect(data) { @@ -677,6 +685,21 @@ function m_HandleAutoCompleteSelect(data) { }); } +/** + * + * @param {object} filter {name, type, value} + * + */ +function m_HandleFilter(filter) { + console.log('HandleFilter!', filter); + + // Filter Nodes and Edges + let marked = { isFilteredOut: true }; + let normal = { isFilteredOut: false }; + m_SetMatchingByNodeLabel(filter.value, marked, normal); + UDATA.SetAppState("D3DATA", D3DATA); +} + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ lifecycle RESET handler /*/ @@ -843,6 +866,61 @@ function m_SetMatchingNodesByLabel(str = "", yes = {}, no = {}) { }); return returnMatches; } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. + All others nodes are set to 'no' props. Return matches. + Optionally resets all the NON matching nodes as well. + + Edges are matched if they link to the node. +/*/ +function m_SetMatchingByNodeLabel(str = "", yes = {}, no = {}) { + let returnMatches = []; + str = u_EscapeRegexChars(str.trim()); + if (str === "") return undefined; + const regex = new RegExp(/*'^'+*/ str, "i"); + // First find the nodes + D3DATA.nodes.forEach(node => { + if (regex.test(node.label)) { + for (let key in yes) node[key] = yes[key]; + returnMatches.push(node); + } else { + for (let key in no) node[key] = no[key]; + } + }); + // Then hide all related edges + m_SetMatchingEdgesByNodes(returnMatches, yes, no); + return returnMatches; +} + +/** + * Set edges that link to any node in nodeIDs to 'yes' props. + * All others nodes are set to 'no' props. Return matches. + * + * We set look for ALL nodes at once otherwise, one node can unset + * antoher node. + * + * This is a specialized function because edges need to be matched + * against both source and target. + * + * @param {Array} nodes Array of node objects + * @param {Object} yes e.g. marked = { isFilteredOut: true }; + * @param {Object} no e.g. normal = { isFilteredOut: false }; + */ +function m_SetMatchingEdgesByNodes(nodes, yes = {}, no = {}) { + const nodeIDs = nodes.map(node => node.id); + let returnMatches = []; + D3DATA.edges.forEach(edge => { + if ( nodeIDs.includes(edge.source.id) || nodeIDs.includes(edge.target.id) ) { + for (let key in yes) edge[key] = yes[key]; + returnMatches.push(edge); + } else { + for (let key in no) edge[key] = no[key]; + } + }); + return returnMatches; +} + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Update props of exact matching nodes, returns matches Optionally resets all the NON matching nodes as well From b391b2659b2cc01eadc3b1d08f4a4c9df3e07359 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 15:52:33 -0700 Subject: [PATCH 06/47] config-ip-filter: Add rudimentary filter panel. --- .../view/netcreate/components/InfoPanel.jsx | 54 ++++++++++++------ .../components/filter/FiltersPanel.jsx | 55 +++++++++++++++++++ .../components/filter/StringFilter.jsx | 35 ++++++++++++ 3 files changed, 126 insertions(+), 18 deletions(-) create mode 100644 build/app/view/netcreate/components/filter/FiltersPanel.jsx create mode 100644 build/app/view/netcreate/components/filter/StringFilter.jsx diff --git a/build/app/view/netcreate/components/InfoPanel.jsx b/build/app/view/netcreate/components/InfoPanel.jsx index 50ca4c12..87dc2fa2 100644 --- a/build/app/view/netcreate/components/InfoPanel.jsx +++ b/build/app/view/netcreate/components/InfoPanel.jsx @@ -4,8 +4,10 @@ InfoPanel shows a tab panel for selecting: * hiding (showing the Graph) + * Filters * Nodes Table * Edges Table + * Vocabulary * Help The panel itself can be resized vertically. @@ -26,10 +28,11 @@ const ReactStrap = require('reactstrap'); const { TabContent, TabPane, Nav, NavItem, NavLink, Row, Col, Button } = ReactStrap; const classnames = require('classnames'); -const Help = require('./Help'); -const Vocabulary = require('./Vocabulary'); +const FiltersPanel = require('./filter/FiltersPanel'); const NodeTable = require('./NodeTable'); const EdgeTable = require('./EdgeTable'); +const Vocabulary = require('./Vocabulary'); +const Help = require('./Help'); /// REACT COMPONENT /////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -67,7 +70,7 @@ class InfoPanel extends UNISYS.Component { window.event.stopPropagation(); if (this.state.activeTab !== tab) { this.setState({ activeTab: tab }); - if ((tab === `1`) || (tab === '5')) { + if ((tab === `1`) || (tab === '6')) { // graph or help this.setState({ tabpanelHeight: '50px', // show only tab buttons hideDragger: true, @@ -155,39 +158,47 @@ class InfoPanel extends UNISYS.Component { onClick={() => { this.toggle('1'); this.sendGA('Graph', window.location); } } > Graph - + { this.toggle('2'); this.sendGA('Nodes Table', window.location); }} + onClick={() => { this.toggle('2'); this.sendGA('Filter', window.location); }} > - Nodes Table - + Filters + { this.toggle('3'); this.sendGA('Edges Table', window.location); }} + onClick={() => { this.toggle('3'); this.sendGA('Nodes Table', window.location); }} > - Edges Table - + Nodes Table + { this.toggle('4'); this.sendGA('Vocabulary', window.location); }} + onClick={() => { this.toggle('4'); this.sendGA('Edges Table', window.location); }} > - Vocabulary - + Edges Table + { this.toggle('5'); this.sendGA('Help', window.location); }} + onClick={() => { this.toggle('5'); this.sendGA('Vocabulary', window.location); }} + > + Vocabulary + + + + { this.toggle('6'); this.sendGA('Help', window.location); }} > Help - + @@ -196,25 +207,32 @@ class InfoPanel extends UNISYS.Component { - + - + - + + + + + + + + diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx new file mode 100644 index 00000000..85da6782 --- /dev/null +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -0,0 +1,55 @@ +import FilterGroup from './FilterGroup'; +import React from 'react'; +import StringFilter from './StringFilter'; +const ReactStrap = require('reactstrap'); + +const { Input, Label } = ReactStrap; + +const UNISYS = require('unisys/client'); +var UDATA = null; + +// Storybook has problems with loading unisys without relative ref +// but even with relative ref, it can't load submodules +// const UNISYS = require('../../../../unisys/client'); +// class FiltersPanel extends React.Component { + +// Dummy Data +const filterData = { + id: '1', + name: 'Label', + type: 'contains', + value: 'tacitus' +}; + +class FiltersPanel extends UNISYS.Component { + constructor({ filterGroups, onFiltersChange, tableHeight }) { + super(); + + console.log('filters is', filterGroups) + + this.OnFilterChange = this.OnFilterChange.bind(this); + + /// Initialize UNISYS DATA LINK for REACT + UDATA = UNISYS.NewDataLink(this); + } // constructor + + OnFilterChange (filter) { + console.log('onFilterChange', filter); + UDATA.LocalCall('FILTER', filter); + } + + render() { + const { tableHeight } = this.props; + return ( +
+ +
+ ) + } +} + +module.exports = FiltersPanel; + +// storyboard wants a regular export?!?! +// export default FiltersPanel; diff --git a/build/app/view/netcreate/components/filter/StringFilter.jsx b/build/app/view/netcreate/components/filter/StringFilter.jsx new file mode 100644 index 00000000..5d6e48fe --- /dev/null +++ b/build/app/view/netcreate/components/filter/StringFilter.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +const ReactStrap = require('reactstrap'); +const { Input, Label } = ReactStrap; + +export default function StringFilter({ filter: { id, name, type, value }, onChangeHandler }) { + + console.log('StringFilter: id is', id); + + function HandleChangeLocal(e) { + const filterType = document.getElementById(`filterType-${id}`).value; + const filterValue = document.getElementById(`filterValue-${id}`).value; + // Construct search string + // let result = name; + // result +=':' + filterType + ':' + filterValue; + let result = { name, type: filterType, value: filterValue }; + onChangeHandler(result); + } + + const options = [ + { value: "empty", label: "--"}, + { value: "contains", label: "contains"}, + { value: "not-contains", label: "does not contain"}, + ] + + return ( +
+ +
+ ); +} From 1fbb7ba659c0fd462d4747c426dda466045e640c Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 17:28:10 -0700 Subject: [PATCH 07/47] config-ip-filter: Add "Clear Filters" button. --- build/app/view/netcreate/components/filter/FiltersPanel.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index 85da6782..18df25d6 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -36,6 +36,8 @@ class FiltersPanel extends UNISYS.Component { OnFilterChange (filter) { console.log('onFilterChange', filter); UDATA.LocalCall('FILTER', filter); + OnClearBtnClick() { + UDATA.LocalCall('FILTER', {action: 'clear'}); } render() { @@ -44,6 +46,7 @@ class FiltersPanel extends UNISYS.Component {
+
) } From 38057f3ec45a6c2cf4ad03faf3d986bb243df784 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 17:29:21 -0700 Subject: [PATCH 08/47] config-ip-filter: Show multiple filter groups. --- .../components/filter/FiltersPanel.jsx | 71 ++++++++++++++++--- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index 18df25d6..16cb6bf1 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -3,7 +3,7 @@ import React from 'react'; import StringFilter from './StringFilter'; const ReactStrap = require('reactstrap'); -const { Input, Label } = ReactStrap; +const { Button, Input, Label } = ReactStrap; const UNISYS = require('unisys/client'); var UDATA = null; @@ -14,12 +14,55 @@ var UDATA = null; // class FiltersPanel extends React.Component { // Dummy Data -const filterData = { - id: '1', - name: 'Label', - type: 'contains', - value: 'tacitus' -}; +// const filterData = { +// id: '1', +// name: 'Label', +// type: 'contains', +// value: 'tacitus' +// }; +const filtersDefs = [ + { + label: "Nodes", + filters: [ + { + id: '1', + name: 'Label', + type: 'contains', + value: 'tacitus' + }, + { + id: '2', + name: 'Type', + type: 'not-contains', + value: 'person' + } + ] + }, + { + label: "Edges", + filters: [ + { + id: '1', + name: 'Source', + type: 'contains', + value: 'tacitus' + }, + { + id: '2', + name: 'Type', + type: 'not-contains', + value: 'is related to' + }, + { + id: '3', + name: 'Target', + type: 'contains', + value: 'Rome' + } + ] + } +]; + class FiltersPanel extends UNISYS.Component { constructor({ filterGroups, onFiltersChange, tableHeight }) { @@ -44,8 +87,18 @@ class FiltersPanel extends UNISYS.Component { const { tableHeight } = this.props; return (
- + style={{ + overflow: 'auto', position: 'relative', display: 'block', + maxHeight: tableHeight, + backgroundColor:'rgba(0,0,0,0.1)' + }}> +
+ {filtersDefs.map(def => )} +
) From 18012f22b6f090a374334348a4691c7502b460c4 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 17:30:55 -0700 Subject: [PATCH 09/47] config-ip-filter: Add filter actions. --- .../components/filter/FiltersPanel.jsx | 7 ++-- build/app/view/netcreate/nc-logic.js | 34 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index 16cb6bf1..4d360e53 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -71,14 +71,17 @@ class FiltersPanel extends UNISYS.Component { console.log('filters is', filterGroups) this.OnFilterChange = this.OnFilterChange.bind(this); + this.OnClearBtnClick = this.OnClearBtnClick.bind(this); /// Initialize UNISYS DATA LINK for REACT UDATA = UNISYS.NewDataLink(this); } // constructor - OnFilterChange (filter) { + OnFilterChange(filter) { console.log('onFilterChange', filter); - UDATA.LocalCall('FILTER', filter); + UDATA.LocalCall('FILTER', { action: 'filter-nodes', filter }); + } + OnClearBtnClick() { UDATA.LocalCall('FILTER', {action: 'clear'}); } diff --git a/build/app/view/netcreate/nc-logic.js b/build/app/view/netcreate/nc-logic.js index 24c3293b..d2aa80fd 100644 --- a/build/app/view/netcreate/nc-logic.js +++ b/build/app/view/netcreate/nc-logic.js @@ -687,19 +687,41 @@ function m_HandleAutoCompleteSelect(data) { /** * - * @param {object} filter {name, type, value} + * @param {object} data {action, filter} * */ -function m_HandleFilter(filter) { - console.log('HandleFilter!', filter); +function m_HandleFilter(data) { + console.log('HandleFilter!', data); + + if (data.action === undefined) throw "m_HandleFilter called without action"; + + switch (data.action) { + case 'clear': + m_ClearFilters(); + break; + case 'filter-nodes': + default: + m_FilterNodes(data.filter.value); + break; + } // Filter Nodes and Edges - let marked = { isFilteredOut: true }; - let normal = { isFilteredOut: false }; - m_SetMatchingByNodeLabel(filter.value, marked, normal); + // hard coding filter by node for now. + UDATA.SetAppState("D3DATA", D3DATA); } +function m_FilterNodes(nodeLabelSnippet) { + const marked = { isFilteredOut: true }; + const normal = { isFilteredOut: false }; + m_SetMatchingByNodeLabel(nodeLabelSnippet, marked, normal); +} +function m_ClearFilters() { + const props = { isFilteredOut: false }; + m_SetAllObjs(D3DATA.nodes, props); + m_SetAllObjs(D3DATA.edges, props); +} + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ lifecycle RESET handler /*/ From 06fadef4be95d45c2bef890f09eee14d00ada68f Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Sun, 9 Aug 2020 17:39:32 -0700 Subject: [PATCH 10/47] config-ip-filter: Enumerate filter constants. --- .../components/filter/FilterEnums.js | 13 ++++++++++ .../components/filter/FiltersPanel.jsx | 5 ++-- .../components/filter/StringFilter.jsx | 26 ++++++++++--------- build/app/view/netcreate/nc-logic.js | 11 ++++---- 4 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 build/app/view/netcreate/components/filter/FilterEnums.js diff --git a/build/app/view/netcreate/components/filter/FilterEnums.js b/build/app/view/netcreate/components/filter/FilterEnums.js new file mode 100644 index 00000000..23e548d8 --- /dev/null +++ b/build/app/view/netcreate/components/filter/FilterEnums.js @@ -0,0 +1,13 @@ +const FILTER = {}; + +FILTER.ACTIONS = {}; +FILTER.ACTIONS.CLEAR = "clear"; +FILTER.ACTIONS.FILTER_NODES = "filter-nodes"; +FILTER.ACTIONS.FILTER_EDGES = "filter-edges"; + +FILTER.STRING_OPERATORS = {}; +FILTER.STRING_OPERATORS.EMPTY = "empty"; +FILTER.STRING_OPERATORS.CONTAINS = "contains"; +FILTER.STRING_OPERATORS.NOT_CONTAINS = "not-contains"; + +module.exports = FILTER; diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index 4d360e53..c56764ea 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -1,4 +1,5 @@ import FilterGroup from './FilterGroup'; +import FILTER from './FilterEnums'; import React from 'react'; import StringFilter from './StringFilter'; const ReactStrap = require('reactstrap'); @@ -79,11 +80,11 @@ class FiltersPanel extends UNISYS.Component { OnFilterChange(filter) { console.log('onFilterChange', filter); - UDATA.LocalCall('FILTER', { action: 'filter-nodes', filter }); + UDATA.LocalCall('FILTER', { action: FILTER.ACTIONS.FILTER_NODES , filter }); } OnClearBtnClick() { - UDATA.LocalCall('FILTER', {action: 'clear'}); + UDATA.LocalCall('FILTER', {action: FILTER.ACTIONS.CLEAR }); } render() { diff --git a/build/app/view/netcreate/components/filter/StringFilter.jsx b/build/app/view/netcreate/components/filter/StringFilter.jsx index 5d6e48fe..9815a2b8 100644 --- a/build/app/view/netcreate/components/filter/StringFilter.jsx +++ b/build/app/view/netcreate/components/filter/StringFilter.jsx @@ -1,6 +1,7 @@ +import FILTER from './FilterEnums'; import React from 'react'; const ReactStrap = require('reactstrap'); -const { Input, Label } = ReactStrap; +const { Form, FormGroup, Input, Label } = ReactStrap; export default function StringFilter({ filter: { id, name, type, value }, onChangeHandler }) { @@ -16,20 +17,21 @@ export default function StringFilter({ filter: { id, name, type, value }, onChan onChangeHandler(result); } - const options = [ - { value: "empty", label: "--"}, - { value: "contains", label: "contains"}, - { value: "not-contains", label: "does not contain"}, + const OPERATORS = [ + { value: FILTER.STRING_OPERATORS.EMPTY, label: "--"}, + { value: FILTER.STRING_OPERATORS.CONTAINS, label: "contains"}, + { value: FILTER.STRING_OPERATORS.NOT_CONTAINS, label: "does not contain"}, ] return ( -
-
+ + + ); } diff --git a/build/app/view/netcreate/nc-logic.js b/build/app/view/netcreate/nc-logic.js index d2aa80fd..062390b8 100644 --- a/build/app/view/netcreate/nc-logic.js +++ b/build/app/view/netcreate/nc-logic.js @@ -39,6 +39,7 @@ const DBG = false; /// LIBRARIES ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import FILTER from './components/filter/FilterEnums'; const SETTINGS = require("settings"); const UNISYS = require("unisys/client"); const JSCLI = require("system/util/jscli"); @@ -696,18 +697,16 @@ function m_HandleFilter(data) { if (data.action === undefined) throw "m_HandleFilter called without action"; switch (data.action) { - case 'clear': + case FILTER.ACTIONS.CLEAR: m_ClearFilters(); break; - case 'filter-nodes': + case FILTER.ACTIONS.FILTER_EDGES: + break; + case FILTER.ACTIONS.FILTER_NODES: default: m_FilterNodes(data.filter.value); break; } - - // Filter Nodes and Edges - // hard coding filter by node for now. - UDATA.SetAppState("D3DATA", D3DATA); } From b6ec3fd66e87c94abba314ad63c8b71306028302 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Mon, 10 Aug 2020 14:04:44 -0700 Subject: [PATCH 11/47] config-ip-filter: Move `filter-logic` to its own module. --- build/app/view/netcreate/filter-logic.js | 151 +++++++++++++++++++++++ build/app/view/netcreate/nc-logic.js | 102 ++------------- 2 files changed, 158 insertions(+), 95 deletions(-) create mode 100644 build/app/view/netcreate/filter-logic.js diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js new file mode 100644 index 00000000..838862cd --- /dev/null +++ b/build/app/view/netcreate/filter-logic.js @@ -0,0 +1,151 @@ +/*//////////////////////////////// ABOUT \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*\ + + FILTER LOGIC + +\*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ + +const DBG = false; + +/// LIBRARIES ///////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +import FILTER from './components/filter/FilterEnums'; +const UNISYS = require("unisys/client"); + +/// INITIALIZE MODULE ///////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +var MOD = UNISYS.NewModule(module.id); +var UDATA = UNISYS.NewDataLink(MOD); + +/// APP STATE/DATA STRUCTURES ///////////////////////////////////////////////// +var D3DATA = null; // see above for description +var TEMPLATE = null; // template definition for prompts +const PROMPTS = require("system/util/prompts"); +const NCLOGIC = require("./nc-logic"); + +/// CONSTANTS ///////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const DATASET = window.NC_CONFIG.dataset || "netcreate"; +const TEMPLATE_URL = `templates/${DATASET}.json`; + + +/// UNISYS HANDLERS /////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ lifecycle INITIALIZE handler +/*/ +MOD.Hook("INITIALIZE", () => { + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ FILTER is called by FiltersPanel when user has updated filter. + /*/ + UDATA.HandleMessage("FILTER", function(data) { + D3DATA = UDATA.AppState("D3DATA"); // 8/10/20 REVIEW: Is this the best way to get current data? + m_HandleFilter(data); + }); + +}); // end UNISYS_INIT + + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ INIT HANDLERS +/*/ + +/** + * + * @param {object} data {action, filter} + * + */ +function m_HandleFilter(data) { + console.log('HandleFilter!', data); + + if (data.action === undefined) throw "m_HandleFilter called without action"; + + switch (data.action) { + case FILTER.ACTIONS.CLEAR: + m_ClearFilters(); + break; + case FILTER.ACTIONS.FILTER_EDGES: + break; + case FILTER.ACTIONS.FILTER_NODES: + default: + m_FilterNodes(data.filter.value); + break; + } + UDATA.SetAppState("D3DATA", D3DATA); +} + +function m_FilterNodes(nodeLabelSnippet) { + const marked = { isFilteredOut: true }; + const normal = { isFilteredOut: false }; + m_SetMatchingByNodeLabel(nodeLabelSnippet, marked, normal); +} +function m_ClearFilters() { + const props = { isFilteredOut: false }; + NCLOGIC.SetAllObjs(D3DATA.nodes, props); + NCLOGIC.SetAllObjs(D3DATA.edges, props); +} + + + +/// OBJECT HELPERS //////////////////////////////////////////////////////////// + + +/// NODE HELPERS ////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. + All others nodes are set to 'no' props. Return matches. + Optionally resets all the NON matching nodes as well. + + Edges are matched if they link to the node. +/*/ +function m_SetMatchingByNodeLabel(str = "", yes = {}, no = {}) { + let returnMatches = []; + str = NCLOGIC.EscapeRegexChars(str.trim()); + if (str === "") return undefined; + const regex = new RegExp(/*'^'+*/ str, "i"); + // First find the nodes + D3DATA.nodes.forEach(node => { + if (regex.test(node.label)) { + for (let key in yes) node[key] = yes[key]; + returnMatches.push(node); + } else { + for (let key in no) node[key] = no[key]; + } + }); + // Then hide all related edges + m_SetMatchingEdgesByNodes(returnMatches, yes, no); + return returnMatches; +} + +/** + * Set edges that link to any node in nodeIDs to 'yes' props. + * All others nodes are set to 'no' props. Return matches. + * + * We set look for ALL nodes at once otherwise, one node can unset + * antoher node. + * + * This is a specialized function because edges need to be matched + * against both source and target. + * + * @param {Array} nodes Array of node objects + * @param {Object} yes e.g. marked = { isFilteredOut: true }; + * @param {Object} no e.g. normal = { isFilteredOut: false }; + */ +function m_SetMatchingEdgesByNodes(nodes, yes = {}, no = {}) { + const nodeIDs = nodes.map(node => node.id); + let returnMatches = []; + D3DATA.edges.forEach(edge => { + if ( nodeIDs.includes(edge.source.id) || nodeIDs.includes(edge.target.id) ) { + for (let key in yes) edge[key] = yes[key]; + returnMatches.push(edge); + } else { + for (let key in no) edge[key] = no[key]; + } + }); + return returnMatches; +} + + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/// EXPORT CLASS DEFINITION /////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +module.exports = MOD; diff --git a/build/app/view/netcreate/nc-logic.js b/build/app/view/netcreate/nc-logic.js index 062390b8..16b742fd 100644 --- a/build/app/view/netcreate/nc-logic.js +++ b/build/app/view/netcreate/nc-logic.js @@ -39,7 +39,6 @@ const DBG = false; /// LIBRARIES ///////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -import FILTER from './components/filter/FilterEnums'; const SETTINGS = require("settings"); const UNISYS = require("unisys/client"); const JSCLI = require("system/util/jscli"); @@ -670,15 +669,13 @@ MOD.Hook("INITIALIZE", () => { m_HandleAutoCompleteSelect(data); }); - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ FILTER is called by FiltersPanel when user has updated filter. - /*/ - UDATA.HandleMessage("FILTER", function(data) { - m_HandleFilter(data); - }); - }); // end UNISYS_INIT + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ INIT HANDLERS +/*/ + function m_HandleAutoCompleteSelect(data) { if (DBG) console.log("ACL: Setting activeAutoCompleteId to", data.id); UDATA.SetAppState("ACTIVEAUTOCOMPLETE", { @@ -686,41 +683,6 @@ function m_HandleAutoCompleteSelect(data) { }); } -/** - * - * @param {object} data {action, filter} - * - */ -function m_HandleFilter(data) { - console.log('HandleFilter!', data); - - if (data.action === undefined) throw "m_HandleFilter called without action"; - - switch (data.action) { - case FILTER.ACTIONS.CLEAR: - m_ClearFilters(); - break; - case FILTER.ACTIONS.FILTER_EDGES: - break; - case FILTER.ACTIONS.FILTER_NODES: - default: - m_FilterNodes(data.filter.value); - break; - } - UDATA.SetAppState("D3DATA", D3DATA); -} - -function m_FilterNodes(nodeLabelSnippet) { - const marked = { isFilteredOut: true }; - const normal = { isFilteredOut: false }; - m_SetMatchingByNodeLabel(nodeLabelSnippet, marked, normal); -} -function m_ClearFilters() { - const props = { isFilteredOut: false }; - m_SetAllObjs(D3DATA.nodes, props); - m_SetAllObjs(D3DATA.edges, props); -} - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ lifecycle RESET handler /*/ @@ -827,6 +789,7 @@ function m_SetAllObjs(obj_list, all = {}) { for (let key in all) obj[key] = all[key]; }); } +MOD.SetAllObj = m_SetAllObjs; // Expose for filter-logic.js /// NODE HELPERS ////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -887,59 +850,7 @@ function m_SetMatchingNodesByLabel(str = "", yes = {}, no = {}) { }); return returnMatches; } -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. - All others nodes are set to 'no' props. Return matches. - Optionally resets all the NON matching nodes as well. - - Edges are matched if they link to the node. -/*/ -function m_SetMatchingByNodeLabel(str = "", yes = {}, no = {}) { - let returnMatches = []; - str = u_EscapeRegexChars(str.trim()); - if (str === "") return undefined; - const regex = new RegExp(/*'^'+*/ str, "i"); - // First find the nodes - D3DATA.nodes.forEach(node => { - if (regex.test(node.label)) { - for (let key in yes) node[key] = yes[key]; - returnMatches.push(node); - } else { - for (let key in no) node[key] = no[key]; - } - }); - // Then hide all related edges - m_SetMatchingEdgesByNodes(returnMatches, yes, no); - return returnMatches; -} -/** - * Set edges that link to any node in nodeIDs to 'yes' props. - * All others nodes are set to 'no' props. Return matches. - * - * We set look for ALL nodes at once otherwise, one node can unset - * antoher node. - * - * This is a specialized function because edges need to be matched - * against both source and target. - * - * @param {Array} nodes Array of node objects - * @param {Object} yes e.g. marked = { isFilteredOut: true }; - * @param {Object} no e.g. normal = { isFilteredOut: false }; - */ -function m_SetMatchingEdgesByNodes(nodes, yes = {}, no = {}) { - const nodeIDs = nodes.map(node => node.id); - let returnMatches = []; - D3DATA.edges.forEach(edge => { - if ( nodeIDs.includes(edge.source.id) || nodeIDs.includes(edge.target.id) ) { - for (let key in yes) edge[key] = yes[key]; - returnMatches.push(edge); - } else { - for (let key in no) edge[key] = no[key]; - } - }); - return returnMatches; -} /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1025,6 +936,7 @@ const REGEX_REGEXCHARS = /[.*+?^${}()|[\]\\]/g; function u_EscapeRegexChars(string) { return string.replace(REGEX_REGEXCHARS, "\\$&"); // $& means the whole matched string } +MOD.EscapeRegexChars = u_EscapeRegexChars; // Expose for filter-logic.js /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Convert all IDs to integers Node and Edge IDs should be integers. From ca7b888a2aede5286aa53bbe2acf39f61833185b Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Mon, 10 Aug 2020 15:31:47 -0700 Subject: [PATCH 12/47] config-ip-filter: Add separate key label filter parameters. (template keys have a human friendly label). --- .../components/filter/FiltersPanel.jsx | 35 +++++++++++++------ .../components/filter/StringFilter.jsx | 33 +++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index c56764ea..b6eb419d 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -1,11 +1,12 @@ -import FilterGroup from './FilterGroup'; import FILTER from './FilterEnums'; +import FilterGroup from './FilterGroup'; import React from 'react'; import StringFilter from './StringFilter'; const ReactStrap = require('reactstrap'); const { Button, Input, Label } = ReactStrap; + const UNISYS = require('unisys/client'); var UDATA = null; @@ -27,15 +28,24 @@ const filtersDefs = [ filters: [ { id: '1', - name: 'Label', - type: 'contains', + key: 'label', + keylabel: 'Label', + operator: 'contains', value: 'tacitus' }, { id: '2', - name: 'Type', - type: 'not-contains', + key: 'type', + keylabel: 'Type', + operator: 'contains', value: 'person' + }, + { + id: '3', + key: 'notes', + keylabel: 'Significance', + operator: 'contains', + value: 'xxx' } ] }, @@ -44,20 +54,23 @@ const filtersDefs = [ filters: [ { id: '1', - name: 'Source', - type: 'contains', + key: 'source', + keylabel: 'Source', + operator: 'contains', value: 'tacitus' }, { id: '2', - name: 'Type', - type: 'not-contains', + key: 'type', + keylabel: 'Type', + operator: 'not-contains', value: 'is related to' }, { id: '3', - name: 'Target', - type: 'contains', + key: 'target', + keylabel: 'Target', + operator: 'contains', value: 'Rome' } ] diff --git a/build/app/view/netcreate/components/filter/StringFilter.jsx b/build/app/view/netcreate/components/filter/StringFilter.jsx index 9815a2b8..3cc4492c 100644 --- a/build/app/view/netcreate/components/filter/StringFilter.jsx +++ b/build/app/view/netcreate/components/filter/StringFilter.jsx @@ -3,7 +3,22 @@ import React from 'react'; const ReactStrap = require('reactstrap'); const { Form, FormGroup, Input, Label } = ReactStrap; -export default function StringFilter({ filter: { id, name, type, value }, onChangeHandler }) { +export default function StringFilter({ + filter: { + id, + key, + keylabel, // human friendly display name for the key. This can be customized in the template. + operator, + value + }, + onChangeHandler +}) { + + const OPERATORS = [ + { value: FILTER.STRING_OPERATORS.EMPTY, label: "--"}, + { value: FILTER.STRING_OPERATORS.CONTAINS, label: "contains"}, + { value: FILTER.STRING_OPERATORS.NOT_CONTAINS, label: "does not contain"}, + ] console.log('StringFilter: id is', id); @@ -13,22 +28,18 @@ export default function StringFilter({ filter: { id, name, type, value }, onChan // Construct search string // let result = name; // result +=':' + filterType + ':' + filterValue; - let result = { name, type: filterType, value: filterValue }; + let result = { key, operator: filterType, value: filterValue }; onChangeHandler(result); } - const OPERATORS = [ - { value: FILTER.STRING_OPERATORS.EMPTY, label: "--"}, - { value: FILTER.STRING_OPERATORS.CONTAINS, label: "contains"}, - { value: FILTER.STRING_OPERATORS.NOT_CONTAINS, label: "does not contain"}, - ] - return (
- - - {OPERATORS.map(op => )} + + + {OPERATORS.map(op => + + )} From c3b304bd8adc9f74ed3d5bfe3404b76a1e25a89f Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Mon, 10 Aug 2020 15:35:27 -0700 Subject: [PATCH 13/47] config-ip-filter: Filter by different template node field keys. --- build/app/view/netcreate/filter-logic.js | 30 ++++++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js index 838862cd..9d280126 100644 --- a/build/app/view/netcreate/filter-logic.js +++ b/build/app/view/netcreate/filter-logic.js @@ -50,7 +50,7 @@ MOD.Hook("INITIALIZE", () => { /** * - * @param {object} data {action, filter} + * @param {Object} data {action, filter} * */ function m_HandleFilter(data) { @@ -66,17 +66,37 @@ function m_HandleFilter(data) { break; case FILTER.ACTIONS.FILTER_NODES: default: - m_FilterNodes(data.filter.value); + m_FilterNodes(data.filter); break; } UDATA.SetAppState("D3DATA", D3DATA); } -function m_FilterNodes(nodeLabelSnippet) { +/** + * + * @param {Object} filter {id, key, operator, value} + */ +function m_FilterNodes(filter) { + if ((filter.key === undefined) || + (filter.operator === undefined) || + (filter.value === undefined)) throw `Bad filter ${filter}`; + const marked = { isFilteredOut: true }; const normal = { isFilteredOut: false }; - m_SetMatchingByNodeLabel(nodeLabelSnippet, marked, normal); + + switch (filter.operator) { + case FILTER.STRING_OPERATORS.CONTAINS: + m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); + break; + default: + throw `Unknown filter operator ${filter.operator}`; + break; + } + } + + + function m_ClearFilters() { const props = { isFilteredOut: false }; NCLOGIC.SetAllObjs(D3DATA.nodes, props); @@ -116,7 +136,7 @@ function m_SetMatchingByNodeLabel(str = "", yes = {}, no = {}) { } /** - * Set edges that link to any node in nodeIDs to 'yes' props. + * Set edges that link to any node in nodes to 'yes' props. * All others nodes are set to 'no' props. Return matches. * * We set look for ALL nodes at once otherwise, one node can unset From 89cfa841f42b1f7fbfad958d3c3fd0b069e10392 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Mon, 10 Aug 2020 15:37:10 -0700 Subject: [PATCH 14/47] config-ip-filter: Add HACK to convert old attribute data. (Historically secondary parameters were placed under an 'attributes' object). --- build/app/view/netcreate/filter-logic.js | 42 +++++++++++++++++++----- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js index 9d280126..10072f91 100644 --- a/build/app/view/netcreate/filter-logic.js +++ b/build/app/view/netcreate/filter-logic.js @@ -110,20 +110,46 @@ function m_ClearFilters() { /// NODE HELPERS ////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. - All others nodes are set to 'no' props. Return matches. - Optionally resets all the NON matching nodes as well. - - Edges are matched if they link to the node. -/*/ -function m_SetMatchingByNodeLabel(str = "", yes = {}, no = {}) { +/** + * Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. + * All others nodes are set to 'no' props. Return matches. + * Optionally resets all the NON matching nodes as well. + * + * Edges are matched if they link to the node. + * + * @param {String} keyToSet The template key of the node parameter we want to set, + * e.g. "label" + * @param {String} str The string to search for + * @param {Object} yes e.g. marked = { isFilteredOut: true }; + * @param {Object} no e.g. normal = { isFilteredOut: false }; + */ +const HACKMAP = { + type: "Node_Type", + info: "Extra Info", + notes: "Notes" +} +function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}) { let returnMatches = []; str = NCLOGIC.EscapeRegexChars(str.trim()); if (str === "") return undefined; const regex = new RegExp(/*'^'+*/ str, "i"); // First find the nodes D3DATA.nodes.forEach(node => { - if (regex.test(node.label)) { + + let nodeField = node[keyToSet]; + + // HACK + // The old data model has secondary keys stuffed + // into an `attributes` object. This is a + // holdover from the original pre-netcreate + // data import. If we ever change the data format + // this HACKMAP should be removed. + if (['type', 'info', 'notes'].includes(keyToSet)) { + nodeField = node.attributes[HACKMAP[keyToSet]]; + } + + // Regular Test + if (regex.test(nodeField)) { for (let key in yes) node[key] = yes[key]; returnMatches.push(node); } else { From 58400f8edc3ff6cd8fb07e8a342c8cafbd9265c2 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Mon, 10 Aug 2020 16:16:14 -0700 Subject: [PATCH 15/47] config-ip-filter: Add 'not-contains' filter. --- build/app/view/netcreate/filter-logic.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js index 10072f91..d5848a0a 100644 --- a/build/app/view/netcreate/filter-logic.js +++ b/build/app/view/netcreate/filter-logic.js @@ -65,9 +65,11 @@ function m_HandleFilter(data) { case FILTER.ACTIONS.FILTER_EDGES: break; case FILTER.ACTIONS.FILTER_NODES: - default: m_FilterNodes(data.filter); break; + default: + throw `Unknown filter action ${data.action}`; + break; } UDATA.SetAppState("D3DATA", D3DATA); } @@ -88,6 +90,9 @@ function m_FilterNodes(filter) { case FILTER.STRING_OPERATORS.CONTAINS: m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); break; + case FILTER.STRING_OPERATORS.NOT_CONTAINS: + m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); + break; default: throw `Unknown filter operator ${filter.operator}`; break; @@ -115,6 +120,8 @@ function m_ClearFilters() { * All others nodes are set to 'no' props. Return matches. * Optionally resets all the NON matching nodes as well. * + * If matched, we set `isFiltered` flag to true. + * * Edges are matched if they link to the node. * * @param {String} keyToSet The template key of the node parameter we want to set, @@ -122,20 +129,20 @@ function m_ClearFilters() { * @param {String} str The string to search for * @param {Object} yes e.g. marked = { isFilteredOut: true }; * @param {Object} no e.g. normal = { isFilteredOut: false }; + * @param {Bool} contains regex text contains or not contains */ const HACKMAP = { type: "Node_Type", info: "Extra Info", notes: "Notes" } -function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}) { +function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = true) { let returnMatches = []; str = NCLOGIC.EscapeRegexChars(str.trim()); if (str === "") return undefined; const regex = new RegExp(/*'^'+*/ str, "i"); // First find the nodes D3DATA.nodes.forEach(node => { - let nodeField = node[keyToSet]; // HACK @@ -148,8 +155,14 @@ function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}) { nodeField = node.attributes[HACKMAP[keyToSet]]; } - // Regular Test - if (regex.test(nodeField)) { + let matches; + if (contains) { + matches = !regex.test(nodeField); + } else { + matches = regex.test(nodeField); + } + + if (matches) { for (let key in yes) node[key] = yes[key]; returnMatches.push(node); } else { From 1c4b521c78e7124e3511c5f0081744476b0e38d1 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Wed, 12 Aug 2020 12:01:57 -0700 Subject: [PATCH 16/47] config-ip-filter: Add transitiion animation to opacity change. --- build/app/view/netcreate/components/d3-simplenetgraph.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build/app/view/netcreate/components/d3-simplenetgraph.js b/build/app/view/netcreate/components/d3-simplenetgraph.js index 62c90229..6d16bd8e 100644 --- a/build/app/view/netcreate/components/d3-simplenetgraph.js +++ b/build/app/view/netcreate/components/d3-simplenetgraph.js @@ -411,6 +411,8 @@ class D3NetGraph { d.size = radius // save the calculated size return this.defaultSize + (this.defaultSize * d.weight / 2) }) + .transition() + .duration(500) .style("opacity", d => { // console.log(d); return d.isFilteredOut ? 0 : 1.0 @@ -431,6 +433,8 @@ class D3NetGraph { return undefined; // don't set font weight }) .text((d) => { return d.label }) // in case text is updated + .transition() + .duration(500) .style("opacity", d => { return d.isFilteredOut ? 0 : 1.0 }); @@ -468,6 +472,8 @@ class D3NetGraph { .classed("selected", (d) => { return d.selected }) .style('stroke', this._UpdateLinkStrokeColor) .style('stroke-width', this._UpdateLinkStrokeWidth) + .transition() + .duration(500) .style("opacity", d => { return d.isFilteredOut ? 0 : 1.0 }); From 9567e811b7173822e470d779da7cdde6c8449f82 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Wed, 12 Aug 2020 12:05:23 -0700 Subject: [PATCH 17/47] config-ip-filter: Rename operator "EMPTY" as "NO_OP" because changing operation to blank needs to trigger an action as well. --- build/app/view/netcreate/components/filter/FilterEnums.js | 2 +- build/app/view/netcreate/components/filter/StringFilter.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/app/view/netcreate/components/filter/FilterEnums.js b/build/app/view/netcreate/components/filter/FilterEnums.js index 23e548d8..0c9fa1ee 100644 --- a/build/app/view/netcreate/components/filter/FilterEnums.js +++ b/build/app/view/netcreate/components/filter/FilterEnums.js @@ -6,7 +6,7 @@ FILTER.ACTIONS.FILTER_NODES = "filter-nodes"; FILTER.ACTIONS.FILTER_EDGES = "filter-edges"; FILTER.STRING_OPERATORS = {}; -FILTER.STRING_OPERATORS.EMPTY = "empty"; +FILTER.STRING_OPERATORS.NO_OP = "no-op"; FILTER.STRING_OPERATORS.CONTAINS = "contains"; FILTER.STRING_OPERATORS.NOT_CONTAINS = "not-contains"; diff --git a/build/app/view/netcreate/components/filter/StringFilter.jsx b/build/app/view/netcreate/components/filter/StringFilter.jsx index 3cc4492c..1d3b29fe 100644 --- a/build/app/view/netcreate/components/filter/StringFilter.jsx +++ b/build/app/view/netcreate/components/filter/StringFilter.jsx @@ -15,7 +15,6 @@ export default function StringFilter({ }) { const OPERATORS = [ - { value: FILTER.STRING_OPERATORS.EMPTY, label: "--"}, { value: FILTER.STRING_OPERATORS.CONTAINS, label: "contains"}, { value: FILTER.STRING_OPERATORS.NOT_CONTAINS, label: "does not contain"}, ] @@ -30,6 +29,7 @@ export default function StringFilter({ // result +=':' + filterType + ':' + filterValue; let result = { key, operator: filterType, value: filterValue }; onChangeHandler(result); + { value: FILTER.STRING_OPERATORS.NO_OP, label: "--"}, } return ( From 21ec7cf3965bc523a46a0bd477d62f3b3dc1b226 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Wed, 12 Aug 2020 12:14:20 -0700 Subject: [PATCH 18/47] config-ip-filter: Rough first pass at working filter. Only the first Node filter works. Edge filtering is not implemented yet. --- build/app/view/netcreate/filter-logic.js | 152 ++++++++++++++++++++--- build/app/view/netcreate/nc-logic.js | 2 +- 2 files changed, 139 insertions(+), 15 deletions(-) diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js index d5848a0a..b3ea49ed 100644 --- a/build/app/view/netcreate/filter-logic.js +++ b/build/app/view/netcreate/filter-logic.js @@ -2,6 +2,12 @@ FILTER LOGIC + + FILTERDEFS = { + nodes: { ...filters }, + edges: { ...filters } + } + \*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ * //////////////////////////////////////*/ const DBG = false; @@ -17,7 +23,6 @@ var MOD = UNISYS.NewModule(module.id); var UDATA = UNISYS.NewDataLink(MOD); /// APP STATE/DATA STRUCTURES ///////////////////////////////////////////////// -var D3DATA = null; // see above for description var TEMPLATE = null; // template definition for prompts const PROMPTS = require("system/util/prompts"); const NCLOGIC = require("./nc-logic"); @@ -33,11 +38,25 @@ const TEMPLATE_URL = `templates/${DATASET}.json`; /*/ lifecycle INITIALIZE handler /*/ MOD.Hook("INITIALIZE", () => { + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ FILTER_SET is called by StringFilter when user has updated filter. + /*/ + UDATA.HandleMessage("FILTER_SET", data => { + m_HandleFilterSet(data); + }) + + // is this the right listner? + UDATA.OnAppStateChange("FILTERDEFS", data => { + console.error('OnAppStateChange: FILTER', data); + m_HandleFilterDefsUpdate(data); + }); + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ FILTER is called by FiltersPanel when user has updated filter. + This triggers the actual filtering. /*/ - UDATA.HandleMessage("FILTER", function(data) { - D3DATA = UDATA.AppState("D3DATA"); // 8/10/20 REVIEW: Is this the best way to get current data? + UDATA.HandleMessage("FILTER", data => { m_HandleFilter(data); }); @@ -48,25 +67,87 @@ MOD.Hook("INITIALIZE", () => { /*/ INIT HANDLERS /*/ +/** + * + * @param {Object} data {defs} + */ +function m_HandleFilterDefsUpdate(data) { + m_ApplyFilters(data); +} + +/** + * Define an individual filter + * @param {Object} data {group, filter} + */ +function m_HandleFilterSet(data) { + console.error('received', data); + + const FILTERDEFS = UDATA.AppState("FILTERDEFS").defs; // already an object + console.error('FILTERDEFS is', FILTERDEFS); + + // assume node for now + // should be checking data.group to determine which set to use + let nodeFilters = FILTERDEFS[0].filters; + + // set it + const index = nodeFilters.findIndex(f => f.id === data.filter.id); + nodeFilters.splice(index, 1, data.filter); + FILTERDEFS[0].filters = nodeFilters; + + console.log('FILTERDEFS spliced is now', FILTERDEFS); // already an object + UDATA.SetAppState("FILTERDEFS", { defs: FILTERDEFS }); + // UDATA.LocalCall('FILTERDEFS_UPDATED', FILTERDEFS); +} + +/** + * Walk down the list of filters and apply them all + */ +function m_ApplyFilters(data) { + // HACK + // just grab the first filter for now while we figure + // out how to handle the whole round trip + // eventually need to apply ALL filters + // data.defs[0] = nodes + // data.defs[0][0] = first filter + // const filter = data.defs[0].filters[0]; + // m_HandleFilter({ + // action: FILTER.ACTIONS.FILTER_NODES, + // filter + // }); + + // hack in selection for now + const nodeFilters = data.defs[0].filters; + const edgeFilters = data.defs[1].filters; + + nodeFilters.forEach(filter => { + m_HandleFilter({ action: FILTER.ACTIONS.FILTER_NODES, filter}) + }) + edgeFilters.forEach(filter => { + m_HandleFilter({ action: FILTER.ACTIONS.FILTER_EDGES, filter}) + }) +} + /** * * @param {Object} data {action, filter} * */ function m_HandleFilter(data) { - console.log('HandleFilter!', data); - + console.log('m_HandleFilter!', data); + const D3DATA = UDATA.AppState("D3DATA"); if (data.action === undefined) throw "m_HandleFilter called without action"; switch (data.action) { case FILTER.ACTIONS.CLEAR: - m_ClearFilters(); - break; - case FILTER.ACTIONS.FILTER_EDGES: + m_ClearFilters(D3DATA.nodes); + m_ClearFilters(D3DATA.edges); break; case FILTER.ACTIONS.FILTER_NODES: m_FilterNodes(data.filter); break; + case FILTER.ACTIONS.FILTER_EDGES: + m_FilterEdges(data.filter); + break; default: throw `Unknown filter action ${data.action}`; break; @@ -79,13 +160,17 @@ function m_HandleFilter(data) { * @param {Object} filter {id, key, operator, value} */ function m_FilterNodes(filter) { + console.log('...m_FilterNodes', filter); if ((filter.key === undefined) || (filter.operator === undefined) || - (filter.value === undefined)) throw `Bad filter ${filter}`; + (filter.value === undefined)) return; // nothing to filter const marked = { isFilteredOut: true }; const normal = { isFilteredOut: false }; + // FIXME + // If value is cleared, how do we clear the search? + switch (filter.operator) { case FILTER.STRING_OPERATORS.CONTAINS: m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); @@ -93,6 +178,9 @@ function m_FilterNodes(filter) { case FILTER.STRING_OPERATORS.NOT_CONTAINS: m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); break; + case FILTER.STRING_OPERATORS.NO_OP: + // ignore + break; default: throw `Unknown filter operator ${filter.operator}`; break; @@ -100,12 +188,41 @@ function m_FilterNodes(filter) { } +/** + * + * @param {Object} filter {id, key, operator, value} + */ +function m_FilterEdges(filter) { + console.log('m_FilterEdges', filter); + if ((filter.key === undefined) || + (filter.operator === undefined) || + (filter.value === undefined)) return; // nothing to filter + const marked = { isFilteredOut: true }; + const normal = { isFilteredOut: false }; -function m_ClearFilters() { + switch (filter.operator) { + case FILTER.STRING_OPERATORS.CONTAINS: + // m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); + break; + case FILTER.STRING_OPERATORS.NOT_CONTAINS: + // m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); + break; + case FILTER.STRING_OPERATORS.NO_OP: + // ignore + break; + default: + throw `Unknown filter operator ${filter.operator}`; + break; + } + +} + + +function m_ClearFilters(arr) { + console.log('Clearing Filters!!!!') const props = { isFilteredOut: false }; - NCLOGIC.SetAllObjs(D3DATA.nodes, props); - NCLOGIC.SetAllObjs(D3DATA.edges, props); + NCLOGIC.SetAllObjs(arr, props); } @@ -137,9 +254,10 @@ const HACKMAP = { notes: "Notes" } function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = true) { + const D3DATA = UDATA.AppState("D3DATA"); // 8/10/20 REVIEW: Is this the best way to get current data? let returnMatches = []; str = NCLOGIC.EscapeRegexChars(str.trim()); - if (str === "") return undefined; + const regex = new RegExp(/*'^'+*/ str, "i"); // First find the nodes D3DATA.nodes.forEach(node => { @@ -156,16 +274,21 @@ function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = } let matches; - if (contains) { + if (str === "") { + // empty string doesn't match anything + matches = false; + } else if (contains) { matches = !regex.test(nodeField); } else { matches = regex.test(nodeField); } if (matches) { + console.log('......filtering out', node.label); for (let key in yes) node[key] = yes[key]; returnMatches.push(node); } else { + console.log('......unfiltering', node.label); for (let key in no) node[key] = no[key]; } }); @@ -191,6 +314,7 @@ function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = function m_SetMatchingEdgesByNodes(nodes, yes = {}, no = {}) { const nodeIDs = nodes.map(node => node.id); let returnMatches = []; + const D3DATA = UDATA.AppState("D3DATA"); D3DATA.edges.forEach(edge => { if ( nodeIDs.includes(edge.source.id) || nodeIDs.includes(edge.target.id) ) { for (let key in yes) edge[key] = yes[key]; diff --git a/build/app/view/netcreate/nc-logic.js b/build/app/view/netcreate/nc-logic.js index 16b742fd..5ae56d72 100644 --- a/build/app/view/netcreate/nc-logic.js +++ b/build/app/view/netcreate/nc-logic.js @@ -789,7 +789,7 @@ function m_SetAllObjs(obj_list, all = {}) { for (let key in all) obj[key] = all[key]; }); } -MOD.SetAllObj = m_SetAllObjs; // Expose for filter-logic.js +MOD.SetAllObjs = m_SetAllObjs; // Expose for filter-logic.js /// NODE HELPERS ////////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 25c58f23d03b961846197cd6d15d9f323417028c Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Wed, 12 Aug 2020 15:52:54 -0700 Subject: [PATCH 19/47] config-ip-filter: Working basic mulitiple filter for nodes and edges. --- build/app/view/netcreate/NetCreate.jsx | 2 +- .../components/filter/FilterEnums.js | 6 + build/app/view/netcreate/filter-logic.js | 302 ++++++++++++++++-- 3 files changed, 274 insertions(+), 36 deletions(-) diff --git a/build/app/view/netcreate/NetCreate.jsx b/build/app/view/netcreate/NetCreate.jsx index d5eaa335..da40eda7 100644 --- a/build/app/view/netcreate/NetCreate.jsx +++ b/build/app/view/netcreate/NetCreate.jsx @@ -49,7 +49,7 @@ const Search = require('./components/Search'); const NodeSelector = require('./components/NodeSelector'); const InfoPanel = require('./components/InfoPanel'); const NCLOGIC = require('./nc-logic'); // require to bootstrap data loading - +const FILTERLOGIC = require('./filter-logic'); // handles filtering functions /// REACT COMPONENT /////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/app/view/netcreate/components/filter/FilterEnums.js b/build/app/view/netcreate/components/filter/FilterEnums.js index 0c9fa1ee..d35c12a6 100644 --- a/build/app/view/netcreate/components/filter/FilterEnums.js +++ b/build/app/view/netcreate/components/filter/FilterEnums.js @@ -1,5 +1,11 @@ const FILTER = {}; +// Special Edge Keys mapped to node objects +// Used by m_IsEdgeMatchedByFilter to find node labels +FILTER.KEY = {}; +FILTER.KEY.SOURCE = "source"; +FILTER.KEY.TARGET = "target"; + FILTER.ACTIONS = {}; FILTER.ACTIONS.CLEAR = "clear"; FILTER.ACTIONS.FILTER_NODES = "filter-nodes"; diff --git a/build/app/view/netcreate/filter-logic.js b/build/app/view/netcreate/filter-logic.js index b3ea49ed..f55e2283 100644 --- a/build/app/view/netcreate/filter-logic.js +++ b/build/app/view/netcreate/filter-logic.js @@ -32,6 +32,15 @@ const NCLOGIC = require("./nc-logic"); const DATASET = window.NC_CONFIG.dataset || "netcreate"; const TEMPLATE_URL = `templates/${DATASET}.json`; +const FILTER_MARKED = { isFilteredOut: true }; +const FILTER_NORMAL = { isFilteredOut: false }; + +// Map to convert old 'attributes' data formats +const HACKMAP = { + type: "Node_Type", + info: "Extra Info", + notes: "Notes" +} /// UNISYS HANDLERS /////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -42,14 +51,14 @@ MOD.Hook("INITIALIZE", () => { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ FILTER_SET is called by StringFilter when user has updated filter. /*/ - UDATA.HandleMessage("FILTER_SET", data => { - m_HandleFilterSet(data); + UDATA.HandleMessage("FILTER_DEFINE", data => { + m_FilterDefine(data); }) - // is this the right listner? UDATA.OnAppStateChange("FILTERDEFS", data => { console.error('OnAppStateChange: FILTER', data); - m_HandleFilterDefsUpdate(data); + // The filter defs have been updated, so apply the filters. + m_FiltersApply(data); }); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -57,52 +66,275 @@ MOD.Hook("INITIALIZE", () => { This triggers the actual filtering. /*/ UDATA.HandleMessage("FILTER", data => { - m_HandleFilter(data); + m_FilterApply(data); }); }); // end UNISYS_INIT +/// UDATA HANDLERS //////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/*/ INIT HANDLERS -/*/ - -/** - * - * @param {Object} data {defs} - */ -function m_HandleFilterDefsUpdate(data) { - m_ApplyFilters(data); -} /** * Define an individual filter * @param {Object} data {group, filter} */ -function m_HandleFilterSet(data) { - console.error('received', data); +function m_FilterDefine(data) { + console.error('FILTER_DEFINE received', data); const FILTERDEFS = UDATA.AppState("FILTERDEFS").defs; // already an object console.error('FILTERDEFS is', FILTERDEFS); - // assume node for now - // should be checking data.group to determine which set to use - let nodeFilters = FILTERDEFS[0].filters; - // set it - const index = nodeFilters.findIndex(f => f.id === data.filter.id); - nodeFilters.splice(index, 1, data.filter); - FILTERDEFS[0].filters = nodeFilters; + // HACK map to array for now + // FILTERDEFS should probably use object, not array + if (data.group === "node") { + let nodeFilters = FILTERDEFS[0].filters; + const index = nodeFilters.findIndex(f => f.id === data.filter.id); + nodeFilters.splice(index, 1, data.filter); + FILTERDEFS[0].filters = nodeFilters; + } else if (data.group === "edge") { + let edgeFilters = FILTERDEFS[1].filters; + const index = edgeFilters.findIndex(f => f.id === data.filter.id); + edgeFilters.splice(index, 1, data.filter); + FILTERDEFS[1].filters = edgeFilters; + } else { + throw `FILTER_DEFINE called with unknown group: ${data.group}`; + } console.log('FILTERDEFS spliced is now', FILTERDEFS); // already an object UDATA.SetAppState("FILTERDEFS", { defs: FILTERDEFS }); - // UDATA.LocalCall('FILTERDEFS_UPDATED', FILTERDEFS); } /** * Walk down the list of filters and apply them all + * @param {Object} data A UDATA pkt {defs} + */ +function m_FiltersApply(data) { + const FDATA = data.defs; + + // hack in selection for now + // we should update the data.defs to use objects + // rather than an array? + const nodeFilters = FDATA[0].filters; + const edgeFilters = FDATA[1].filters; + + m_FiltersApplyToNodes(nodeFilters); + m_FiltersApplyToEdges(edgeFilters); +} + + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ NODE FILTERS +/*/ + +/** + * Side effect: + * D3DATA.nodes are updated with `isFilteredOut` flags. + * + * @param {Array} filters + */ +function m_FiltersApplyToNodes(filters) { + // console.log('m_FiltersApplyNodes', filters); + const D3DATA = UDATA.AppState("D3DATA"); + D3DATA.nodes.forEach(node => { + m_FiltersApplyToNode(node, filters); + }); + UDATA.SetAppState("D3DATA", D3DATA); +} + +function m_FiltersApplyToNode(node, filters) { + // console.log('m_FiltersApplyToNode', node, filters); + let all_no_op = true; + let matched = true; + // implicit AND. ALL filters must return true. + filters.forEach(filter => { + if (filter.operator === FILTER.STRING_OPERATORS.NO_OP) return; // skip no_op + all_no_op = false; + if (!m_IsNodeMatchedByFilter(node, filter)) { + matched = false; + } + }); + if (all_no_op) { + node.isFilteredOut = false; // no filters, revert + } else { + // node is filtered out if it fails any filter tests + node.isFilteredOut = !matched; + } +} + +function m_IsNodeMatchedByFilter(node, filter) { + // console.log('...m_IsNodeMatchedByFilter', filter); + if ((filter.key === undefined) || + (filter.operator === undefined) || + (filter.value === undefined)) { + // console.log('......nothing to filter match = false'); + return false; // nothing to filter + } + + let nodeStr; + // HACK + // The old data model has secondary keys stuffed + // into an `attributes` object. This is a + // holdover from the original pre-netcreate + // data import. If we ever change the data format + // this HACKMAP should be removed. + if (['type', 'info', 'notes'].includes(filter.key)) { + nodeStr = node.attributes[HACKMAP[filter.key]]; + } else { + nodeStr = node[filter.key]; + } + switch (filter.operator) { + case FILTER.STRING_OPERATORS.CONTAINS: + return m_MatchString(filter.value, nodeStr, true); + break; + case FILTER.STRING_OPERATORS.NOT_CONTAINS: + return m_MatchString(filter.value, nodeStr, false); + break; + default: + throw `Unknown filter operator ${filter.operator}`; + break; + } +} + +function m_MatchString(pin, haystack, contains = true) { + pin = NCLOGIC.EscapeRegexChars(pin.trim()); + const regex = new RegExp(/*'^'+*/ pin, "i"); + let matches; + if (pin === "") { + // empty string matches everything + matches = true; + } else if (contains) { + matches = regex.test(haystack); + } else { + matches = !regex.test(haystack); + } + console.log('######looking for pin', pin, 'in ', haystack, ' MATCHES:', matches); + return matches; +} + + + + +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ EDGE FILTERS +/*/ + +function m_FiltersApplyToEdges(filters) { + console.log('m_FiltersApplyToEdges', filters); + const D3DATA = UDATA.AppState("D3DATA"); + D3DATA.edges.forEach(edge => { + m_FiltersApplyToEdge(edge, filters); + }); + UDATA.SetAppState("D3DATA", D3DATA); +} + +function m_FiltersApplyToEdge(edge, filters) { + console.log('m_FiltersApplyToEdge', edge, filters); + + // regardless of filter definition, + // hide edge if any attached node is filtered out. + if (edge.source.isFilteredOut || edge.target.isFilteredOut) { + console.error('...edge source or target is filtered out',edge.source,edge.target) + edge.isFilteredOut = true; // no filters, revert + return; + } + + console.log('...edge source or target is still visible'); + + let all_no_op = true; + let matched = true; + // implicit AND. ALL filters must return true. + filters.forEach(filter => { + if (filter.operator === FILTER.STRING_OPERATORS.NO_OP) return; // skip no_op + all_no_op = false; + if (!m_IsEdgeMatchedByFilter(edge, filter)) { + console.error('NO MATCH!'); + matched = false; + } + }); + if (all_no_op) { + edge.isFilteredOut = false; // no filters, revert + } else { + // edge is filtered out if it fails any filter tests + edge.isFilteredOut = !matched; + } +} + +function m_IsEdgeMatchedByFilter(edge, filter) { + console.log('...m_IsEdgeMatchedByFilter', edge[filter.key], filter.value); + if ((filter.key === undefined) || + (filter.operator === undefined) || + (filter.value === undefined)) { + console.log('......nothing to filter match = false'); + return false; // nothing to filter + } + + // edges require special handling because `source` and `target` + // point to node data, not simple strings. + let edgeStr; + switch (filter.key) { + case FILTER.KEY.SOURCE: + edgeStr = edge.source.label; + break; + case FILTER.KEY.TARGET: + edgeStr = edge.target.label; + break; + default: + edgeStr = edge[filter.key]; + break; + } + switch (filter.operator) { + case FILTER.STRING_OPERATORS.CONTAINS: + return m_MatchString(filter.value, edgeStr, true); + break; + case FILTER.STRING_OPERATORS.NOT_CONTAINS: + return m_MatchString(filter.value, edgeStr, false); + break; + default: + throw `Unknown filter operator ${filter.operator}`; + break; + } +} + + // if any attached node is filtered, then don't show self. + + +// Handle Clear request: FILTERS_CLEAR? + + + + + + + + + + + + + + + + + + + + + +/// FIRST PASS FILTERING ////////////////////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/// +/// DEPRECATED!!!! +/// +/// This approach applied filtering filter by filter. +/// Instead we need to apply all filters at once? + +/** + * Walk down the list of filters and apply them all + * @param {Object} data A UDATA pkt {defs} */ -function m_ApplyFilters(data) { +function xm_FiltersApply(data) { // HACK // just grab the first filter for now while we figure // out how to handle the whole round trip @@ -116,23 +348,28 @@ function m_ApplyFilters(data) { // }); // hack in selection for now + // we should update the data.defs to use objects + // rather than an array? const nodeFilters = data.defs[0].filters; const edgeFilters = data.defs[1].filters; nodeFilters.forEach(filter => { - m_HandleFilter({ action: FILTER.ACTIONS.FILTER_NODES, filter}) + m_FilterApply({ action: FILTER.ACTIONS.FILTER_NODES, filter}) }) edgeFilters.forEach(filter => { - m_HandleFilter({ action: FILTER.ACTIONS.FILTER_EDGES, filter}) + m_FilterApply({ action: FILTER.ACTIONS.FILTER_EDGES, filter}) }) } /** - * + * Apply a specific group of filters + * This is triggered by: + * 1. AppState "FILTER" request, or + * 2. AppState "FILTERDEFS" update * @param {Object} data {action, filter} * */ -function m_HandleFilter(data) { +function m_FilterApply(data) { console.log('m_HandleFilter!', data); const D3DATA = UDATA.AppState("D3DATA"); if (data.action === undefined) throw "m_HandleFilter called without action"; @@ -248,11 +485,6 @@ function m_ClearFilters(arr) { * @param {Object} no e.g. normal = { isFilteredOut: false }; * @param {Bool} contains regex text contains or not contains */ -const HACKMAP = { - type: "Node_Type", - info: "Extra Info", - notes: "Notes" -} function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = true) { const D3DATA = UDATA.AppState("D3DATA"); // 8/10/20 REVIEW: Is this the best way to get current data? let returnMatches = []; From 1c317bec1bcef4392cd2df1d7ab73b419770c6dc Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Wed, 12 Aug 2020 23:27:25 -0700 Subject: [PATCH 20/47] config-ip-filter: Convert StringFilter to React.Component class. --- .../components/filter/StringFilter.jsx | 139 +++++++++++++----- 1 file changed, 100 insertions(+), 39 deletions(-) diff --git a/build/app/view/netcreate/components/filter/StringFilter.jsx b/build/app/view/netcreate/components/filter/StringFilter.jsx index 1d3b29fe..0483eafa 100644 --- a/build/app/view/netcreate/components/filter/StringFilter.jsx +++ b/build/app/view/netcreate/components/filter/StringFilter.jsx @@ -3,46 +3,107 @@ import React from 'react'; const ReactStrap = require('reactstrap'); const { Form, FormGroup, Input, Label } = ReactStrap; -export default function StringFilter({ - filter: { - id, - key, - keylabel, // human friendly display name for the key. This can be customized in the template. - operator, - value - }, - onChangeHandler -}) { - - const OPERATORS = [ - { value: FILTER.STRING_OPERATORS.CONTAINS, label: "contains"}, - { value: FILTER.STRING_OPERATORS.NOT_CONTAINS, label: "does not contain"}, - ] - - console.log('StringFilter: id is', id); - - function HandleChangeLocal(e) { - const filterType = document.getElementById(`filterType-${id}`).value; - const filterValue = document.getElementById(`filterValue-${id}`).value; - // Construct search string - // let result = name; - // result +=':' + filterType + ':' + filterValue; - let result = { key, operator: filterType, value: filterValue }; - onChangeHandler(result); +const UNISYS = require('unisys/client'); +var UDATA = null; + +const OPERATORS = [ { value: FILTER.STRING_OPERATORS.NO_OP, label: "--"}, + { value: FILTER.STRING_OPERATORS.CONTAINS, label: "contains"}, + { value: FILTER.STRING_OPERATORS.NOT_CONTAINS, label: "does not contain"}, +] + +/*/ + + StringFilter + + props + { + group // node or edge + filter: { + id, + key, // node field key from the template + keylabel, // human friendly display name for the key. This can be customized in the template. + operator, + value + }, + onChangeHandler // callback function + } + + +/*/ +class StringFilter extends React.Component { + + constructor({ + group, + filter: {id, key, keylabel, operator, value}, + onChangeHandler + }) { + super(); + this.OnChangeOperator = this.OnChangeOperator.bind(this); + this.OnChangeValue = this.OnChangeValue.bind(this); + this.TriggerChangeHandler = this.TriggerChangeHandler.bind(this); + + this.state = { + operator: FILTER.STRING_OPERATORS.NO_OP, // Used locally to define result + value: '' // Used locally to define result + }; + + /// Initialize UNISYS DATA LINK for REACT + UDATA = UNISYS.NewDataLink(this); } - return ( - - - - - {OPERATORS.map(op => - - )} - - - - - ); + OnChangeOperator(e) { + this.setState({ + operator: e.target.value + }, this.TriggerChangeHandler); + } + + OnChangeValue(e) { + this.setState({ + value: e.target.value + }, this.TriggerChangeHandler); + } + + TriggerChangeHandler() { + const { id, key, keylabel } = this.props.filter; + + // Allow NO_OP so user can reset operator to blank + // if (this.state.operator === FILTER.STRING_OPERATORS.NO_OP) return; + + const filter = { + id, + key, + keylabel, + operator: this.state.operator, + value: this.state.value + }; + UDATA.LocalCall('FILTER_DEFINE', { + group: this.props.group, + filter + }); // set a SINGLE filter + } + + render() { + const { id, key, keylabel, operator, value } = this.props.filter; + return ( +
+ + + + {OPERATORS.map(op => + + )} + + + +
+ ); + } } + +export default StringFilter; From b42d039c276eb3105f9c87eda967dae4fdca1c23 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Thu, 13 Aug 2020 10:48:08 -0700 Subject: [PATCH 21/47] config-ip-filter: Rough pass at reading filters from templates. Works. --- build/app/assets/templates/_default.template | 16 +- .../components/filter/FilterEnums.js | 41 ++ .../components/filter/FiltersPanel.jsx | 141 +++- build/app/view/netcreate/filter-logic.js | 625 ++++++++++-------- 4 files changed, 521 insertions(+), 302 deletions(-) diff --git a/build/app/assets/templates/_default.template b/build/app/assets/templates/_default.template index 245b8f86..787334bd 100644 --- a/build/app/assets/templates/_default.template +++ b/build/app/assets/templates/_default.template @@ -28,6 +28,7 @@ "nodePrompts": { "label": { + "type": "string", "label": "Label", "help": "A short title for the node", "includeInGraphTooltip": true, @@ -36,6 +37,7 @@ "_cmt4": "/// `Label` is always required and cannot be hidden" }, "type": { + "type": "select", "label": "Type", "help": "Multiple people are a 'Group'", "includeInGraphTooltip": true, @@ -75,29 +77,34 @@ ] }, "degrees": { + "type": "number", "label": "Degrees", "help": "Number of edges.", "includeInGraphTooltip": true, "hidden": false }, "notes": { + "type": "string", "label": "Significance", - "help": "Add some details.", + "help": "Add some details.", "includeInGraphTooltip": true, "hidden": false }, "info": { + "type": "string", "label": "Geocode or Date", "help": "Use latitude/longitude or a date mm/dd/yyy", "includeInGraphTooltip": true, "hidden": true }, "updated":{ + "type": "string", "label": "Last update", "includeInGraphTooltip": true, "_cmt4": "//updated is included in the edge tables if in admin mode, and in the graph tooltip if indicated here. Probably those should be more consistent." }, "delete": { + "type": "hidden", "hidden": false } }, @@ -107,11 +114,13 @@ "edgePrompts": { "edgeIsLockedMessage": "This edge is currently being edited by someone else, please try again later.", "source": { + "type": "node", "label": "Source", "help": "", "hidden": false }, "type": { + "type": "select", "label": "Type", "help": "", "hidden": false, @@ -147,26 +156,31 @@ ] }, "target": { + "type": "node", "label": "Target", "help": "", "hidden": false }, "notes": { + "type": "string", "label": "Signficance", "help": "", "hidden": false }, "info": { + "type": "string", "label": "Approximate Date of Interaction", "help": "", "hidden": true }, "citation": { + "type": "string", "label": "Citation", "help": "Enter Chapter number.", "hidden": false }, "category": { + "type": "string", "label": "Category", "help": "", "hidden": true diff --git a/build/app/view/netcreate/components/filter/FilterEnums.js b/build/app/view/netcreate/components/filter/FilterEnums.js index d35c12a6..dbcf1d32 100644 --- a/build/app/view/netcreate/components/filter/FilterEnums.js +++ b/build/app/view/netcreate/components/filter/FilterEnums.js @@ -1,5 +1,13 @@ const FILTER = {}; +FILTER.TYPES = {}; +FILTER.TYPES.STRING = 'string'; +FILTER.TYPES.NUMBER = 'number'; +FILTER.TYPES.SELECT = 'select'; +FILTER.TYPES.HIDDEN = 'hidden'; +FILTER.TYPES.NODE = 'node'; // edge source / target + + // Special Edge Keys mapped to node objects // Used by m_IsEdgeMatchedByFilter to find node labels FILTER.KEY = {}; @@ -16,4 +24,37 @@ FILTER.STRING_OPERATORS.NO_OP = "no-op"; FILTER.STRING_OPERATORS.CONTAINS = "contains"; FILTER.STRING_OPERATORS.NOT_CONTAINS = "not-contains"; +FILTER.SELECT_OPERATORS = {}; +FILTER.SELECT_OPERATORS.CONTAINS = "contains"; +FILTER.SELECT_OPERATORS.NOT_CONTAINS = "not-contains"; + +FILTER.NUMBER_OPERATORS = {}; +FILTER.NUMBER_OPERATORS.GT = "gt"; +FILTER.NUMBER_OPERATORS.GT_EQ = "gt_eq"; +FILTER.NUMBER_OPERATORS.LT = "lt"; +FILTER.NUMBER_OPERATORS.LT_EQ = "lt-eq"; +FILTER.NUMBER_OPERATORS.EQ = "eq"; +FILTER.NUMBER_OPERATORS.NOT_EQ = "not-eq"; + +/*/ + +Filter UDATA Messages + +All Filters operations + + FILTERDEFS Set the FILTERDEFS data object + FILTERDATA? FILTER_DATA? + FDATA? FLTRDATA + + FILTER_RESET Resets filter form to blank state + +Individual Filter Operations + + FILTER_DEFINE Define a new individual filter + + +/*/ + + + module.exports = FILTER; diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index b6eb419d..657962b7 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -6,7 +6,6 @@ const ReactStrap = require('reactstrap'); const { Button, Input, Label } = ReactStrap; - const UNISYS = require('unisys/client'); var UDATA = null; @@ -15,63 +14,101 @@ var UDATA = null; // const UNISYS = require('../../../../unisys/client'); // class FiltersPanel extends React.Component { -// Dummy Data -// const filterData = { -// id: '1', -// name: 'Label', -// type: 'contains', -// value: 'tacitus' -// }; -const filtersDefs = [ - { + +// Playing with alternative representation: +// PROS +// * More concise +// CONS +// * Restricts ability to create two filters on the same key +const FILTERDEF = { + nodes: { label: "Nodes", + filters: { + label: { + keylabel: 'Label', + operator: 'contains', + value: 'tacitus' + }, + type: { + keylabel: 'Type', + operator: 'not-contains', + value: 'person' + }, + notes: { + keylabel: 'Significance', + operator: 'contains', + value: 'xxx' + } + } + }, + edges: { + label: "Edges", + filters: {} + } +} + + + +// eventually generate this from template? +let filterDefs = [ + { + group: "node", + label: "Nodes -- Show me all nodes where...", filters: [ { id: '1', key: 'label', keylabel: 'Label', - operator: 'contains', - value: 'tacitus' + operator: 'no-op', + value: '' + }, + { + id: '4', + key: 'label', + keylabel: 'Label', + operator: 'no-op', + value: '' }, { id: '2', key: 'type', keylabel: 'Type', - operator: 'contains', - value: 'person' + operator: 'no-op', + value: '' }, { id: '3', key: 'notes', keylabel: 'Significance', - operator: 'contains', - value: 'xxx' + operator: 'no-op', + value: '' } ] }, { + group: "edge", label: "Edges", filters: [ { - id: '1', + id: '5', key: 'source', keylabel: 'Source', - operator: 'contains', - value: 'tacitus' + operator: 'no-op', + value: '' }, { - id: '2', + id: '6', key: 'type', keylabel: 'Type', - operator: 'not-contains', - value: 'is related to' + operator: 'no-op', + value: '' }, { - id: '3', + id: '7', key: 'target', keylabel: 'Target', - operator: 'contains', - value: 'Rome' + operator: 'no-op', + value: '' } ] } @@ -82,35 +119,69 @@ class FiltersPanel extends UNISYS.Component { constructor({ filterGroups, onFiltersChange, tableHeight }) { super(); - console.log('filters is', filterGroups) - - this.OnFilterChange = this.OnFilterChange.bind(this); + this.UpdateFilterDefs = this.UpdateFilterDefs.bind(this); + this.OnFilterReset = this.OnFilterReset.bind(this); this.OnClearBtnClick = this.OnClearBtnClick.bind(this); /// Initialize UNISYS DATA LINK for REACT UDATA = UNISYS.NewDataLink(this); + +console.error('######## fieldPanel Constructor') + // Load Templates + let FDATA = UDATA.AppState("FILTERDEFS"); + console.error('####### FDATA', FDATA) + this.state = { defs: FDATA.defs }; + +// defs should be triggered by filter-logic +// but constructor doesn't init until fairly late. + // // HACK in filter set here for now + // // Eventually this should be read from the template file + // UDATA.SetAppState("FILTERDEFS", { defs: filterDefs }); + + UDATA.OnAppStateChange("FILTERDEFS", this.UpdateFilterDefs); + + UDATA.HandleMessage("FILTER_RESET", this.OnFilterReset); + } // constructor - OnFilterChange(filter) { - console.log('onFilterChange', filter); - UDATA.LocalCall('FILTER', { action: FILTER.ACTIONS.FILTER_NODES , filter }); + + UpdateFilterDefs(data) { + console.error('####fieldpanel got state change', data) + this.setState({ defs: data.defs }, () => { + console.error('fieldpanel updated state is', this.state.defs); + }); + } + + // Reset the form + OnFilterReset() { + console.error('RESETING') + UDATA.SetAppState("FILTERDEFS", { defs: [{group: "node", filters:[]},{group: "edge", filters:[]}] }); } OnClearBtnClick() { - UDATA.LocalCall('FILTER', {action: FILTER.ACTIONS.CLEAR }); + this.OnFilterReset(); + // UDATA.LocalCall('FILTER_CLEAR'); + } + + componentWillUnmount() { + console.error('gracefully unsubscribe!') } render() { const { tableHeight } = this.props; + const { defs } = this.state; return (
-
- {filtersDefs.map(def => + {defs.map(def => { + console.error('filter-logic INITIALIZE'); - /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ FILTER_SET is called by StringFilter when user has updated filter. - /*/ - UDATA.HandleMessage("FILTER_DEFINE", data => { - m_FilterDefine(data); - }) + UDATA.OnAppStateChange("TEMPLATE", data => { + m_ImportFilters(); + }); UDATA.OnAppStateChange("FILTERDEFS", data => { console.error('OnAppStateChange: FILTER', data); // The filter defs have been updated, so apply the filters. - m_FiltersApply(data); + m_FiltersApply(); }); /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ FILTER is called by FiltersPanel when user has updated filter. - This triggers the actual filtering. + /*/ FILTER_DEFINE is called by StringFilter when user has updated filter. /*/ - UDATA.HandleMessage("FILTER", data => { - m_FilterApply(data); + UDATA.HandleMessage("FILTER_DEFINE", data => { + m_FilterDefine(data); + }) + + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /*/ FILTER_CLEAR is called by FiltersPanel when user clicks "Clear Filters" button + /*/ + UDATA.HandleMessage("FILTER_CLEAR", () => { + m_ClearFilters(); }); }); // end UNISYS_INIT +/// IMPORT FILTER DEFINITIONS ///////////////////////////////////////////////// +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +/** + * Loads filters from template file + */ +function m_ImportFilters() { + console.error('m_ImportFilters'); + + TEMPLATE = UDATA.AppState("TEMPLATE"); + + let nodePrompts = TEMPLATE.nodePrompts; + let edgePrompts = TEMPLATE.edgePrompts; + console.log('TEMPLATES', nodePrompts, edgePrompts); + + let nodeFilters = { + group: "node", + label: "Node Filters", + filters: m_ImportPrompts(nodePrompts) + }; + let edgeFilters = { + group: "edge", + label: "Edge Filters", + filters: m_ImportPrompts(edgePrompts) + }; + + let fdata = { + defs: [nodeFilters, edgeFilters] + }; + + // console.error('imported template into filtersdef', fdata); + UDATA.SetAppState("FILTERDEFS", fdata); + +} + +function m_ImportPrompts(prompts) { + let filters = []; + let counter = 0; + for (const [key, prompt] of Object.entries(prompts)) { + console.log(`key ${key} label ${prompt.label} type ${prompt.type}`); + + let operator; + switch (prompt.type) { + case FILTER.TYPES.STRING: + operator = FILTER.STRING_OPERATORS.NO_OP; // default to no_op + break; + case FILTER.TYPES.NUMBER: + console.error('skipping NUMBER for now'); + // operator = FILTER.NUMBER_OPERATORS.GT; // skip for now + break; + case FILTER.TYPES.SELECT: + console.error('skipping SELECT for now'); + break; + case FILTER.TYPES.NODE: + operator = FILTER.STRING_OPERATORS.NO_OP; // default to no_op + break; + case FILTER.TYPES.HIDDEN: + break; + default: + // edge template item "edgeIsLockedMessage" will trigger this message + console.warn(`Unknown node prompt type ${prompt.type} for ${prompt}`); + break; + } + if (operator === undefined) continue; // don't add filter if operator is hidden + let filter = { + id: counter++, + key: key, + type: prompt.type, + keylabel: prompt.label, + operator: operator, + value: '' + }; + filters.push(filter); + } + return filters; +} + /// UDATA HANDLERS //////////////////////////////////////////////////////////// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /** * Define an individual filter * @param {Object} data {group, filter} @@ -110,8 +189,10 @@ function m_FilterDefine(data) { * Walk down the list of filters and apply them all * @param {Object} data A UDATA pkt {defs} */ -function m_FiltersApply(data) { - const FDATA = data.defs; +function m_FiltersApply() { + const FDATA = UDATA.AppState("FILTERDEFS").defs; + + console.error('@@@@@ m_FiltersApply', FDATA); // hack in selection for now // we should update the data.defs to use objects @@ -123,6 +204,17 @@ function m_FiltersApply(data) { m_FiltersApplyToEdges(edgeFilters); } +function m_ClearFilters(arr) { + console.log('Clearing Filters!!!!') + const props = { isFilteredOut: false }; + + const FDATA = UDATA.AppState("FILTERDEFS"); + NCLOGIC.SetAllObjs(FDATA.defs[0].filters, props); // clear nodes + NCLOGIC.SetAllObjs(FDATA.defs[1].filters, props); // clear props + + m_FiltersApply(); +} + /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ NODE FILTERS @@ -221,7 +313,7 @@ function m_MatchString(pin, haystack, contains = true) { /*/ function m_FiltersApplyToEdges(filters) { - console.log('m_FiltersApplyToEdges', filters); + // console.log('m_FiltersApplyToEdges', filters); const D3DATA = UDATA.AppState("D3DATA"); D3DATA.edges.forEach(edge => { m_FiltersApplyToEdge(edge, filters); @@ -230,18 +322,16 @@ function m_FiltersApplyToEdges(filters) { } function m_FiltersApplyToEdge(edge, filters) { - console.log('m_FiltersApplyToEdge', edge, filters); + // console.log('m_FiltersApplyToEdge', edge, filters); // regardless of filter definition, // hide edge if any attached node is filtered out. if (edge.source.isFilteredOut || edge.target.isFilteredOut) { - console.error('...edge source or target is filtered out',edge.source,edge.target) + // console.error('...edge source or target is filtered out',edge.source,edge.target) edge.isFilteredOut = true; // no filters, revert return; } - console.log('...edge source or target is still visible'); - let all_no_op = true; let matched = true; // implicit AND. ALL filters must return true. @@ -249,7 +339,7 @@ function m_FiltersApplyToEdge(edge, filters) { if (filter.operator === FILTER.STRING_OPERATORS.NO_OP) return; // skip no_op all_no_op = false; if (!m_IsEdgeMatchedByFilter(edge, filter)) { - console.error('NO MATCH!'); + // console.error('NO MATCH!'); matched = false; } }); @@ -262,28 +352,36 @@ function m_FiltersApplyToEdge(edge, filters) { } function m_IsEdgeMatchedByFilter(edge, filter) { - console.log('...m_IsEdgeMatchedByFilter', edge[filter.key], filter.value); + // console.log('...m_IsEdgeMatchedByFilter', edge[filter.key], filter.value); if ((filter.key === undefined) || (filter.operator === undefined) || (filter.value === undefined)) { - console.log('......nothing to filter match = false'); return false; // nothing to filter } // edges require special handling because `source` and `target` // point to node data, not simple strings. let edgeStr; - switch (filter.key) { - case FILTER.KEY.SOURCE: - edgeStr = edge.source.label; - break; - case FILTER.KEY.TARGET: - edgeStr = edge.target.label; - break; - default: - edgeStr = edge[filter.key]; - break; + // switch (filter.key) { + // case FILTER.KEY.SOURCE: + // edgeStr = edge.source.label; + // break; + // case FILTER.KEY.TARGET: + // edgeStr = edge.target.label; + // break; + // default: + // edgeStr = edge[filter.key]; + // break; + // } + + if (filter.type === FILTER.TYPES.NODE) { + edgeStr = edge[filter.key].label; // search on the source/target node label + } else { + edgeStr = edge[filter.key]; } + + console.error('####match edgestr',edgeStr, 'to', filter.value) + switch (filter.operator) { case FILTER.STRING_OPERATORS.CONTAINS: return m_MatchString(filter.value, edgeStr, true); @@ -330,233 +428,228 @@ function m_IsEdgeMatchedByFilter(edge, filter) { /// This approach applied filtering filter by filter. /// Instead we need to apply all filters at once? -/** - * Walk down the list of filters and apply them all - * @param {Object} data A UDATA pkt {defs} - */ -function xm_FiltersApply(data) { - // HACK - // just grab the first filter for now while we figure - // out how to handle the whole round trip - // eventually need to apply ALL filters - // data.defs[0] = nodes - // data.defs[0][0] = first filter - // const filter = data.defs[0].filters[0]; - // m_HandleFilter({ - // action: FILTER.ACTIONS.FILTER_NODES, - // filter - // }); - - // hack in selection for now - // we should update the data.defs to use objects - // rather than an array? - const nodeFilters = data.defs[0].filters; - const edgeFilters = data.defs[1].filters; - - nodeFilters.forEach(filter => { - m_FilterApply({ action: FILTER.ACTIONS.FILTER_NODES, filter}) - }) - edgeFilters.forEach(filter => { - m_FilterApply({ action: FILTER.ACTIONS.FILTER_EDGES, filter}) - }) -} - -/** - * Apply a specific group of filters - * This is triggered by: - * 1. AppState "FILTER" request, or - * 2. AppState "FILTERDEFS" update - * @param {Object} data {action, filter} - * - */ -function m_FilterApply(data) { - console.log('m_HandleFilter!', data); - const D3DATA = UDATA.AppState("D3DATA"); - if (data.action === undefined) throw "m_HandleFilter called without action"; - - switch (data.action) { - case FILTER.ACTIONS.CLEAR: - m_ClearFilters(D3DATA.nodes); - m_ClearFilters(D3DATA.edges); - break; - case FILTER.ACTIONS.FILTER_NODES: - m_FilterNodes(data.filter); - break; - case FILTER.ACTIONS.FILTER_EDGES: - m_FilterEdges(data.filter); - break; - default: - throw `Unknown filter action ${data.action}`; - break; - } - UDATA.SetAppState("D3DATA", D3DATA); -} - -/** - * - * @param {Object} filter {id, key, operator, value} - */ -function m_FilterNodes(filter) { - console.log('...m_FilterNodes', filter); - if ((filter.key === undefined) || - (filter.operator === undefined) || - (filter.value === undefined)) return; // nothing to filter - - const marked = { isFilteredOut: true }; - const normal = { isFilteredOut: false }; - - // FIXME - // If value is cleared, how do we clear the search? - - switch (filter.operator) { - case FILTER.STRING_OPERATORS.CONTAINS: - m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); - break; - case FILTER.STRING_OPERATORS.NOT_CONTAINS: - m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); - break; - case FILTER.STRING_OPERATORS.NO_OP: - // ignore - break; - default: - throw `Unknown filter operator ${filter.operator}`; - break; - } - -} - -/** - * - * @param {Object} filter {id, key, operator, value} - */ -function m_FilterEdges(filter) { - console.log('m_FilterEdges', filter); - if ((filter.key === undefined) || - (filter.operator === undefined) || - (filter.value === undefined)) return; // nothing to filter - - const marked = { isFilteredOut: true }; - const normal = { isFilteredOut: false }; - - switch (filter.operator) { - case FILTER.STRING_OPERATORS.CONTAINS: - // m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); - break; - case FILTER.STRING_OPERATORS.NOT_CONTAINS: - // m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); - break; - case FILTER.STRING_OPERATORS.NO_OP: - // ignore - break; - default: - throw `Unknown filter operator ${filter.operator}`; - break; - } - -} - - -function m_ClearFilters(arr) { - console.log('Clearing Filters!!!!') - const props = { isFilteredOut: false }; - NCLOGIC.SetAllObjs(arr, props); -} - - - -/// OBJECT HELPERS //////////////////////////////////////////////////////////// - - -/// NODE HELPERS ////////////////////////////////////////////////////////////// -/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -/** - * Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. - * All others nodes are set to 'no' props. Return matches. - * Optionally resets all the NON matching nodes as well. - * - * If matched, we set `isFiltered` flag to true. - * - * Edges are matched if they link to the node. - * - * @param {String} keyToSet The template key of the node parameter we want to set, - * e.g. "label" - * @param {String} str The string to search for - * @param {Object} yes e.g. marked = { isFilteredOut: true }; - * @param {Object} no e.g. normal = { isFilteredOut: false }; - * @param {Bool} contains regex text contains or not contains - */ -function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = true) { - const D3DATA = UDATA.AppState("D3DATA"); // 8/10/20 REVIEW: Is this the best way to get current data? - let returnMatches = []; - str = NCLOGIC.EscapeRegexChars(str.trim()); - - const regex = new RegExp(/*'^'+*/ str, "i"); - // First find the nodes - D3DATA.nodes.forEach(node => { - let nodeField = node[keyToSet]; - - // HACK - // The old data model has secondary keys stuffed - // into an `attributes` object. This is a - // holdover from the original pre-netcreate - // data import. If we ever change the data format - // this HACKMAP should be removed. - if (['type', 'info', 'notes'].includes(keyToSet)) { - nodeField = node.attributes[HACKMAP[keyToSet]]; - } - - let matches; - if (str === "") { - // empty string doesn't match anything - matches = false; - } else if (contains) { - matches = !regex.test(nodeField); - } else { - matches = regex.test(nodeField); - } - - if (matches) { - console.log('......filtering out', node.label); - for (let key in yes) node[key] = yes[key]; - returnMatches.push(node); - } else { - console.log('......unfiltering', node.label); - for (let key in no) node[key] = no[key]; - } - }); - // Then hide all related edges - m_SetMatchingEdgesByNodes(returnMatches, yes, no); - return returnMatches; -} - -/** - * Set edges that link to any node in nodes to 'yes' props. - * All others nodes are set to 'no' props. Return matches. - * - * We set look for ALL nodes at once otherwise, one node can unset - * antoher node. - * - * This is a specialized function because edges need to be matched - * against both source and target. - * - * @param {Array} nodes Array of node objects - * @param {Object} yes e.g. marked = { isFilteredOut: true }; - * @param {Object} no e.g. normal = { isFilteredOut: false }; - */ -function m_SetMatchingEdgesByNodes(nodes, yes = {}, no = {}) { - const nodeIDs = nodes.map(node => node.id); - let returnMatches = []; - const D3DATA = UDATA.AppState("D3DATA"); - D3DATA.edges.forEach(edge => { - if ( nodeIDs.includes(edge.source.id) || nodeIDs.includes(edge.target.id) ) { - for (let key in yes) edge[key] = yes[key]; - returnMatches.push(edge); - } else { - for (let key in no) edge[key] = no[key]; - } - }); - return returnMatches; -} +// /** +// * Walk down the list of filters and apply them all +// * @param {Object} data A UDATA pkt {defs} +// */ +// function xm_FiltersApply(data) { +// // HACK +// // just grab the first filter for now while we figure +// // out how to handle the whole round trip +// // eventually need to apply ALL filters +// // data.defs[0] = nodes +// // data.defs[0][0] = first filter +// // const filter = data.defs[0].filters[0]; +// // m_HandleFilter({ +// // action: FILTER.ACTIONS.FILTER_NODES, +// // filter +// // }); + +// // hack in selection for now +// // we should update the data.defs to use objects +// // rather than an array? +// const nodeFilters = data.defs[0].filters; +// const edgeFilters = data.defs[1].filters; + +// nodeFilters.forEach(filter => { +// m_FilterApply({ action: FILTER.ACTIONS.FILTER_NODES, filter}) +// }) +// edgeFilters.forEach(filter => { +// m_FilterApply({ action: FILTER.ACTIONS.FILTER_EDGES, filter}) +// }) +// } + +// /** +// * Apply a specific group of filters +// * This is triggered by: +// * 1. AppState "FILTER" request, or +// * 2. AppState "FILTERDEFS" update +// * @param {Object} data {action, filter} +// * +// */ +// function m_FilterApply(data) { +// console.log('m_HandleFilter!', data); +// const D3DATA = UDATA.AppState("D3DATA"); +// if (data.action === undefined) throw "m_HandleFilter called without action"; + +// switch (data.action) { +// case FILTER.ACTIONS.CLEAR: +// m_ClearFilters(D3DATA.nodes); +// m_ClearFilters(D3DATA.edges); +// break; +// case FILTER.ACTIONS.FILTER_NODES: +// m_FilterNodes(data.filter); +// break; +// case FILTER.ACTIONS.FILTER_EDGES: +// m_FilterEdges(data.filter); +// break; +// default: +// throw `Unknown filter action ${data.action}`; +// break; +// } +// UDATA.SetAppState("D3DATA", D3DATA); +// } + +// /** +// * +// * @param {Object} filter {id, key, operator, value} +// */ +// function m_FilterNodes(filter) { +// console.log('...m_FilterNodes', filter); +// if ((filter.key === undefined) || +// (filter.operator === undefined) || +// (filter.value === undefined)) return; // nothing to filter + +// const marked = { isFilteredOut: true }; +// const normal = { isFilteredOut: false }; + +// // FIXME +// // If value is cleared, how do we clear the search? + +// switch (filter.operator) { +// case FILTER.STRING_OPERATORS.CONTAINS: +// m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); +// break; +// case FILTER.STRING_OPERATORS.NOT_CONTAINS: +// m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); +// break; +// case FILTER.STRING_OPERATORS.NO_OP: +// // ignore +// break; +// default: +// throw `Unknown filter operator ${filter.operator}`; +// break; +// } + +// } + +// /** +// * +// * @param {Object} filter {id, key, operator, value} +// */ +// function m_FilterEdges(filter) { +// console.log('m_FilterEdges', filter); +// if ((filter.key === undefined) || +// (filter.operator === undefined) || +// (filter.value === undefined)) return; // nothing to filter + +// const marked = { isFilteredOut: true }; +// const normal = { isFilteredOut: false }; + +// switch (filter.operator) { +// case FILTER.STRING_OPERATORS.CONTAINS: +// // m_SetMatchingNodesKey(filter.key, filter.value, marked, normal); +// break; +// case FILTER.STRING_OPERATORS.NOT_CONTAINS: +// // m_SetMatchingNodesKey(filter.key, filter.value, marked, normal, false); +// break; +// case FILTER.STRING_OPERATORS.NO_OP: +// // ignore +// break; +// default: +// throw `Unknown filter operator ${filter.operator}`; +// break; +// } + +// } + + + + + +// /// OBJECT HELPERS //////////////////////////////////////////////////////////// + + +// /// NODE HELPERS ////////////////////////////////////////////////////////////// +// /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// /** +// * Set nodes & EDGES that PARTIALLY match 'str' to 'yes' props. +// * All others nodes are set to 'no' props. Return matches. +// * Optionally resets all the NON matching nodes as well. +// * +// * If matched, we set `isFiltered` flag to true. +// * +// * Edges are matched if they link to the node. +// * +// * @param {String} keyToSet The template key of the node parameter we want to set, +// * e.g. "label" +// * @param {String} str The string to search for +// * @param {Object} yes e.g. marked = { isFilteredOut: true }; +// * @param {Object} no e.g. normal = { isFilteredOut: false }; +// * @param {Bool} contains regex text contains or not contains +// */ +// function m_SetMatchingNodesKey(keyToSet, str = "", yes = {}, no = {}, contains = true) { +// const D3DATA = UDATA.AppState("D3DATA"); // 8/10/20 REVIEW: Is this the best way to get current data? +// let returnMatches = []; +// str = NCLOGIC.EscapeRegexChars(str.trim()); + +// const regex = new RegExp(/*'^'+*/ str, "i"); +// // First find the nodes +// D3DATA.nodes.forEach(node => { +// let nodeField = node[keyToSet]; + +// // HACK +// // The old data model has secondary keys stuffed +// // into an `attributes` object. This is a +// // holdover from the original pre-netcreate +// // data import. If we ever change the data format +// // this HACKMAP should be removed. +// if (['type', 'info', 'notes'].includes(keyToSet)) { +// nodeField = node.attributes[HACKMAP[keyToSet]]; +// } + +// let matches; +// if (str === "") { +// // empty string doesn't match anything +// matches = false; +// } else if (contains) { +// matches = !regex.test(nodeField); +// } else { +// matches = regex.test(nodeField); +// } + +// if (matches) { +// console.log('......filtering out', node.label); +// for (let key in yes) node[key] = yes[key]; +// returnMatches.push(node); +// } else { +// console.log('......unfiltering', node.label); +// for (let key in no) node[key] = no[key]; +// } +// }); +// // Then hide all related edges +// m_SetMatchingEdgesByNodes(returnMatches, yes, no); +// return returnMatches; +// } + +// /** +// * Set edges that link to any node in nodes to 'yes' props. +// * All others nodes are set to 'no' props. Return matches. +// * +// * We set look for ALL nodes at once otherwise, one node can unset +// * antoher node. +// * +// * This is a specialized function because edges need to be matched +// * against both source and target. +// * +// * @param {Array} nodes Array of node objects +// * @param {Object} yes e.g. marked = { isFilteredOut: true }; +// * @param {Object} no e.g. normal = { isFilteredOut: false }; +// */ +// function m_SetMatchingEdgesByNodes(nodes, yes = {}, no = {}) { +// const nodeIDs = nodes.map(node => node.id); +// let returnMatches = []; +// const D3DATA = UDATA.AppState("D3DATA"); +// D3DATA.edges.forEach(edge => { +// if ( nodeIDs.includes(edge.source.id) || nodeIDs.includes(edge.target.id) ) { +// for (let key in yes) edge[key] = yes[key]; +// returnMatches.push(edge); +// } else { +// for (let key in no) edge[key] = no[key]; +// } +// }); +// return returnMatches; +// } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 854801446b99d38a6d0b45590d6fd9425c8af64f Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Thu, 13 Aug 2020 10:49:40 -0700 Subject: [PATCH 22/47] config-ip-label: Convert FILITERDEFS to FDATA, streamline data model. --- .../components/filter/FiltersPanel.jsx | 127 +--------- .../components/filter/StringFilter.jsx | 3 +- build/app/view/netcreate/filter-logic.js | 230 ++++++++++++------ 3 files changed, 167 insertions(+), 193 deletions(-) diff --git a/build/app/view/netcreate/components/filter/FiltersPanel.jsx b/build/app/view/netcreate/components/filter/FiltersPanel.jsx index 657962b7..f844d419 100644 --- a/build/app/view/netcreate/components/filter/FiltersPanel.jsx +++ b/build/app/view/netcreate/components/filter/FiltersPanel.jsx @@ -9,112 +9,13 @@ const { Button, Input, Label } = ReactStrap; const UNISYS = require('unisys/client'); var UDATA = null; + // Storybook has problems with loading unisys without relative ref // but even with relative ref, it can't load submodules // const UNISYS = require('../../../../unisys/client'); // class FiltersPanel extends React.Component { -// Playing with alternative representation: -// PROS -// * More concise -// CONS -// * Restricts ability to create two filters on the same key -const FILTERDEF = { - nodes: { - label: "Nodes", - filters: { - label: { - keylabel: 'Label', - operator: 'contains', - value: 'tacitus' - }, - type: { - keylabel: 'Type', - operator: 'not-contains', - value: 'person' - }, - notes: { - keylabel: 'Significance', - operator: 'contains', - value: 'xxx' - } - } - }, - edges: { - label: "Edges", - filters: {} - } -} - - - -// eventually generate this from template? -let filterDefs = [ - { - group: "node", - label: "Nodes -- Show me all nodes where...", - filters: [ - { - id: '1', - key: 'label', - keylabel: 'Label', - operator: 'no-op', - value: '' - }, - { - id: '4', - key: 'label', - keylabel: 'Label', - operator: 'no-op', - value: '' - }, - { - id: '2', - key: 'type', - keylabel: 'Type', - operator: 'no-op', - value: '' - }, - { - id: '3', - key: 'notes', - keylabel: 'Significance', - operator: 'no-op', - value: '' - } - ] - }, - { - group: "edge", - label: "Edges", - filters: [ - { - id: '5', - key: 'source', - keylabel: 'Source', - operator: 'no-op', - value: '' - }, - { - id: '6', - key: 'type', - keylabel: 'Type', - operator: 'no-op', - value: '' - }, - { - id: '7', - key: 'target', - keylabel: 'Target', - operator: 'no-op', - value: '' - } - ] - } -]; - - class FiltersPanel extends UNISYS.Component { constructor({ filterGroups, onFiltersChange, tableHeight }) { super(); @@ -126,36 +27,32 @@ class FiltersPanel extends UNISYS.Component { /// Initialize UNISYS DATA LINK for REACT UDATA = UNISYS.NewDataLink(this); + console.error('######## fieldPanel Constructor') // Load Templates - let FDATA = UDATA.AppState("FILTERDEFS"); + // The intial `OnAppStateChange("FILTERDEFS")` event when the template is + // first loaded is called well before FiltersPanel is + // even constructed. So we need to explicitly load it here. + let FDATA = UDATA.AppState("FDATA"); console.error('####### FDATA', FDATA) - this.state = { defs: FDATA.defs }; - -// defs should be triggered by filter-logic -// but constructor doesn't init until fairly late. - // // HACK in filter set here for now - // // Eventually this should be read from the template file - // UDATA.SetAppState("FILTERDEFS", { defs: filterDefs }); - - UDATA.OnAppStateChange("FILTERDEFS", this.UpdateFilterDefs); + this.state = FDATA; + UDATA.OnAppStateChange("FDATA", this.UpdateFilterDefs); UDATA.HandleMessage("FILTER_RESET", this.OnFilterReset); - } // constructor UpdateFilterDefs(data) { console.error('####fieldpanel got state change', data) - this.setState({ defs: data.defs }, () => { - console.error('fieldpanel updated state is', this.state.defs); + this.setState(data, () => { + console.error('fieldpanel updated state is', this.state); }); } // Reset the form OnFilterReset() { console.error('RESETING') - UDATA.SetAppState("FILTERDEFS", { defs: [{group: "node", filters:[]},{group: "edge", filters:[]}] }); + UDATA.SetAppState("FDATA", { defs: [{group: "node", filters:[]},{group: "edge", filters:[]}] }); } OnClearBtnClick() { @@ -169,7 +66,7 @@ console.error('######## fieldPanel Constructor') render() { const { tableHeight } = this.props; - const { defs } = this.state; + const defs = [this.state.nodes, this.state.edges]; return (
{ m_ImportFilters(); }); - UDATA.OnAppStateChange("FILTERDEFS", data => { + UDATA.OnAppStateChange("FDATA", data => { console.error('OnAppStateChange: FILTER', data); // The filter defs have been updated, so apply the filters. m_FiltersApply(); @@ -80,40 +194,31 @@ MOD.Hook("INITIALIZE", () => { * Loads filters from template file */ function m_ImportFilters() { - console.error('m_ImportFilters'); - TEMPLATE = UDATA.AppState("TEMPLATE"); let nodePrompts = TEMPLATE.nodePrompts; let edgePrompts = TEMPLATE.edgePrompts; - console.log('TEMPLATES', nodePrompts, edgePrompts); - - let nodeFilters = { - group: "node", - label: "Node Filters", - filters: m_ImportPrompts(nodePrompts) - }; - let edgeFilters = { - group: "edge", - label: "Edge Filters", - filters: m_ImportPrompts(edgePrompts) - }; let fdata = { - defs: [nodeFilters, edgeFilters] + nodes: { + group: "nodes", // this needs to be passed to StringFilter + label: "Node Filters", + filters: m_ImportPrompts(nodePrompts) + }, + edges: { + group: "edges", // this needs to be passed to StringFilter + label: "Edge Filters", + filters: m_ImportPrompts(edgePrompts) + } }; - // console.error('imported template into filtersdef', fdata); - UDATA.SetAppState("FILTERDEFS", fdata); - + UDATA.SetAppState("FDATA", fdata); } function m_ImportPrompts(prompts) { let filters = []; let counter = 0; for (const [key, prompt] of Object.entries(prompts)) { - console.log(`key ${key} label ${prompt.label} type ${prompt.type}`); - let operator; switch (prompt.type) { case FILTER.TYPES.STRING: @@ -161,28 +266,25 @@ function m_ImportPrompts(prompts) { function m_FilterDefine(data) { console.error('FILTER_DEFINE received', data); - const FILTERDEFS = UDATA.AppState("FILTERDEFS").defs; // already an object - console.error('FILTERDEFS is', FILTERDEFS); - + const FDATA = UDATA.AppState("FDATA"); + console.error('FDATA is', FDATA); - // HACK map to array for now - // FILTERDEFS should probably use object, not array - if (data.group === "node") { - let nodeFilters = FILTERDEFS[0].filters; + if (data.group === "nodes") { + let nodeFilters = FDATA.nodes.filters; const index = nodeFilters.findIndex(f => f.id === data.filter.id); nodeFilters.splice(index, 1, data.filter); - FILTERDEFS[0].filters = nodeFilters; - } else if (data.group === "edge") { - let edgeFilters = FILTERDEFS[1].filters; + FDATA.nodes.filters = nodeFilters; + } else if (data.group === "edges") { + let edgeFilters = FDATA.edges.filters; const index = edgeFilters.findIndex(f => f.id === data.filter.id); edgeFilters.splice(index, 1, data.filter); - FILTERDEFS[1].filters = edgeFilters; + FDATA.edges.filters = edgeFilters; } else { throw `FILTER_DEFINE called with unknown group: ${data.group}`; } - console.log('FILTERDEFS spliced is now', FILTERDEFS); // already an object - UDATA.SetAppState("FILTERDEFS", { defs: FILTERDEFS }); + console.log('FDATA spliced is now', FDATA); // already an object + UDATA.SetAppState("FDATA", FDATA); } /** @@ -190,28 +292,25 @@ function m_FilterDefine(data) { * @param {Object} data A UDATA pkt {defs} */ function m_FiltersApply() { - const FDATA = UDATA.AppState("FILTERDEFS").defs; + const FDATA = UDATA.AppState("FDATA"); console.error('@@@@@ m_FiltersApply', FDATA); // hack in selection for now // we should update the data.defs to use objects // rather than an array? - const nodeFilters = FDATA[0].filters; - const edgeFilters = FDATA[1].filters; + const nodeFilters = FDATA.nodes.filters; + const edgeFilters = FDATA.edges.filters; m_FiltersApplyToNodes(nodeFilters); m_FiltersApplyToEdges(edgeFilters); } -function m_ClearFilters(arr) { - console.log('Clearing Filters!!!!') +function m_ClearFilters() { const props = { isFilteredOut: false }; - - const FDATA = UDATA.AppState("FILTERDEFS"); - NCLOGIC.SetAllObjs(FDATA.defs[0].filters, props); // clear nodes - NCLOGIC.SetAllObjs(FDATA.defs[1].filters, props); // clear props - + const FDATA = UDATA.AppState("FDATA"); + NCLOGIC.SetAllObjs(FDATA.nodes.filters, props); // clear nodes + NCLOGIC.SetAllObjs(FDATA.edges.filters, props); // clear props m_FiltersApply(); } @@ -227,7 +326,6 @@ function m_ClearFilters(arr) { * @param {Array} filters */ function m_FiltersApplyToNodes(filters) { - // console.log('m_FiltersApplyNodes', filters); const D3DATA = UDATA.AppState("D3DATA"); D3DATA.nodes.forEach(node => { m_FiltersApplyToNode(node, filters); @@ -236,7 +334,6 @@ function m_FiltersApplyToNodes(filters) { } function m_FiltersApplyToNode(node, filters) { - // console.log('m_FiltersApplyToNode', node, filters); let all_no_op = true; let matched = true; // implicit AND. ALL filters must return true. @@ -248,7 +345,8 @@ function m_FiltersApplyToNode(node, filters) { } }); if (all_no_op) { - node.isFilteredOut = false; // no filters, revert + // no filters defined, undo isFilteredOut + node.isFilteredOut = false; } else { // node is filtered out if it fails any filter tests node.isFilteredOut = !matched; @@ -256,11 +354,9 @@ function m_FiltersApplyToNode(node, filters) { } function m_IsNodeMatchedByFilter(node, filter) { - // console.log('...m_IsNodeMatchedByFilter', filter); if ((filter.key === undefined) || (filter.operator === undefined) || (filter.value === undefined)) { - // console.log('......nothing to filter match = false'); return false; // nothing to filter } @@ -301,7 +397,6 @@ function m_MatchString(pin, haystack, contains = true) { } else { matches = !regex.test(haystack); } - console.log('######looking for pin', pin, 'in ', haystack, ' MATCHES:', matches); return matches; } @@ -313,7 +408,6 @@ function m_MatchString(pin, haystack, contains = true) { /*/ function m_FiltersApplyToEdges(filters) { - // console.log('m_FiltersApplyToEdges', filters); const D3DATA = UDATA.AppState("D3DATA"); D3DATA.edges.forEach(edge => { m_FiltersApplyToEdge(edge, filters); @@ -322,12 +416,9 @@ function m_FiltersApplyToEdges(filters) { } function m_FiltersApplyToEdge(edge, filters) { - // console.log('m_FiltersApplyToEdge', edge, filters); - // regardless of filter definition, - // hide edge if any attached node is filtered out. + // always hide edge if it's attached to a filtered node if (edge.source.isFilteredOut || edge.target.isFilteredOut) { - // console.error('...edge source or target is filtered out',edge.source,edge.target) edge.isFilteredOut = true; // no filters, revert return; } @@ -339,20 +430,19 @@ function m_FiltersApplyToEdge(edge, filters) { if (filter.operator === FILTER.STRING_OPERATORS.NO_OP) return; // skip no_op all_no_op = false; if (!m_IsEdgeMatchedByFilter(edge, filter)) { - // console.error('NO MATCH!'); matched = false; } }); if (all_no_op) { - edge.isFilteredOut = false; // no filters, revert + // no filters defined, undo isFilteredOut + edge.isFilteredOut = false; } else { - // edge is filtered out if it fails any filter tests + // edge is filtered out if it fails ANY filter tests edge.isFilteredOut = !matched; } } function m_IsEdgeMatchedByFilter(edge, filter) { - // console.log('...m_IsEdgeMatchedByFilter', edge[filter.key], filter.value); if ((filter.key === undefined) || (filter.operator === undefined) || (filter.value === undefined)) { @@ -362,26 +452,12 @@ function m_IsEdgeMatchedByFilter(edge, filter) { // edges require special handling because `source` and `target` // point to node data, not simple strings. let edgeStr; - // switch (filter.key) { - // case FILTER.KEY.SOURCE: - // edgeStr = edge.source.label; - // break; - // case FILTER.KEY.TARGET: - // edgeStr = edge.target.label; - // break; - // default: - // edgeStr = edge[filter.key]; - // break; - // } - if (filter.type === FILTER.TYPES.NODE) { edgeStr = edge[filter.key].label; // search on the source/target node label } else { edgeStr = edge[filter.key]; } - console.error('####match edgestr',edgeStr, 'to', filter.value) - switch (filter.operator) { case FILTER.STRING_OPERATORS.CONTAINS: return m_MatchString(filter.value, edgeStr, true); From cfc9e4e576202f3d82e4d884ae43e09e610ca138 Mon Sep 17 00:00:00 2001 From: Ben Loh Date: Thu, 13 Aug 2020 10:51:09 -0700 Subject: [PATCH 23/47] config-ip-filter: Remove padding from InfoPanel. --- build/app/view/netcreate/NetCreate.jsx | 2 +- .../view/netcreate/components/InfoPanel.jsx | 20 +++++++------------ .../view/netcreate/components/NetGraph.jsx | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/build/app/view/netcreate/NetCreate.jsx b/build/app/view/netcreate/NetCreate.jsx index da40eda7..252b9107 100644 --- a/build/app/view/netcreate/NetCreate.jsx +++ b/build/app/view/netcreate/NetCreate.jsx @@ -133,7 +133,7 @@ const FILTERLOGIC = require('./filter-logic'); // handles filtering functions
-
+
Please contact Professor diff --git a/build/app/view/netcreate/components/InfoPanel.jsx b/build/app/view/netcreate/components/InfoPanel.jsx index 87dc2fa2..6128c7b5 100644 --- a/build/app/view/netcreate/components/InfoPanel.jsx +++ b/build/app/view/netcreate/components/InfoPanel.jsx @@ -108,10 +108,9 @@ class InfoPanel extends UNISYS.Component { e.stopPropagation(); let top = e.clientY + this.state.draggerMouseOffsetY; this.setState({ - tabpanelHeight: (top - this.state.tabpanelTop) + 'px', - tableHeight: (top - this.state.tabpanelTop - 55) + 'px', // Hacked tab button + thead offset - draggerTop: top + 'px', - savedTabpanelHeight: (top - this.state.tabpanelTop) + 'px', // remember height when switching tabs + tabpanelHeight: (top - this.state.tabpanelTop - 40) + 'px', + tableHeight: (top - this.state.tabpanelTop) + 'px', + savedTabpanelHeight: (top - this.state.tabpanelTop - 40) + 'px', // remember height when switching tabs bIgnoreTableUpdates: true // ignore this update at the table level if it is a large data set }); } @@ -150,7 +149,7 @@ class InfoPanel extends UNISYS.Component { return (
+ style={{ height: tabpanelHeight, overflow: 'hidden', backgroundColor: '#eee'}}> - + - - - - - + @@ -244,8 +239,7 @@ class InfoPanel extends UNISYS.Component {