TBR: oncall: update oncall dashboard.
Change-Id: Ie41b46e90bedf448d410f449e5f6d7747d4e4896
diff --git a/oncall/client/browser/appstate-manager.js b/oncall/client/browser/appstate-manager.js
deleted file mode 100644
index 22c39b1..0000000
--- a/oncall/client/browser/appstate-manager.js
+++ /dev/null
@@ -1,132 +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.
-
-/**
- * App state manager.
- */
-
-var hg = require('mercury');
-var qs = require('qs');
-var url = require('url');
-
-module.exports = {
- init: init,
- getAppState: getAppState,
- setAppState: setAppState,
- getCurState: getCurState,
-};
-
-/**
- * An observable to store app's state which will be encoded in url parameters.
- */
-var appState = hg.varhash({});
-
-/** The default app state. */
-var defaultAppState = {
- // The level of view.
- // - global: all (aggregated) metrics in all zones.
- // - zone: all metrics in a specific zone.
- // - instance: all metrics for a specific instance.
- level: 'global',
-
- // The data aggregation type for global view.
- // In global view, each metric (e.g. nginx qps) in a certain zone might have
- // multiple instances (e.g. multiple nginx workers). We need to aggregate data
- // from all those instances to a single one.
- // We currently support 'Max' and 'Average'.
- globalLevelAggType: 'Max',
-
- // The zone to show in the zone level.
- zoneLevelZone: '',
-
- // The type of metrics to show in the zone level.
- // This could either be 'CloudServices' or 'Nginx'.
- zoneLevelType: '',
-
- // The instance to show in the instance level.
- instanceLevelInstance: '',
-
- // The zone of the instance in the instance level.
- instanceLevelZone: ''
-};
-
-/**
- * A flag indicating whether to trigger history.pushState when
- * app state changes.
- */
-var pushHistoryState = true;
-
-/**
- * Sets up app state and its various event listeners.
- * @param {callback} stateChangedListener - The callback that handles app state
- * changes.
- */
-function init(stateChangedListener) {
- // When appState changes, push the state to browser's history.
- appState(function(data) {
- if (pushHistoryState) {
- var str = qs.stringify(data);
- window.history.pushState(undefined, '',
- window.location.origin + window.location.pathname + '?' + str);
- }
- stateChangedListener(getCurState());
- });
-
- // Get the current state from url parameters.
- var initState = qs.parse(url.parse(window.location.href).query);
- // Fill in default values.
- Object.keys(defaultAppState).forEach(function(key) {
- if (!initState[key]) {
- initState[key] = defaultAppState[key];
- }
- });
- appState.set(initState);
-
- // When the history state changes, we update the appState observable
- // which will trigger its change listener defined above.
- window.addEventListener('popstate', function(event) {
- // We don't want to mess with history states in this case.
- pushHistoryState = false;
- appState.set(qs.parse(url.parse(window.location.href).query));
- pushHistoryState = true;
- });
-}
-
-/**
- * Gets an app state entry for the given name.
- * @param {string} name - The name of the app state entry.
- * @return {string}
- */
-function getAppState(name) {
- return appState()[name];
-}
-
-/**
- * Sets app state.
- * @param {Object} stateObj - The state object to set.
- */
-function setAppState(stateObj) {
- var curState = appState();
- Object.keys(stateObj).forEach(function(key) {
- curState[key] = stateObj[key];
- });
- appState.set(curState);
-}
-
-/**
- * Gets the current app state.
- * @return {Object}
- */
-function getCurState() {
- // For some reason, the object returned by appState() doesn't have
- // Object.prototype as its prototype, which will cause issues in some other
- // places. To workaround this, we return a cloned object which has
- // Object.prototype.
- var obj = {};
- var curAppState = appState();
- Object.keys(curAppState).forEach(function(key) {
- obj[key] = curAppState[key];
- });
- return obj;
-}
diff --git a/oncall/client/browser/components/data-table/index.js b/oncall/client/browser/components/data-table/index.js
deleted file mode 100644
index 53bdf2d..0000000
--- a/oncall/client/browser/components/data-table/index.js
+++ /dev/null
@@ -1,34 +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 simple data table.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-
-module.exports = create;
-module.exports.render = render;
-
-/** Constructor. */
-function create(data) {
- var state = hg.state({
- title: data.title,
- rows: data.rows
- });
- return state;
-}
-
-/** The main render function. */
-function render(state) {
- return h('div.data-table', [
- h('div.data-table-title', state.title),
- h('table', state.rows.map(function(row) {
- return h('tr', row.map(function(col) {
- return h('td', col !== '' ? col : 'n/a');
- }));
- }))
- ]);
-}
diff --git a/oncall/client/browser/components/full-graph/index.js b/oncall/client/browser/components/full-graph/index.js
deleted file mode 100644
index ada32a1..0000000
--- a/oncall/client/browser/components/full-graph/index.js
+++ /dev/null
@@ -1,266 +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 graph showing multiple metrics in different colors along with their
- * summary items showing their current/pass values and line colors. User can
- * "select" one or more metrics by clicking on their summary items. The graph
- * then will only show selected metrics (if the selection is not empty).
- *
- * The graph will also overlay a graph of the metric stored in the
- * "hoveredMetric" observable passed from some other component.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var svg = require('virtual-dom/virtual-hyperscript/svg');
-var dateformat = require('dateformat');
-
-var Consts = require('../../constants');
-var MouseMoveEvent = require('../../mousemove-handler');
-var Util = require('../../util.js');
-
-module.exports = create;
-module.exports.render = render;
-
-var colors = [
- '#16a085',
- '#2980b9',
- '#8e44ad',
- '#808B96',
- '#f39c12',
- '#d35400',
- '#27ae60',
- '#c0392b'
-];
-
-/** Constructor. */
-function create(data) {
- // Transform the raw data to metrics array.
- // Also calculate time and value range across all metrics.
- var minTime = Number.MAX_VALUE;
- var maxTime = 0;
- var minValue = Number.MAX_VALUE;
- var maxValue = 0;
- var metrics = data.metrics.map(function(metric, index) {
- minTime = Math.min(minTime, metric.MinTime);
- maxTime = Math.max(maxTime, metric.MaxTime);
- minValue = Math.min(minValue, metric.MinValue);
- maxValue = Math.max(maxValue, metric.MaxValue);
- return {
- label: metric.Name,
- value: metric.CurrentValue,
- color: colors[index % colors.length],
- threshold: metric.Threshold,
- healthy: metric.Healthy,
- historyTimestamps: metric.HistoryTimestamps,
- historyValues: metric.HistoryValues,
- };
- });
-
- // Calculate the overall health for all metrics.
- var healthy = true;
- metrics.forEach(function(metric) {
- healthy &= metric.healthy;
- });
-
- var state = hg.state({
- // The timestamp when the data is loaded from the backend server.
- collectionTimestamp: data.collectionTimestamp,
-
- // The graph title.
- title: data.title,
-
- // Time range.
- minTime: minTime,
- maxTime: maxTime,
-
- // Value range.
- minValue: minValue,
- maxValue: maxValue,
-
- // Metrics shown in this graph.
- metrics: metrics,
-
- // Overall health.
- healthy: healthy,
-
- // Keep track of selected metrics.
- // A metric line will be visible if it is selected.
- selectedMetrics: hg.varhash({}),
-
- // See comments in instance-view/index.js.
- mouseOffsetFactor: data.mouseOffsetFactor,
- hoveredMetric: data.hoveredMetric,
-
- channels: {
- mouseMove: mouseMove,
- mouseOut: mouseOut,
- mouseClickOnSummaryItem: mouseClickOnSummaryItem
- }
- });
-
- 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 moves out of the graph. */
-function mouseOut(state) {
- state.mouseOffsetFactor.set(-1);
-}
-
-/** Callback when user clicks on a metric's summary item. */
-function mouseClickOnSummaryItem(state, label) {
- if (!state.selectedMetrics.get(label)) {
- state.selectedMetrics.put(label, 1);
- } else {
- state.selectedMetrics.delete(label);
- }
-}
-
-/** The main render function. */
-function render(state) {
- // Render graphs.
- var items = [
- renderContent(state),
- renderThreshold(state)
- ];
-
- // Render an overlay graph for the metric stored in state.hoveredMetric.
- if (Object.keys(state.hoveredMetric).length !== 0) {
- var metric = state.hoveredMetric;
- var numPoints = metric.historyTimestamps.length;
- var minTime = metric.historyTimestamps[0];
- var maxTime = metric.historyTimestamps[numPoints - 1];
- items.push(svg('svg', {
- 'viewBox': '0 0 100 100',
- 'preserveAspectRatio': 'none'
- }, [
- Util.renderMetric(state.hoveredMetric,
- minTime, maxTime, metric.minValue, metric.maxValue, 1, '#AAA', 1)
- ]));
- }
-
- // Render mouse line.
- var offset = 100 * state.mouseOffsetFactor;
- items.push(svg('svg', {
- 'class': 'overlay',
- 'viewBox': '0 0 100 100',
- 'preserveAspectRatio': 'none'
- }, [
- svg('polyline', {
- 'points': offset + ',0 ' + offset + ',100'
- })
- ]));
-
- var endDate = new Date(state.collectionTimestamp * 1000);
- var startDate = new Date((state.collectionTimestamp - 3600) * 1000);
- return h('div.full-graph-container', [
- h('div.full-graph-title', state.title),
- h('div.content-container', [
- // The summary row at the top of the graph.
- renderSummaryRow(state),
- // The main graph.
- h('div.full-graph', {
- 'ev-mousemove': new MouseMoveEvent(state.channels.mouseMove),
- 'ev-mouseout': hg.send(state.channels.mouseOut)
- }, items),
- // The time axis at the bottom.
- h('div.time', [
- h('div.time-label', dateformat(startDate, 'HH:MM')),
- h('div.time-label', dateformat(endDate, 'HH:MM'))
- ])
- ])
- ]);
-}
-
-/** Renders the main graph. */
-function renderContent(state) {
- var items = state.metrics.map(function(metric) {
- var strokeOpacity = isMetricVisible(state.selectedMetrics, metric.label) ?
- 1 : 0.1;
- var metricLine = Util.renderMetric(
- metric, state.minTime, state.maxTime,
- state.minValue, state.maxValue, 1.5, metric.color, strokeOpacity);
- return metricLine;
- });
- return svg('svg', {
- 'class': 'content',
- 'viewBox': '0 0 100 100',
- 'preserveAspectRatio': 'none'
- }, items);
-}
-
-/** Renders the threshold line. */
-function renderThreshold(state) {
- var minThreshold = Number.MAX_VALUE;
- state.metrics.forEach(function(metric) {
- minThreshold = Math.min(minThreshold, metric.threshold);
- });
- var thresholdOffset = Util.getOffsetForValue(
- minThreshold, 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'
- })
- ]);
-}
-
-/** Renders the summary row. */
-function renderSummaryRow(state) {
- var items = state.metrics.map(function(metric) {
- var curValue = Util.formatValue(
- Util.interpolateValue(metric.value, state.mouseOffsetFactor,
- metric.historyTimestamps, metric.historyValues));
- var metricSummaryClassNames = [];
- if (!isMetricVisible(state.selectedMetrics, metric.label)) {
- metricSummaryClassNames.push('hidden');
- }
- if (!metric.healthy) {
- metricSummaryClassNames.push('unhealthy');
- }
- var metricValueClassNames = [];
- if (state.mouseOffsetFactor >= 0) {
- metricValueClassNames.push('historyValue');
- }
- if (!metric.healthy) {
- metricValueClassNames.push('unhealthy');
- }
- return h('div.metric-summary-item', {
- className: metricSummaryClassNames.join(' '),
- 'ev-click': hg.send(state.channels.mouseClickOnSummaryItem, metric.label)
- }, [
- h('div.metric-summary-title', Consts.getDisplayName(metric.label)),
- h('div.metric-summary-value', {
- className: metricValueClassNames.join(' ')
- }, curValue),
- h('div.metric-summary-color', {
- 'style': {backgroundColor: metric.color}
- })
- ]);
- });
- return h('div.metric-summary-container', items);
-}
-
-/**
- * Checks whether the given metric is visible based on the metrics stored in
- * the selectedMetrics object.
- * @param {Object} selectedMetrics - An object indexed by metric names.
- * @param {string} metric - The name of the metric to check.
- * @return {boolean}
- */
-function isMetricVisible(selectedMetrics, metric) {
- return Object.keys(selectedMetrics).length === 0 || selectedMetrics[metric];
-}
diff --git a/oncall/client/browser/components/instance-view/index.js b/oncall/client/browser/components/instance-view/index.js
deleted file mode 100644
index c6f9c05..0000000
--- a/oncall/client/browser/components/instance-view/index.js
+++ /dev/null
@@ -1,180 +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.
-
-/*
- * The instance view.
- *
- * In this view, we show detailed data of vanadium services or nginx servers for
- * a single GCE instance.
- *
- * The page is divided into several sections:
- * - We first show a large graph for the main metrics we care about the most,
- * such as "vanadium services request latency".
- * - We then show various "helper" stats that can help people diagnose problems
- * in the main metrics, such as "mounted nodes in mounttable".
- * - After that, we show metrics for the GCE instance itself, such as CPU usage,
- * disk usage, etc. It also has links to let user ssh into the machine and see
- * its log.
- * - For vanadium services, it will also show a table for services'
- * metadata/buildinfo.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var dateformat = require('dateformat');
-
-var Consts = require('../../constants');
-var fullGraphComponent = require('../full-graph');
-var metricsGroupComponent = require('../metrics-group');
-var dataTableComponent = require('../data-table');
-var Util = require('../../util');
-
-module.exports = create;
-module.exports.render = render;
-
-/** Constructor. */
-function create(data) {
- if (data === null) {
- return null;
- }
-
- var instance = data.appState.instanceLevelInstance;
- var isVanadiumServices = instance.startsWith('vanadium');
-
- // Main metrics.
- // For vanadium services, we show request latency as the main metrics.
- // For nginx servers, we show its load.
- var mainMetrics = isVanadiumServices ?
- data.data.CloudServiceLatency : data.data.NginxLoad;
- var mainMetricsTitle = isVanadiumServices ? 'REQUEST LATENCY' : 'SERVER LOAD';
- var mainMetricKeys = isVanadiumServices ?
- Consts.metricKeys.latency : Consts.metricKeys.nginxLoad;
- var sortedMainMetrics = mainMetricKeys.map(function(metric) {
- return mainMetrics[metric];
- });
-
- // Helper metrics.
- // For now, we only have helper metrics for vanadium services, which are
- // various stats from mounttable.
- var helperMetrics = isVanadiumServices ? data.data.CloudServiceStats : null;
- var helperMetricsTitle = isVanadiumServices ? 'STATS' : '';
- var helperMetricKeys = isVanadiumServices ? Consts.metricKeys.stats : null;
- var sortedHelperMetrics = [];
- if (isVanadiumServices) {
- sortedHelperMetrics = helperMetricKeys.map(function(metric) {
- return helperMetrics[metric];
- });
- }
-
- // GCE metrics.
- var gceMetrics = isVanadiumServices ?
- data.data.CloudServiceGCE : data.data.NginxGCE;
- var gceMetricsTitle = 'GCE';
- var gceMetricKeys = Consts.metricKeys.gce;
- var sortedGCEMetrics = gceMetricKeys.map(function(metric) {
- return gceMetrics[metric];
- });
- var instanceId = data.data.GCEInfo.Id;
-
- // Data table for showing vanadium services' metadata/buildinfo.
- var dataTableData = data.data.CloudServiceBuildInfo;
- var rows = [];
- if (!Util.isEmptyObj(dataTableData)) {
- rows.push(['SERVICE', 'PRISTINE', 'SNAPSHOT', 'TIME', 'USER', 'LINKS']);
- Object.keys(dataTableData).sort().forEach(function(serviceName) {
- var rowData = dataTableData[serviceName];
- var curRow = [
- Consts.getDisplayName(serviceName),
- rowData.IsPristine, rowData.Snapshot,
- dateformat(new Date(rowData.Time * 1000), 'yyyymmdd-HH:MM'),
- rowData.User,
- h('a', {
- href: genLogsLink(instanceId, 'v-' + rowData.ServiceName + '.info'),
- target: '_blank'
- }, 'log')];
- rows.push(curRow);
- });
- }
-
- // These two observables keep track of which non-main metric the mouse cursor
- // is currently hovering over, as well as its relative X position.
- //
- // When a non-main metric is hovered over, we will overlay its data points in
- // the main metrics graph for easier comparison. We also render a "mouse line"
- // across all graphs.
- var hoveredMetric = hg.struct({});
- var mouseOffsetFactor = hg.value(-1);
-
- var state = hg.state({
- data: data.data,
- appState: hg.struct(data.appState),
-
- mainMetricsGraph: fullGraphComponent({
- title: mainMetricsTitle,
- collectionTimestamp: data.collectionTimestamp,
- metrics: sortedMainMetrics,
- mouseOffsetFactor: mouseOffsetFactor,
- hoveredMetric: hoveredMetric
- }),
-
- helperMetricsGroup: isVanadiumServices ? metricsGroupComponent({
- title: helperMetricsTitle,
- metrics: sortedHelperMetrics,
- mouseOffsetFactor: mouseOffsetFactor,
- hoveredMetric: hoveredMetric
- }) : null,
-
- gceMetricsGroup: metricsGroupComponent({
- title: gceMetricsTitle,
- metrics: sortedGCEMetrics,
- mouseOffsetFactor: mouseOffsetFactor,
- hoveredMetric: hoveredMetric,
- links: [
- {
- name: 'ssh',
- link: genCloudSSHLink(data.appState.instanceLevelZone, instance)
- },
- {
- name: 'log',
- link: genLogsLink(instanceId, '')
- }
- ],
- }),
-
- dataTable: rows.length > 0 ? dataTableComponent({
- title: 'BUILD INFO',
- rows: rows
- }) : null
- });
-
- return state;
-}
-
-/** The main render function. */
-function render(state) {
- var items = [
- fullGraphComponent.render(state.mainMetricsGraph)
- ];
- if (state.helperMetricsGroup) {
- items.push(metricsGroupComponent.render(state.helperMetricsGroup));
- }
- items.push(metricsGroupComponent.render(state.gceMetricsGroup));
- if (state.dataTable) {
- items.push(dataTableComponent.render(state.dataTable));
- }
- return h('div.instance-view', items);
-}
-
-function genCloudSSHLink(zone, instance) {
- return 'https://cloudssh.developers.google.com/projects/' +
- 'vanadium-production/zones/' + zone +
- '/instances/' + instance + '?authuser=0&hl=en_US';
-}
-
-function genLogsLink(instanceId, logName) {
- return 'https://pantheon.corp.google.com/project/vanadium-production/logs?' +
- 'service=compute.googleapis.com&key1=instance&key2=' + instanceId +
- '&logName=' + logName +
- '&minLogLevel=0&expandAll=false&timezone=America%2FLos_Angeles';
-}
diff --git a/oncall/client/browser/components/metric-actions-panel/index.js b/oncall/client/browser/components/metric-actions-panel/index.js
new file mode 100644
index 0000000..6ddec8e
--- /dev/null
+++ b/oncall/client/browser/components/metric-actions-panel/index.js
@@ -0,0 +1,90 @@
+// 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.
+
+var hg = require('mercury');
+var h = require('mercury').h;
+
+var Util = require('../../util');
+
+module.exports = {
+ render: render
+};
+
+var DEFAULT_NAMESPACE_ID = '4b20e0dc-cacf-11e5-87ec-42010af0020b';
+
+/** The main render function. */
+function render(state, selectedMetric, data) {
+ var panel = h('div.metric-actions-content', [
+ h('div.row', [
+ h('div.item-label', 'Service Name'),
+ h('div.item-value', selectedMetric.serviceName)
+ ]),
+ h('div.row', [
+ h('div.item-label', 'Service Version'),
+ h('div.item-value', selectedMetric.metricData.ServiceVersion)
+ ]),
+ h('div.row', [
+ h('div.item-label', 'Metric Type'),
+ h('div.item-value',
+ selectedMetric.metricData.ResultType.replace('resultType', ''))
+ ]),
+ h('div.row', [
+ h('div.item-label', 'Metric Name'),
+ h('div.item-value', selectedMetric.metricData.MetricName)
+ ]),
+ h('div.row', [
+ h('div.item-label', 'Current Value'),
+ h('div.item-value',
+ Util.formatValue(selectedMetric.metricData.CurrentValue))
+ ]),
+ 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,
+ target: '_blank'
+ }, selectedMetric.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:' +
+ DEFAULT_NAMESPACE_ID + ':' + selectedMetric.metricData.PodUID,
+ target: '_blank'
+ }, selectedMetric.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],
+ 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')),
+ ]),
+ h('div.row', [
+ h('div.item-label', 'Pod Status'),
+ h('div.item-value', selectedMetric.metricData.PodStatus)
+ ]),
+ h('div.row', [
+ h('div.item-label', 'Zone'),
+ h('div.item-value', selectedMetric.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/page-header/index.js b/oncall/client/browser/components/page-header/index.js
index f1178b8..342c4e7 100644
--- a/oncall/client/browser/components/page-header/index.js
+++ b/oncall/client/browser/components/page-header/index.js
@@ -17,7 +17,6 @@
var h = require('mercury').h;
var dateformat = require('dateformat');
-var AppStateMgr = require('../../appstate-manager');
var staleDataThresholdInSec = 900;
module.exports = create;
@@ -26,8 +25,9 @@
/** Constructor. */
function create(data) {
var state = hg.state({
- // The timestamp when the current data was loaded from the backend server.
- collectionTimestamp: hg.value(data.collectionTimestamp),
+ // The time period of the current data.
+ startTimestamp: hg.value(data.startTimestamp),
+ endTimestamp: hg.value(data.endTimestamp),
// IDs of current oncalls.
oncallIds: hg.array(data.oncallIds),
@@ -36,35 +36,12 @@
loadingData: hg.value(data.loadingData),
// Whether there is any error loading data.
- hasLoadingFailure: hg.value(data.hasLoadingFailure),
-
- channels: {
- clickNavItem: clickNavItem
- }
+ hasLoadingFailure: hg.value(data.hasLoadingFailure)
});
return state;
}
-/** Callback when a navigation item is clicked. */
-function clickNavItem(state, data) {
- if (data.level ==='global') {
- AppStateMgr.setAppState({
- 'level': data.level,
- 'zoneLevelZone': '',
- 'zoneLevelType': '',
- 'instanceLevelInstance': '',
- 'instanceLevelZone': ''
- });
- } else if (data.level === 'zone') {
- AppStateMgr.setAppState({
- 'level': data.level,
- 'instanceLevelInstance': '',
- 'instanceLevelZone': ''
- });
- }
-}
-
/** The main render function. */
function render(state) {
// Oncalls' pictures.
@@ -82,13 +59,13 @@
var timeClass = '.time';
var infoClass = '.info';
var staleData = false;
- if (state.collectionTimestamp >= 0) {
- var date = new Date(state.collectionTimestamp * 1000);
+ if (state.endTimestamp >= 0) {
+ var date = new Date(state.endTimestamp * 1000);
strTime = dateformat(date);
// Check stale data.
var curTs = Math.round(new Date().getTime() / 1000.0);
- if (curTs - state.collectionTimestamp > staleDataThresholdInSec) {
+ if (curTs - state.endTimestamp > staleDataThresholdInSec) {
infoClass += '.stale-data';
staleData = true;
}
@@ -107,49 +84,15 @@
timeClass += '.failure';
}
- // Current view level and navigation items.
- var navTitle = '';
- var navItems = [];
- var level = AppStateMgr.getAppState('level');
- var zoneLevelZone = AppStateMgr.getAppState('zoneLevelZone');
- if (level === 'global') {
- navTitle = 'Global Status';
- } else if (level === 'zone') {
- var zoneType = AppStateMgr.getAppState('zoneLevelType')
- .startsWith('CloudService') ? 'Vanadium Services' : 'Nginx';
- navTitle = zoneType + ' @ ' + zoneLevelZone;
- navItems.push(h('div.navitems-container', [
- h('div.navitem', {
- 'ev-click': hg.send(state.channels.clickNavItem, {level: 'global'})
- }, 'GLOBAL ←')
- ]));
- } else if (level === 'instance') {
- var instanceType = AppStateMgr.getAppState('instanceLevelInstance')
- .startsWith('vanadium') ? 'Vanadium Services' : 'Nginx';
- navTitle =
- instanceType + ' @ ' + AppStateMgr.getAppState('instanceLevelInstance');
- navItems.push(h('div.navitems-container', [
- h('div.navitem', {
- 'ev-click': hg.send(state.channels.clickNavItem, {level: 'global'})
- }, 'GLOBAL ←'),
- h('div.navitem', {
- 'ev-click': hg.send(state.channels.clickNavItem,
- {level: 'zone', zone: zoneLevelZone})
- }, zoneLevelZone.toUpperCase() + ' ←')
- ]));
- }
-
- navItems.push(h('div.navtitle', h('span', navTitle.toUpperCase())));
return h('div.header', [
h('div' + infoClass, [
h('div.dashboard-title', [
h('div#logo', ''),
- h('div.title-and-time', [
- h('div.title', 'Oncall Dashboard'),
- h('div' + timeClass, strTime)
- ])
]),
- h('div.navtitle-container', navItems),
+ h('div.title-and-time', [
+ h('div.title', 'Vanadium Oncall Dashboard'),
+ h('div' + timeClass, strTime)
+ ]),
h('div.pics', pics)
])
]);
diff --git a/oncall/client/browser/components/status-table/index.js b/oncall/client/browser/components/status-table/index.js
new file mode 100644
index 0000000..43246f9
--- /dev/null
+++ b/oncall/client/browser/components/status-table/index.js
@@ -0,0 +1,291 @@
+// 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.
+
+var hg = require('mercury');
+var h = require('mercury').h;
+var svg = require('virtual-dom/virtual-hyperscript/svg');
+
+var Consts = require('../../constants');
+var MouseMoveHandler = require('../../mousemove-handler.js');
+var Util = require('../../util');
+
+module.exports = create;
+module.exports.render = render;
+
+var tableRows = [
+ // Mounttable.
+ {
+ rowHeader: Consts.metricNames.MN_MOUNTTABLE,
+ columns: [
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+ label: 'LATENCY',
+ metricName: Consts.metricNames.MN_MOUNTTABLE,
+ threshold: 2000
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_QPS,
+ label: 'QPS',
+ metricName: Consts.metricNames.MN_MOUNTTABLE
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
+ label: 'BUILD AGE (h)',
+ metricName: Consts.metricNames.MN_MOUNTTABLE
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_COUNTERS,
+ label: 'MOUNTED SERVERS',
+ metricName: Consts.metricNames.MN_MT_MOUNTED_SERVERS
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_COUNTERS,
+ label: 'NODES',
+ metricName: Consts.metricNames.MN_MT_NODES
+ },
+ ]
+ },
+ // 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,
+ columns: [
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+ label: 'LATENCY',
+ metricName: Consts.metricNames.MN_PROXY,
+ threshold: 2000
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_QPS,
+ label: 'QPS',
+ metricName: Consts.metricNames.MN_PROXY
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
+ label: 'BUILD AGE (h)',
+ metricName: Consts.metricNames.MN_PROXY
+ }
+ ]
+ },
+ // Identityd.
+ {
+ rowHeader: Consts.metricNames.MN_IDENTITY,
+ columns: [
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+ label: 'LATENCY (MACAROON)',
+ metricName: Consts.metricNames.MN_MACAROON,
+ threshold: 2000
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+ label: 'LATENCY (BINARY DISCHARGER)',
+ metricName: Consts.metricNames.MN_BINARY_DISCHARGER,
+ threshold: 2000
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
+ label: 'LATENCY (GOOGLE IDENTITY)',
+ metricName: Consts.metricNames.MN_GOOGLE_IDEN,
+ threshold: 2000
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_QPS,
+ label: 'QPS',
+ metricName: Consts.metricNames.MN_IDENTITY
+ },
+ {
+ dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
+ label: 'BUILD AGE (h)',
+ metricName: Consts.metricNames.MN_IDENTITY
+ }
+ ]
+ }
+];
+
+/** Constructor. */
+function create(data) {
+ if (!data) {
+ return null;
+ }
+
+ return hg.state({
+ // Raw data.
+ data: hg.struct(data.data),
+
+ mouseOffsetFactor: hg.value(-1),
+ showMetricActionsPanel: hg.value(false),
+
+ channels: {
+ mouseMoveOnSparkline: mouseMoveOnSparkline,
+ mouseOutOfSparkline: mouseOutOfSparkline,
+ }
+ });
+}
+
+/** 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(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 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 (metricData && metricData.HistoryTimestamps) {
+ points = Util.genPolylinePoints(
+ metricData.HistoryTimestamps, metricData.HistoryValues,
+ data.MinTime, data.MaxTime,
+ metricData.MinValue, metricData.MaxValue);
+ }
+ var curValue = Util.formatValue(metricData.CurrentValue);
+ // Handle error when getting time series.
+ var hasErrors = metricData.ErrMsg !== '';
+ var extraColMetricClass = '';
+ if (hasErrors) {
+ curValue = '?';
+ extraColMetricClass = '.err';
+ }
+ // Handle current value over threshold.
+ var overThreshold = (
+ colData.threshold && metricData.CurrentValue >= colData.threshold);
+ var thresholdValue = -1;
+ 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 > 300) {
+ extraColMetricClass = '.stale';
+ }
+ } else {
+ extraColMetricClass = '.unhealthy';
+ }
+
+ // 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';
+ }
+
+ 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])
+ ]);
+ });
+ var items = [colHeader];
+ items = items.concat(sparkLines);
+ return h('div.col', items);
+ });
+ cols.unshift(h('div.row-header', Consts.getDisplayName(rowData.rowHeader)));
+ return h('div.row', cols);
+ });
+
+ return h('div.status-table', rows);
+}
+
+/**
+ * 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) {
+ return svg('svg', {
+ 'class': 'threshold',
+ 'viewBox': '0 0 100 100',
+ 'preserveAspectRatio': 'none'
+ }, [
+ svg('path', {
+ 'd': 'M 0 ' + value + ' L 100 ' + value,
+ 'stroke-dasharray': '2,2'
+ }),
+ ]);
+}
+
+/**
+ * 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/components/summary-table/columns.js b/oncall/client/browser/components/summary-table/columns.js
deleted file mode 100644
index c58c3a1..0000000
--- a/oncall/client/browser/components/summary-table/columns.js
+++ /dev/null
@@ -1,230 +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.
-
-/**
- * Columns of the summary table.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-var svg = require('virtual-dom/virtual-hyperscript/svg');
-
-var AppStateMgr = require('../../appstate-manager');
-var Consts = require('../../constants');
-var MouseMoveHandler = require('../../mousemove-handler.js');
-var Util = require('../../util');
-var summaryTableMetricNamesColumnComponent = require('./metricnames-column');
-
-module.exports = {
- render: render
-};
-
-/** The main render function. */
-function render(state) {
- var data = state.data.Zones;
- var level = AppStateMgr.getAppState('level');
-
- var cols = [hg.partial(summaryTableMetricNamesColumnComponent.render, state)];
- if (level === 'global') {
- cols = cols.concat(genCellsInGlobalLevel(state, data));
- } else if (level === 'zone') {
- cols = cols.concat(genCellsInZoneLevel(state, data));
- }
- return h('div.columns-container', cols);
-}
-
-/** Generates data cells in the "globel" level. */
-function genCellsInGlobalLevel(state, data) {
- var aggType = AppStateMgr.getAppState('globalLevelAggType');
- var cols = Consts.orderedZones.filter(function(zone) {
- return data[zone];
- }).map(function(zone) {
- var aggData = data[zone][aggType];
- var rows = [];
- Consts.mainMetrics.forEach(function(curMetric, index) {
- var dataKey = curMetric.dataKey;
- var metricKey = curMetric.metricKey;
- if (!Util.isEmptyObj(aggData[dataKey])) {
- rows.push(renderCell(
- state, aggData, aggData.Range.MinTime, aggData.Range.MaxTime,
- dataKey, metricKey, zone, zone));
- } else {
- rows.push(renderFillerCell(state));
- }
- if (curMetric.addMinorDivider) {
- rows.push(h('div.minor-divider'));
- }
- if (curMetric.addMajorDivider) {
- rows.push(h('div.major-divider'));
- }
- });
- return h('div.zone-col', rows);
- });
- return cols;
-}
-
-/** Generates data cells in the "zone" level. */
-function genCellsInZoneLevel(state, data) {
- var zoneLevelZone = AppStateMgr.getAppState('zoneLevelZone');
- var zoneLevelType = AppStateMgr.getAppState('zoneLevelType');
-
- // Filter metrics and instances by zone level type (CloudService or Nginx).
- var metrics = (zoneLevelType === 'CloudService' ?
- Consts.cloudServiceMetrics.concat(Consts.cloudServiceGCEMetrics) :
- Consts.nginxMetrics.concat(Consts.nginxGCEMetrics));
- var instancePrefix = (zoneLevelType === 'CloudService' ?
- 'vanadium-' : 'nginx-');
- var instanceNames = Object.keys(data[zoneLevelZone].Instances);
- var cols = instanceNames.sort().filter(function(name) {
- return name.startsWith(instancePrefix);
- }).map(function(instance) {
- var instanceData = data[zoneLevelZone].Instances[instance];
- var rows = [];
- // Calculate the min/max timestamps across all metrics.
- var minTime = Number.MAX_VALUE;
- var maxTime = 0;
- metrics.forEach(function(curMetric) {
- var dataKey = curMetric.dataKey;
- var metricKey = curMetric.metricKey;
- if (!Util.isEmptyObj(instanceData[dataKey])) {
- var metricData = instanceData[dataKey][metricKey];
- if (metricData) {
- if (metricData.MinTime < minTime) {
- minTime = metricData.MinTime;
- }
- if (metricData.MaxTime > maxTime) {
- maxTime = metricData.MaxTime;
- }
- }
- }
- });
- metrics.forEach(function(curMetric, index) {
- var dataKey = curMetric.dataKey;
- var metricKey = curMetric.metricKey;
- if (!Util.isEmptyObj(instanceData[dataKey])) {
- rows.push(renderCell(
- state, instanceData, minTime, maxTime,
- dataKey, metricKey, instance, zoneLevelZone));
- } else {
- rows.push(renderFillerCell(state));
- }
- if (curMetric.addMinorDivider) {
- rows.push(h('div.minor-divider'));
- }
- if (curMetric.addMajorDivider) {
- rows.push(h('div.major-divider'));
- }
- });
- return h('div.zone-col', rows);
- });
- return cols;
-}
-
-/**
- * Renders a single table cell.
- * @param {hg.state} state - The component's state.
- * @param {Object} cellData - The data object for the cell.
- * @param {Number} minTime - The minimum time to render the sparkline for.
- * @param {Number} maxTime - The maximum time to render the sparkline for.
- * @param {string} dataKey - See comments of createMetric function in
- * constants.js.
- * @param {string} metricKey - See comments of createMetric function in
- * constants.js.
- * @param {string} column - The column name of the cell.
- * @param {string} zone - The zone name associated with the cell.
- */
-function renderCell(state, cellData, minTime, maxTime,
- dataKey, metricKey, column, zone) {
- var data = cellData[dataKey][metricKey];
- var health = 'unhealthy';
- if (data) {
- health = (data.Healthy ? 'healthy' : 'unhealthy');
- }
-
- // 100 is the default logical width of any svg graphs.
- var points = '0,100 100,100';
- var mouseOffset = 100 * state.mouseMoveCellData.offsetFactor;
- var mouseEventData = {column: column, dataKey: dataKey, metricKey: metricKey};
- if (data && data.HistoryTimestamps) {
- points = Util.genPolylinePoints(
- data.HistoryTimestamps, data.HistoryValues,
- minTime, maxTime, data.MinValue, data.MaxValue);
- }
-
- // Show mouse line based on the dataKey type (CloudService or Nginx).
- var mouseMoveOnCloudServices =
- state.mouseMoveCellData.dataKey.startsWith('CloudService');
- var mouseMoveOnNginx = state.mouseMoveCellData.dataKey.startsWith('Nginx');
- var showMouseLine = (column === state.mouseMoveCellData.column &&
- ((dataKey.startsWith('CloudService') && mouseMoveOnCloudServices) ||
- (dataKey.startsWith('Nginx') && mouseMoveOnNginx)));
-
- var curValue = data ? data.CurrentValue : 0;
- var curValueClass = 'value';
- if (data && showMouseLine) {
- curValue = Util.interpolateValue(
- data.CurrentValue, state.mouseMoveCellData.offsetFactor,
- data.HistoryTimestamps, data.HistoryValues);
- curValueClass += '.history';
- }
- var formattedCurValue = data ? Util.formatValue(curValue) : '?';
- var items = [
- h('div.highlight-overlay'),
- h('div.mouse-line', showMouseLine ? renderMouseLine(mouseOffset) : []),
- h('div.sparkline', {
- 'ev-mousemove': new MouseMoveHandler(
- state.channels.mouseMoveOnSparkline, mouseEventData),
- 'ev-mouseout': hg.send(state.channels.mouseOutOfSparkline)
- }, renderSparkline(points)),
- h('div.' + curValueClass, formattedCurValue)
- ];
-
- var cellClassName = 'zone-col-cell.cell.' + health;
- return h('div.' + cellClassName, {
- 'ev-mouseout': hg.send(state.channels.mouseOutOfTableCell),
- 'ev-mouseover': hg.send(state.channels.mouseOverTableCell, mouseEventData),
- 'ev-click': hg.send(state.channels.clickCell, {
- column: column,
- dataKey: dataKey,
- zone: zone
- })
- }, items);
-}
-
-/** Renders a empty filler table cell. */
-function renderFillerCell(state) {
- return h('div.zone-col-cell-filler.cell', {
- 'ev-mouseout': hg.send(state.channels.mouseOutOfTableCell)
- });
-}
-
-/**
- * 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/browser/components/summary-table/header.js b/oncall/client/browser/components/summary-table/header.js
deleted file mode 100644
index cc5230d..0000000
--- a/oncall/client/browser/components/summary-table/header.js
+++ /dev/null
@@ -1,161 +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.
-
-/**
- * The header row of the summary table.
- *
- * In the "global" view, it shows the aggregation switch and all the zones. In
- * the "zone" view, it shows all the instances for that zone.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-
-var Consts = require('../../constants');
-var AppStateMgr = require('../../appstate-manager');
-var Util = require('../../util');
-
-module.exports = {
- render: render
-};
-
-/** The main render function. */
-function render(state) {
- var data = state.data.Zones;
- var level = AppStateMgr.getAppState('level');
-
- // First column is the aggregation switch.
- var cols = [renderGlobalLevelAggSwitch(state)];
-
- // Generate the rest of the columns.
- var headers = [];
- if (level === 'global') {
- // In the "global" level, add all the zones.
- headers = Consts.orderedZones.filter(function(zone) {
- return data[zone];
- });
- } else if (level === 'zone') {
- // In the "zone" level, add instances based on the type.
- var zoneLevelZone = AppStateMgr.getAppState('zoneLevelZone');
- var zoneLevelType = AppStateMgr.getAppState('zoneLevelType');
- var instancePrefix =
- (zoneLevelType === 'CloudService' ? 'vanadium-' : 'nginx-');
- // Filter instances by instancePrefix (determined by zoneLevelType).
- headers = Object.keys(data[zoneLevelZone].Instances)
- .sort().filter(function(name) {
- return name.startsWith(instancePrefix);
- });
- }
- cols = cols.concat(renderHeaders(state, headers));
-
- return h('div.zone-names-row', cols);
-}
-
-/** Renders the aggregtion type switch. */
-function renderGlobalLevelAggSwitch(state) {
- var aggType = AppStateMgr.getAppState('globalLevelAggType');
- var level = AppStateMgr.getAppState('level');
- if (level === 'global') {
- // Only generate the switch (two links: average and max) in the "global"
- // level to specify the aggregation method for all the cell data.
- return h('div.view-type.zone-name-cell', [
- h('div.average', {
- className: aggType === 'Average' ? 'selected' : '',
- 'ev-click': hg.send(state.channels.changeGlobalLevelAggType, 'Average')
- }, 'average'),
- h('div.max', {
- className: aggType === 'Max' ? 'selected' : '',
- 'ev-click': hg.send(state.channels.changeGlobalLevelAggType, 'Max')
- }, 'max')
- ]);
- } else {
- // In the "zone" level, don't show the switch.
- return h('div.view-type-dummy');
- }
-}
-
-/**
- * Renders the given headers.
- * @param {hg.state} state
- * @param {Array<string>} headers
- */
-function renderHeaders(state, headers) {
- var hoveredColumn = state.hoveredCellData.column;
- return headers.map(function(header) {
- var className = 'zone-name-cell';
- if (header === hoveredColumn) {
- className += '.highlight';
- }
- if (!checkMetricsHealthForHeader(state, header)) {
- className += '.unhealthy';
- }
- return h('div.' + className, h('span', shortenHeaderLabel(header)));
- });
-}
-
-/**
- * Checks whether all the metrics for the given header are healthy.
- * @param {hg.state} state
- * @param {string} header
- * @return {boolean}
- */
-function checkMetricsHealthForHeader(state, header) {
- var data = state.data.Zones;
- var aggType = AppStateMgr.getAppState('globalLevelAggType');
- var level = AppStateMgr.getAppState('level');
- var isHealthy = true;
-
- // In different levels, we check different health data for a given header.
- // The health data is already calculated and recorded in the data object
- // loaded from the backend server.
- var i, curMetric;
- if (level === 'global') {
- var zone = header;
- var aggData = data[zone][aggType];
- for (i = 0; i < Consts.mainMetrics.length; i++) {
- curMetric = Consts.mainMetrics[i];
- if (!Util.isEmptyObj(aggData[curMetric.dataKey])) {
- var metricAggData = aggData[curMetric.dataKey][curMetric.metricKey];
- if (!metricAggData || !metricAggData.Healthy) {
- isHealthy = false;
- break;
- }
- }
- }
- } else if (level === 'zone') {
- var instance = header;
- var zoneLevelZone = AppStateMgr.getAppState('zoneLevelZone');
- var zoneLevelType = AppStateMgr.getAppState('zoneLevelType');
- var metrics = (zoneLevelType === 'CloudService' ?
- Consts.cloudServiceMetrics : Consts.nginxMetrics);
- var instanceData = data[zoneLevelZone].Instances[instance];
- for (i = 0; i < metrics.length; i++) {
- curMetric = metrics[i];
- if (!Util.isEmptyObj(instanceData[curMetric.dataKey])) {
- var metricData = instanceData[curMetric.dataKey][curMetric.metricKey];
- if (!metricData || !metricData.Healthy) {
- isHealthy = false;
- break;
- }
- }
- }
- }
-
- return isHealthy;
-}
-
-/**
- * Shortens the header label by removing the common prefix.
- * @param {string} header
- * @return {string} The shortened header label.
- */
-function shortenHeaderLabel(header) {
- if (header.startsWith('vanadium')) {
- return header.replace('vanadium-', '');
- }
- if (header.startsWith('nginx')) {
- return header.replace('nginx-', '');
- }
- return header;
-}
diff --git a/oncall/client/browser/components/summary-table/index.js b/oncall/client/browser/components/summary-table/index.js
deleted file mode 100644
index f2c8117..0000000
--- a/oncall/client/browser/components/summary-table/index.js
+++ /dev/null
@@ -1,156 +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.
-
-/**
- * The summary table showing the data in "global" and "zone" level.
- *
- * In the "global" level, the table's columnes are zones with available data,
- * and its rows are metrics for vanadium services and nginx servers. Each table
- * cell (for zone Z and metric M) shows data aggregated across all the instances
- * in that zone for that metric.The aggregation method is either "average" or
- * "max", and can be selected at the top left corner of the summary table.
- *
- * Users can "drill down" to the "zone" level by clicking on any table cell.
- *
- * In the "zone" level, the table's columns are instances in that zone, and its
- * rows are metrics for EITHER vanadium services or nginx servers (depending on
- * which cell users click in the "global" level). In this level, users can drill
- * further down to the "instance" level by clicking on any table cell. The
- * instance view will be rendered by the "instance-view" component.
- */
-
-var hg = require('mercury');
-var h = require('mercury').h;
-
-var AppStateMgr = require('../../appstate-manager');
-var summaryTableHeaderComponent = require('./header');
-var summaryTableColumnsComponent = require('./columns');
-
-module.exports = create;
-module.exports.render = render;
-
-/** Constructor. */
-function create(data) {
- if (!data) {
- return null;
- }
-
- return hg.state({
- // Raw data.
- data: hg.struct(data.data),
-
- // Keeps track of which cell the mouse cursor is hovering over.
- //
- // This will be used to highlight the hovered table cell as well as the
- // corresponding column/row header cell.
- hoveredCellData: hg.struct({
- // Column name.
- column: '',
- // See "createMetric" function in constants.js.
- dataKey: '',
- // See "createMetric" function in constants.js.
- metricKey: ''
- }),
-
- // Keeps track of the cell and relative location of the mouse cursor.
- //
- // This will be used to render a mouse line in all related table cells for
- // easier data comparison.
- mouseMoveCellData: hg.struct({
- // Column name.
- column: '',
- // See "createMetric" function in constants.js.
- dataKey: '',
- // See "createMetric" function in constants.js.
- offsetFactor: -1
- }),
-
- channels: {
- changeGlobalLevelAggType: changeGlobalLevelAggType,
- clickCell: clickCell,
- mouseMoveOnSparkline: mouseMoveOnSparkline,
- mouseOutOfSparkline: mouseOutOfSparkline,
- mouseOverTableCell: mouseOverTableCell,
- mouseOutOfTableCell: mouseOutOfTableCell
- }
- });
-}
-
-/**
- * Callback for changing the aggregation type in the "global" level.
- */
-function changeGlobalLevelAggType(state, newAggType) {
- AppStateMgr.setAppState({'globalLevelAggType': newAggType});
-}
-
-/**
- * Callback for clicking a table cell.
- */
-function clickCell(state, data) {
- var level = AppStateMgr.getAppState('level');
- if (level === 'global') {
- // Clicking a cell in the "global" level will go to the corresponding "zone"
- // level.
- var zoneLevelType =
- data.dataKey.startsWith('CloudService') ? 'CloudService' : 'Nginx';
- AppStateMgr.setAppState({
- 'level': 'zone',
- 'zoneLevelZone': data.column,
- 'zoneLevelType': zoneLevelType
- });
- } else if (level === 'zone') {
- // Clicking a cell in the "zone" level will go to the corresponding
- // "instance" level.
- AppStateMgr.setAppState({
- 'level': 'instance',
- 'instanceLevelInstance': data.column,
- 'instanceLevelZone': data.zone,
- });
- }
-}
-
-/** Callback for moving mouse on sparkline. */
-function mouseMoveOnSparkline(state, data) {
- state.mouseMoveCellData.set({
- column: data.column,
- dataKey: data.dataKey,
- offsetFactor: data.f
- });
-}
-
-/** Callback for moving mouse out of sparkline. */
-function mouseOutOfSparkline(state) {
- state.mouseMoveCellData.set({
- column: '',
- dataKey: '',
- offsetFactor: -1
- });
-}
-
-/** Callback for moving mouse over a table cell. */
-function mouseOverTableCell(state, data) {
- state.hoveredCellData.set({
- column: data.column,
- dataKey: data.dataKey,
- metricKey: data.metricKey
- });
-}
-
-/** Callback for moving mouse out of a table cell. */
-function mouseOutOfTableCell(state) {
- state.hoveredCellData.set({
- column: '',
- dataKey: '',
- metricKey: ''
- });
-}
-
-/** The main render function. */
-function render(state) {
- // Delegate rendering to sub-components.
- return h('div.summary-table', [
- hg.partial(summaryTableHeaderComponent.render, state),
- hg.partial(summaryTableColumnsComponent.render, state)
- ]);
-}
diff --git a/oncall/client/browser/components/summary-table/metricnames-column.js b/oncall/client/browser/components/summary-table/metricnames-column.js
deleted file mode 100644
index 0e0bf74..0000000
--- a/oncall/client/browser/components/summary-table/metricnames-column.js
+++ /dev/null
@@ -1,115 +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.
-
-/**
- * Metric names column (the 1st column) of the summary table.
- */
-
-var h = require('mercury').h;
-
-var AppStateMgr = require('../../appstate-manager');
-var Consts = require('../../constants');
-var Util = require('../../util');
-
-module.exports = {
- render: render
-};
-
-/** The main render function. */
-function render(state) {
- var dataKey = state.hoveredCellData.dataKey;
- var metricKey = state.hoveredCellData.metricKey;
- var level = AppStateMgr.getAppState('level');
-
- // In the "globel" level, we show all metrics.
- // In the "zone" level, we only show either cloud services metrics or nginx
- // metrics.
- var metricNameRows = [];
- var metrics = Consts.mainMetrics;
- if (level === 'zone') {
- var zoneLevelType = AppStateMgr.getAppState('zoneLevelType');
- metrics = (zoneLevelType === 'CloudService' ?
- Consts.cloudServiceMetrics.concat(Consts.cloudServiceGCEMetrics) :
- Consts.nginxMetrics.concat(Consts.nginxGCEMetrics));
- }
-
- metrics.forEach(function(curMetric, index) {
- var className = 'metric-name-cell.cell';
- if (curMetric.dataKey === dataKey && curMetric.metricKey === metricKey) {
- className += '.highlight';
- }
- if (!checkHealthForMetric(state, curMetric)) {
- className += '.unhealthy';
- }
- // secontionLabel is the light grey label above each section of the table.
- // For example: NGINX SERVER LOAD, NGINX GCE, etc.
- if (curMetric.sectionLabel) {
- var sectionLabelClassName = '.section-label';
- if (curMetric.dataKey === dataKey) {
- sectionLabelClassName += '.highlight-section';
- }
- metricNameRows.push(h('div.' + className, [
- h('div' + sectionLabelClassName , h('span', curMetric.sectionLabel)),
- h('div', h('span', curMetric.label))
- ]));
- } else {
- metricNameRows.push(h('div.' + className, h('span', curMetric.label)));
- }
- if (curMetric.addMinorDivider) {
- metricNameRows.push(h('div.minor-divider'));
- }
- if (curMetric.addMajorDivider) {
- metricNameRows.push(h('div.major-divider'));
- }
- });
- return h('div.metric-names-col', metricNameRows);
-}
-
-
-/**
- * Checks whether the given metric across all the zones/instances is healthy.
- * @param {hg.state} state
- * @param {Object} metric
- * @return {boolean}
- */
-function checkHealthForMetric(state, metric) {
- var data = state.data.Zones;
- var aggType = AppStateMgr.getAppState('globalLevelAggType');
- var level = AppStateMgr.getAppState('level');
- var isHealthy = true;
-
- var i;
- if (level === 'global') {
- var availableZones = Consts.orderedZones.filter(function(zone) {
- return data[zone];
- });
- for (i = 0; i < availableZones.length; i++) {
- var zone = availableZones[i];
- var aggData = data[zone][aggType];
- if (!Util.isEmptyObj(aggData[metric.dataKey])) {
- var metricAggData = aggData[metric.dataKey][metric.metricKey];
- if (!metricAggData || !metricAggData.Healthy) {
- isHealthy = false;
- break;
- }
- }
- }
- } else if (level === 'zone') {
- var zoneLevelZone = AppStateMgr.getAppState('zoneLevelZone');
- var instances = Object.keys(data[zoneLevelZone].Instances).sort();
- for (i = 0; i < instances.length; i++) {
- var instance = instances[i];
- var instanceData = data[zoneLevelZone].Instances[instance];
- if (!Util.isEmptyObj(instanceData[metric.dataKey])) {
- var metricData = instanceData[metric.dataKey][metric.metricKey];
- if (!metricData || !metricData.Healthy) {
- isHealthy = false;
- break;
- }
- }
- }
- }
-
- return isHealthy;
-}
diff --git a/oncall/client/browser/constants.js b/oncall/client/browser/constants.js
index db66902..6963893 100644
--- a/oncall/client/browser/constants.js
+++ b/oncall/client/browser/constants.js
@@ -16,6 +16,27 @@
ASIA_EAST1_C: 'asia-east1-c',
});
+/** Constants for all the metric names in the raw data. */
+var metricNames = Object.freeze({
+ MN_BINARY_DISCHARGER: 'binary discharger',
+ MN_GOOGLE_IDEN: 'google identity service',
+ MN_IDENTITY: 'identity service',
+ MN_MACAROON: 'macaroon service',
+ MN_MOUNTTABLE: 'mounttable',
+ MN_PROXY: 'proxy service',
+ MN_ROLE: 'role service',
+ MN_MT_MOUNTED_SERVERS: 'mounttable mounted servers',
+ MN_MT_NODES: 'mounttable nodes'
+});
+
+/** Constants for all the keys in raw data. */
+var dataKeys = Object.freeze({
+ DK_SERVICE_LATENCY: 'ServiceLatency',
+ DK_SERVICE_COUNTERS: 'ServiceCounters',
+ DK_SERVICE_QPS: 'ServiceQPS',
+ DK_SERVICE_METADATA: 'ServiceMetadata'
+});
+
/** A map from long name strings to their shorter forms. */
var displayNames = Object.freeze({
'active-connections': 'ACTIVE CONN',
@@ -35,10 +56,11 @@
'groups service latency': 'GROUPS',
'healthCheckLatency': 'HEALTH LAT',
'identityd': 'IDEN',
+ 'identity service': 'IDENTITY',
'macaroon service': 'MACAROON',
'macaroon service latency': 'MACAROON',
'memory-usage': 'RAM%',
- 'mounttable': 'MTB',
+ 'mounttable': 'MOUNTTABLE',
'mounttabled': 'MTB',
'mounttable latency': 'MTB',
'mounttable mounted servers': 'MTB SERVERS',
@@ -197,6 +219,8 @@
module.exports = {
zones: zones,
+ metricNames: metricNames,
+ dataKeys: dataKeys,
orderedZones: [
zones.US_CENTRAL1_C,
zones.US_CENTRAL1_A,
diff --git a/oncall/client/browser/index.js b/oncall/client/browser/index.js
index 75ce280..8151bc9 100644
--- a/oncall/client/browser/index.js
+++ b/oncall/client/browser/index.js
@@ -7,11 +7,10 @@
var request = require('superagent');
var cookie = require('cookie');
-var AppStateMgr = require('./appstate-manager');
-var instanceViewComponent = require('./components/instance-view');
var pageHeaderComponent = require('./components/page-header');
var settingsPanelComponent = require('./components/settings');
-var summaryTableComponent = require('./components/summary-table');
+var statusTableComponent = require('./components/status-table');
+var metricActionsPanelComponent = require('./components/metric-actions-panel');
/**
* A variable to store the most update-to-date dashboard data.
@@ -20,9 +19,11 @@
*/
var curData;
-// Initializes the app state manager and sets appStateChanged function as the
-// callback for app state changes.
-AppStateMgr.init(appStateChanged);
+/**
+ * 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');
@@ -36,35 +37,50 @@
var state = hg.state({
// The header panel at the top of the page.
pageHeader: pageHeaderComponent({
- collectionTimestamp: -1,
+ startTimestamp: -1,
+ endTimestamp: -1,
oncallIds: ['_unknown', '_unknown'],
loadingData: false,
hasLoadingFailure: false
}),
components: hg.varhash({
- // The summary table showing data on the "global" and "zone" level.
- summaryTable: summaryTableComponent(null),
+ // The status table showing service status.
+ statusTable: statusTableComponent(null),
// The view showing data on the "instance" level.
- instanceView: instanceViewComponent(null),
+ // instanceView: instanceViewComponent(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')
}),
channels: {
+ mouseClickOnMetric: mouseClickOnMetric,
changeTheme: changeTheme,
clickOnSettingsGear: clickOnSettingsGear,
- closeSettingsPanel: closeSettingsPanel
+ 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);
+}
+
/** Callback when user clicks on the settings gear. */
function clickOnSettingsGear(state) {
state.showSettingsPanel.set(true);
@@ -88,23 +104,26 @@
var mainContent = [
pageHeaderComponent.render(state.pageHeader),
];
- if (state.components.summaryTable) {
+ if (state.components.statusTable) {
mainContent.push(
- h('div.main-container',
- summaryTableComponent.render(state.components.summaryTable)));
+ h('div.main-container',
+ statusTableComponent.render(state, state.components.statusTable))
+ );
}
- if (state.components.instanceView) {
- mainContent.push(
- h('div.main-container',
- instanceViewComponent.render(state.components.instanceView)));
- }
- mainContent.push(h('div.settings-gear', {
- 'ev-click': hg.send(state.channels.clickOnSettingsGear)
- }));
+ mainContent.push(
+ h('div.settings-gear', {
+ 'ev-click': hg.send(state.channels.clickOnSettingsGear)
+ })
+ );
if (state.showSettingsPanel) {
mainContent.push(hg.partial(settingsPanelComponent.render, state));
}
+ if (state.showMetricActionsPanel) {
+ mainContent.push(hg.partial(metricActionsPanelComponent.render, state,
+ selectedMetricData, curData));
+ }
+
var className = state.settings.darkTheme ? 'main.darkTheme' : 'main';
return h('div.' + className, mainContent);
};
@@ -121,7 +140,7 @@
.accept('json')
.timeout(30000)
.end(function(err, res) {
- if (!res.ok || err) {
+ if (!res || !res.ok || err) {
state.pageHeader.hasLoadingFailure.set(true);
} else {
state.pageHeader.hasLoadingFailure.set(false);
@@ -137,58 +156,36 @@
function processData(newData) {
// Update components.
curData = newData;
- updateComponents(AppStateMgr.getCurState());
+ updateComponents();
// Update the data loading indicator.
state.pageHeader.loadingData.set(false);
}
/**
- * Callback function for app state changes.
- * @param {Object} curAppState - App's current state object.
- */
-function appStateChanged(curAppState) {
- if (!state) {
- return;
- }
- updateComponents(curAppState);
-}
-
-/**
* Updates all page components.
- * @param {Object} curAppState - App's current state object.
*/
-function updateComponents(curAppState) {
+function updateComponents() {
// Update page header.
- state.pageHeader.collectionTimestamp.set(curData.CollectionTimestamp);
- state.pageHeader.oncallIds.set(curData.OncallIDs.split(','));
+ state.pageHeader.endTimestamp.set(curData.MaxTime);
+ state.pageHeader.oncallIds.set(curData.Oncalls);
- // Update summary table when the current view level is NOT "instance".
- var summaryTableData = null;
- if (curAppState.level !== 'instance') {
- summaryTableData = summaryTableComponent({
- data: curData
- });
- }
- state.components.put('summaryTable', summaryTableData);
-
- // Update instance view when the current view level is "instance".
- var instanceViewData = null;
- if (curAppState.level === 'instance') {
- var instance = curAppState.instanceLevelInstance;
- instanceViewData = instanceViewComponent({
- collectionTimestamp: curData.CollectionTimestamp,
- data: curData.Zones[curAppState.instanceLevelZone].Instances[instance],
- appState: curAppState
- });
- }
- state.components.put('instanceView' ,instanceViewData);
+ // Update status table.
+ var statusTableData = statusTableComponent({
+ data: curData
+ });
+ state.components.put('statusTable', statusTableData);
}
// Add an event handler for closing settings panel when esc key is pressed.
document.onkeydown = function(evt) {
- if (evt.keyCode === 27 && state.showSettingsPanel()) {
- state.showSettingsPanel.set(false);
+ if (evt.keyCode === 27) {
+ if (state.showSettingsPanel()) {
+ state.showSettingsPanel.set(false);
+ }
+ if (state.showMetricActionsPanel()) {
+ state.showMetricActionsPanel.set(false);
+ }
}
};
diff --git a/oncall/client/stylesheets/components/data-table.css b/oncall/client/stylesheets/components/data-table.css
deleted file mode 100644
index 7495350..0000000
--- a/oncall/client/stylesheets/components/data-table.css
+++ /dev/null
@@ -1,60 +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. */
-
-div.data-table {
- margin-top: var(--subsection-margin-top);
-}
-
-
-div.data-table div.data-table-title {
- inherits: div.group-title;
- display: flex;
- justify-content: space-between;
- width: 100%;
-}
-
-div.data-table tr {
- line-height: 14px;
-}
-
-div.data-table tr:first-child {
- line-height: 18px;
-}
-
-div.data-table td {
- padding-right: 12px;
-}
-
-div.data-table table {
- color: var(--dark-green-grey);
- font-size: 12px;
- margin-top: 4px;
-}
-
-.darkTheme div.data-table table {
- color: var(--light-green-grey);
-}
-
-.darkTheme div.data-table a {
- color: LightBlue;
-}
-
-div.data-table tr:first-child td {
- font-weight: var(--font-weight-medium);
-}
-
-div.data-table div.data-table-content {
- padding-top: 2px;
- font-size: 12px;
- display: flex;
-}
-
-div.data-table div.type-header {
- font-weight: var(--font-weight-medium);
- line-height: 20px;
-}
-
-div.data-table div.column {
- margin-right: 20px;
-}
diff --git a/oncall/client/stylesheets/components/full-graph.css b/oncall/client/stylesheets/components/full-graph.css
deleted file mode 100644
index 23f2575..0000000
--- a/oncall/client/stylesheets/components/full-graph.css
+++ /dev/null
@@ -1,149 +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. */
-
-div.full-graph-container {
-}
-
-div.full-graph-container div.full-graph-title {
- inherits: div.group-title;
-}
-
-div.full-graph-container div.metric-summary-container {
- display: flex;
-}
-
-div.full-graph-container div.metric-summary-item {
- flex-grow: 1;
- padding-top: 4px;
- position: relative;
- width: 100%;
- cursor: pointer;
- margin-right: 2px;
-}
-
-div.full-graph-container div.metric-summary-item.hidden {
- opacity: 0.15;
-}
-
-div.full-graph-container div.metric-summary-item.unhealthy {
- background: var(--red-stripes-background-dimmed);
-}
-
-div.full-graph-container div.metric-summary-item:last-child {
- margin-right: 0px !important;
-}
-
-div.full-graph-container div.metric-summary-title {
- font-size: 12px;
- color: var(--dark-green-grey);
- line-height: 14px;
- padding-top: 4px;
-}
-
-.darkTheme div.full-graph-container div.metric-summary-title {
- color: var(--light-green-grey);
-}
-
-div.full-graph-container div.metric-summary-value {
- font-size: 16px;
- color: var(--cyan-800);
- font-weight: var(--font-weight-medium);
- line-height: 20px;
- margin-bottom: 4px;
-}
-
-.darkTheme div.full-graph-container div.metric-summary-value {
- color: var(--light-green);
-}
-
-div.full-graph-container div.metric-summary-value.unhealthy {
- color: var(--dark-red);
-}
-
-.darkTheme div.full-graph-container div.metric-summary-value.unhealthy {
- color: var(--light-red);
-}
-
-div.full-graph-container div.metric-summary-color {
- height: 2px;
-}
-
-div.full-graph-container div.full-graph {
- width: 100%;
- height: var(--full-graph-height);
- position: relative;
-}
-
-div.full-graph-container div.full-graph div.graph-y-axis {
- width: var(--graph-number-div-width);
-}
-
-div.full-graph-container div.full-graph svg {
- position: absolute;
- top: 0px;
- left: 0px;
- height: 100%;
- width: 100%;
-}
-
-div.full-graph-container div.full-graph svg polyline {
- vector-effect: non-scaling-stroke;
- fill: none;
-}
-
-div.full-graph-container div.full-graph svg.content polyline {
- stroke: var(--light-grey);
- stroke-width: 2;
-}
-
-div.full-graph-container svg.threshold-line {
- shape-rendering: crispedges;
-}
-
-div.full-graph-container svg.threshold-line path {
- vector-effect: non-scaling-stroke;
- fill: none;
- stroke: var(--dark-red);
- stroke-width: 1;
-}
-
-div.full-graph-container div.full-graph div.threshold {
- border-bottom: 2px dotted var(--dark-red);
-}
-
-div.full-graph-container div.full-graph svg.overlay polyline {
- stroke: #CCC;
- stroke-width: 1;
-}
-
-.darkTheme div.full-graph-container div.full-graph svg.overlay polyline {
- stroke: #666;
-}
-
-div.full-graph-container svg.overlay {
- shape-rendering: crispedges;
-}
-
-div.full-graph-container div.full-graph div.number {
- /* background-color: var(--light-grey);*/
- width: var(--graph-number-div-width);
- height: var(--full-graph-height);
-}
-
-div.full-graph-container div.time {
- height: var(--graph-time-div-height);
- border-top: 1px solid var(--light-grey);
- justify-content: space-between;
- display: flex;
-}
-
-.darkTheme div.full-graph-container div.time {
- border-top: 1px solid #333;
-}
-
-div.full-graph-container div.time-label {
- font-size: 10px;
- color: var(--medium-grey);
- padding: 0px 2px;
-}
diff --git a/oncall/client/stylesheets/components/header.css b/oncall/client/stylesheets/components/header.css
index 470c123..f16e3f1 100644
--- a/oncall/client/stylesheets/components/header.css
+++ b/oncall/client/stylesheets/components/header.css
@@ -29,6 +29,7 @@
}
div.header div.dashboard-title {
+ width: 120px;
padding-left: 10px;
display: flex;
}
@@ -54,21 +55,19 @@
div.header div.title-and-time {
display: flex;
flex-direction: column;
- padding-top: 1px;
+ align-items: center;
}
div.header div.title {
- font-size: 16px;
+ font-size: 18px;
font-weight: var(--font-weight-medium);
- line-height: 22px;
- height: 22px;
+ padding-bottom: 4px;
}
div.header div.time {
font-size: 10px;
height: 12px;
line-height: 12px;
- padding-left: 1px;
}
div.header div.time.failure {
@@ -85,6 +84,8 @@
div.header div.pics {
display: flex;
padding-right: 5px;
+ width: 120px;
+ justify-content: flex-end;
}
div.header div.pics img {
diff --git a/oncall/client/stylesheets/components/instance-view.css b/oncall/client/stylesheets/components/instance-view.css
deleted file mode 100644
index a65d674..0000000
--- a/oncall/client/stylesheets/components/instance-view.css
+++ /dev/null
@@ -1,8 +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. */
-
-div.instance-view {
- margin-top: 12px;
- width: 640px;
-}
diff --git a/oncall/client/stylesheets/components/metric-actions-panel.css b/oncall/client/stylesheets/components/metric-actions-panel.css
new file mode 100644
index 0000000..5507a61
--- /dev/null
+++ b/oncall/client/stylesheets/components/metric-actions-panel.css
@@ -0,0 +1,59 @@
+/* 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-actions-container {
+ z-index: 999;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.2);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+div.metric-actions-container div.metric-actions-content {
+ background-color: white;
+ border: 1px black solid;
+ width: 440px;
+ padding: 12px;
+ padding-top: 16px;
+}
+
+div.metric-actions-container div.title {
+ font-weight: var(--font-weight-medium);
+ padding: 4px 0px 16px 0px;
+}
+
+div.metric-actions-container div.btn-close {
+ padding-top: 10px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ color: #999;
+}
+
+div.metric-actions-container div.btn-close:hover {
+ color: #444;
+}
+
+div.metric-actions-container div.row {
+ display: flex;
+ flex-direction: row;
+}
+
+div.metric-actions-container div.space {
+ height: 14px;
+ width: 100%;
+}
+
+div.metric-actions-container div.item-label {
+ font-weight: var(--font-weight-medium);
+ width: 140px;
+ padding-right: 10px;
+}
+
+div.metric-actions-container div.item-link {
+ margin-left: 10px;
+}
+
diff --git a/oncall/client/stylesheets/components/metrics-group.css b/oncall/client/stylesheets/components/metrics-group.css
deleted file mode 100644
index 0b12cfd..0000000
--- a/oncall/client/stylesheets/components/metrics-group.css
+++ /dev/null
@@ -1,53 +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. */
-
-div.metrics-group {
- margin-top: var(--subsection-margin-top);
-}
-
-div.metrics-group div.link-container {
- display: flex;
-}
-
-div.metrics-group a {
- text-decoration: none;
- height: 100%;
- color: var(--dark-green-grey);
- padding: 0px 4px;
- cursor: pointer;
- font-size: 12px;
- width: 30px;
- text-align: center;
-}
-
-.darkTheme div.metrics-group a {
- color: var(--light-green-grey);
-}
-
-div.metrics-group a:hover {
- background-color: #DDD;
-}
-
-.darkTheme div.metrics-group a:hover {
- background-color: var(--dark-highlight);
-}
-
-div.metrics-group-title-container {
- inherits: div.group-title;
- display: flex;
- justify-content: space-between;
- width: 100%;
-}
-
-div.metrics-group-title {
- width: 100%;
-}
-
-div.metrics-group-row {
- display: flex;
-}
-
-div.metrics-group-row div:last-child {
- border-right: none !important;
-}
diff --git a/oncall/client/stylesheets/components/status-table.css b/oncall/client/stylesheets/components/status-table.css
new file mode 100644
index 0000000..3926cf8
--- /dev/null
+++ b/oncall/client/stylesheets/components/status-table.css
@@ -0,0 +1,154 @@
+/* Copyright 2016 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.status-table {
+ color: var(--dark-green-grey);
+ padding-top: 20px;
+ margin-right: 120px;
+}
+
+.darkTheme div.status-table {
+ color: var(--light-green-grey);
+}
+
+div.status-table .row {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 30px;
+}
+
+div.status-table .row-header {
+ width: 120px;
+ text-align: right;
+ padding-top: 15px;
+ padding-right: 10px;
+}
+
+div.status-table .col {
+ margin-left: 8px;
+}
+
+div.status-table .col-header {
+ font-size: 12px;
+ 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 {
+ position: absolute;
+ left: 166px;
+ top: 6px;
+ color: white;
+ font-size: 14px;
+ height: 38px;
+ line-height: 38px;
+}
+
+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: 6px;
+ left: 6px;
+ width: 156px;
+ height: 40px;
+}
+
+div.status-table .col-metric svg {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 156px;
+ height: 40px;
+}
+
+div.status-table .col-metric svg.mouse-line {
+ position: absolute;
+ top: 0px;
+ left: 6px;
+ width: 156px;
+ height: 50px;
+}
+
+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;
+}
diff --git a/oncall/client/stylesheets/components/summary-table.css b/oncall/client/stylesheets/components/summary-table.css
deleted file mode 100644
index 8b9aa36..0000000
--- a/oncall/client/stylesheets/components/summary-table.css
+++ /dev/null
@@ -1,297 +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. */
-
-div.summary-table {
- display: flex;
- flex-direction: column;
- font-size: 14px;
- color: var(--dark-green-grey);
- margin-top: 12px;
- margin-bottom: 12px;
- margin-right: var(--summary-table-metric-names-col-width);
- width: 100%;
-}
-
-.darkTheme div.summary-table {
- color: var(--light-green-grey);
-}
-
-div.summary-table .columns-container {
- display: flex;
- flex-grow: 1;
- justify-content: center;
-}
-
-div.summary-table .subtable-title {
- display: flex;
- flex-direction: column;
- justify-content: center;
- transform: rotate(-90deg);
- padding: 0px 6px;
- width: 30px;
- white-space: nowrap;
-}
-
-div.summary-table .cell {
- flex-grow: 1;
- min-height: 24px;
- max-height: 50px;
- display: flex;
- flex-direction: column;
- justify-content: center;
-}
-
-div.summary-table .cell.healthy {
- background: var(--cyan-800);
-}
-
-.darkTheme div.summary-table .cell.healthy {
- background: DarkGreen;
-}
-
-div.summary-table .cell.unhealthy {
- background: #AA0000;
-}
-
-div.summary-table .metric-names-col {
- display: flex;
- flex-direction: column;
- font-weight: var(--font-weight-medium);
- text-align: right;
- width: var(--summary-table-metric-names-col-width);
- font-size: 15px;
-}
-
-div.summary-table .metric-name-cell {
- padding-right: 10px;
- position: relative;
-}
-
-div.summary-table .section-label {
- position: absolute;
- top: -19px;
- left: var(--summary-table-metric-names-col-width);
- width: 100%;
- font-size: 12px;
- font-weight: var(--font-weight-regular) !important;
- color: #999;
- text-align: left;
-}
-
-.darkTheme div.summary-table .section-label {
- color: #666;
-}
-
-div.summary-table .section-label.highlight-section {
- color: var(--dark-green-grey);
-}
-
-.darkTheme div.summary-table .section-label.highlight-section {
- color: #999;
-}
-
-div.summary-table .metric-name-cell.unhealthy {
- background-color: white;
- color: #AA0000;
-}
-
-.darkTheme div.summary-table .metric-name-cell.unhealthy {
- background-color: var(--dark-background);
-}
-
-div.summary-table .highlight {
- background-color: var(--light-grey) !important;
-}
-
-.darkTheme div.summary-table .highlight {
- background-color: var(--dark-highlight) !important;
-}
-
-div.summary-table .minor-divider {
- min-height: 30px;
- max-height: 30px;
- flex-grow: 0;
-}
-
-div.summary-table .major-divider {
- min-height: 30px;
- max-height: 30px;
- flex-grow: 0;
-}
-
-div.summary-table .dummy-divider {
- min-height: 1px;
- max-height: 1px;
- flex-grow: 0;
-}
-
-div.summary-table .zone-names-row {
- display: flex;
- min-height: var(--summary-table-zone-row-height);
- margin-bottom: 20px;
- justify-content: center;
-}
-
-div.summary-table .zone-names-col {
-}
-
-div.summary-table .view-type {
- display: flex;
- justify-content: flex-end;
- text-transform: none !important;
- color: #AAA;
- width: var(--summary-table-metric-names-col-width) !important;
- padding-right: 10px;
- font-size: 12px !important;
- flex-grow: 0 !important;
-}
-
-div.summary-table .view-type div {
- margin-left: 8px;
- cursor: pointer;
-}
-
-div.summary-table .view-type div.selected {
- color: var(--cyan-800) !important;
- cursor: default !important;
-}
-
-.darkTheme div.summary-table .view-type div.selected {
- color: var(--light-green) !important;
-}
-
-div.summary-table .view-type div:hover {
- color: var(--dark-green-grey);
-}
-
-div.summary-table .view-type-dummy {
- height: var(--summary-table-zone-row-height);
- width: var(--summary-table-metric-names-col-width) !important;
-}
-
-div.summary-table .zone-name-cell {
- height: var(--summary-table-zone-row-height);
- line-height: var(--summary-table-zone-row-height);
- text-transform: uppercase;
- font-weight: var(--font-weight-medium);
- min-width: var(--zone-column-min-width);
- max-width: var(--zone-column-max-width);
- text-align: center;
- flex-grow: 1;
- font-size: 16px;
-}
-
-div.summary-table .zone-name-cell.unhealthy {
- background-color: white;
- color: #AA0000;
-}
-
-.darkTheme div.summary-table .zone-name-cell.unhealthy {
- background-color: var(--dark-background);
-}
-
-div.summary-table .zone-col {
- display: flex;
- flex-direction: column;
- min-width: var(--zone-column-min-width);
- max-width: var(--zone-column-max-width);
- flex-grow: 1;
-}
-
-div.summary-table div.zone-col-cell {
- position: relative;
- cursor: pointer;
-}
-
-div.summary-table div.zone-col-cell div.highlight-overlay {
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0px;
- left: 0px;
- background-color: white;
- opacity: 0;
-}
-
-div.summary-table div.zone-col-cell:hover div.highlight-overlay {
- opacity: 0.1;
-}
-
-div.summary-table div.zone-col-cell div.value {
- position: absolute;
- height: 100%;
- top: 0px;
- right: 6px;
- font-size: 18px;
- color: white;
- display: flex;
- flex-direction: column;
- justify-content: center;
- width: 48px;
-}
-
-div.summary-table div.zone-col-cell div.value.history {
- color: #B2DADD !important;
-}
-
-.darkTheme div.summary-table div.zone-col-cell div.value.history {
- color: #AAA !important;
-}
-
-div.summary-table div.zone-col-cell.unhealthy div.value.history {
- color: #EECCCC !important;
-}
-
-div.summary-table div.zone-col-cell div.sparkline {
- position: absolute;
- top: 0px;
- right: 60px;
- left: 10px;
- bottom: 0px;
- display: flex;
-}
-
-div.summary-table div.zone-col-cell div.mouse-line {
- position: absolute;
- top: 0px;
- right: 60px;
- left: 10px;
- bottom: 0px;
- display: flex;
-}
-
-div.summary-table div.zone-col-cell svg.mouse-line {
- shape-rendering: crispedges;
-}
-
-div.summary-table div.zone-col-cell svg.mouse-line polyline {
- vector-effect: non-scaling-stroke;
- fill: none;
- stroke: #339CA5;
- stroke-width: 1;
-}
-
-.darkTheme div.summary-table div.zone-col-cell svg.mouse-line polyline {
- stroke: #338333;
-}
-
-div.summary-table div.zone-col-cell.unhealthy svg.mouse-line polyline {
- stroke: #C44D4D !important;
-}
-
-div.summary-table div.zone-col-cell svg {
- width: 100%;
-}
-
-div.summary-table div.zone-col-cell svg.content {
- margin-top: 5px;
- margin-bottom: 5px;
-}
-
-div.summary-table div.zone-col-cell svg.content polyline {
- vector-effect: non-scaling-stroke;
- fill: none;
- stroke: white;
- stroke-width: 0.8;
-}
diff --git a/oncall/client/stylesheets/index.css b/oncall/client/stylesheets/index.css
index cbef6db..10beca3 100644
--- a/oncall/client/stylesheets/index.css
+++ b/oncall/client/stylesheets/index.css
@@ -6,12 +6,9 @@
@import "./reset.css";
@import "./typography.css";
@import "./common.css";
-@import "./components/data-table.css";
@import "./components/header.css";
-@import "./components/full-graph.css";
-@import "./components/instance-view.css";
@import "./components/main.css";
-@import "./components/metrics-group.css";
+@import "./components/metric-actions-panel.css";
@import "./components/metric-item.css";
+@import "./components/status-table.css";
@import "./components/settings.css";
-@import "./components/summary-table.css";
diff --git a/oncall/client/stylesheets/variables.css b/oncall/client/stylesheets/variables.css
index e0ea8f8..1edc3cd 100644
--- a/oncall/client/stylesheets/variables.css
+++ b/oncall/client/stylesheets/variables.css
@@ -49,7 +49,6 @@
rgba(255, 0, 0, 0.3) 20px
);
--white-transparent: rgba(255, 255, 255, 0.4);
- --blueish-white: #EBECEC;
--dark-background: #222;
--dark-highlight: #333;
@@ -64,24 +63,18 @@
--section-shadow: rgba(0, 0, 0, 0.30) 0px 0px 4px 0px;
/* Heights */
+ --col-metric-height: 50px;
--header-height: 60px;
--zone-title-height: 40px;
--full-graph-height: 240px;
--graph-time-div-height: 20px;
- --metric-item-height: 58px;
+ --metric-item-height: 50px;
--metric-item-title-height: 16px;
--metric-item-value-height: 16px;
- --gce-instance-name-height: 30px;
--sparkline-height: 24px;
- --group-title-height: 20px;
- --subsection-margin-top: 36px;
- --summary-table-zone-row-height: 30px;
/* Width */
+ --col-metric-width: 206px;
--graph-number-div-width: 30px;
- --metric-item-width: 20%;
- --zone-header-width: 160px;
- --summary-table-metric-names-col-width: 150px;
- --zone-column-min-width: 130px;
- --zone-column-max-width: 240px;
+ --metric-item-width: 200px;
}