Skip to content

Commit

Permalink
add SSEs for Field Report updates, listen for them from frontend
Browse files Browse the repository at this point in the history
this starts background-refreshing the Field Reports and Field Report
pages based on changes to Field Report entities. I still have TODOs
in here around making some of that more efficient and on making the
incident (and incidents?) page listen for updates as well.
  • Loading branch information
srabraham committed Jan 16, 2025
1 parent 94d2237 commit c4ed16c
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Each month below should look like the following, using the same ordering for the

- Introduced "striking" of report entries. This allows a user to hide an outdated/inaccurate entry, such that it doesn't appear by default on the Incident or Field Report page. https://github.com/burningmantech/ranger-ims-server/issues/249
- Added help modals, toggled by pressing "?", which show keyboard shortcuts for the current page. https://github.com/burningmantech/ranger-ims-server/issues/1482
- Started publishing Field Report entity updates to the web clients (via server-sent events), and started automatically background-updating the Field Reports (table) and Field Report pages on updates. https://github.com/burningmantech/ranger-ims-server/issues/1498

### Fixed

Expand Down
22 changes: 21 additions & 1 deletion src/ims/application/_eventsource.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from zope.interface import implementer

from ims.ext.json_ext import jsonTextFromObject
from ims.model import Incident
from ims.model import FieldReport, Incident


__all__ = ("DataStoreEventSourceLogObserver",)
Expand Down Expand Up @@ -136,7 +136,27 @@ def _transmogrify(self, loggerEvent: Mapping[str, Any]) -> Event | None:
"event_id": eventName,
"incident_number": incidentNumber,
}
elif eventClass is FieldReport:
fieldReport = loggerEvent.get("fieldReport", None)

if fieldReport is None:
fieldReportNumber = loggerEvent.get("fieldReportNumber", None)
eventName = loggerEvent.get("eventID", "")
else:
fieldReportNumber = fieldReport.number
eventName = fieldReport.eventID

if fieldReportNumber is None:
self._log.critical(
"Unable to determine field report number from store event: {event}",
event=loggerEvent,
)
return None

