dbus-vis: A floating detail info window

Adds a draggable floating window for showing information regarding
highlighted messages.

Signed-off-by: Sui Chen <suichen@google.com>
Change-Id: If35cdd8962f102934934677d6160a555b56df99c
diff --git a/dbus-vis/dbus_pcap_loader.js b/dbus-vis/dbus_pcap_loader.js
index 9118cb3..dc7d321 100644
--- a/dbus-vis/dbus_pcap_loader.js
+++ b/dbus-vis/dbus_pcap_loader.js
@@ -285,7 +285,7 @@
         }
         let entry = [
           'mc', timestamp, serial, sender, destination, path, iface, member,
-          timestamp_end, payload, ''
+          timestamp_end, payload, packet, ''
         ];
 
         // Legacy IPMI interface uses method call for IPMI response
diff --git a/dbus-vis/dbus_vis.css b/dbus-vis/dbus_vis.css
index b230c0a..d31a274 100644
--- a/dbus-vis/dbus_vis.css
+++ b/dbus-vis/dbus_vis.css
@@ -61,6 +61,7 @@
 	width: 100%; height: 100%;
 	background-color: rgba(128, 128, 128, 0.6);
 	display: none;
+	z-index: 10;
 }
 
 #blocker_caption {
@@ -110,4 +111,30 @@
 #span_group_by_dbus, #span_group_by_ipmi, #span_group_by_boost_asio_handler,
 #div_navi_and_replay{
   display: none;
+}
+
+.info_panel {
+	position: absolute;
+	z-index: 9;
+	border: 1px solid #888;
+}
+
+.info_panel_header {
+	padding: 2px;
+	background-color: #CCC;
+	cursor: move;
+	z-index: 10;
+}
+
+.info_panel_content {
+	background-color: #EEE;
+	max-height: 800px;
+	overflow-y: scroll;
+	max-width: 400px;
+	overflow-x: scroll;
+}
+
+#highlighted_messages {
+	top: 10px; left: 10px;
+	display: none
 }
\ No newline at end of file
diff --git a/dbus-vis/index.html b/dbus-vis/index.html
index d387ea5..0af3c08 100644
--- a/dbus-vis/index.html
+++ b/dbus-vis/index.html
@@ -11,147 +11,153 @@
   <body>
     <div id="div_title"><span>
     <b>DBus &amp; IPMI Request Visualizer</b>
-		</span>
-		</div>
+    </span>
+    </div>
     <div id="title_mode_select">
-			<span>
-				<input type="radio" id="radio_open_file" name="mode" value="open_file"></input>
-  			<label for="radio_open_file">Open an existing file</label>
-  			<input type="radio" id="radio_capture" name="mode" value="capture"></input>
-  			<label for="radio_capture">Capture on a BMC</label>
+      <span>
+        <input type="radio" id="radio_open_file" name="mode" value="open_file"></input>
+        <label for="radio_open_file">Open an existing file</label>
+        <input type="radio" id="radio_capture" name="mode" value="capture"></input>
+        <label for="radio_capture">Capture on a BMC</label>
       </span>
-		</div>
-		<div id="title_open_file">
-			<span><b>File name: </b></span>
-			<span id="file_name"></span>
-			<button id="btn_open_file">Open file</button>
-			<br />
-		</div>
-		<div id="title_capture">
-			<span><b>Capture: </b></span>
-			<input type="text" id="text_hostname" value=""></input>
-			<button id="btn_start_capture">Start Capture</button>
-			<button id="btn_stop_capture">Stop Capture</button>
-			<select id="select_capture_mode">
-				<option value="live">Live</option>
-				<option value="staged">Staged (IPMI detailed)</option>
-				<option value="staged2">Staged (DBus + IPMI)</option>
-			</select>
+    </div>
+    <div id="title_open_file">
+      <span><b>File name: </b></span>
+      <span id="file_name"></span>
+      <button id="btn_open_file">Open file</button>
+      <br />
+    </div>
+    <div id="title_capture">
+      <span><b>Capture: </b></span>
+      <input type="text" id="text_hostname" value=""></input>
+      <button id="btn_start_capture">Start Capture</button>
+      <button id="btn_stop_capture">Stop Capture</button>
+      <select id="select_capture_mode">
+        <option value="live">Live</option>
+        <option value="staged">Staged (IPMI detailed)</option>
+        <option value="staged2">Staged (DBus + IPMI)</option>
+      </select>
     </div>    
