|
|
@@ -0,0 +1,188 @@
|
|
|
+// Third party
|
|
|
+import { Component } from '@angular/core';
|
|
|
+import { ActivatedRoute } from '@angular/router';
|
|
|
+
|
|
|
+// Common
|
|
|
+import { HttpCheckStatus, ServiceCheckData } from '../../../../../common/lib/http-check-data.module';
|
|
|
+
|
|
|
+// App
|
|
|
+import { convertToStatusTimelineData } from 'src/app/lib/conversions.lib';
|
|
|
+import { ServiceApiService } from 'src/app/services/service-api.service';
|
|
|
+
|
|
|
+@Component({
|
|
|
+ selector: 'app-service-check-detail-page',
|
|
|
+ templateUrl: './service-check-detail-page.component.html',
|
|
|
+ styleUrls: ['./service-check-detail-page.component.scss'],
|
|
|
+ host: { class: 'd-flex flex-column h-100' }
|
|
|
+})
|
|
|
+export class ServiceCheckDetailPageComponent {
|
|
|
+ private rawData: ServiceCheckData[] = [];
|
|
|
+ private params?: { serverID: number; serviceID: number };
|
|
|
+
|
|
|
+ public statusData?: StatusTimelineData[];
|
|
|
+ public serviceCheck?: HttpCheckConfig;
|
|
|
+
|
|
|
+ public log: {
|
|
|
+ start?: Date;
|
|
|
+ end?: Date;
|
|
|
+ visible?: { first: Date; last?: Date };
|
|
|
+ renderHighlight?: { offsetLeft: number; width: number };
|
|
|
+ entries?: ServiceCheckData[];
|
|
|
+ loading?: boolean;
|
|
|
+ } = {};
|
|
|
+
|
|
|
+ constructor(route: ActivatedRoute, private serviceApi: ServiceApiService) {
|
|
|
+ route.params.subscribe({
|
|
|
+ next: params => {
|
|
|
+ this.params = { serverID: Number(params['serverID']), serviceID: Number(params['serviceID']) };
|
|
|
+ this.load(this.params.serverID, this.params.serviceID);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async load(serverID: number, serviceID: number) {
|
|
|
+ try {
|
|
|
+ console.log('Loading Detail Page for Service Check:', { serverID, serviceID });
|
|
|
+
|
|
|
+ this.serviceCheck = await this.serviceApi.getServiceCheck(serverID, serviceID);
|
|
|
+
|
|
|
+ const end = new Date();
|
|
|
+ const start = new Date(end.getTime() - 1000 * 60 * 60 * 24);
|
|
|
+ this.rawData = await this.serviceApi.queryServiceData(serverID, serviceID, start, end);
|
|
|
+ this.statusData = convertToStatusTimelineData(start, end, this.rawData);
|
|
|
+
|
|
|
+ const pageEnd = end;
|
|
|
+ const pageStart = new Date(end.getTime() - 1000 * 60 * 60 * 4);
|
|
|
+ this.log.entries = await this.queryAndFillServiceLog(serverID, serviceID, pageStart, pageEnd, this.serviceCheck.interval);
|
|
|
+
|
|
|
+ this.log.end = end;
|
|
|
+ this.log.start = start;
|
|
|
+
|
|
|
+ setTimeout(this.logScroll.bind(this));
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ logScroll(event?: Event) {
|
|
|
+ if (!this.log.start || !this.log.end) return;
|
|
|
+
|
|
|
+ const scrollContainer = (event ? event.target : document.getElementById('service-check-logs-scroller')) as HTMLDivElement;
|
|
|
+ this.log.visible = this.getVisibleTimespan(scrollContainer);
|
|
|
+
|
|
|
+ const absoluteWidth = this.log.end.getTime() - this.log.start.getTime();
|
|
|
+ this.log.renderHighlight = {
|
|
|
+ offsetLeft: ((this.log.visible.first.getTime() - this.log.start.getTime()) / absoluteWidth) * 100,
|
|
|
+ width: (((this.log.visible.last ?? this.log.visible.first).getTime() - this.log.visible.first.getTime()) / absoluteWidth) * 100
|
|
|
+ };
|
|
|
+
|
|
|
+ if (this.log.visible.first.getTime() === this.log.entries?.[0]?.time.getTime()) {
|
|
|
+ this.reloadOnScroll();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async reloadOnScroll() {
|
|
|
+ if (this.log.loading) return;
|
|
|
+ if (!this.log.start || !this.log.end || !this.params || !this.serviceCheck || !this.log.visible) return;
|
|
|
+ try {
|
|
|
+ this.log.loading = true;
|
|
|
+
|
|
|
+ const end = new Date(this.log.visible.first.getTime() - 1000);
|
|
|
+ let start = new Date(end.getTime() - 1000 * 60 * 60 * 2);
|
|
|
+ if (start.getTime() < this.log.start.getTime()) start = this.log.start;
|
|
|
+
|
|
|
+ const entries = await this.queryAndFillServiceLog(this.params.serverID, this.params.serviceID, start, end, this.serviceCheck.interval);
|
|
|
+ this.log.entries = [...entries, ...(this.log.entries ?? [])];
|
|
|
+ this.log.loading = false;
|
|
|
+
|
|
|
+ setTimeout(this.logScroll.bind(this), 1000);
|
|
|
+ } catch (err) {
|
|
|
+ console.error(err);
|
|
|
+ this.log.loading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private getVisibleTimespan(scrollContainer: HTMLDivElement) {
|
|
|
+ const trsInView: HTMLTableRowElement[] = [];
|
|
|
+ scrollContainer.querySelectorAll('tr').forEach(tr => {
|
|
|
+ if (tr.offsetTop + tr.clientHeight >= scrollContainer.scrollTop && tr.offsetTop <= scrollContainer.clientHeight + scrollContainer.scrollTop) {
|
|
|
+ trsInView.push(tr);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const datesInView = trsInView.map(tr => new Date(tr.dataset['time'] ?? ''));
|
|
|
+ const first = datesInView.reduce((res, cur) => (cur.getTime() < res ? cur.getTime() : res), Number.MAX_SAFE_INTEGER);
|
|
|
+ const last = datesInView.reduce((res, cur) => (cur.getTime() > res ? cur.getTime() : res), Number.MIN_SAFE_INTEGER);
|
|
|
+
|
|
|
+ return { first: new Date(first), last: new Date(last) };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async queryAndFillServiceLog(serverID: number, serviceID: number, start: Date, end: Date, interval: number) {
|
|
|
+ const expectedNum = Math.floor((end.getTime() - start.getTime()) / (interval * 1000));
|
|
|
+ if (expectedNum <= 0) return [];
|
|
|
+
|
|
|
+ const toleranceMs = 2500;
|
|
|
+ const entries = await this.serviceApi.queryServiceLog(serverID, serviceID, start, end);
|
|
|
+ if (entries.length < expectedNum) {
|
|
|
+ if (entries.length) {
|
|
|
+ // Insert dummy log entries before first existing entry?
|
|
|
+ if (entries[0].time.getTime() - start.getTime() > interval * 1000 + toleranceMs) {
|
|
|
+ const desiredNum = Math.floor((entries[0].time.getTime() - start.getTime()) / (interval * 1000));
|
|
|
+ entries.unshift(
|
|
|
+ ...Array(desiredNum)
|
|
|
+ .fill(0)
|
|
|
+ .map((v, i) => new Date(entries[0].time.getTime() - i * interval * 1000))
|
|
|
+ .reverse()
|
|
|
+ .map(this.dummyEntry)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Insert dummy log entries after last existing entry?
|
|
|
+ if (end.getTime() - entries[entries.length - 1].time.getTime() > interval * 1000 + toleranceMs) {
|
|
|
+ const desiredNum = Math.floor((end.getTime() - entries[entries.length - 1].time.getTime()) / (interval * 1000));
|
|
|
+ entries.push(
|
|
|
+ ...Array(desiredNum)
|
|
|
+ .fill(0)
|
|
|
+ .map((v, i) => new Date(entries[entries.length - 1].time.getTime() + i * interval * 1000))
|
|
|
+ .map(this.dummyEntry)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Insert dummy log entries in between existing entries?
|
|
|
+ entries
|
|
|
+ .slice()
|
|
|
+ .reverse()
|
|
|
+ .forEach((entry, invIdx, arr) => {
|
|
|
+ if (invIdx === 0) return; // skip last/newest entry
|
|
|
+
|
|
|
+ const newerEntry = arr[invIdx - 1];
|
|
|
+ const i = arr.length - 1 - invIdx;
|
|
|
+ if (newerEntry.time.getTime() - entry.time.getTime() > interval * 1000 + toleranceMs) {
|
|
|
+ const desiredNum = Math.floor((newerEntry.time.getTime() - entry.time.getTime()) / (interval * 1000));
|
|
|
+ entries.splice(
|
|
|
+ i,
|
|
|
+ 0,
|
|
|
+ ...Array(desiredNum)
|
|
|
+ .fill(0)
|
|
|
+ .map((v, idx) => new Date(entry.time.getTime() + idx * interval * 1000))
|
|
|
+ .map(this.dummyEntry)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Response all empty -> fill with #expectedNum dummy entries
|
|
|
+ entries.push(
|
|
|
+ ...Array(expectedNum)
|
|
|
+ .fill(0)
|
|
|
+ .map((v, i) => new Date(start.getTime() + i * interval * 1000))
|
|
|
+ .map(this.dummyEntry)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return entries;
|
|
|
+ }
|
|
|
+
|
|
|
+ private dummyEntry(time: Date) {
|
|
|
+ return { time, data: [{ status: HttpCheckStatus.Invalid, message: 'missing log entry' }] };
|
|
|
+ }
|
|
|
+}
|