TBR: update oncall dashboard to better support replicated services.

PresubmitTest: none
Change-Id: I4d77db1b8304578cf181326cf554c873c024763e
diff --git a/oncall/client/browser/components/metric-actions-panel/index.js b/oncall/client/browser/components/metric-actions-panel/index.js
index 4cf02a6..3271289 100644
--- a/oncall/client/browser/components/metric-actions-panel/index.js
+++ b/oncall/client/browser/components/metric-actions-panel/index.js
@@ -2,94 +2,254 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+var dateformat = require('dateformat');
 var hg = require('mercury');
 var h = require('mercury').h;
 
+var Consts = require('../../constants');
+var MouseMoveHandler = require('../../mousemove-handler.js');
 var Util = require('../../util');
 
-module.exports = {
-  render: render
-};
+module.exports = create;
+module.exports.render = render;
 
 var VANADIUM_PRODUCTION_NAMESPACE_ID = '4b20e0dc-cacf-11e5-87ec-42010af0020b';
 var AUTH_PRODUCTION_NAMESPACE_ID = '2b6d405a-b4a6-11e5-9776-42010af000a6';
 
+/** Constructor. */
+function create(data) {
+  if (!data) {
+    return null;
+  }
+
+  return hg.state({
+    selectedMetric: hg.struct(data.selectedMetric),
+    selectedMetricIndex: hg.value(data.selectedMetricIndex),
+    visible: hg.value(data.visible),
+
+    mouseOffsetFactor: hg.value(-1),
+
+    channels: {
+      mouseClickOnMetric: mouseClickOnMetric,
+      closeMetricActionsPanel: closeMetricActionsPanel,
+      mouseMoveOnSparkline: mouseMoveOnSparkline,
+      mouseOutOfSparkline: mouseOutOfSparkline
+    }
+  });
+}
+
+/** Callback for moving mouse on sparkline. */
+function mouseClickOnMetric(state, data) {
+  state.selectedMetricIndex.set(data.index);
+}
+
+function closeMetricActionsPanel(state) {
+  state.visible.set(false);
+}
+
+/** Callback for moving mouse on sparkline. */
+function mouseMoveOnSparkline(state, data) {
+  state.mouseOffsetFactor.set(data.f);
+}
+
+/** Callback for moving mouse out of sparkline. */
+function mouseOutOfSparkline(state) {
+  state.mouseOffsetFactor.set(-1);
+}
+
 /** The main render function. */