-		<div id="title_capture_info">
-			<div id="capture_info">Info will appear here</div>
-		</div>
+    <div id="title_capture_info">
+      <div id="capture_info">Info will appear here</div>
+    </div>
 
     <hr/> 
 
-		<div id="div_group_by">
+    <div id="div_group_by">
     <span id="span_group_by_dbus">
-				Group DBus Requests by:
-				<input type="checkbox" id="dbus_column1" value="Type">Type</input>
-				<input type="checkbox" id="dbus_column2" value="Serial">Serial</input>
-				<input type="checkbox" id="dbus_column3" value="Sender">Sender</input>
-				<input type="checkbox" id="dbus_column4" value="Destination">Destination</input>
-				<input type="checkbox" id="dbus_column5" value="Path"checked>Path</input>
-				<input type="checkbox" id="dbus_column6" value="Interface" checked>Interface</input>
-				<input type="checkbox" id="dbus_column7" value="Member">Member</input>
+        Group DBus Requests by:
+        <input type="checkbox" id="dbus_column1" value="Type">Type</input>
+        <input type="checkbox" id="dbus_column2" value="Serial">Serial</input>
+        <input type="checkbox" id="dbus_column3" value="Sender">Sender</input>
+        <input type="checkbox" id="dbus_column4" value="Destination">Destination</input>
+        <input type="checkbox" id="dbus_column5" value="Path"checked>Path</input>
+        <input type="checkbox" id="dbus_column6" value="Interface" checked>Interface</input>
+        <input type="checkbox" id="dbus_column7" value="Member">Member</input>
     </span>
     <span id="span_group_by_ipmi">
-			  Group IPMI Requests by:
-				<input type="checkbox" id="c1" value="NetFN" checked>NetFN</input>
-				<input type="checkbox" id="c2" value="CMD"     checked>CMD</input>
+        Group IPMI Requests by:
+        <input type="checkbox" id="c1" value="NetFN" checked>NetFN</input>
+        <input type="checkbox" id="c2" value="CMD"     checked>CMD</input>
     </span>
     <div id="span_group_by_boost_asio_handler">
-			  Group Asio Handlers by:
-				<input type="checkbox" id="bah1" value="Layout Level" checked>Layout Level</input>
-				<input type="checkbox" id="bah2" value="Description">Description</input>
-				<input type="checkbox" id="bah3" value="Description1">Description w/ addresses removed</input>
+        Group Asio Handlers by:
+        <input type="checkbox" id="bah1" value="Layout Level" checked>Layout Level</input>
+        <input type="checkbox" id="bah2" value="Description">Description</input>
+        <input type="checkbox" id="bah3" value="Description1">Description w/ addresses removed</input>
     </div>
-		</div>
+    </div>
       
