service-check-detail-page.component.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. // Third party
  2. import { Component } from '@angular/core';
  3. import { ActivatedRoute } from '@angular/router';
  4. // Common
  5. import { HttpCheckStatus, ServiceCheckData } from '../../../../../common/lib/http-check-data.module';
  6. // App
  7. import { convertToStatusTimelineData } from 'src/app/lib/conversions.lib';
  8. import { ServiceApiService } from 'src/app/services/service-api.service';
  9. @Component({
  10. selector: 'app-service-check-detail-page',
  11. templateUrl: './service-check-detail-page.component.html',
  12. styleUrls: ['./service-check-detail-page.component.scss'],
  13. host: { class: 'd-flex flex-column h-100' }
  14. })
  15. export class ServiceCheckDetailPageComponent {
  16. private rawData: ServiceCheckData[] = [];
  17. private params?: { serverID: number; serviceID: number };
  18. public statusData?: StatusTimelineData[];
  19. public serviceCheck?: HttpCheckConfig;
  20. public log: {
  21. start?: Date;
  22. end?: Date;
  23. visible?: { first: Date; last?: Date };
  24. renderHighlight?: { offsetLeft: number; width: number };
  25. entries?: ServiceCheckData[];
  26. loading?: boolean;
  27. } = {};
  28. constructor(route: ActivatedRoute, private serviceApi: ServiceApiService) {
  29. route.params.subscribe({
  30. next: params => {
  31. this.params = { serverID: Number(params['serverID']), serviceID: Number(params['serviceID']) };
  32. this.load(this.params.serverID, this.params.serviceID);
  33. }
  34. });
  35. }
  36. async load(serverID: number, serviceID: number) {
  37. try {
  38. console.log('Loading Detail Page for Service Check:', { serverID, serviceID });
  39. this.serviceCheck = await this.serviceApi.getServiceCheck(serverID, serviceID);
  40. const end = new Date();
  41. const start = new Date(end.getTime() - 1000 * 60 * 60 * 24);
  42. this.rawData = await this.serviceApi.queryServiceData(serverID, serviceID, start, end);
  43. this.statusData = convertToStatusTimelineData(start, end, this.rawData);
  44. const pageEnd = end;
  45. const pageStart = new Date(end.getTime() - 1000 * 60 * 60 * 4);
  46. this.log.entries = await this.queryAndFillServiceLog(serverID, serviceID, pageStart, pageEnd, this.serviceCheck.interval);
  47. this.log.end = end;
  48. this.log.start = start;
  49. setTimeout(this.logScroll.bind(this));
  50. } catch (err) {
  51. console.error(err);
  52. }
  53. }
  54. logScroll(event?: Event) {
  55. if (!this.log.start || !this.log.end) return;
  56. const scrollContainer = (event ? event.target : document.getElementById('service-check-logs-scroller')) as HTMLDivElement;
  57. this.log.visible = this.getVisibleTimespan(scrollContainer);
  58. const absoluteWidth = this.log.end.getTime() - this.log.start.getTime();
  59. this.log.renderHighlight = {
  60. offsetLeft: ((this.log.visible.first.getTime() - this.log.start.getTime()) / absoluteWidth) * 100,
  61. width: (((this.log.visible.last ?? this.log.visible.first).getTime() - this.log.visible.first.getTime()) / absoluteWidth) * 100
  62. };
  63. if (this.log.visible.first.getTime() === this.log.entries?.[0]?.time.getTime()) {
  64. this.reloadOnScroll();
  65. }
  66. }
  67. private async reloadOnScroll() {
  68. if (this.log.loading) return;
  69. if (!this.log.start || !this.log.end || !this.params || !this.serviceCheck || !this.log.visible) return;
  70. try {
  71. this.log.loading = true;
  72. const end = new Date(this.log.visible.first.getTime() - 1000);
  73. let start = new Date(end.getTime() - 1000 * 60 * 60 * 2);
  74. if (start.getTime() < this.log.start.getTime()) start = this.log.start;
  75. const entries = await this.queryAndFillServiceLog(this.params.serverID, this.params.serviceID, start, end, this.serviceCheck.interval);
  76. this.log.entries = [...entries, ...(this.log.entries ?? [])];
  77. this.log.loading = false;
  78. setTimeout(this.logScroll.bind(this), 1000);
  79. } catch (err) {
  80. console.error(err);
  81. this.log.loading = false;
  82. }
  83. }
  84. private getVisibleTimespan(scrollContainer: HTMLDivElement) {
  85. const trsInView: HTMLTableRowElement[] = [];
  86. scrollContainer.querySelectorAll('tr').forEach(tr => {
  87. if (tr.offsetTop + tr.clientHeight >= scrollContainer.scrollTop && tr.offsetTop <= scrollContainer.clientHeight + scrollContainer.scrollTop) {
  88. trsInView.push(tr);
  89. }
  90. });
  91. const datesInView = trsInView.map(tr => new Date(tr.dataset['time'] ?? ''));
  92. const first = datesInView.reduce((res, cur) => (cur.getTime() < res ? cur.getTime() : res), Number.MAX_SAFE_INTEGER);
  93. const last = datesInView.reduce((res, cur) => (cur.getTime() > res ? cur.getTime() : res), Number.MIN_SAFE_INTEGER);
  94. return { first: new Date(first), last: new Date(last) };
  95. }
  96. private async queryAndFillServiceLog(serverID: number, serviceID: number, start: Date, end: Date, interval: number) {
  97. const expectedNum = Math.floor((end.getTime() - start.getTime()) / (interval * 1000));
  98. if (expectedNum <= 0) return [];
  99. const toleranceMs = 2500;
  100. const entries = await this.serviceApi.queryServiceLog(serverID, serviceID, start, end);
  101. if (entries.length < expectedNum) {
  102. if (entries.length) {
  103. // Insert dummy log entries before first existing entry?
  104. if (entries[0].time.getTime() - start.getTime() > interval * 1000 + toleranceMs) {
  105. const desiredNum = Math.floor((entries[0].time.getTime() - start.getTime()) / (interval * 1000));
  106. entries.unshift(
  107. ...Array(desiredNum)
  108. .fill(0)
  109. .map((v, i) => new Date(entries[0].time.getTime() - i * interval * 1000))
  110. .reverse()
  111. .map(this.dummyEntry)
  112. );
  113. }
  114. // Insert dummy log entries after last existing entry?
  115. if (end.getTime() - entries[entries.length - 1].time.getTime() > interval * 1000 + toleranceMs) {
  116. const desiredNum = Math.floor((end.getTime() - entries[entries.length - 1].time.getTime()) / (interval * 1000));
  117. entries.push(
  118. ...Array(desiredNum)
  119. .fill(0)
  120. .map((v, i) => new Date(entries[entries.length - 1].time.getTime() + i * interval * 1000))
  121. .map(this.dummyEntry)
  122. );
  123. }
  124. // Insert dummy log entries in between existing entries?
  125. entries
  126. .slice()
  127. .reverse()
  128. .forEach((entry, invIdx, arr) => {
  129. if (invIdx === 0) return; // skip last/newest entry
  130. const newerEntry = arr[invIdx - 1];
  131. const i = arr.length - 1 - invIdx;
  132. if (newerEntry.time.getTime() - entry.time.getTime() > interval * 1000 + toleranceMs) {
  133. const desiredNum = Math.floor((newerEntry.time.getTime() - entry.time.getTime()) / (interval * 1000));
  134. entries.splice(
  135. i,
  136. 0,
  137. ...Array(desiredNum)
  138. .fill(0)
  139. .map((v, idx) => new Date(entry.time.getTime() + idx * interval * 1000))
  140. .map(this.dummyEntry)
  141. );
  142. }
  143. });
  144. } else {
  145. // Response all empty -> fill with #expectedNum dummy entries
  146. entries.push(
  147. ...Array(expectedNum)
  148. .fill(0)
  149. .map((v, i) => new Date(start.getTime() + i * interval * 1000))
  150. .map(this.dummyEntry)
  151. );
  152. }
  153. }
  154. return entries;
  155. }
  156. private dummyEntry(time: Date) {
  157. return { time, data: [{ status: HttpCheckStatus.Invalid, message: 'missing log entry' }] };
  158. }
  159. }