From f830489b3770ed5d327794786be9094a22a8504a Mon Sep 17 00:00:00 2001
From: Dmytro Bogatov <dmytro@dbogatov.org>
Date: Tue, 8 Aug 2017 14:06:38 +0300
Subject: [PATCH] Rerender data tables on plot zoom.

---
 client/ts/metric.ts                          |  1 +
 client/ts/modules/constants.ts               |  6 ++
 client/ts/modules/metric-page/abstract.ts    | 77 +++++++++++++++++---
 client/ts/modules/metric-page/cpu-load.ts    | 21 +++++-
 client/ts/modules/metric-page/health.ts      | 35 ++++++---
 client/ts/modules/metric-page/ping.ts        | 21 +++++-
 client/ts/modules/metric-page/user-action.ts | 21 +++++-
 src/web/Controllers/View/HomeController.cs   | 42 ++++++++++-
 src/web/Views/Home/Metric.cshtml             |  4 +
 9 files changed, 197 insertions(+), 31 deletions(-)

diff --git a/client/ts/metric.ts b/client/ts/metric.ts
index 11c8fa5..593929b 100644
--- a/client/ts/metric.ts
+++ b/client/ts/metric.ts
@@ -13,6 +13,7 @@ import "bootstrap"
 
 declare var source: string;
 declare var type: number;
+
 declare var min: number;
 declare var max: number;
 
diff --git a/client/ts/modules/constants.ts b/client/ts/modules/constants.ts
index 3d5e165..67224ff 100644
--- a/client/ts/modules/constants.ts
+++ b/client/ts/modules/constants.ts
@@ -55,4 +55,10 @@ export class Constants {
 	 * of 5 minutes and sums of user actions will be displayed per interval.
 	 */
 	static USER_ACTIONS_AGGREGATION_INTERVAL : number = 30;
+
+	/**
+	 * The interval in milliseconds that defines a default time frame of data
+	 * on metric pages.
+	 */
+	static METRIC_PAGE_DATA_PREVIEW : number = 2 * 60 * 60 * 1000;
 }
diff --git a/client/ts/modules/metric-page/abstract.ts b/client/ts/modules/metric-page/abstract.ts
index 0d60904..6ae2115 100644
--- a/client/ts/modules/metric-page/abstract.ts
+++ b/client/ts/modules/metric-page/abstract.ts
@@ -10,8 +10,12 @@ import "../../vendor/jquery.flot.time.js";
 import "../../vendor/jquery.flot.selection.js";
 import "../../vendor/jquery.flot.threshold.js";
 import "../../vendor/jquery.flot.tooltip.js";
+import { Constants } from "../constants";
 
 
+declare var start: number;
+declare var end: number;
+
 /**
  * Represents set of procedures for rendering metric page.
  * 
@@ -23,6 +27,7 @@ export abstract class MetricPage<T extends Metric<DataPoint>> {
 	protected metric: T;
 
 	protected dataTablesRendered: boolean = false;
+	protected dataTable : DataTables.DataTable;
 
 	/**
 	 * Minimal theoretical value for data series.
@@ -50,10 +55,21 @@ export abstract class MetricPage<T extends Metric<DataPoint>> {
 	protected minData: number;
 	protected maxData: number;
 
+	protected start: Date = null;
+	protected end: Date = null;
+
 	constructor(min: number, max: number) {
 		this.max = max;
 		this.min = min;
 
+		if (start > 0) {
+			this.start = new Date(start);
+		}
+
+		if (end > 0) {
+			this.end = new Date(end);
+		}
+
 		window.setTimeout(async () => {
 
 			await this.metric.loadData(60 * 60 * 24 * 3); // 3 days
@@ -84,7 +100,7 @@ export abstract class MetricPage<T extends Metric<DataPoint>> {
 	 * @memberOf MetricPage
 	 */
 	private renderPlot(): void {
-		
+
 		var plot: any = $.plot(
 			$("#metric-detailed-plot"),
 			this.formattedData,
@@ -113,17 +129,46 @@ export abstract class MetricPage<T extends Metric<DataPoint>> {
 
 			// don't fire event on the overview to prevent eternal loop
 			overview.setSelection(ranges, true);
+
+			this.renderTable(true, new Date(ranges.xaxis.from), new Date(ranges.xaxis.to));
 		}));
 
 		$("#metric-overview-plot").bind("plotselected", <any>((event, ranges) => {
 			plot.setSelection(ranges);
 		}));
 