-		<div id="div_canvas">
-				<div id="blocker">
-						<div id="blocker_caption">AAAAA</div>
-				</div>
-				<div id="welcome_screen">
-						<div id="welcome_screen_content">
-								<br/><span>Welcome! Please <button id="btn_open_file2">Open file</button> to get timeline view/s.<span>
-								<br/>
-								<br/>Supported file types:
-								<ol>
-										<li>DBus pcap file</li>
-										<li>Boost ASIO handler log file</li>
-								</ol>
-								One file from each type (2 files in total) can be viewed simultaneously.
-						</div>
-						<hr />
-						<div id="dbus_pcap_status_content">
-							dbus-pcap status goes here
-						</div>
-						<div id="dbus_pcap_error_content">
-							The <b>dbus-pcap</b> script is not found; dbus-vis needs <b>dbus-pcap</b> for parsing PCAP files.<br/><br/>
-							Click to down <b>dbus-pcap</b> from: <br/>
-								https://raw.githubusercontent.com/openbmc/openbmc-tools/08ce0a5bad2b5c970af567c2e9888d444afe3946/dbus-pcap/dbus-pcap<br/><br/>
-							<button id="btn_download_dbus_pcap">Download to dbus-vis folder</button>
-						</div>
-						<div id="scapy_error_content">
-							The <b>scapy</b> Python module is not installed. dbus-vis depends on dbus-pcap, which in turn depends on the scapy Python module.
-							
-							Please install it using either of the following commands:<br/>
-							<br/>
-							python3 -m pip install scapy<br/>
-							sudo apt install python3-scapy<br/>
-							<br/>
-							After installation, refresh dbus-vis with Ctrl+R.
-						</div>
-				</div>
-				<canvas id="my_canvas_dbus" width="1400" height="600"></canvas>
-				<canvas id="my_canvas_ipmi" width="1400" height="200"></canvas>
-				<canvas id="my_canvas_boost_asio_handler" width="1400" height="200"></canvas>
-		</div>
+    <div id="div_canvas">
+        <div id="blocker">
+            <div id="blocker_caption">AAAAA</div>
+        </div>
+        <div id="welcome_screen">
+            <div id="welcome_screen_content">
+                <br/><span>Welcome! Please <button id="btn_open_file2">Open file</button> to get timeline view/s.<span>
+                <br/>
+                <br/>Supported file types:
+                <ol>
+                    <li>DBus pcap file</li>
+                    <li>Boost ASIO handler log file</li>
+                </ol>
+                One file from each type (2 files in total) can be viewed simultaneously.
+            </div>
+            <hr />
+            <div id="dbus_pcap_status_content">
+              dbus-pcap status goes here
+            </div>
+            <div id="dbus_pcap_error_content">
+              The <b>dbus-pcap</b> script is not found; dbus-vis needs <b>dbus-pcap</b> for parsing PCAP files.<br/><br/>
+              Click to down <b>dbus-pcap</b> from: <br/>
+                https://raw.githubusercontent.com/openbmc/openbmc-tools/08ce0a5bad2b5c970af567c2e9888d444afe3946/dbus-pcap/dbus-pcap<br/><br/>
+              <button id="btn_download_dbus_pcap">Download to dbus-vis folder</button>
+            </div>
+            <div id="scapy_error_content">
+              The <b>scapy</b> Python module is not installed. dbus-vis depends on dbus-pcap, which in turn depends on the scapy Python module.
+              
+              Please install it using either of the following commands:<br/>
+              <br/>
+              python3 -m pip install scapy<br/>
+              sudo apt install python3-scapy<br/>
+              <br/>
+              After installation, refresh dbus-vis with Ctrl+R.
+            </div>
+        </div>
+        <canvas id="my_canvas_dbus" width="1400" height="600"></canvas>
+        <canvas id="my_canvas_ipmi" width="1400" height="200"></canvas>
+        <canvas id="my_canvas_boost_asio_handler" width="1400" height="200"></canvas>
+    </div>
 