-function render(state, selectedMetric, data) {
+function render(state, curData) {
+  var colData = state.selectedMetric.colData;
+  var metricsData = curData[colData.dataKey][colData.metricName];
+
+  var list = renderReplicaList(state, metricsData, curData);
+  var panel = renderMetric(
+      state, metricsData[state.selectedMetricIndex],
+      state.selectedMetric.serviceName, curData);
+  return h('div.metric-actions-container',
+      h('div.inner-container', [list, panel])
+  );
+}
+
+function renderReplicaList(state, metricsData, curData) {
+  var colData = state.selectedMetric.colData;
+  var list = metricsData.map(function(metricData, index) {
+    var curIndex = index;
+
+    var points = '0,100 100,100';
+    var timestamps = metricData.HistoryTimestamps;
+    var values = metricData.HistoryValues;
+    if (timestamps.length > 0 && values.length > 0) {
+      points = Util.genPolylinePoints(
+        timestamps, values,
+        curData.MinTime, curData.MaxTime,
+        metricData.MinValue, metricData.MaxValue);
+    }
+    var curValue = Util.formatValue(metricData.CurrentValue);
+    var extraCurValueClass = '';
+    var mouseOffset = 100 * state.mouseOffsetFactor;
+    if (mouseOffset >= 0) {
+      curValue = Util.formatValue(Util.interpolateValue(
+          metricData.CurrentValue, state.mouseOffsetFactor,
+          timestamps, values));
+      extraCurValueClass = '.history';
+    }
+
+    // Handle error when getting time series.
+    var hasErrors = metricData.ErrMsg !== '';
+    var extraColMetricClass = '';
+    if (hasErrors) {
+      curValue = '?';
+      extraColMetricClass = '.warning';
+    }
+    // Handle current value over threshold.
+    var overThreshold = (
+        colData.threshold && metricData.CurrentValue >= colData.threshold);
+    var thresholdValue = -100;
+    if (overThreshold) {
+      extraColMetricClass = '.fatal';
+      thresholdValue = (colData.threshold-metricData.MinValue)/
+          (metricData.MaxValue-metricData.MinValue)*100.0;
+    }
+    // Handle stale data.
+    var tsLen = timestamps.length;
+    if (tsLen > 0) {
+      var lastTimestamp = timestamps[tsLen - 1];
+      if (curData.MaxTime - lastTimestamp >
+          Consts.stableDataThresholdInSeconds) {
+        extraColMetricClass = '.warning';
+      }
+    } else {
+      extraColMetricClass = '.warning';
+    }
+
+    if (index === state.selectedMetricIndex) {
+      extraColMetricClass += '.selected';
+    }
+
+    var sparkline = h('div.col-metric' + extraColMetricClass, {
+      'ev-click': hg.send(state.channels.mouseClickOnMetric, {
+        index: curIndex
+      })
+    }, [
+      h('div.highlight-overlay'),
+      Util.renderMouseLine(mouseOffset),
+      h('div.sparkline', {
+        'ev-mousemove': new MouseMoveHandler(
+            state.channels.mouseMoveOnSparkline),
+        'ev-mouseout': hg.send(state.channels.mouseOutOfSparkline)
+      }, [
+        //renderThreshold(thresholdValue),
+        Util.renderSparkline(points)
+      ]),
+      h('div.cur-value' + extraCurValueClass, [curValue])
+    ]);
+    return sparkline;
+  });
+  return h('div.metric-actions-list', list);
+}
+
+function renderMetric(state, metricData, serviceName, curData) {
   var namespaceId = VANADIUM_PRODUCTION_NAMESPACE_ID;
-  if (selectedMetric.metricData.Project === 'vanadium-auth-production') {
+  if (metricData.Project === 'vanadium-auth-production') {
     namespaceId = AUTH_PRODUCTION_NAMESPACE_ID;
   }
-  var panel = h('div.metric-actions-content', [
+
+  // Calculate current timestamp.
+  var curTimestamp = curData.MaxTime;
+  var extraCurTimestampClass = '';
+  if (metricData.HistoryTimestamps && metricData.HistoryTimestamps.length > 0) {
+    curTimestamp = metricData.HistoryTimestamps[
+        metricData.HistoryTimestamps.length - 1];
+  }
+  if (state.mouseOffsetFactor >= 0) {
+    curTimestamp =
+        (curData.MaxTime - curData.MinTime) * state.mouseOffsetFactor +
+        curData.MinTime;
+    extraCurTimestampClass = '.history';
+  }
+
+  // Calculate current value.
+  var curValue = Util.formatValue(metricData.CurrentValue);
+  var extraCurValueClass = '';
+  var mouseOffset = 100 * state.mouseOffsetFactor;
+  if (mouseOffset >= 0) {
+    curValue = Util.formatValue(Util.interpolateValue(
+        metricData.CurrentValue, state.mouseOffsetFactor,
+        metricData.HistoryTimestamps, metricData.HistoryValues));
+    extraCurValueClass = '.history';
+  }
+
+  return h('div.metric-actions-content', [
       h('div.row', [
         h('div.item-label', 'Service Name'),
-        h('div.item-value', selectedMetric.serviceName)
+        h('div.item-value', serviceName)
       ]),
       h('div.row', [
         h('div.item-label', 'Service Version'),
-        h('div.item-value', selectedMetric.metricData.ServiceVersion)
+        h('div.item-value', metricData.ServiceVersion)
       ]),
       h('div.row', [
         h('div.item-label', 'Metric Type'),
         h('div.item-value',
-          selectedMetric.metricData.ResultType.replace('resultType', ''))
+          metricData.ResultType.replace('resultType', ''))
       ]),
       h('div.row', [
         h('div.item-label', 'Metric Name'),
-        h('div.item-value', selectedMetric.metricData.MetricName)
+        h('div.item-value', metricData.MetricName)
       ]),
       h('div.row', [
         h('div.item-label', 'Current Value'),
-        h('div.item-value',
-          Util.formatValue(selectedMetric.metricData.CurrentValue))
+        h('div.item-value' + extraCurValueClass, curValue)
+      ]),
+      h('div.row', [
+        h('div.item-label', 'Current Time'),
+        h('div.item-value' + extraCurTimestampClass,
+          curTimestamp ===
+            '?' ? '?' : dateformat(new Date(curTimestamp * 1000)))
       ]),
       h('div.row', [
         h('div.item-label', 'Logs'),
         h('div.item-value', h('a', {
-          href: 'logs?p=' + selectedMetric.metricData.Project +
-              '&z=' + selectedMetric.metricData.Zone +
-              '&d=' + selectedMetric.metricData.PodName +
-              '&c=' + selectedMetric.metricData.MainContainer,
+          href: 'logs?p=' + metricData.Project +
+              '&z=' + metricData.Zone +
+              '&d=' + metricData.PodName +
+              '&c=' + metricData.MainContainer,
           target: '_blank'
-        }, selectedMetric.metricData.MainContainer)),
+        }, metricData.MainContainer)),
       ]),
       h('div.space'),
       h('div.row', [
         h('div.item-label', 'Pod Name'),
         h('div.item-value', h('a', {
           href: 'https://app.google.stackdriver.com/gke/pod/1009941:vanadium:' +
-              namespaceId + ':' + selectedMetric.metricData.PodUID,
+              namespaceId + ':' + metricData.PodUID,
           target: '_blank'
-        }, selectedMetric.metricData.Instance)),
+        }, metricData.Instance)),
       ]),
       h('div.row', [
         h('div.item-label', 'Pod Node'),
         h('div.item-value', h('a', {
           href: 'https://app.google.stackdriver.com/instances/' +
-              data.Instances[selectedMetric.metricData.PodNode],
+              curData.Instances[metricData.PodNode],
           target: '_blank'
-        }, selectedMetric.metricData.PodNode)),
-      ]),
-      h('div.row', [
-        h('div.item-label', 'Pod Config'),
-        h('div.item-value', h('a', {
-          href: 'cfg?p=' + selectedMetric.metricData.Project +
-              '&z=' + selectedMetric.metricData.Zone +
-              '&d=' + selectedMetric.metricData.PodName,
-          target: '_blank'
-        }, 'cfg')),
+        }, metricData.PodNode)),
       ]),
       h('div.row', [
         h('div.item-label', 'Pod Status'),
-        h('div.item-value', selectedMetric.metricData.PodStatus)
+        h('div.item-value', h('a', {
+          href: 'cfg?p=' + metricData.Project +
+              '&z=' + metricData.Zone +
+              '&d=' + metricData.PodName,
+          target: '_blank'
+        }, 'status')),
       ]),
       h('div.row', [
         h('div.item-label', 'Zone'),
-        h('div.item-value', selectedMetric.metricData.Zone)
+        h('div.item-value', metricData.Zone)
       ]),
       h('div.btn-close', {
         'ev-click': hg.send(state.channels.closeMetricActionsPanel)
       }, 'Close')
   ]);
-  return h('div.metric-actions-container', panel);
 }