-		// if latest data point is more than 2 hours ago
-		// select recent 2 hours in plot
-		if (new Date().getTime() - this.minData > 2 * 60 * 60 * 1000) {
-			let from = new Date().getTime() - 2 * 60 * 60 * 1000;
-			plot.setSelection({ xaxis: { from: from, to: this.maxData }, yaxis: { from: 0, to: 0 } });
+		if (this.start != null) {
+
+			let from = Math.min(
+				Math.max(
+					this.start.getTime(),
+					this.minData
+				),
+				this.maxData)
+				;
+
+			let to =
+				this.end == null ?
+					Math.min(
+						this.maxData,
+						this.start.getTime() + Constants.METRIC_PAGE_DATA_PREVIEW
+					) :
+					Math.max(
+						Math.min(
+							this.end.getTime(),
+							this.maxData
+						),
+						this.minData)
+				;
+
+			plot.setSelection({ xaxis: { from: from, to: to }, yaxis: { from: 0, to: 0 } });
+		} else {
+			// if latest data point is more than 2 hours ago
+			// select recent 2 hours in plot
+			if (new Date().getTime() - this.minData > Constants.METRIC_PAGE_DATA_PREVIEW) {
+				let from = new Date().getTime() - Constants.METRIC_PAGE_DATA_PREVIEW;
+				plot.setSelection({ xaxis: { from: from, to: this.maxData }, yaxis: { from: 0, to: 0 } });
+			}
 		}
 	}
 
@@ -135,7 +180,19 @@ export abstract class MetricPage<T extends Metric<DataPoint>> {
 	 * 
 	 * @memberOf MetricPage
 	 */
-	protected abstract renderTable(): void;
+
+	/**
+	 * Renders data tables in the UI.
+	 * Does not load the data.
+	 * 
+	 * @protected
+	 * @abstract
+	 * @param {boolean} redraw if data table has to be force-redrawn; implies non-null values of start and end;
+	 * @param {Date} start start date for filter
+	 * @param {Date} end end date for filter
+	 * @memberof MetricPage
+	 */
+	protected abstract renderTable(redraw: boolean, start: Date, end: Date): void;
 
 	/**
 	 * Renders numeric values in the UI.
@@ -160,10 +217,10 @@ export abstract class MetricPage<T extends Metric<DataPoint>> {
 	 */
 	public render(): void {
 
-		this.configurePlot()		
+		this.configurePlot()
 		this.renderPlot();
-		
+
 		this.renderValues();
-		this.renderTable();
+		this.renderTable(false, null, null);
 	}
 }
diff --git a/client/ts/modules/metric-page/cpu-load.ts b/client/ts/modules/metric-page/cpu-load.ts
index 3b21f22..5dea416 100644
--- a/client/ts/modules/metric-page/cpu-load.ts
+++ b/client/ts/modules/metric-page/cpu-load.ts
@@ -106,9 +106,13 @@ export class CpuLoadMetricPage extends MetricPage<Metric<CpuLoadDataPoint>> {
 		};
 	};
 