message = {
"event_id": eventName,
"field_report_number": fieldReportNumber,
}
else:
self._log.critical(
"Unknown data store event class {eventClass} sent event: {event}",
Expand Down
15 changes: 15 additions & 0 deletions src/ims/element/static/field_report.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ function initFieldReportPage() {
disableEditing();
loadAndDisplayFieldReport(loadedFieldReport);

// Updates...it's fine to ignore the returned promise here
requestEventSourceLock();

const fieldReportChannel = new BroadcastChannel(fieldReportChannelName);
fieldReportChannel.onmessage = function (e) {
const number = e.data["field_report_number"];
const event = e.data["event_id"]
const updateAll = e.data["update_all"];

if (updateAll || (event === eventID && number === fieldReportNumber)) {
console.log("Got field report update: " + number);
loadAndDisplayFieldReport();
}
}

// Keyboard shortcuts
document.addEventListener("keydown", function(e) {
// No shortcuts when an input field is active
Expand Down
13 changes: 13 additions & 0 deletions src/ims/element/static/field_reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,25 @@ function initFieldReportsTable() {
requestEventSourceLock();
const fieldReportChannel = new BroadcastChannel(fieldReportChannelName);
fieldReportChannel.onmessage = function (e) {
if (e.data["update_all"]) {
console.log("Reloading the whole table to be cautious, as an SSE was missed")
fieldReportsTable.ajax.reload(clearErrorMessage);
return;
}

const number = e.data["field_report_number"];
const event = e.data["event_id"]
if (event !== eventID) {
return;
}
console.log("Got field report update: " + number);
// TODO(issue/1498): this reloads the entire Field Report table on any
// update to any Field Report. That's not ideal. The thing of which
// to be mindful when GETting a particular single Field Report is that
// limited access users will receive errors when they try to access
// Field Reports for which they're not authorized, and those errors
// show up in the browser console. I'd like to find a way to avoid
// bringing those errors into the console constantly.
fieldReportsTable.ajax.reload(clearErrorMessage);
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/ims/element/static/ims.js
Original file line number Diff line number Diff line change
Expand Up @@ -1080,9 +1080,10 @@ function subscribeToUpdates(closed) {
send.postMessage(JSON.parse(e.data));
}, true);

// TODO: this will never receive any events currently, since the server isn't configured to
// fire events for FieldReports. See
// https://github.com/burningmantech/ranger-ims-server/blob/954498eb125bb9a83d2b922361abef4935f228ba/src/ims/application/_eventsource.py#L113-L135
// TODO(issue/1498): SSEs are now firing for Field Report updates, but we need
// to find an appropriate way for the various pages to handle these updates
// (i.e. without excessive volume of API calls or "unauthorized" errors from
// users with limited access).
eventSource.addEventListener("FieldReport", function(e) {
const send = new BroadcastChannel(fieldReportChannelName);
localStorage.setItem(lastSseIDKey, e.lastEventId);
Expand Down
9 changes: 7 additions & 2 deletions src/ims/element/static/incident.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ function initIncidentPage() {
loadAndDisplayFieldReports();
}
}
// TODO(issue/1498): this page doesn't currently listen for Field Report
// updates, but it probably should. Those updates could be used to add
// to the merged report entries or to the list of Field Reports available
// to be attached. We just want to be careful not to reload all the Field
// Reports on any update, lest we introduce heightened latency.

// Keyboard shortcuts
document.addEventListener("keydown", function(e) {
Expand Down Expand Up @@ -270,7 +275,7 @@ function localLoadPersonnel() {
// Load incident types
//

var incidentTypes = null;
let incidentTypes = null;


function loadIncidentTypesAndCache(success) {
Expand Down Expand Up @@ -722,7 +727,7 @@ function drawMergedReportEntries() {
}
}

entries.sort(compareReportEntries)
entries.sort(compareReportEntries);

drawReportEntries(entries);
}
Expand Down
30 changes: 25 additions & 5 deletions src/ims/store/_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,19 @@ def _notifyIncidentUpdate(
incidentNumber=incidentNumber,
)

def _notifyFieldReportUpdate(
self,
eventID: str,
fieldReportNumber: int,
) -> None:
# This will trigger the DataStoreEventSourceLogObserver
self._log.info(
"Firing field report update event for {eventID}#{fieldReportNumber}",
storeWriteClass=FieldReport,
eventID=eventID,
fieldReportNumber=fieldReportNumber,
)

def _createAndAttachReportEntriesToIncident(
self,
eventID: str,
Expand Down Expand Up @@ -1673,7 +1686,6 @@ def _createAndAttachReportEntriesToFieldReport(
self._log.info(
"Attached report entries to field report "
"{eventID}#{fieldReportNumber}: {reportEntries}",
storeWriteClass=FieldReport,
eventID=eventID,
fieldReportNumber=fieldReportNumber,
reportEntries=reportEntries,
Expand Down Expand Up @@ -1749,10 +1761,13 @@ def createFieldReport(

self._log.info(
"Created field report: {fieldReport}",
storeWriteClass=FieldReport,
fieldReport=fieldReport,
)

self._notifyFieldReportUpdate(
eventID=fieldReport.eventID, fieldReportNumber=fieldReport.number
)

return fieldReport

async def createFieldReport(
Expand Down Expand Up @@ -1828,6 +1843,10 @@ def setFieldReportAttribute(txn: Transaction) -> None:
author=author,
)

self._notifyFieldReportUpdate(
eventID=eventID, fieldReportNumber=fieldReportNumber
)

async def setFieldReport_summary(
self,
eventID: str,
Expand Down Expand Up @@ -1888,6 +1907,9 @@ def addReportEntriesToFieldReport(txn: Transaction) -> None:
error=e,
)
raise
self._notifyFieldReportUpdate(
eventID=eventID, fieldReportNumber=fieldReportNumber
)

###
# Incident to Field Report Relationships
Expand Down Expand Up @@ -2031,9 +2053,7 @@ def setStricken(txn: Transaction) -> None:
error=e,
)
raise
# We still need a notify function like this
# We should also notify the linked incident, if any
# self._notifyFieldReportUpdate(eventID, fieldReportNumber)
self._notifyFieldReportUpdate(eventID, fieldReportNumber)


@frozen(kw_only=True)
Expand Down

0 comments on commit c4ed16c

Please sign in to comment.