diff --git a/oncall/client/browser/components/metric-item/index.js b/oncall/client/browser/components/metric-item/index.js
deleted file mode 100644
index 2bfb29f..0000000
--- a/oncall/client/browser/components/metric-item/index.js
+++ /dev/null
@@ -1,192 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-/**
- * A metric item is a compact graph in the instance view. It only shows the
- * title, current/pass value, and a sparkline for the metric's data points.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var svg = require('virtual-dom/virtual-hyperscript/svg');
-var uuid = require('uuid');
-
-var Consts = require('../../constants');
-var MouseMoveEvent = require('../../mousemove-handler');
-var Util = require('../../util');
-
-module.exports = create;
-module.exports.render = render;
-
-function create(data) {
-  var state = hg.state({
-    // Graph title.
-    label: data.metric.Name,
-
-    // Current value.
-    value: data.metric.CurrentValue,
-
-    // Value range.
-    minValue: data.metric.MinValue,
-    maxValue: data.metric.MaxValue,
-
-    // Data points
-    historyTimestamps: data.metric.HistoryTimestamps,
-    historyValues: data.metric.HistoryValues,
-
-    // Threshold value.
-    threshold: data.metric.Threshold,
-
-    // Current health.
-    healthy: data.metric.Healthy,
-
-    // The id of a mask for masking the mouse line within the graph area.
-    svgMaskId: uuid.v1(),
-
-    // See comments in instance-view/index.js.
-    mouseOffsetFactor: data.mouseOffsetFactor,
-    hoveredMetric: data.hoveredMetric,
-
-    channels: {
-      mouseMove: mouseMove,
-      mouseOut: mouseOut,
-      mouseOver: mouseOver
-    }
-  });
-
-  return state;
-}
-
-/** Callback when mouse is moving on the graph. */
-function mouseMove(state, data) {
-  if (state.mouseOffsetFactor() !== data.f) {
-    state.mouseOffsetFactor.set(data.f);
-  }
-}
-
-/** Callback when mouse is out of the graph. */
-function mouseOut(state) {
-  state.mouseOffsetFactor.set(-1);
-  state.hoveredMetric.set({});
-}
-
-/** Callback when mouse is over the graph. */
-function mouseOver(state) {
-  state.hoveredMetric.set({
-    label: state.label,
-    value: state.value,
-    minValue: state.minValue,
-    maxValue: state.maxValue,
-    historyTimestamps: state.historyTimestamps,
-    historyValues: state.historyValues
-  });
-}
-
-/** The main render function. */
-function render(state) {
-  var curValue = Util.formatValue(Util.interpolateValue(
-      state.value, state.mouseOffsetFactor,
-      state.historyTimestamps, state.historyValues));
-  var valueClassNames = [];
-  if (state.mouseOffsetFactor >= 0) {
-    valueClassNames.push('historyValue');
-  }
-  if (!state.healthy) {
-    valueClassNames.push('unhealthy');
-  }
-  return h('div.metric-item', {
-    className: state.healthy ? '' : 'unhealthy',
-    'ev-mouseout': hg.send(state.channels.mouseOut),
-    'ev-mouseover': hg.send(state.channels.mouseOver)
-  }, [
-      h('div.metric-item-title', Consts.getDisplayName(state.label)),
-      h('div.metric-item-value', {
-        className: valueClassNames.join(' ')
-      }, curValue),
-      renderGraph(state)
-  ]);
-}
-
-/** Renders the main graph. */
-function renderGraph(state) {
-  var mouseOffset = 100 * state.mouseOffsetFactor;
-  var minTime = state.historyTimestamps[0];
-  var maxTime = state.historyTimestamps[state.historyTimestamps.length - 1];
-  var points = Util.genPolylinePoints(
-      state.historyTimestamps, state.historyValues,
-      minTime, maxTime, state.minValue, state.maxValue);
-
-  var items = [
-    renderSparkline(points),
-    renderMouseLine(points, mouseOffset, state)
-  ];
-  if (state.threshold >= 0) {
-    items.push(renderThresholdLine(state));
-  }
-
-  return h('div.sparkline', {
-    'ev-mousemove': new MouseMoveEvent(state.channels.mouseMove),
-  }, items);
-}
-
-/** Renders the sparkline for the metric's data points. */
-function renderSparkline(points) {
-  return svg('svg', {
-    'class': 'content',
-    'viewBox': '0 0 100 100',
-    'preserveAspectRatio': 'none'
-  }, [
-    svg('polyline', {'points': points}),
-    svg('polygon', {'points': '0,100 ' + points + ' 100,100 0,100'})
-  ]);
-}
-
-/**
- * Renders the mouse line at the given offset.
- *
- * For better appearance, we mask the mouse line within the graph area.
- */
-function renderMouseLine(points, mouseOffset, state) {
-  var maskId = 'mask-' + state.svgMaskId;
-  return svg('svg', {
-    'class': 'mouse-line',
-    'viewBox': '0 0 100 100',
-    'preserveAspectRatio': 'none'
-  }, [
-    svg('defs', [
-      svg('mask', {
-        'id': maskId,
-        'x': 0,
-        'y': 0,
-        'width': 100,
-        'height': 100
-      }, [
-        svg('polygon', {
-          'points': '0,100 ' + points + ' 100,100 0,100',
-          'style': {'fill': '#ffffff'}
-        })
-      ])
-    ]),
-    svg('polyline', {
-      'points': mouseOffset + ',0 ' + mouseOffset + ',100',
-      'mask': 'url(#' + maskId + ')'
-    })
-  ]);
-}
-
-/** Renders the threshold line. */
-function renderThresholdLine(state) {
-  var thresholdOffset = Util.getOffsetForValue(
-      state.threshold, state.minValue, state.maxValue);
-  return svg('svg', {
-    'class': 'threshold-line',
-    'viewBox': '0 0 100 100',
-    'preserveAspectRatio': 'none',
-  }, [
-    svg('path', {
-      'd': 'M0 ' + thresholdOffset + ' L 100 ' + thresholdOffset,
-      'stroke-dasharray': '2,2'
-    })
-  ]);
-}
diff --git a/oncall/client/browser/components/metrics-group/index.js b/oncall/client/browser/components/metrics-group/index.js
deleted file mode 100644
index 09a0ad4..0000000
--- a/oncall/client/browser/components/metrics-group/index.js
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-/**
- * A metric group shows a set of metric items in multiple rows where each row
- * has "numColumns" items. It can also show a set of links (optionally) after
- * the group title.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-
-var metricItemComponent = require('../metric-item');
-
-module.exports = create;
-module.exports.render = render;
-
-var numColumns = 5;
-
-/** Constructor. */
-function create(data) {
-  // Transform data.metrics to an array of metricItemComponents.
-  var metrics = data.metrics.map(function(metric) {
-    return metricItemComponent({
-      metric: metric,
-      mouseOffsetFactor: data.mouseOffsetFactor,
-      hoveredMetric: data.hoveredMetric
-    });
-  });
-
-  // Calculate overall health.
-  var healthy = true;
-  metrics.forEach(function(metric) {
-    healthy &= metric.healthy;
-  });
-
-  var state = hg.state({
-    title: data.title,
-    links: data.links,
-    healthy: healthy,
-    metricItems: hg.array(metrics)
-  });
-
-  return state;
-}
-
-/** The main render function. */
-function render(state) {
-  // Organize metric items in rows and fill the empty space with fillers.
-  var itemSize = state.metricItems.length;
-  var paddedItemSize = (Math.ceil(itemSize / numColumns)) * numColumns;
-  var rows = [];
-  var curRow = null;
-  for (var i = 0; i < paddedItemSize; i++) {
-    if (i % numColumns  === 0) {
-      if (curRow !== null) {
-        rows.push(h('div.metrics-group-row', curRow));
-      }
-      curRow = [];
-    }
-    if (i > itemSize - 1) {
-      curRow.push(h('div.metric-item-filler'));
-    } else {
-      curRow.push(metricItemComponent.render(state.metricItems[i]));
-    }
-  }
-  rows.push(h('div.metrics-group-row', curRow));
-
-  // Group title and links.
-  var titleItems = [
-      h('div.metrics-group-title', h('span', state.title))
-  ];
-  if (state.links) {
-    var links = state.links.map(function(link) {
-      return h('a', {
-        href: link.link,
-        target: '_blank'
-      }, link.name);
-    });
-    titleItems.push(h('div.link-container', links));
-  }
-
-  return h('div.metrics-group', [
-      h('div.metrics-group-title-container', titleItems),
-      h('div.metrics-group-items-container', [
-        h('div.metrics-group-items', rows)
-      ])
-  ]);
-}
diff --git a/oncall/client/browser/components/status-table/index.js b/oncall/client/browser/components/status-table/index.js
index be3e7d6..b3640d0 100644
--- a/oncall/client/browser/components/status-table/index.js
+++ b/oncall/client/browser/components/status-table/index.js
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+var dateformat = require('dateformat');
 var hg = require('mercury');
 var h = require('mercury').h;
 var svg = require('virtual-dom/virtual-hyperscript/svg');