-		<div id="div_navi_and_replay">
-			<div>
-			  <span>
-				Navigation Control:
-				<button id="btn_zoom_in">Zoom In</button>
-				<button id="btn_zoom_out">Zoom Out</button>
-				<button id="btn_pan_left">&lt;&lt;</button>
-				<button id="btn_pan_right">&gt;&gt;</button>
-				<button id="btn_zoom_reset">Reset</button>
-				</span>
-			</div>
-			<div>Keyboard: [Left]/[right] arrow keys to pan; [Up]/[down] arrow keys to zoom in/out; Hold [shift] to move faster<br/>Mouse: [Left click]: highlight an interval; [wheel up/down]: zoom in/out; click overflow triangles to warp to out-of-viewport requests
-			</div>
-			<div id="highlight_hint">Click highlighted region to zoom into the region</div>
-			<div>
-				<input type="checkbox" id="cb_debuginfo">Show Debug Info</input>
-			</div>
-			<br/>
-			<div id="ipmi_replay">
-				<span>Generate replay commands for the </span><span id="highlight_count">0</span><span> highlighted IPMI requests:</span>
-				<br/>
-				<span>For replaying through "ipmitool" on host or BMC:
-				<button id="gen_replay_ipmitool1">Individual calls</button>
-				<button id="gen_replay_ipmitool2">exec command list</button></span>
-				<br/>
-				<span>For replaying through "busctl" on BMC:
-				<button id="gen_replay_ipmid_legacy">Legacy Interface (for btbridged)</button>
-				<button id="gen_replay_ipmid_new">New Interface (for kcsbridged / netipmid)</button></span>
-				<textarea rows="10" cols="150" id="ipmi_replay_output"></textarea>
-			</div>
-		</div> <!-- navi and replay -->
+    <div id="div_navi_and_replay">
+      <div>
+        <span>
+        Navigation Control:
+        <button id="btn_zoom_in">Zoom In</button>
+        <button id="btn_zoom_out">Zoom Out</button>
+        <button id="btn_pan_left">&lt;&lt;</button>
+        <button id="btn_pan_right">&gt;&gt;</button>
+        <button id="btn_zoom_reset">Reset</button>
+        </span>
+      </div>
+      <div>Keyboard: [Left]/[right] arrow keys to pan; [Up]/[down] arrow keys to zoom in/out; Hold [shift] to move faster<br/>Mouse: [Left click]: highlight an interval; [wheel up/down]: zoom in/out; click overflow triangles to warp to out-of-viewport requests
+      </div>
+      <div id="highlight_hint">Click highlighted region to zoom into the region</div>
+      <div>
+        <input type="checkbox" id="cb_debuginfo">Show Debug Info</input>
+      </div>
+      <br/>
+      <div id="ipmi_replay">
+        <span>Generate replay commands for the </span><span id="highlight_count">0</span><span> highlighted IPMI requests:</span>
+        <br/>
+        <span>For replaying through "ipmitool" on host or BMC:
+        <button id="gen_replay_ipmitool1">Individual calls</button>
+        <button id="gen_replay_ipmitool2">exec command list</button></span>
+        <br/>
+        <span>For replaying through "busctl" on BMC:
+        <button id="gen_replay_ipmid_legacy">Legacy Interface (for btbridged)</button>
+        <button id="gen_replay_ipmid_new">New Interface (for kcsbridged / netipmid)</button></span>
+        <textarea rows="10" cols="150" id="ipmi_replay_output"></textarea>
+      </div>
+    </div> <!-- navi and replay -->
     <br/>
 
+    <div id="highlighted_messages" class="info_panel">
+      <div id="highlighted_messages_header" class="info_panel_header">Info of selected range</div>
+      <div id="highlighted_messages_content" class="info_panel_content"></div>
+    </div>
+
     <!-- You can also require other files to run in this process -->
     <script src="./timeline_view.js"></script>
     <script src="./dbus_timeline_vis.js"></script>
     <script src="./ipmi_timeline_vis.js"></script>
-		<script src="./boost_handler_timeline_vis.js"></script>
+    <script src="./boost_handler_timeline_vis.js"></script>
     <script src="./ipmi_parse.js"></script>
     <script src="./ipmi_capture.js"></script>
     <script src="./renderer.js"></script>
-		<script src="./dbus_pcap_loader.js"></script>
-		<script src="./initialization.js"></script>
-		<link rel="stylesheet" href="./dbus_vis.css">
+    <script src="./dbus_pcap_loader.js"></script>
+    <script src="./info_panel.js"></script>
+    <script src="./initialization.js"></script>
+    <link rel="stylesheet" href="./dbus_vis.css">
   </body>
 </html>
