From 207ba6718445425329b17139480d10705391ad7a Mon Sep 17 00:00:00 2001 From: tariqksoliman Date: Tue, 14 Feb 2023 14:21:38 -0800 Subject: [PATCH 1/2] #331 Websocket aware configure --- API/Backend/Config/routes/configs.js | 9 +++- API/Backend/Config/setup.js | 6 +++ config/css/config.css | 2 +- config/js/config.js | 65 +++++++++++++++++++++++++++- config/js/websocket.js | 61 ++++++++++++++++++++++++++ docs/pages/Setup/ENVs/ENVs.md | 4 ++ sample.env | 2 + src/essence/essence.js | 14 ++++-- views/configure.pug | 4 ++ 9 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 config/js/websocket.js diff --git a/API/Backend/Config/routes/configs.js b/API/Backend/Config/routes/configs.js index a8df2497..8a29ada3 100644 --- a/API/Backend/Config/routes/configs.js +++ b/API/Backend/Config/routes/configs.js @@ -250,6 +250,13 @@ if (fullAccess) function upsert(req, res, next, cb, info) { let hasVersion = false; req.body = req.body || {}; + console.log(info); + info = info || { + type: "upsert", + }; + info.route = "config"; + info.id = req.body.id; + info.mission = req.body.mission; if (req.body.version != null) hasVersion = true; let versionConfig = null; @@ -697,7 +704,7 @@ function addLayer(req, res, next, cb, forceConfig, caller = "addLayer") { // user defined UUIDs. We remove the proposed_uuid key after using it to check for unique UUIDs. Utils.traverseLayers([req.body.layer], (layer) => { if (layer.uuid != null) { - layer.proposed_uuid = layer.uuid; + layer.proposed_uuid = layer.uuid; } }); diff --git a/API/Backend/Config/setup.js b/API/Backend/Config/setup.js index 02882b17..78a19ab8 100644 --- a/API/Backend/Config/setup.js +++ b/API/Backend/Config/setup.js @@ -18,6 +18,12 @@ let setup = { user: user, AUTH: process.env.AUTH, NODE_ENV: process.env.NODE_ENV, + PORT: process.env.PORT || "8888", + ENABLE_CONFIG_WEBSOCKETS: process.env.ENABLE_CONFIG_WEBSOCKETS, + ROOT_PATH: + process.env.NODE_ENV === "development" + ? "" + : process.env.ROOT_PATH || "", }); } ); diff --git a/config/css/config.css b/config/css/config.css index bec1e1a4..a905ca48 100644 --- a/config/css/config.css +++ b/config/css/config.css @@ -452,7 +452,7 @@ textarea { #toast-container { pointer-events: none; - top: 110px !important; + top: 48px !important; right: 6px !important; } diff --git a/config/js/config.js b/config/js/config.js index 32471bc1..9bb1969a 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -1,6 +1,9 @@ //So that each layer bar will always have a unique id var grandLayerCounter = 0; var mission = ""; +var configId = -1; +var lockConfig = false; +var lockConfigCount; //The active mission filepath var missionPath = ""; var tData; @@ -260,6 +263,7 @@ function initialize() { mission = $(this).find("a").html(); missionPath = calls.missionPath + mission + "/config.json"; + configId = parseInt(Math.random() * 100000); $.ajax({ type: calls.get.type, @@ -272,6 +276,8 @@ function initialize() { if (data.status == "success") { var cData = data.config; + clearLockConfig(); + for (var e in tabEditors) { tabEditors[e].setValue(""); } @@ -2519,12 +2525,27 @@ function addMission() { } function saveConfig(json) { + if (lockConfig === true) { + toast( + "error", + `This configuartion changed while you were working on it. Cannot save without refresh or ${lockConfigCount} more attempt${ + lockConfigCount != 1 ? "s" : "" + } at saving to force it.`, + 5000 + ); + lockConfigCount--; + if (lockConfigCount <= 0) { + clearLockConfig(); + } + return; + } $.ajax({ type: calls.upsert.type, url: calls.upsert.url, data: { mission: mission, config: JSON.stringify(json), + id: configId, }, success: function (data) { if (data.status == "success") { @@ -2928,6 +2949,7 @@ function populateVersions(versions) { data: { mission: $(this).attr("mission"), version: $(this).attr("version"), + id: configId, }, success: function (data) { if (data.status == "success") { @@ -3046,7 +3068,7 @@ function getDuplicatedNames(json) { } let toastId = 0; -function toast(type, message, duration) { +function toast(type, message, duration, className) { let color = "#000000"; switch (type) { case "success": @@ -3062,8 +3084,47 @@ function toast(type, message, duration) { return; } const id = `toast_${type}_${toastId}`; - Materialize.toast(`${message}`, duration || 4000); + Materialize.toast( + `${message}`, + duration || 4000 + ); $(`#${id}`).parent().css("background-color", color); toastId++; } + +const lockConfigTypes = { + main: null, + disconnect: null, +}; +function setLockConfig(type) { + clearLockConfig(type); + lockConfig = true; + lockConfigTypes[type || "main"] = false; + lockConfigCount = 4; + + toast( + "warning", + type === "disconnect" + ? "Websocket disconnected. You will not be able to save until it reconnects." + : "This configuration changed while you were working on it. You must refresh.", + 100000000000, + "lockConfigToast" + ); +} +function clearLockConfig(type) { + lockConfigTypes[type || "main"] = false; + + let canUnlock = true; + Object.keys(lockConfigTypes).forEach((k) => { + if (lockConfigTypes[k] === true) canUnlock = false; + }); + if (canUnlock) { + lockConfig = false; + document + .querySelectorAll(".lockConfigToast") + .forEach((el) => el.parentNode.remove()); + } +} diff --git a/config/js/websocket.js b/config/js/websocket.js new file mode 100644 index 00000000..72752673 --- /dev/null +++ b/config/js/websocket.js @@ -0,0 +1,61 @@ +const Websocket = { + initialWebSocketRetryInterval: 60000, // 1 minute + webSocketRetryInterval: 60000, // Start with this time and double if disconnected + webSocketPingInterval: null, + init: function () { + if (typeof window.setLockConfig === "function") + window.clearLockConfig("disconnect"); + + const port = parseInt(window.mmgisglobal.PORT || "8888", 10); + const protocol = + window.location.protocol.indexOf("https") !== -1 ? "wss" : "ws"; + + const path = + window.mmgisglobal.NODE_ENV === "development" + ? `${protocol}://localhost:${port}${window.mmgisglobal.ROOT_PATH}/` + : `${protocol}://${window.location.host}${window.mmgisglobal.ROOT_PATH}/`; + + // Create WebSocket connection. + const socket = new WebSocket(path); + + // Connection opened + socket.addEventListener("open", (event) => { + Websocket.webSocketRetryInterval = + Websocket.initialWebSocketRetryInterval; + clearInterval(Websocket.webSocketPingInterval); + }); + + // Listen for messages + socket.addEventListener("message", (event) => { + try { + const data = JSON.parse(event.data); + if ( + data?.info?.route === "config" && + parseInt(data?.info?.id || -1) !== window.configId && + data?.info?.mission === window.mission + ) { + if (typeof window.setLockConfig === "function") + window.setLockConfig(); + } + } catch (err) {} + }); + + socket.addEventListener("close", (event) => { + if (typeof window.setLockConfig === "function") + window.setLockConfig("disconnect"); + + clearInterval(Websocket.webSocketPingInterval); + Websocket.webSocketPingInterval = setInterval( + Websocket.init, + Websocket.webSocketRetryInterval + ); // 1 minute + Websocket.webSocketRetryInterval *= 2; + }); + }, +}; + +if ( + mmgisglobal.ENABLE_CONFIG_WEBSOCKETS === "true" || + mmgisglobal.ENABLE_CONFIG_WEBSOCKETS === true +) + Websocket.init(); diff --git a/docs/pages/Setup/ENVs/ENVs.md b/docs/pages/Setup/ENVs/ENVs.md index 1426890a..d739cb8c 100644 --- a/docs/pages/Setup/ENVs/ENVs.md +++ b/docs/pages/Setup/ENVs/ENVs.md @@ -115,3 +115,7 @@ LDAP group of leads (users with elevated permissions) | string | default `''` #### `ENABLE_MMGIS_WEBSOCKETS=` If true, enables the backend MMGIS websockets to tell clients to update layers | boolean | default `false` + +#### `ENABLE_CONFIG_WEBSOCKETS=` + +If true, notifications are sent to /configure users whenever the current mission's configuration object changes out from under them and thne puts (overridable) limits on saving | boolean | default `false` diff --git a/sample.env b/sample.env index 3cfac0bd..c13686c0 100644 --- a/sample.env +++ b/sample.env @@ -62,3 +62,5 @@ LEADS=["user1"] # If true, enables the backend MMGIS websockets to tell clients to update layers ENABLE_MMGIS_WEBSOCKETS=false +# If true, notifications are sent to /configure users whenever the configuration objects changes out from under them and puts (overridable) limits on saving. +ENABLE_CONFIG_WEBSOCKETS=false diff --git a/src/essence/essence.js b/src/essence/essence.js index 68a77ef5..38861b2d 100644 --- a/src/essence/essence.js +++ b/src/essence/essence.js @@ -254,6 +254,10 @@ var essence = { ) } ) + } else { + if (parsed.body && parsed.body.config) { + UserInterface_.updateLayerUpdateButton('RELOAD') + } } } else { if (parsed.body && parsed.body.config) { @@ -370,13 +374,17 @@ var essence = { window.mmgisglobal.PORT && window.mmgisglobal.ENABLE_MMGIS_WEBSOCKETS === 'true' ) { - const port = parseInt(process.env.PORT || '8888', 10) + const port = parseInt(window.mmgisglobal.PORT || '8888', 10) const protocol = window.location.protocol.indexOf('https') !== -1 ? 'wss' : 'ws' const path = window.mmgisglobal.NODE_ENV === 'development' - ? `${protocol}://localhost:${port}/` - : `${protocol}://${window.location.host}/` + ? `${protocol}://localhost:${port}${ + window.mmgisglobal.ROOT_PATH || '' + }/` + : `${protocol}://${window.location.host}${ + window.mmgisglobal.ROOT_PATH || '' + }/` essence.connectWebSocket(path, true) essence.webSocketPingInterval = setInterval( diff --git a/views/configure.pug b/views/configure.pug index 8c9fb483..0b52b059 100644 --- a/views/configure.pug +++ b/views/configure.pug @@ -32,12 +32,16 @@ script. mmgisglobal.SHOW_AUTH_TIMEOUT = true; mmgisglobal.user = '#{user}'; mmgisglobal.NODE_ENV = '#{NODE_ENV}'; + mmgisglobal.ROOT_PATH = '#{ROOT_PATH}'; + mmgisglobal.PORT = '#{PORT}' + mmgisglobal.ENABLE_CONFIG_WEBSOCKETS = '#{ENABLE_CONFIG_WEBSOCKETS}' script(type='text/javascript' src='config/js/calls.js') script(type='text/javascript' src='config/js/keys.js') script(type='text/javascript' src='config/js/datasets.js') script(type='text/javascript' src='config/js/geodatasets.js') script(type='text/javascript' src='config/js/webhooks.js') script(type='text/javascript' src='config/js/config.js') +script(type='text/javascript' src='config/js/websocket.js') script(type='text/javascript' src='config/pre/RefreshAuth.js') #leftPanel From 660f6d8c30fccbb58a6db177d47133722271ee4e Mon Sep 17 00:00:00 2001 From: tariqksoliman Date: Tue, 14 Feb 2023 14:24:40 -0800 Subject: [PATCH 2/2] Remove console.log --- API/Backend/Config/routes/configs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/Backend/Config/routes/configs.js b/API/Backend/Config/routes/configs.js index 8a29ada3..fcd7bb28 100644 --- a/API/Backend/Config/routes/configs.js +++ b/API/Backend/Config/routes/configs.js @@ -250,7 +250,7 @@ if (fullAccess) function upsert(req, res, next, cb, info) { let hasVersion = false; req.body = req.body || {}; - console.log(info); + info = info || { type: "upsert", };