@@ -46,28 +47,6 @@
       },
     ]
   },
-  // Roled.
-  {
-    rowHeader: Consts.metricNames.MN_ROLE,
-    columns: [
-      {
-        dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
-        label: 'LATENCY',
-        metricName: Consts.metricNames.MN_ROLE,
-        threshold: 2000
-      },
-      {
-        dataKey: Consts.dataKeys.DK_SERVICE_QPS,
-        label: 'QPS',
-        metricName: Consts.metricNames.MN_ROLE
-      },
-      {
-        dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
-        label: 'BUILD AGE (h)',
-        metricName: Consts.metricNames.MN_ROLE
-      }
-    ]
-  },
   // Proxy.
   {
     rowHeader: Consts.metricNames.MN_PROXY,
@@ -90,6 +69,28 @@
       }
     ]
   },
+  // Roled.
+  {
+    rowHeader: Consts.metricNames.MN_ROLE,
+    columns: [
+      {
+        dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+        label: 'LATENCY',
+        metricName: Consts.metricNames.MN_ROLE,
+        threshold: 2000
+      },
+      {
+        dataKey: Consts.dataKeys.DK_SERVICE_QPS,
+        label: 'QPS',
+        metricName: Consts.metricNames.MN_ROLE
+      },
+      {
+        dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
+        label: 'BUILD AGE (h)',
+        metricName: Consts.metricNames.MN_ROLE
+      }
+    ]
+  },
   // Identityd.
   {
     rowHeader: Consts.metricNames.MN_IDENTITY,
@@ -139,6 +140,28 @@
         metricName: Consts.metricNames.MN_BENCHMARKS
       }
     ]
+  },
+  // Syncbase allocator.
+  {
+    rowHeader: Consts.metricNames.MN_SB_ALLOCATOR,
+    columns: [
+      {
+        dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+        label: 'LATENCY',
+        metricName: Consts.metricNames.MN_SB_ALLOCATOR,
+        threshold: 2000
+      },
+      {
+        dataKey: Consts.dataKeys.DK_SERVICE_QPS,
+        label: 'QPS',
+        metricName: Consts.metricNames.MN_SB_ALLOCATOR
+      },
+      {
+        dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
+        label: 'BUILD AGE (h)',
+        metricName: Consts.metricNames.MN_SB_ALLOCATOR
+      }
+    ]
   }
 ];
 
@@ -153,10 +176,13 @@
     data: hg.struct(data.data),
 
     mouseOffsetFactor: hg.value(-1),
-    showMetricActionsPanel: hg.value(false),
+
+    mouseOverDataKey: hg.value(''),
+    mouseOverMetricName: hg.value(''),
 
     channels: {
       mouseMoveOnSparkline: mouseMoveOnSparkline,
+      mouseOverSparkline: mouseOverSparkline,
       mouseOutOfSparkline: mouseOutOfSparkline,
     }
   });
@@ -167,93 +193,186 @@
   state.mouseOffsetFactor.set(data.f);
 }
 