diff --git a/dbus-vis/info_panel.js b/dbus-vis/info_panel.js
new file mode 100644
index 0000000..0d1130c
--- /dev/null
+++ b/dbus-vis/info_panel.js
@@ -0,0 +1,133 @@
+// This function accomplishes drag-and-drop gestures by attaching temporary onmouse{up,move} events to the document
+// Only the header is draggable, the content is not
+function DragElement(elt) {
+  var x1=0, y1=0, x2=0, y2=0;
+  let header = document.getElementById(elt.id + "_header");
+  if (header == undefined) {
+    return;
+  }
+  header.onmousedown = DragMouseDown;
+
+  function DragMouseDown(e) {
+    e = e || window.event;
+    e.preventDefault();
+    // get the mouse cursor position at startup:
+    x2 = e.clientX;
+    y2 = e.clientY;
+    document.onmouseup = MouseUp;
+    // call a function whenever the cursor moves:
+    document.onmousemove = MouseMove;
+  }
+
+  function MouseMove(e) {
+    e = e || window.event;
+    e.preventDefault();
+    x1 = x2 - e.clientX; y1 = y2 - e.clientY;
+    x2 = e.clientX; y2 = e.clientY;
+    elt.style.top = (elt.offsetTop - y1) + "px";
+    elt.style.left= (elt.offsetLeft- x1) + "px";
+  }
+
+  function MouseUp() {
+    document.onmouseup = null;
+    document.onmousemove = null;
+  }
+}
+
+function GetTotalCount(titles_and_intervals) {
+  let ret = 0;
+  titles_and_intervals.forEach((ti) => {
+    ret += ti[1].length;
+  });
+  return ret;
+}
+
+var g_info_panel_table = undefined;
+
+// 6 rows
+// Serial, Sender, Dest, Path, Iface, Member
+function AddOneRowToTable(t, content) {
+  let tr = document.createElement("tr");
+  let td = document.createElement("td");
+  td.colSpan = 6;
+  t.appendChild(tr);
+  tr.appendChild(td);
+  td.innerText = content;
+}
+
+function AddDBusRowHeaderToTable(t) {
+  const headers = [ "Serial", "Sender", "Destination", "Path", "Iface", "Member" ];
+  let tr = document.createElement("tr");
+  headers.forEach((h) => {
+    let td = document.createElement("td");
+    td.innerText = h;
+    td.style.backgroundColor = "#888";
+    tr.appendChild(td);
+  });
+  t.appendChild(tr);
+}
+
+function AddDBusMessageToTable(t, m) {
+  const minfo = m[2];
+  const cols = [
+    minfo[2], // serial
+    minfo[3], // sender
+    minfo[4], // destination
+    minfo[5], // path
+    minfo[6], // iface
+    minfo[7], // member
+  ];
+  let tr = document.createElement("tr");
+  cols.forEach((c) => {
+    let td = document.createElement("td");
+    td.innerText = c;
+    td.onclick = () => {
+      console.log(m);
+    };
+    tr.appendChild(td);
+  });
+  t.appendChild(tr);
+}
+
+function UpdateHighlightedMessagesInfoPanel() {
+  const MAX_ENTRIES_SHOWN = 10000; // Show a maximum of this many
+  const ti_dbus = dbus_timeline_view.HighlightedMessages();
+  const ti_ipmi = ipmi_timeline_view.HighlightedMessages();
+
+  if (g_info_panel_table != undefined) {
+    g_info_panel_table.remove(); // Remove from DOM tree
+  }
+  
+  g_info_panel_table = document.createElement("table");
+
+  const p = document.getElementById("highlighted_messages");
+  p.style.display = "block";
+
+  const x = document.getElementById("highlighted_messages_content");
+  x.appendChild(g_info_panel_table);
+  
+  let nshown = 0;
+  const count_dbus = GetTotalCount(ti_dbus);
+  if (count_dbus > 0) {
+    AddOneRowToTable(g_info_panel_table, count_dbus + " DBus messages");
+    AddDBusRowHeaderToTable(g_info_panel_table);
+    for (let i=0; i<ti_dbus.length; i++) {
+      const title_and_msgs = ti_dbus[i];
+      // Title
+      AddOneRowToTable(g_info_panel_table, title_and_msgs[0]);
+      for (let j=0; j<title_and_msgs[1].length; j++) {
+        nshown ++;
+        if (nshown > MAX_ENTRIES_SHOWN) break;
+        AddDBusMessageToTable(g_info_panel_table, title_and_msgs[1][j]);
+        // Messages
+      }
+    }
+  }
+  const count_ipmi = GetTotalCount(ti_ipmi);
+  if (count_ipmi > 0) {
+    AddOneRowToTable(g_info_panel_table, count_ipmi + " IPMI messages");
+  }
+  if (nshown > MAX_ENTRIES_SHOWN) {
+    AddOneRowToTable(g_info_panel_table, "Showing only " + MAX_ENTRIES_SHOWN + " entries");
+  }
+}
\ No newline at end of file
diff --git a/dbus-vis/initialization.js b/dbus-vis/initialization.js
index bc85c8f..a7f209e 100644
--- a/dbus-vis/initialization.js
+++ b/dbus-vis/initialization.js
@@ -177,6 +177,8 @@
   v3.AccentColor = '#E22';
 
   CheckDependencies();
