diff --git a/build/app/assets/templates/alexander.json b/build/app/assets/templates/alexander.json index dd1a26296..287b6672a 100644 --- a/build/app/assets/templates/alexander.json +++ b/build/app/assets/templates/alexander.json @@ -20,7 +20,7 @@ "label": "Label", "help": "A short title for the node", "duplicateWarning": "You’re entering a duplicate node. Do you want to View the Existing node, or Continue creating?", - "sourceNodeIsLockedMessage": "This node is currently being editted by someone else, please try again later.", + "sourceNodeIsLockedMessage": "This node is currently being edited by someone else, please try again later.", "_cmt4": "/// `Label` is always required and cannot be hidden" }, "type": { @@ -84,6 +84,7 @@ "edgePrompts": { + "edgeIsLockedMessage": "This edge is currently being edited by someone else, please try again later.", "source": { "label": "Source", "help": "", diff --git a/build/app/unisys/server-database.js b/build/app/unisys/server-database.js index 48567912e..9b44d2611 100644 --- a/build/app/unisys/server-database.js +++ b/build/app/unisys/server-database.js @@ -65,6 +65,7 @@ DB.InitializeDatabase = function(options = {}) { m_locked_nodes = new Set(); EDGES = m_db.getCollection("edges"); if (EDGES === null) EDGES = m_db.addCollection("edges"); + m_locked_edges = new Set(); // initialize unique set manager m_dupe_set = new Set(); @@ -215,7 +216,45 @@ function m_IsInvalidNode ( nodeID ) { function m_MakeLockError( info ) { return { NOP:`ERR`, INFO:info }; } - +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +DB.PKT_RequestLockEdge = function (pkt) { + let { edgeID } = pkt.Data(); + let errcode = m_IsInvalidEdge(edgeID); + if (errcode) return errcode; + // check if edge is already locked + if (m_locked_edges.has(edgeID)) return m_MakeLockError(`edgeID ${edgeID} is already locked`); + // SUCCESS + // single matching edge exists and is not yet locked, so lock it + m_locked_edges.add(edgeID); + return { edgeID, locked: true }; +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +DB.PKT_RequestUnlockEdge = function (pkt) { + let { edgeID } = pkt.Data(); + let errcode = m_IsInvalidEdge(edgeID); + if (errcode) return errcode; + // check that edge is already locked + if (m_locked_edges.has(edgeID)) { + m_locked_edges.delete(edgeID); + return { edgeID, unlocked: true }; + } + // this is an error because nodeID wasn't in the lock table + return m_MakeLockError(`edgeID ${edgeID} was not locked so can't unlock`); +}; +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +function m_IsInvalidEdge(edgeID) { + if (!edgeID) return m_MakeLockError(`undefined edgeID`); + edgeID = Number.parseInt(edgeID, 10); + if (isNaN(edgeID)) return m_MakeLockError(`edgeID was not a number`); + if (edgeID < 0) return m_MakeLockError(`edgeID ${edgeID} must be positive integer`); + if (edgeID > m_max_edgeID) return m_MakeLockError(`edgeID ${edgeID} is out of range`); + // find if the node exists + let matches = EDGES.find({ id: edgeID }); + if (matches.length === 0) return m_MakeLockError(`edgeID ${edgeID} not found`); + if (matches.length > 1) return m_MakeLockError(`edgeID ${edgeID} matches multiple entries...critical error!`); + // no retval is no error! + return undefined; +} /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DB.PKT_Update = function(pkt) { let { node, edge, nodeID, replacementNodeID, edgeID } = pkt.Data(); diff --git a/build/app/unisys/server.js b/build/app/unisys/server.js index ca05c59ed..478fde973 100644 --- a/build/app/unisys/server.js +++ b/build/app/unisys/server.js @@ -94,7 +94,17 @@ var UNISYS = {}; return UDB.PKT_RequestUnlockNode(pkt); }); - UNET.HandleMessage('SRV_DBGETEDGEID',function(pkt) { + UNET.HandleMessage('SRV_DBLOCKEDGE', function (pkt) { + if (DBG) console.log(PR, sprint_message(pkt)); + return UDB.PKT_RequestLockEdge(pkt); + }); + + UNET.HandleMessage('SRV_DBUNLOCKEDGE', function (pkt) { + if (DBG) console.log(PR, sprint_message(pkt)); + return UDB.PKT_RequestUnlockEdge(pkt); + }); + + UNET.HandleMessage('SRV_DBGETEDGEID', function (pkt) { if (DBG) console.log(PR,sprint_message(pkt)); return UDB.PKT_GetNewEdgeID(pkt); }); diff --git a/build/app/view/netcreate/components/EdgeEditor.jsx b/build/app/view/netcreate/components/EdgeEditor.jsx index 2f6781019..205509996 100644 --- a/build/app/view/netcreate/components/EdgeEditor.jsx +++ b/build/app/view/netcreate/components/EdgeEditor.jsx @@ -49,11 +49,19 @@ by the EdgeEditor to determine whether it should display the edge nodes as targets or sources. + ## STATES - ## TECHNICAL DESCRIPTION + dbIsLocked + If someone else has selected the edge for editing, + this flag will cause the dbIsLockedMessage + to be displayed. This is only checked when + the user clicks "Edit". - ## TESTING + ## TECHNICAL DESCRIPTION + + + ## TESTING Displaying Current Edge(s) @@ -218,6 +226,7 @@ class EdgeEditor extends UNISYS.Component { id: '' }, isLocked: true, // User has not logged in, don't allow edge edit + dbIsLocked: false, // Server Database is locked because someone else is editing isEditable: false, // Form is in an edtiable state isExpanded: false, // Show EdgeEditor Component in Summary view vs Expanded view sourceIsEditable:false, // Source ndoe field is only editable when source is not parent @@ -236,6 +245,7 @@ class EdgeEditor extends UNISYS.Component { this.onButtonClick = this.onButtonClick.bind(this); this.onDeleteButtonClick = this.onDeleteButtonClick.bind(this); this.onEditButtonClick = this.onEditButtonClick.bind(this); + this.requestEdit = this.requestEdit.bind(this); this.onSwapSourceAndTarget = this.onSwapSourceAndTarget.bind(this); this.onChangeSource = this.onChangeSource.bind(this); this.onChangeTarget = this.onChangeTarget.bind(this); @@ -304,6 +314,7 @@ class EdgeEditor extends UNISYS.Component { }, isEditable: false, isExpanded: false, // Summary view vs Expanded view + dbIsLocked: false, sourceIsEditable: false, // Source ndoe field is only editable when source is not parent hasValidSource: false, // Used by SwapSourceAndTarget and the Change Source button targetIsEditable: false, // Target ndoe field is only editable when target is not parent @@ -313,7 +324,7 @@ class EdgeEditor extends UNISYS.Component { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ populate formdata from D3DATA /*/ loadSourceAndTarget () { - if (DBG) console.log('EdgeEditor.loadSourceAndTarget!') + if (DBG) console.log('EdgeEditor.loadSourceAndTarget!') let edgeID = this.props.edgeID || ''; // Clean Data @@ -327,6 +338,7 @@ class EdgeEditor extends UNISYS.Component { throw 'EdgeEditor: Passed edgeID'+edgeID+'not found!'; } let edge = edges[0]; + if (DBG) console.log('EdgeEditor.loadSourceAndTarget: Loading edge', edge); let sourceNode, sourceNodes, targetNode, targetNodes; @@ -404,7 +416,8 @@ class EdgeEditor extends UNISYS.Component { isNewEdge: false }, sourceNode: sourceNode, - targetNode: targetNode + targetNode: targetNode, + dbIsLocked: false }) } @@ -417,16 +430,24 @@ class EdgeEditor extends UNISYS.Component { When a node is selected via the AutoComplete field, the SELECTION state is updated. So EdgeEditor needs to listen to the SELECTION state in order to know the target node has been selected. + SELECTION is also triggered when the network updates an edge. /*/ handleSelection ( data ) { if (DBG) console.log('EdgeEditor',this.props.edgeID,'got SELECTION data',data); - // If edge is not being edited, ignore the selection - if (!this.state.isEditable && - !(this.state.sourceIsEditable || this.state.targetIsEditable) ) return; + + // If we're one of the edges that have been updated, and we're not currently being edited, + // then update the data. + // If we're not currently being edited, then if edges have been updated, update self + let updatedEdge = data.edges.find((edge) => { return edge.id === this.state.formData.id; }); + if (!this.state.isEditable && updatedEdge!==undefined) { + if (DBG) console.log('EdgeEditor: Updating edges with', updatedEdge); + this.loadSourceAndTarget(); + return; + } // Technically we probably ought to also check to make sure we're the current - // activeAutoCompleteId, but we wouldn't be edtiable if we weren't. - if (data.nodes && data.nodes.length>0) { + // activeAutoCompleteId, but we wouldn't be editable if we weren't. + if (this.state.isEditable && data.nodes && data.nodes.length > 0) { // A node was selected, so load it let node = data.nodes[0]; @@ -457,7 +478,7 @@ class EdgeEditor extends UNISYS.Component { // TARGET if (DBG) console.log('EdgeEditor.handleSelection:',this.props.edgeID,'setting target node to',node); - // Set targetNpde state + // Set targetNode state this.setState({ targetNode: node }); @@ -500,17 +521,13 @@ class EdgeEditor extends UNISYS.Component { /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ Someone externally has selected an edge for editing. - Usually someone has clicked a button in the EdgeList to edit an edge + Usually someone has clicked a button in the EdgeTable to edit an edge /*/ handleEdgeEdit ( data ) { if (DBG) console.log('EdgeEditor',this.state.formData.id,': got state EDGE_EDIT',data,'formData is',this.state.formData); if (this.state.formData.id === data.edgeID) { - this.setState({ - isExpanded: true, - isEditable: true - }); + this.requestEdit(); } - this.AppCall('EDGEEDIT_LOCK', { edgeID: this.props.edgeID }); } // handleEdgeEdit /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -542,8 +559,17 @@ class EdgeEditor extends UNISYS.Component { this.setState({ isEditable: false, targetIsEditable: false }); this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); this.AppCall('AUTOCOMPLETE_SELECT',{id:'search'}); + // unlock + this.NetCall('SRV_DBUNLOCKEDGEE', { edgeID: this.state.formData.id }) + .then((data) => { + if (data.NOP) { + if (DBG) console.log(`SERVER SAYS: ${data.NOP} ${data.INFO}`); + } else if (data.unlocked) { + if (DBG) console.log(`SERVER SAYS: unlock success! you have released Edge ${data.edgeID}`); + this.setState({ dbIsLocked: false }); + } + }); } - } else { // expand, but don't set the autocomplete field, since we're not editing this.setState({ isExpanded: true }); @@ -553,14 +579,13 @@ class EdgeEditor extends UNISYS.Component { /*/ /*/ onDeleteButtonClick () { this.clearForm(); - this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); + this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); // inform NodeSelector this.AppCall('DB_UPDATE',{edgeID:this.props.edgeID}); } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ /*/ onEditButtonClick () { - this.setState({ isEditable: true }); - this.AppCall('EDGEEDIT_LOCK', { edgeID: this.props.edgeID }); + this.requestEdit(this.state.formData.id); // Don't allow editing of the source or target fields. // If you want to change the edge, delete this one and create a new one. @@ -574,6 +599,30 @@ class EdgeEditor extends UNISYS.Component { } /// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /*/ +/*/ requestEdit() { + let edgeID = this.state.formData.id; + if (edgeID && edgeID!=='' && !isNaN(edgeID) && (typeof edgeID ==="number")) { + this.NetCall('SRV_DBLOCKEDGE', { edgeID: edgeID }) + .then((data) => { + if (data.NOP) { + // Edge is locked, can't edit + if (DBG) console.log(`SERVER SAYS: ${data.NOP} ${data.INFO}`); + this.setState({ dbIsLocked: true }); + } else if (data.locked) { + if (DBG) console.log(`SERVER SAYS: lock success! you can edit Edge ${data.edgeID}`); + if (DBG) console.log(`SERVER SAYS: unlock the edge after successful DBUPDATE`); + this.setState({ + isEditable: true, + isExpanded: true, + dbIsLocked: false + }); + this.AppCall('EDGEEDIT_LOCK', { edgeID: this.props.edgeID }); // inform NodeSelector + } + }); + } + } +/// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +/*/ /*/ onSwapSourceAndTarget () { let formData = this.state.formData; @@ -669,12 +718,24 @@ class EdgeEditor extends UNISYS.Component { } if (DBG) console.group('EdgeEntry.onSubmit submitting',edge) - this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); + this.AppCall('EDGEEDIT_UNLOCK', { edgeID: this.props.edgeID }); // inform NodeSelector // pass currentAutoComplete back to nodeselector this.AppCall('AUTOCOMPLETE_SELECT',{id:'search'}); this.setState({ isEditable: false, sourceIsEditable: false, targetIsEditable: false }); - this.AppCall('DB_UPDATE',{ edge }); - } // onSubmit + this.AppCall('DB_UPDATE', { edge }) + .then(() => { + this.NetCall('SRV_DBUNLOCKEDGE', { edgeID: edge.id }) + .then((data) => { + if (data.NOP) { + if (DBG) console.log(`SERVER SAYS: ${data.NOP} ${data.INFO}`); + } else if (data.unlocked) { + if (DBG) console.log(`SERVER SAYS: unlock success! you have released Edge ${data.edgeID}`); + this.setState({ dbIsLocked: false }); + } + }); + }); + + } // onSubmit @@ -814,7 +875,7 @@ class EdgeEditor extends UNISYS.Component {   + >{this.state.isEditable ? "Add New Edge" : "Edit Edge"}  + diff --git a/build/app/view/netcreate/components/NodeSelector.jsx b/build/app/view/netcreate/components/NodeSelector.jsx index 9951708c8..d1e3345e5 100644 --- a/build/app/view/netcreate/components/NodeSelector.jsx +++ b/build/app/view/netcreate/components/NodeSelector.jsx @@ -58,9 +58,9 @@ isEditable If true, form fields are enabled for editing If false, form is readonly - sourceNodeIsLocked + dbIsLocked If someone else has selected the node for editing, - this flag will cause the sourceNodeIsLockedMessage + this flag will cause the dbIsLockedMessage to be displayed. This is only checked when the user clicks "Edit". @@ -151,7 +151,7 @@ class NodeSelector extends UNISYS.Component { edges: [], isLocked: true, edgesAreLocked: false, - sourceNodeIsLocked: false, + dbIsLocked: false, isEditable: false, isValid: false, isDuplicateNodeLabel: false, @@ -229,9 +229,36 @@ class NodeSelector extends UNISYS.Component { updated after the edit is completed, so new edges are added then. /*/ UDATA.HandleMessage("EDGE_UPDATE", (data) => { + if (DBG) console.log('NodeSelector: Received EDGE_UPDATE edgesAreLocked', this.state.edgesAreLocked, data); let currentNodeID = this.state.formData.id; - let updatedNodeIDs = [data.edge.source.id, data.edge.target.id]; + /* EDGE_UPDATES are triggered under two circumnstances: + a. When an existing edge is updated + b. When a new edge is created + The call sequence is: + 1. EdgeEditor.Submit calls datastore.DB_UPDATE + 2. datastore.DB_UPDATE calls server.SRV_DBUPDATE + 3. server.SRV_DBUPDATE broadcasts EDGE_UPDATE + At this point, edge.source and edge.target are broadcast as Numbers. + 4. EDGE_UPDATE is handled by: + a. nc-logic.handleMessage("EDGE_UPDATE"), and + b. NodeSelector.handlemMessage("EDGE_UPDATE") (this method) + 5. nc-logic.handleMessage("EDGE_UPDATE") processes the data and + actually adds a new edge or updates the existing edge in D3DATA. + *** The key is that there is a difference in how it's handled. + For updates, the edge is simply updated. + But for new edges, the edge object is updated and then pushed to D3DATA. + 6. When the edge object is pushed to D3DATA, D3 processes it and converts + edge.source and edge.target into node objects. + *** By the time NodeSelector receives the edge data, edge.source and + edge.target are node objects, not numbers. + So this method needs to account for the fact that edge.source and edge.target might be + received as either numbers or objects. + */ + let sourceID = typeof data.edge.source === "number" ? data.edge.source : data.edge.source.id; + let targetID = typeof data.edge.target === "number" ? data.edge.target : data.edge.target.id; + let updatedNodeIDs = [sourceID, targetID]; if (updatedNodeIDs.includes(currentNodeID) && !this.state.edgesAreLocked) { + if (DBG) console.log('NodeSelector: EDGE UPDATE: Calling SOURCE_SELECT!'); UDATA.LocalCall('SOURCE_SELECT', { nodeIDs: [currentNodeID] }); } }); @@ -269,7 +296,7 @@ class NodeSelector extends UNISYS.Component { isNewNode: true }, edges: [], - sourceNodeIsLocked: false, + dbIsLocked: false, isEditable: false, isValid: false, isDuplicateNodeLabel: false, @@ -366,7 +393,7 @@ class NodeSelector extends UNISYS.Component { // * force exit? // * prevent load? // * prevent selection? - if (DBG) console.error('NodeSelector: Already editing, ignoring SELECTION'); + if (DBG) console.log('NodeSelector: Already editing, ignoring SELECTION'); } this.validateForm(); @@ -453,7 +480,7 @@ class NodeSelector extends UNISYS.Component { id: node.id, isNewNode: false }, - sourceNodeIsLocked: false, + dbIsLocked: false, isEditable: false, isDuplicateNodeLabel: false }); @@ -590,11 +617,11 @@ class NodeSelector extends UNISYS.Component { .then((data)=>{ if (data.NOP) { console.log(`SERVER SAYS: ${data.NOP} ${data.INFO}`); - this.setState({ sourceNodeIsLocked: true }); + this.setState({ dbIsLocked: true }); } else if (data.locked) { console.log(`SERVER SAYS: lock success! you can edit Node ${data.nodeID}`); console.log(`SERVER SAYS: unlock the node after successful DBUPDATE`); - this.setState({ sourceNodeIsLocked: false }); + this.setState({ dbIsLocked: false }); this.editNode(); } }); @@ -664,7 +691,7 @@ class NodeSelector extends UNISYS.Component { console.log(`SERVER SAYS: ${data.NOP} ${data.INFO}`); } else if (data.unlocked) { console.log(`SERVER SAYS: unlock success! you have released Node ${data.nodeID}`); - this.setState({ sourceNodeIsLocked: false }); + this.setState({ dbIsLocked: false }); } }); } @@ -726,7 +753,7 @@ class NodeSelector extends UNISYS.Component { console.log(`SERVER SAYS: ${data.NOP} ${data.INFO}`); } else if (data.unlocked) { console.log(`SERVER SAYS: unlock success! you have released Node ${data.nodeID}`); - this.setState({ sourceNodeIsLocked: false }); + this.setState({ dbIsLocked: false }); } }); }); @@ -817,7 +844,7 @@ class NodeSelector extends UNISYS.Component { hidden={this.state.isLocked || this.state.isEditable || (this.state.formData.id==='') } onClick={this.onEditButtonClick} >Edit Node - +