+/** Callback for moving mouse over a sparkline. */
+function mouseOverSparkline(state, colData) {
+  state.mouseOverDataKey.set(colData.dataKey);
+  state.mouseOverMetricName.set(colData.metricName);
+}
+
 /** Callback for moving mouse out of sparkline. */
 function mouseOutOfSparkline(state) {
   state.mouseOffsetFactor.set(-1);
+  state.mouseOverDataKey.set('');
+  state.mouseOverMetricName.set('');
 }
 
 /** The main render function. */
 function render(globalState, state) {
   var data = state.data;
 
-  var rows = tableRows.map(function(rowData) {
-    var cols = rowData.columns.map(function(colData) {
-      // Create a column for a metric.
-      var colHeader = h('div.col-header', colData.label);
+  var rows = tableRows.map(function(rowData, rowIndex) {
+    var numWarnings = 0;
+    var numReplicas = 0;
+    var hasFatalErrors = false;
+    var cols = rowData.columns.map(function(colData, colIndex) {
+      // Create a column for each metric.
+      //
+      // Use the first column (usually latency) to determine number of replicas.
       var metricsData = data[colData.dataKey][colData.metricName];
-      var sparkLines = metricsData.map(function(metricData) {
-        // 100 is the default logical width of any svg graphs.
-        var points = '0,100 100,100';
+      if (numReplicas === 0) {
+        numReplicas = metricsData.length;
+      }
+
+      // Calculate average from all replicas.
+      var avg = {};   // timestamps -> [values from replicas]
+      var numErrors = 0;
+      metricsData.forEach(function(metricData) {
         if (metricData && metricData.HistoryTimestamps) {
-          points = Util.genPolylinePoints(
-            metricData.HistoryTimestamps, metricData.HistoryValues,
-            data.MinTime, data.MaxTime,
-            metricData.MinValue, metricData.MaxValue);
+          for (var i = 0; i < metricData.HistoryTimestamps.length; i++) {
+            var t = metricData.HistoryTimestamps[i];
+            var v = metricData.HistoryValues[i];
+            if (!avg[t]) {
+              avg[t] = [];
+            }
+            avg[t].push(v);
+          }
         }
-        var curValue = Util.formatValue(metricData.CurrentValue);
+
+        var hasError = false;
+
         // Handle error when getting time series.
-        var hasErrors = metricData.ErrMsg !== '';
-        var extraColMetricClass = '';
-        if (hasErrors) {
-          curValue = '?';
-          extraColMetricClass = '.err';
+        if (metricData.ErrMsg !== '') {
+          hasError = true;
         }
-        // Handle current value over threshold.
-        var overThreshold = (
-            colData.threshold && metricData.CurrentValue >= colData.threshold);
-        var thresholdValue = -100;
-        if (overThreshold) {
-          extraColMetricClass = '.unhealthy';
-          thresholdValue = (colData.threshold-metricData.MinValue)/
-              (metricData.MaxValue-metricData.MinValue)*100.0;
-        }
+
         // Handle stale data.
         var tsLen = metricData.HistoryTimestamps.length;
         if (tsLen > 0) {
           var lastTimestamp = metricData.HistoryTimestamps[tsLen - 1];
-          if (data.MaxTime - lastTimestamp > 600) {
-            extraColMetricClass = '.stale';
+          if (data.MaxTime - lastTimestamp >
+              Consts.stableDataThresholdInSeconds) {
+            hasError = true;
           }
         } else {
-          extraColMetricClass = '.unhealthy';
+          hasError = true;
         }
 
-        // Mouse line.
-        var extraCurValueClass = '';
-        var mouseOffset = 100 * state.mouseOffsetFactor;
-        if (!hasErrors && mouseOffset >= 0) {
-          curValue = Util.formatValue(Util.interpolateValue(
-              metricData.CurrentValue, state.mouseOffsetFactor,
-              metricData.HistoryTimestamps, metricData.HistoryValues));
-          extraCurValueClass = '.history';
+        // Value over threshold.
+        var overThreshold = (
+            colData.threshold && metricData.CurrentValue >= colData.threshold);
+        if (overThreshold) {
+          hasError = true;
         }
 
-        return h('div.col-metric' + extraColMetricClass, {
-          'title': hasErrors ?
-              metricData.ErrMsg : metricData.Instance + ', ' + metricData.Zone,
-          'ev-click': hg.send(globalState.channels.mouseClickOnMetric, {
-            metricData: metricData,
-            serviceName: rowData.rowHeader
-          })
-        }, [
-          h('div.highlight-overlay'),
-          renderMouseLine(mouseOffset),
-          h('div.sparkline', {
-            'ev-mousemove': new MouseMoveHandler(
-                state.channels.mouseMoveOnSparkline),
-            'ev-mouseout': hg.send(state.channels.mouseOutOfSparkline)
-          }, [
-            renderThreshold(thresholdValue),
-            renderSparkline(points)
-          ]),
-          h('div.cur-value' + extraCurValueClass, [curValue])
-        ]);
+        if (hasError) {
+          numErrors++;
+        }
       });
-      var items = [colHeader];
-      items = items.concat(sparkLines);
+      var extraColMetricClass = '';
+      if (numErrors > 0) {
+        extraColMetricClass = '.warning';
+        // Use the latency column to estimate how many replicas are in warning
+        // state.
+        if (colData.label === 'LATENCY') {
+          numWarnings = numErrors;
+        }
+      }
+
+      // Prepare data for average timeseries.
+      var avgTimestamps = [];
+      var avgValues = [];
+      var avgMinValue = Number.MAX_VALUE;
+      var avgMaxValue = 0;
+      Object.keys(avg).sort().forEach(function(t) {
+        avgTimestamps.push(parseInt(t));
+        var values = avg[t];
+        var sum = 0;
+        values.forEach(function(v) {
+          sum += v;
+        });
+        var avgValue = sum / values.length;
+        avgValues.push(avgValue);
+        avgMinValue = Math.min(avgMinValue, avgValue);
+        avgMaxValue = Math.max(avgMaxValue, avgValue);
+      });
+
+      // Render sparkline for average values.
+      //
+      // 100 is the default logical width of any svg graphs.
+      var points = '0,100 100,100';
+      if (avgTimestamps.length > 0 && avgValues.length > 0) {
+        points = Util.genPolylinePoints(
+          avgTimestamps, avgValues,
+          data.MinTime, data.MaxTime,
+          avgMinValue, avgMaxValue);
+      }
+      var avgCurValue = 0;
+      if (avgValues.length > 0) {
+        avgCurValue = avgValues[avgValues.length - 1];
+      }
+      var curValue = Util.formatValue(avgCurValue);
+      var extraCurValueClass = '';
+      var mouseOffset = 100 * state.mouseOffsetFactor;
+      if (mouseOffset >= 0) {
+        curValue = Util.formatValue(Util.interpolateValue(
+            avgCurValue, state.mouseOffsetFactor,
+            avgTimestamps, avgValues));
+        extraCurValueClass = '.history';
+      }
+
+      // Handle avg value over threshold.
+      var overThreshold = (
+          colData.threshold && avgCurValue >= colData.threshold);
+      var thresholdValue = -100;
+      if (overThreshold) {
+        hasFatalErrors = true;
+        extraColMetricClass = '.fatal';
+        thresholdValue = (colData.threshold-avgMinValue)/
+            (avgMaxValue - avgMinValue)*100.0;
+      }
+
+      var sparklineItems = [
+        h('div.highlight-overlay'),
+        Util.renderMouseLine(mouseOffset),
+        h('div.sparkline', {
+          'ev-mousemove': new MouseMoveHandler(
+              state.channels.mouseMoveOnSparkline),
+          'ev-mouseover': hg.send(state.channels.mouseOverSparkline, colData),
+          'ev-mouseout': hg.send(state.channels.mouseOutOfSparkline),
+        }, [
+          renderThreshold(thresholdValue),
+          Util.renderSparkline(points)
+        ]),
+        h('div.cur-value' + extraCurValueClass, [curValue])
+      ];
+      if (state.mouseOffsetFactor >= 0 &&
+          state.mouseOverDataKey === colData.dataKey &&
+          state.mouseOverMetricName === colData.metricName) {
+        var curTimestamp =
+            (data.MaxTime - data.MinTime) * state.mouseOffsetFactor +
+            data.MinTime;
+        sparklineItems.push(
+            h('div.mouseover-time', dateformat(new Date(curTimestamp * 1000))));
+      }
+      var sparkline = h('div.col-metric' + extraColMetricClass, {
+        'ev-click': hg.send(globalState.channels.mouseClickOnMetric, {
+          serviceName: rowData.rowHeader,
+          colData: colData
+        })
+      }, sparklineItems);
+
+      var items = [h('div.col-header', colData.label), sparkline];
       return h('div.col', items);
     });
-    cols.unshift(h('div.row-header', Consts.getDisplayName(rowData.rowHeader)));
+    var headerExtraClass = (numWarnings !== 0) ? '.warning' : '';
+    if (hasFatalErrors) {
+      headerExtraClass = '.fatal';
+    }
+    cols.unshift(h('div.row-header' + headerExtraClass, [
+        h('div.header-label', Consts.getDisplayName(rowData.rowHeader)),
+        h('div.header-numhealthy',
+          ((numReplicas - numWarnings) + '/' + numReplicas))
+    ]));
     return h('div.row', cols);
   });
 
@@ -261,20 +380,6 @@
 }
 
 /**
- * Renders sparkline for the given points.
- * @param {string} points - A string in the form of "x1,y1 x2,y2 ...".
- */
-function renderSparkline(points) {
-  return svg('svg', {
-    'class': 'content',
-    'viewBox': '0 0 100 100',
-    'preserveAspectRatio': 'none'
-  }, [
-    svg('polyline', {'points': points}),
-  ]);
-}
-
-/**
  * Renders threshold line.
  */
 function renderThreshold(value) {
@@ -289,19 +394,3 @@
     }),
   ]);
 }
-
-/**
- * Renders mouse line at the given offset.
- * @param {Number} mouseOffset - The logical offset for the mouse line.
- */
-function renderMouseLine(mouseOffset) {
-  return svg('svg', {
-    'class': 'mouse-line',
-    'viewBox': '0 0 100 100',
-    'preserveAspectRatio': 'none'
-  }, [
-    svg('polyline', {
-      'points': mouseOffset + ',0 ' + mouseOffset + ',100'
-    })
-  ]);
-}
diff --git a/oncall/client/browser/constants.js b/oncall/client/browser/constants.js
index 1ea8d25..cef9e91 100644
--- a/oncall/client/browser/constants.js
+++ b/oncall/client/browser/constants.js
@@ -25,6 +25,7 @@
   MN_MOUNTTABLE: 'mounttable',
   MN_PROXY: 'proxy service',
   MN_ROLE: 'role service',
+  MN_SB_ALLOCATOR: 'syncbase allocator',
   MN_MT_MOUNTED_SERVERS: 'mounttable mounted servers',
   MN_MT_NODES: 'mounttable nodes'
 });
@@ -74,6 +75,7 @@
   'role service': 'ROLES',
   'role service latency': 'ROLES',
   'roled': 'ROLED',
+  'syncbase allocator': 'SB ALLOCATOR',
   'tcpconn': 'TCP CONN',
   'waiting-connections': 'WAITING CONN',
   'writing-connections': 'WRITING CONN'
@@ -175,6 +177,8 @@
 /** All available metric objects. */
 var mainMetrics = cloudServiceMetrics.concat(nginxMetrics);
 
+var stableDataThresholdInSeconds = 600;
+
 /**
  * Creates a metric.
  *
@@ -238,5 +242,6 @@
   cloudServiceGCEMetrics: cloudServiceGCEMetrics,
   nginxMetrics: nginxMetrics,
   nginxGCEMetrics: nginxGCEMetrics,
-  mainMetrics: mainMetrics
+  mainMetrics: mainMetrics,
+  stableDataThresholdInSeconds: stableDataThresholdInSeconds
 };
diff --git a/oncall/client/browser/index.js b/oncall/client/browser/index.js
index 8151bc9..f07a6e4 100644
--- a/oncall/client/browser/index.js
+++ b/oncall/client/browser/index.js
@@ -19,12 +19,6 @@
  */
 var curData;
 
-/**
- * A variable to keep track of current metric data shown in the
- * metric actions panel.
- */
-var selectedMetricData;
-
 // Ask mercury to listen to mousemove/mouseout/mouseover events.
 hg.Delegator().listenTo('mousemove');
 hg.Delegator().listenTo('mouseout');
@@ -48,15 +42,13 @@
     // The status table showing service status.
     statusTable: statusTableComponent(null),
 
-    // The view showing data on the "instance" level.
-    // instanceView: instanceViewComponent(null),
+    // The metric actions panel.
+    metricActionsPanel: metricActionsPanelComponent(null)
   }),
 
   // Whether to show settings panel.
   showSettingsPanel: hg.value(false),
 
-  showMetricActionsPanel: hg.value(false),
-
   // Settings stored in cookies.
   settings: hg.varhash({
     darkTheme: hg.value(cookies.darkTheme === 'true')
@@ -67,18 +59,17 @@
     changeTheme: changeTheme,
     clickOnSettingsGear: clickOnSettingsGear,
     closeSettingsPanel: closeSettingsPanel,
-    closeMetricActionsPanel: closeMetricActionsPanel
   }
 });
 
 /** Callback for clicking on a metric. */
 function mouseClickOnMetric(state, data) {
-  selectedMetricData = data;
-  state.showMetricActionsPanel.set(true);
-}
-
-function closeMetricActionsPanel(state) {
-  state.showMetricActionsPanel.set(false);
+  var metricActionPanelData = metricActionsPanelComponent({
+    selectedMetric: data,
+    selectedMetricIndex: 0,
+    visible: true
+  });
+  state.components.put('metricActionsPanel', metricActionPanelData);
 }
 
 /** Callback when user clicks on the settings gear. */