+
+  DragElement(document.getElementById("highlighted_messages"));
 }
 
 var g_WelcomeScreen = document.getElementById('welcome_screen');
diff --git a/dbus-vis/timeline_view.js b/dbus-vis/timeline_view.js
index 2971ec6..39f98a0 100644
--- a/dbus-vis/timeline_view.js
+++ b/dbus-vis/timeline_view.js
@@ -626,6 +626,9 @@
     this.UnhighlightIfEmpty();
     this.IsHighlightDirty = true;
     this.MouseState.hoveredSide = undefined;
+
+    // If highlighted area changed, update the info panel
+    UpdateHighlightedMessagesInfoPanel();
   }
 
   UnhighlightIfEmpty() {
@@ -704,7 +707,7 @@
         if (IsIntersected(lbub, vars.highlightedInterval)) {
           vars.numIntersected++;
           isHighlighted = true;
-          vars.currHighlightedReqs.push(intervals_j[i][2]);
+          vars.currHighlightedReqs.push(intervals_j[i][2]);  // TODO: change the name to avoid confusion with HighlightedMessages
         }
       }
 
@@ -1682,6 +1685,33 @@
       }
     }  // End IsCanvasDirty
   }
+
+  // Returns list of highlighted messages.
+  // Format: [ Title, [Message] ]
+  HighlightedMessages() {
+    let ret = [];
+    if (this.HighlightedRegion.t0 == -999 || this.HighlightedRegion.t1 == -999) { return ret; }
+    const lb = Math.min(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
+    const ub = Math.max(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
+    for (let i=0; i<this.Titles.length; i++) {
+      const title = this.Titles[i];
+      if (title.header == true) continue;  // Do not include headers. TODO: Allow rectangular selection
+
+      const line = [ title.title, [] ];
+      const interval_idx = title.intervals_idxes[0];
+      const intervals_i = this.Intervals[interval_idx];
+      for (let j=0; j<intervals_i.length; j++) {
+        const m = intervals_i[j];
+        if (!(m[0] > ub || m[1] < lb)) {
+          line[1].push(m);
+        }
+      }
+      if (line[1].length > 0) {
+        ret.push(line);
+      }
+    }
+    return ret;
+  }
 };
 
 // The extended classes have their own way of drawing popups for hovered entries