-	protected renderTable(): void {
+	protected renderTable(redraw: boolean, start: Date, end: Date): void {
 
-		if (!this.dataTablesRendered) {
+		if (!this.dataTablesRendered || redraw) {
+
+			if (this.dataTablesRendered) {
+				this.dataTable.destroy();
+			}
 
 			let header = `
 				<tr>
@@ -124,6 +128,17 @@ export class CpuLoadMetricPage extends MetricPage<Metric<CpuLoadDataPoint>> {
 				this.metric
 					.data
 					.map(dp => <CpuLoadDataPoint>dp)
+					.filter((value, index, array) => {
+						if (start != null && value.timestamp < start) {
+							return false;
+						}
+
+						if (end != null && value.timestamp > end) {
+							return false;
+						}
+
+						return true;
+					})
 					.map(
 					dp => `
 						<tr>
@@ -135,7 +150,7 @@ export class CpuLoadMetricPage extends MetricPage<Metric<CpuLoadDataPoint>> {
 					.join()
 			);
 
-			$('#metric-data').DataTable({
+			this.dataTable = $('#metric-data').DataTable({
 				"order": [[0, "desc"]],
 				lengthChange: false,
 				searching: false,
diff --git a/client/ts/modules/metric-page/health.ts b/client/ts/modules/metric-page/health.ts
index 0756f9d..cb2924d 100644
--- a/client/ts/modules/metric-page/health.ts
+++ b/client/ts/modules/metric-page/health.ts
@@ -111,9 +111,13 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> {
 		};
 	};
 
-	protected renderTable(): void {
+	protected renderTable(redraw: boolean, start: Date, end: Date): void {
 
-		if (!this.dataTablesRendered) {
+		if (!this.dataTablesRendered || redraw) {
+
+			if (this.dataTablesRendered) {
+				this.dataTable.destroy();
+			}
 
 			let header = `
 				<tr>
@@ -130,6 +134,17 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> {
 				this.metric
 					.data
 					.map(dp => <HealthDataPoint>dp)
+					.filter((value, index, array) => {
+						if (start != null && value.timestamp < start) {
+							return false;
+						}
+
+						if (end != null && value.timestamp > end) {
+							return false;
+						}
+
+						return true;
+					})
 					.map(
 					dp => `
 						<tr>
@@ -160,7 +175,7 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> {
 					.join()
 			);
 
-			$('#metric-data').DataTable({
+			this.dataTable = $('#metric-data').DataTable({
 				"order": [[0, "desc"]],
 				lengthChange: false,
 				searching: false,
@@ -173,8 +188,8 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> {
 			'showDetails',
 			(e: CustomEvent) => {
 
-				let data : any[] = e.detail.data;
-				let timestamp : Date = e.detail.timestamp;
+				let data: any[] = e.detail.data;
+				let timestamp: Date = e.detail.timestamp;
 
 				let code = `
 					<div 
@@ -211,17 +226,17 @@ export class HealthMetricPage extends MetricPage<Metric<HealthDataPoint>> {
 												</thead>
 												<tbody>
 													${
-														data
-															.sortByProperty(el => el.Source)
-															.map(el => `
+					data
+						.sortByProperty(el => el.Source)
+						.map(el => `
 																<tr>
 																	<th>${el.Type}</th>
 																	<th>${el.Source}</th>
 																	<th>${el.Label}</th>
 																</tr>
 															`)
-															.join("")
-													}
+						.join("")
+					}
 												</tbody>
 											</table>
 										</div>										
diff --git a/client/ts/modules/metric-page/ping.ts b/client/ts/modules/metric-page/ping.ts
index f81fae8..90e41a1 100644
--- a/client/ts/modules/metric-page/ping.ts
+++ b/client/ts/modules/metric-page/ping.ts
@@ -149,9 +149,13 @@ export class PingMetricPage extends MetricPage<Metric<PingDataPoint>> {
 		};
 	};
 
-	protected renderTable(): void {
+	protected renderTable(redraw: boolean, start: Date, end: Date): void {
 
-		if (!this.dataTablesRendered) {
+		if (!this.dataTablesRendered || redraw) {
+
+			if (this.dataTablesRendered) {
+				this.dataTable.destroy();
+			}
 
 			let header = `
 				<tr>
@@ -168,6 +172,17 @@ export class PingMetricPage extends MetricPage<Metric<PingDataPoint>> {
 				this.metric
 					.data
 					.map(dp => <PingDataPoint>dp)
+					.filter((value, index, array) => {
+						if (start != null && value.timestamp < start) {
+							return false;
+						}
+
+						if (end != null && value.timestamp > end) {
+							return false;
+						}
+
+						return true;
+					})
 					.map(
 					dp => `
 						<tr>
@@ -180,7 +195,7 @@ export class PingMetricPage extends MetricPage<Metric<PingDataPoint>> {
 					.join()
 			);
 
-			$('#metric-data').DataTable({
+			this.dataTable = $('#metric-data').DataTable({
 				"order": [[0, "desc"]],
 				lengthChange: false,
 				searching: false,
diff --git a/client/ts/modules/metric-page/user-action.ts b/client/ts/modules/metric-page/user-action.ts
index 2ad7368..6044c8c 100644
--- a/client/ts/modules/metric-page/user-action.ts
+++ b/client/ts/modules/metric-page/user-action.ts
@@ -154,9 +154,13 @@ export class UserActionMetricPage extends MetricPage<Metric<UserActionDataPoint>
 		};
 	};
 
-	protected renderTable(): void {
+	protected renderTable(redraw: boolean, start: Date, end: Date): void {
 
-		if (!this.dataTablesRendered) {
+		if (!this.dataTablesRendered || redraw) {
+
+			if (this.dataTablesRendered) {
+				this.dataTable.destroy();
+			}
 
 			let header = `
 				<tr>
@@ -173,6 +177,17 @@ export class UserActionMetricPage extends MetricPage<Metric<UserActionDataPoint>
 				this.metric
 					.data
 					.map(dp => <UserActionDataPoint>dp)
+					.filter((value, index, array) => {
+						if (start != null && value.timestamp < start) {
+							return false;
+						}
+
+						if (end != null && value.timestamp > end) {
+							return false;
+						}
+
+						return true;
+					})
 					.map(
 					dp => `
 						<tr>
@@ -185,7 +200,7 @@ export class UserActionMetricPage extends MetricPage<Metric<UserActionDataPoint>
 					.join()
 			);
 
-			$('#metric-data').DataTable({
+			this.dataTable = $('#metric-data').DataTable({
 				"order": [[0, "desc"]],
 				lengthChange: false,
 				searching: false,
diff --git a/src/web/Controllers/View/HomeController.cs b/src/web/Controllers/View/HomeController.cs
index 63404c8..14371aa 100755
--- a/src/web/Controllers/View/HomeController.cs
+++ b/src/web/Controllers/View/HomeController.cs
@@ -65,8 +65,8 @@ namespace StatusMonitor.Web.Controllers.View
 			return View(model);
 		}
 
-		[Route("Home/Metric/{type}/{source}")]
-		public async Task<IActionResult> Metric(string type, string source)
+		[Route("Home/Metric/{type}/{source}/{start?}/{end?}")]
+		public async Task<IActionResult> Metric(string type, string source, string start = null, string end = null)
 		{
 			Metrics metricType;
 
@@ -79,6 +79,41 @@ namespace StatusMonitor.Web.Controllers.View
 				return BadRequest("Bad type. Needs to be one of Metrics type.");
 			}
 
+			if (start != null)
+			{
+				try
+				{
+					DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(start));
+				}
+				catch (System.Exception)
+				{
+					return BadRequest("Bad start date. Needs to be the number of milliseconds since Epoch.");
+				}
+			}
+
+			if (end != null)
+			{
+				try
+				{
+					DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(end));
+				}
+				catch (System.Exception)
+				{
+					return BadRequest("Bad end date. Needs to be the number of milliseconds since Epoch.");
+				}
+			}
+
+			if (start != null && end != null)
+			{
+				if (
+					DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(start)) >=
+					DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).AddMilliseconds(Convert.ToInt64(end))
+				)
+				{
+					return BadRequest("Bad dates. End date needs to be greater than the start date.");
+				}
+			}
+
 			var model = await _metricService.GetMetricsAsync(metricType, source);
 
 			if (model.Count() == 0)
@@ -107,6 +142,9 @@ namespace StatusMonitor.Web.Controllers.View
 				ViewBag.Uptime = await _uptime.ComputeUptimeAsync(source);
 			}
 
+			ViewBag.Start = start ?? 0.ToString();
+			ViewBag.End = end ?? 0.ToString();
+
 			return View(model.First());
 		}
 
diff --git a/src/web/Views/Home/Metric.cshtml b/src/web/Views/Home/Metric.cshtml
index 296f686..754e171 100644
--- a/src/web/Views/Home/Metric.cshtml
+++ b/src/web/Views/Home/Metric.cshtml
@@ -186,8 +186,12 @@
     <script>
         var source = "@Model.Source";
         var type = @Model.Type;
+		
 		var min = @ViewData["Min"];
 		var max = @ViewData["Max"];
+
+		var start = @ViewData["Start"];
+		var end = @ViewData["End"];
     </script>
 
     <environment names="Development">
-- 
GitLab