@@ -119,9 +110,11 @@
     mainContent.push(hg.partial(settingsPanelComponent.render, state));
   }
 
-  if (state.showMetricActionsPanel) {
-    mainContent.push(hg.partial(metricActionsPanelComponent.render, state,
-          selectedMetricData, curData));
+  if (state.components.metricActionsPanel &&
+      state.components.metricActionsPanel.visible) {
+    mainContent.push(
+        metricActionsPanelComponent.render(state.components.metricActionsPanel,
+          curData));
   }
 
   var className = state.settings.darkTheme ? 'main.darkTheme' : 'main';
@@ -183,8 +176,8 @@
     if (state.showSettingsPanel()) {
       state.showSettingsPanel.set(false);
     }
-    if (state.showMetricActionsPanel()) {
-      state.showMetricActionsPanel.set(false);
+    if (state.components.metricActionsPanel) {
+      state.components.metricActionsPanel.visible.set(false);
     }
   }
 };
diff --git a/oncall/client/browser/util.js b/oncall/client/browser/util.js
index e93d321..b7cc3be 100644
--- a/oncall/client/browser/util.js
+++ b/oncall/client/browser/util.js
@@ -14,7 +14,9 @@
   genPolylinePoints: genPolylinePoints,
   getOffsetForValue: getOffsetForValue,
   formatValue: formatValue,
