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