diff --git a/idaes/core/ui/fsvis/fsvis.py b/idaes/core/ui/fsvis/fsvis.py index 798b7fbd1a..2376df7ca7 100644 --- a/idaes/core/ui/fsvis/fsvis.py +++ b/idaes/core/ui/fsvis/fsvis.py @@ -53,6 +53,7 @@ def visualize( save: Optional[Union[Path, str, bool]] = None, load_from_saved: bool = True, save_dir: Optional[Path] = None, + save_time_interval = 5000, # 5 seconds overwrite: bool = False, browser: bool = True, port: Optional[int] = None, @@ -78,6 +79,8 @@ def visualize( save_dir: If this argument is given, and ``save`` is not given or a relative path, then it will be used as the directory to save the default or given file. The current working directory is the default. If ``save`` is given and an absolute path, this argument is ignored. + save_time_interval: The time interval that the UI application checks if any changes has occurred + in the graph for it to save the model. Default is 5 seconds overwrite: If True, and the file given by ``save`` exists, overwrite instead of creating a new numbered file. browser: If true, open a browser @@ -104,6 +107,7 @@ def visualize( # Start the web server if web_server is None: web_server = FlowsheetServer(port=port) + web_server.add_setting('save_time_interval', save_time_interval) web_server.start() if not quiet: print("Started visualization server") diff --git a/idaes/core/ui/fsvis/model_server.py b/idaes/core/ui/fsvis/model_server.py index 9288d5c8b0..8e798d3853 100644 --- a/idaes/core/ui/fsvis/model_server.py +++ b/idaes/core/ui/fsvis/model_server.py @@ -58,6 +58,7 @@ def __init__(self, port=None): self._dsm = persist.DataStoreManager() self._flowsheets = {} self._thr = None + self._settings_block = {} @property def port(self): @@ -70,6 +71,36 @@ def start(self): self._thr.setDaemon(True) self._thr.start() + def add_setting(self, key: str, value): + """Add a setting to the flowsheet's settings block. Settings block is + a dict that has general setting values related to the UI server. Such + values could be retrieved to set some settings in the UI. + + An example setting value is the `save_model_time_interval` which sets + the time interval at which the model checks if the graph has changed + or not for the model to be saved. + + Args: + key: Setting name + value: Setting value + """ + self._settings_block[key] = value + + def get_setting(self, key: str): + """Get a setting value from the flowsheet's settings block. + + Args: + key: Setting name + + Returns: + Setting value. None if Setting name (key) doesn't exist + """ + if key not in self._settings_block: + _log.warning(f"key '{key}' is not set in the flowsheet settings block") + return None + return self._settings_block[key] + + def add_flowsheet(self, id_, flowsheet, store: persist.DataStore) -> str: """Add a flowsheet, and also the method of saving it. @@ -228,19 +259,31 @@ def do_GET(self): Routes: * `/app`: Return the web page * `/fs`: Retrieve an updated flowsheet. + * `/setting`: Retrieve a setting value. * `/path/to/file`: Retrieve file stored static directory """ - u, id_ = self._parse_flowsheet_url(self.path) + u, queries = self._parse_flowsheet_url(self.path) + id_ = queries.get("id", None) if queries else None + _log.debug(f"do_GET: path={self.path} id=={id_}") if u.path in ("/app", "/fs") and id_ is None: self.send_error( 400, message=f"Query parameter 'id' is required for '{u.path}'" ) return + if u.path == "/app": self._get_app(id_) elif u.path == "/fs": self._get_fs(id_) + elif u.path == "/setting": + setting_key_ = queries.get("setting_key", None) if queries else None + if setting_key_ is None: + self.send_error( + 400, message=f"Query parameter 'setting_key' is required for '{u.path}'" + ) + return + self._get_setting(setting_key_) else: # Try to serve a file self.directory = _static_dir # keep here: overwritten if set earlier @@ -277,12 +320,25 @@ def _get_fs(self, id_: str): # Return merged flowsheet self._write_json(200, merged) + def _get_setting(self, setting_key_: str): + """Get setting value. + + Args: + id_: Flowsheet identifier + setting_key_: Setting name (key) + + Returns: + Setting value + """ + self._write_json(200, {'setting_value': self.server.get_setting(setting_key_)}) + # === PUT === def do_PUT(self): """Process a request to store data. """ - u, id_ = self._parse_flowsheet_url(self.path) + u, queries = self._parse_flowsheet_url(self.path) + id_ = queries.get("id", None) if queries else None _log.info(f"do_PUT: route={u} id={id_}") if u.path in ("/fs",) and id_ is None: self.send_error( @@ -328,11 +384,10 @@ def _write_html(self, code, page): self.wfile.write(value) def _parse_flowsheet_url(self, path): - u, id_ = urlparse(self.path), None + u, queries = urlparse(path), None if u.query: queries = dict([q.split("=") for q in u.query.split("&")]) - id_ = queries.get("id", None) - return u, id_ + return u, queries # === Logging === diff --git a/idaes/core/ui/fsvis/static/js/main.js b/idaes/core/ui/fsvis/static/js/main.js index 4ab07ee1e0..0d434c4879 100644 --- a/idaes/core/ui/fsvis/static/js/main.js +++ b/idaes/core/ui/fsvis/static/js/main.js @@ -27,6 +27,16 @@ export class App { constructor (flowsheetId) { this.paper = new Paper(this); const url = `/fs?id=${ flowsheetId }`; + + // Adding a special flag to mark that the graph changed + this._is_graph_changed = false; + // Setting name (key) that defines the save model time interval + this._save_time_interval_key = 'save_time_interval'; + this._default_save_time_interval = 5000; // Default time interval + this._save_time_interval = this.getSaveTimeInterval(); + + this.setupGraphChangeChecker(this._save_time_interval); + $.ajax({url: url, datatype: 'json'}) .done((model) => { this.renderModel(model); @@ -92,12 +102,10 @@ export class App { */ refreshModel(url, paper) { // Inform user of progress (1) - // console.debug("paper.model=", paper.model); this.informUser(0, "Refresh: save current values from model"); // First save our version of the model let clientModel = paper.graph; let clientData = JSON.stringify(clientModel.toJSON()); - // console.debug(`Sending to ${url}: ` + clientData); $.ajax({url: url, type: 'PUT', contentType: "application/json", data: clientData}) // On failure inform user and stop .fail(error => this.informUser( @@ -135,7 +143,66 @@ export class App { } /** - * Save the model value. + * Get the save time interval value from the application's setting block. + */ + getSaveTimeInterval() { + let settings_url = "/setting?setting_key=".concat(this._save_time_interval_key); + + let save_time_interval = this._default_save_time_interval; + + $.ajax({url: settings_url, type: 'GET', contentType: "application/json"}) + // On failure inform user and stop + .fail(error => this.informUser( + 2, "Fatal error: cannot get setting value: " + error)) + .done((response) => { + if (response.value != 'None') { + save_time_interval = response.value; + } + else { + this.informUser(1, "Warning: save_time_interval was not set correctly. " + + "Default time value of " + this._default_save_time_interval.toString() + "will be set."); + } + }); + return save_time_interval; + } + + /** + * Set `_is_graph_changed` flag to true. + * + * An example application for this flag is to save the model whenever the + * graph is changed. + */ + graphChanged() { + this._is_graph_changed = true; + } + + /** + * Setup an JS interval that check if the graph has changed and saveModel + * if it does change. + * + * @param wait waiting time before actually saving the model + */ + setupGraphChangeChecker(wait) { + let model_id = $("#idaes-fs-name").data("flowsheetId"); + let flowsheet_url = "/fs?id=".concat(model_id); + + var graphChangedChecker = setInterval(() => { + if (this._is_graph_changed) { + this.saveModel(flowsheet_url, this.paper.graph); + // reset flag + this._is_graph_changed = false; + } + }, wait); + return graphChangedChecker; + } + + /** + * Save the model value. Waiting time could be specified to + * disable multiple redundant saves caused by a stream of events + * + * Changing cell positions & link vertices fire multiple events + * subsequently. That's why we add waiting time before actually + * saving the model. * * This sends a PUT to the server to save the current model value. * @@ -144,7 +211,6 @@ export class App { */ saveModel(url, model) { let clientData = JSON.stringify(model.toJSON()); - // console.debug(`Sending to ${url}: ` + clientData); this.informUser(0, "Save current values from model"); $.ajax({url: url, type: 'PUT', contentType: "application/json", data: clientData}) // On failure inform user and stop diff --git a/idaes/core/ui/fsvis/static/js/paper.js b/idaes/core/ui/fsvis/static/js/paper.js index b92eae000c..4eef91770b 100644 --- a/idaes/core/ui/fsvis/static/js/paper.js +++ b/idaes/core/ui/fsvis/static/js/paper.js @@ -74,8 +74,11 @@ export class Paper { * Register Events before the graph model is loaded */ preSetupRegisterEvents() { - let model_id = $("#idaes-fs-name").data("flowsheetId"); - let url = "/fs?id=".concat(model_id); + + // Save model every time the graph changes + this._graph.on('change:position change:angle change:vertices', () => { + this._app.graphChanged(); + }); // Getting the main elements for the idaes canvas and the stream table // to be able to dispatch highlighting events to the streams existing @@ -182,12 +185,6 @@ export class Paper { idaesCanvas.dispatchEvent(removeHighlightStreamEvent); }); - // Send a post request to the server with the new this._graph - // This is essentially the saving mechanism (for a server instance) for - // right now - // See the comments above the save button for more saving TODOs - self._paper.on('paper:mouseleave', () => {this._app.saveModel(url, self._graph)}); - // Link labels will appear and disappear on right click. Replaces browser context menu self._paper.on("link:contextmenu", function(linkView, evt) { if (linkView.model.label(0)["attrs"]["text"]["display"] === 'none') { diff --git a/idaes/core/ui/fsvis/static/js/toolbar.js b/idaes/core/ui/fsvis/static/js/toolbar.js index 869d945677..030b57c060 100644 --- a/idaes/core/ui/fsvis/static/js/toolbar.js +++ b/idaes/core/ui/fsvis/static/js/toolbar.js @@ -68,12 +68,13 @@ export class Toolbar { // Save event listener document.querySelector("#save-btn").addEventListener("click", () => { - this._app.saveModel(url, this._paper.graph) + this._app.saveModel(url, this._paper.graph); }); // Refresh event listener document.querySelector("#refresh-btn").addEventListener("click", () => { this._app.refreshModel(url, this._paper) + this._app.saveModel(url, this._paper.graph) }); // Flowsheet to SVG export event listener diff --git a/idaes/core/ui/fsvis/tests/test_model_server.py b/idaes/core/ui/fsvis/tests/test_model_server.py index bd15688bcb..186bdad8aa 100644 --- a/idaes/core/ui/fsvis/tests/test_model_server.py +++ b/idaes/core/ui/fsvis/tests/test_model_server.py @@ -113,3 +113,15 @@ def test_flowsheet_server_run(flash_model): print("Bogus PUT") resp = requests.put(f"http://localhost:{srv.port}/fs") assert not resp.ok + # test getting setting values + resp = requests.get(f"http://localhost:{srv.port}/setting") + assert not resp.ok + resp = requests.get(f"http://localhost:{srv.port}/setting?bogus_key=1234") + assert not resp.ok + resp = requests.get(f"http://localhost:{srv.port}/setting?setting_key=save_time_interval") + assert resp.ok + assert resp.json()["setting_value"] == None + srv.add_setting('dummy_setting', 5000) + resp = requests.get(f"http://localhost:{srv.port}/setting?setting_key=dummy_setting") + assert resp.ok + assert resp.json()["setting_value"] == 5000