// 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' }] }; } }