-  isEmptyObj: isEmptyObj
+  isEmptyObj: isEmptyObj,
+  renderSparkline: renderSparkline,
+  renderMouseLine: renderMouseLine
 };
 
 /**
@@ -165,6 +167,9 @@
  * @return {string}
  */
 function formatValue(value) {
+  if (isNaN(value)) {
+    return '?';
+  }
   if (value < 1) {
     value = value.toFixed(2);
   } else if (value < 10) {
@@ -186,3 +191,33 @@
 function isEmptyObj(obj) {
   return Object.keys(obj).length === 0;
 }
+
+/**
+ * Renders sparkline for the given points.
+ * @param {string} points - A string in the form of "x1,y1 x2,y2 ...".
+ */
+function renderSparkline(points) {
+  return svg('svg', {
+    'class': 'content',
+    'viewBox': '0 0 100 100',
+    'preserveAspectRatio': 'none'
+  }, [
+    svg('polyline', {'points': points}),
+  ]);
+}
+
+/**
+ * Renders mouse line at the given offset.
+ * @param {Number} mouseOffset - The logical offset for the mouse line.
+ */
+function renderMouseLine(mouseOffset) {
+  return svg('svg', {
+    'class': 'mouse-line',
+    'viewBox': '0 0 100 100',
+    'preserveAspectRatio': 'none'
+  }, [
+    svg('polyline', {
+      'points': mouseOffset + ',0 ' + mouseOffset + ',100'
+    })
+  ]);
+}
diff --git a/oncall/client/stylesheets/components/metric-actions-panel.css b/oncall/client/stylesheets/components/metric-actions-panel.css
index 5507a61..e284207 100644
--- a/oncall/client/stylesheets/components/metric-actions-panel.css
+++ b/oncall/client/stylesheets/components/metric-actions-panel.css
@@ -7,53 +7,79 @@
   width: 100%;
   height: 100%;
   background-color: rgba(0, 0, 0, 0.2);
+  position: relative;
+}
+
+div.metric-actions-container div.inner-container {
+  position: absolute;
   display: flex;
-  justify-content: center;
-  align-items: center;
+  top: 20%;
+  left: 50%;
+  width: 700px;
+  height: 440px;
+  margin-left: -350px;
+  background-color: white;
+  padding: 10px;
+}
+
+div.metric-actions-container div.metric-actions-list {
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding-right: 12px;
 }
 
 div.metric-actions-container div.metric-actions-content {
-  background-color: white;
-  border: 1px black solid;
   width: 440px;
-  padding: 12px;
-  padding-top: 16px;
+  padding-left: 12px;
+  border-left: 1px #999 dashed;
+  display: flex;
+  flex-direction: column;
 }
 
-div.metric-actions-container div.title {
+div.metric-actions-container div.metric-actions-content div.title {
   font-weight: var(--font-weight-medium);
   padding: 4px 0px 16px 0px;
 }
 
 div.metric-actions-container div.btn-close {
+  position: absolute;
   padding-top: 10px;
   display: flex;
   align-items: center;
   cursor: pointer;
   color: #999;
+  bottom: 10px;
+  right: 10px;
 }
 
 div.metric-actions-container div.btn-close:hover {
   color: #444;
 }
 
-div.metric-actions-container div.row {
+div.metric-actions-container div.metric-actions-content div.row {
   display: flex;
   flex-direction: row;
 }
 
-div.metric-actions-container div.space {
+div.metric-actions-container div.metric-actions-content div.space {
   height: 14px;
   width: 100%;
 }
 
-div.metric-actions-container div.item-label {
+div.metric-actions-container div.metric-actions-content div.item-label {
   font-weight: var(--font-weight-medium);
   width: 140px;
   padding-right: 10px;
 }
 
-div.metric-actions-container div.item-link {
+div.metric-actions-container div.metric-actions-content div.item-value.history {
+  opacity: 0.5;
+}
+
+div.metric-actions-container div.metric-actions-content div.item-link {
   margin-left: 10px;
 }
 
+div.metric-actions-list div.col-metric {
+  border-bottom: 1px dotted #888;
+}
diff --git a/oncall/client/stylesheets/components/metric-item.css b/oncall/client/stylesheets/components/metric-item.css
index 054f980..034efcc 100644
--- a/oncall/client/stylesheets/components/metric-item.css
+++ b/oncall/client/stylesheets/components/metric-item.css
@@ -1,139 +1,117 @@
 /* Copyright 2015 The Vanadium Authors. All rights reserved. */
 /* Use of this source code is governed by a BSD-style */
 /* license that can be found in the LICENSE file. */
-
-div.metric-item {
-  width: var(--metric-item-width);
-  color: var(--dark-green-grey);
-  cursor: pointer;
-  margin-right: 4px;
+.col-metric {
   position: relative;
-}
-
-.darkTheme div.metric-item {
-  color: var(--light-green-grey);
-}
-
-div.metric-item.unhealthy {
-  background: var(--red-stripes-background-dimmed);
-}
-
-div.metric-item:nth-child(1):before {
-  width: 0px !important;
-}
-
-div.metric-item:hover .metric-item-title {
-  background-color: var(--light-grey);
-}
-
-.darkTheme div.metric-item:hover .metric-item-title {
-  background-color: var(--dark-highlight);
-}
-
-div.metric-item div.metric-item-title {
-  padding-top: 4px;
-  padding-bottom: 2px;
-  font-size: 12px;
-  line-height: var(--metric-item-title-height);
-  white-space: nowrap;
-  overflow: hidden;
-}
-
-div.metric-item div.metric-item-graph-container {
+  padding: 6px 4px 6px 4px;
   display: flex;
-  justify-content: space-between;
-  padding-left: 4px;
-  padding-right: 2px;
-}
-
-div.metric-item div.metric-item-value {
-  font-size: 16px;
-  font-weight: var(--font-weight-medium);
-  color: var(--cyan-800);
-  height: var(--metric-item-value-height);
-  line-height: var(--metric-item-value-height);
-  margin-bottom: 4px;
-}
-
-.darkTheme div.metric-item div.metric-item-value {
-  color: var(--light-green);
-}
-
-div.metric-item div.metric-item-value.unhealthy {
-  color: var(--dark-red);
-}
-
-.darkTheme div.metric-item div.metric-item-value.unhealthy {
-  color: var(--light-red);
-}
-
-div.metric-item div.metric-item-graph {
-}
-
-div.metric-item-filler {
-  flex-grow: 1;
-  height: var(--metric-item-height);
-  width: var(--metric-item-width);
-}
-
-div.metric-item div.sparkline {
-  position: relative;
+  width: var(--col-metric-width);
+  height: var(--col-metric-height);
+  background: var(--cyan-800);
+  flex-direction: row;
   cursor: pointer;
-  height: var(--sparkline-height);
 }
 
-div.metric-item div.sparkline svg {
-  width: 100%;
-  height: 100%;
+.col-metric.warning {
+  background-color: var(--warning) !important;
+}
+
+.col-metric.fatal {
+  background-color: var(--fatal) !important;
+}
+
+.darkTheme .col-metric {
+  background: DarkGreen;
+}
+
+.col-metric .cur-value {
+  position: absolute;
+  left: 166px;
+  top: 6px;
+  color: white;
+  font-size: 14px;
+  height: var(--col-metric-content-height);
+  line-height: var(--col-metric-content-height);
+}
+
+.col-metric div.cur-value.history {
+  color: #B2DADD !important;
+}
+
+.darkTheme .col-metric div.cur-value.history {
+  color: #AAA !important;
+}
+
+.col-metric.err .cur-value {
+  color: red !important;
+}
+
+.col-metric div.sparkline {
+  position: absolute;
+  top: 4px;
+  left: 6px;
+  width: 156px;
+  height: var(--col-metric-content-height);
+}
+
+.col-metric svg {
   position: absolute;
   top: 0px;
   left: 0px;
+  width: 156px;
+  height: var(--col-metric-content-height);
 }
 
-div.metric-item div.sparkline svg.content polyline {
+.col-metric svg.mouse-line {
+  position: absolute;
+  top: 0px;
+  left: 6px;
+  width: 156px;
+  height: var(--col-metric-height);
+}
+
+.col-metric svg.content {
+}
+
+.col-metric svg.content polyline {
   vector-effect: non-scaling-stroke;
   fill: none;
-  stroke: var(--dark-green-grey);
-  stroke-width: 2;
+  stroke: white;
+  stroke-width: 0.8;
 }
 
-.darkTheme div.metric-item div.sparkline svg.content polyline {
-  stroke: var(--light-green-grey);
+.col-metric svg.threshold path {
+  vector-effect: non-scaling-stroke;
+  fill: none;
+  stroke: white;
+  stroke-width: 0.8;
 }
 
-div.metric-item div.sparkline svg.content polygon {
-  fill: WhiteSmoke;
-  stroke: none;
-}
-
-.darkTheme div.metric-item div.sparkline svg.content polygon {
-  fill: #333;
-}
-
-div.metric-item svg.mouse-line {
+.col-metric svg.mouse-line {
   shape-rendering: crispedges;
 }
 
-div.metric-item div.sparkline svg.mouse-line polyline {
+.col-metric svg.mouse-line polyline {
   vector-effect: non-scaling-stroke;
   fill: none;
-  stroke: #BBB;
+  stroke: rgba(255, 255, 255, 0.2);
   stroke-width: 1;
 }
 
-div.metric-item div.sparkline svg.mouse-line polygon {
-  vector-effect: non-scaling-stroke;
-  fill: var(--dark-red);
-  stroke: none;
+.col-metric div.highlight-overlay {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0px;
+  left: 0px;
+  background-color: white;
+  opacity: 0;
 }
 
-div.metric-item svg.threshold-line {
-  shape-rendering: crispedges;
+.col-metric:hover div.highlight-overlay {
+  opacity: 0.1;
 }
 
-div.metric-item div.sparkline svg.threshold-line path {
-  vector-effect: non-scaling-stroke;
-  fill: none;
-  stroke: var(--dark-red);
-  stroke-width: 1;
+.col-metric.selected div.highlight-overlay {
+  opacity: 0.2;
 }
diff --git a/oncall/client/stylesheets/components/status-table.css b/oncall/client/stylesheets/components/status-table.css
index e4ae455..7109308 100644
--- a/oncall/client/stylesheets/components/status-table.css
+++ b/oncall/client/stylesheets/components/status-table.css
@@ -21,8 +21,31 @@
 div.status-table .row-header {
   width: 120px;
   text-align: right;
-  padding-top: 15px;
+  padding-top: 19px;
   padding-right: 10px;
+  line-height: 18px;
+}
+
+div.status-table .row-header.warning div {
+  color: var(--warning) !important;
+}
+
+div.status-table .row-header.fatal div {
+  color: var(--fatal) !important;
+}
+
+div.status-table .header-label {
+  font-weight: 500;
+}
+
+div.status-table .header-numhealthy {
+  margin-top: 2px;
+  font-size: 18px;
+  color: var(--cyan-800);
+}
+
+.darkTheme div.status-table .header-numhealthy {
+  color: LightGreen;
 }
 
 div.status-table .col {
@@ -34,121 +57,10 @@
   height: 21px;
 }
 
-div.status-table .col-metric {
-  position: relative;
-  padding: 6px 4px 6px 4px;
-  display: flex;
-  width: var(--col-metric-width);
-  height: var(--col-metric-height);
-  background: var(--cyan-800);
-  flex-direction: row;
-  cursor: pointer;
-}
-
-div.status-table .col-metric.err {
-  background-color: #ffbb55 !important;
-}
-
-div.status-table .col-metric.unhealthy {
-  background-color: #AA0000 !important;
-}
-
-div.status-table .col-metric.stale {
-  background-color: #ffbb55 !important;
-}
-
-.darkTheme div.status-table .col-metric {
-  background: DarkGreen;
-}
-
-div.status-table .col-metric .cur-value {
+div.status-table div.mouseover-time {
   position: absolute;
-  left: 166px;
-  top: 6px;
-  color: white;
-  font-size: 14px;
-  height: var(--col-metric-content-height);
-  line-height: var(--col-metric-content-height);
-}
-
-div.status-table .col-metric div.cur-value.history {
-  color: #B2DADD !important;
-}
-
-.darkTheme div.status-table .col-metric div.cur-value.history {
-  color: #AAA !important;
-}
-
-div.status-table .col-metric.err .cur-value {
-  color: red !important;
-}
-
-div.status-table .col-metric div.sparkline {
-  position: absolute;
-  top: 4px;
-  left: 6px;
-  width: 156px;
-  height: var(--col-metric-content-height);
-}
-
-div.status-table .col-metric svg {
-  position: absolute;
-  top: 0px;
+  top: var(--col-metric-height);
   left: 0px;
-  width: 156px;
-  height: var(--col-metric-content-height);
-}
-
-div.status-table .col-metric svg.mouse-line {
-  position: absolute;
-  top: 0px;
-  left: 6px;
-  width: 156px;
-  height: var(--col-metric-height);
-}
-
-div.status-table .col-metric svg.content {
-}
-
-div.status-table .col-metric svg.content polyline {
-  vector-effect: non-scaling-stroke;
-  fill: none;
-  stroke: white;
-  stroke-width: 0.8;
-}
-
-div.status-table .col-metric svg.threshold path {
-  vector-effect: non-scaling-stroke;
-  fill: none;
-  stroke: white;
-  stroke-width: 0.8;
-}
-
-div.status-table .col-metric svg.mouse-line {
-  shape-rendering: crispedges;
-}
-
-div.status-table .col-metric svg.mouse-line polyline {
-  vector-effect: non-scaling-stroke;
-  fill: none;
-  stroke: #339CA5;
-  stroke-width: 1;
-}
-
-.darkTheme div.status-table .col-metric svg.mouse-line polyline {
-  stroke: #338333;
-}
-
-div.status-table .col-metric div.highlight-overlay {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-  top: 0px;
-  left: 0px;
-  background-color: white;
-  opacity: 0;
-}
-
-div.status-table .col-metric:hover div.highlight-overlay {
-  opacity: 0.1;
+  color: #999;
+  font-size: 11px;
 }
diff --git a/oncall/client/stylesheets/variables.css b/oncall/client/stylesheets/variables.css
index c44e5cc..3a06e16 100644
--- a/oncall/client/stylesheets/variables.css
+++ b/oncall/client/stylesheets/variables.css
@@ -51,6 +51,8 @@
   --white-transparent: rgba(255, 255, 255, 0.4);
   --dark-background: #222;
   --dark-highlight: #333;
+  --warning: #C36900;
+  --fatal: #AA0000;
 
   /* Fonts */
   --primary-font: Roboto;
@@ -63,8 +65,8 @@
   --section-shadow: rgba(0, 0, 0, 0.30) 0px 0px 4px 0px;
 
   /* Heights */
-  --col-metric-height: 36px;
-  --col-metric-content-height: 28px;
+  --col-metric-height: 48px;
+  --col-metric-content-height: 40px;
   --header-height: 60px;
   --zone-title-height: 40px;
   --full-graph-height: 240px;
diff --git a/oncall/serve.go b/oncall/serve.go
index 553b43c..6ee3ad2 100644
--- a/oncall/serve.go
+++ b/oncall/serve.go
@@ -541,6 +541,8 @@
 			retPods[monitoring.SNProxy] = append(retPods[monitoring.SNProxy], pod)
 		case "role":
 			retPods[monitoring.SNRole] = append(retPods[monitoring.SNRole], pod)
+		case "sb-allocator":
+			retPods[monitoring.SNAllocator] = append(retPods[monitoring.SNAllocator], pod)
 		}
 	}
 	// Index nodes names by ids.