• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
18import {
19  Component,
20  ElementRef,
21  EventEmitter,
22  HostListener,
23  Inject,
24  Input,
25  Output,
26  ViewChild,
27} from '@angular/core';
28import {MatSelectChange} from '@angular/material/select';
29
30import {DOMUtils} from 'common/dom_utils';
31import {Timestamp, TimestampFormatType} from 'common/time/time';
32import {TimeUtils} from 'common/time/time_utils';
33import {TraceType} from 'trace/trace_type';
34import {TextFilter} from 'viewers/common/text_filter';
35import {LogEntry, LogField, LogHeader} from 'viewers/common/ui_data_log';
36import {
37  LogFilterChangeDetail,
38  LogTextFilterChangeDetail,
39  TimestampClickDetail,
40  ViewerEvents,
41} from 'viewers/common/viewer_events';
42import {
43  inlineButtonStyle,
44  timeButtonStyle,
45} from 'viewers/components/styles/clickable_property.styles';
46import {currentElementStyle} from 'viewers/components/styles/current_element.styles';
47import {logComponentStyles} from 'viewers/components/styles/log_component.styles';
48import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles';
49import {
50  viewerCardInnerStyle,
51  viewerCardStyle,
52} from 'viewers/components/styles/viewer_card.styles';
53
54@Component({
55  selector: 'log-view',
56  template: `
57    <div class="view-header" *ngIf="title">
58      <div class="title-section">
59        <collapsible-section-title
60            class="log-title"
61            [title]="title"
62            (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
63
64        <div class="filters" *ngIf="showFiltersInTitle && getHeadersWithFilters().length > 0">
65          <div class="filter" *ngFor="let header of getHeadersWithFilters()"
66               [class]="header.spec.cssClass">
67            <select-with-filter
68                *ngIf="(header.filter.options?.length ?? 0) > 0"
69                [label]="header.spec.name"
70                [options]="header.filter.options"
71                [outerFilterWidth]="header.filter.outerFilterWidthCss"
72                [innerFilterWidth]="header.filter.innerFilterWidthCss"
73                formFieldClass="no-border-top-field"
74                (selectChange)="onFilterChange($event, header)">
75            </select-with-filter>
76          </div>
77        </div>
78      </div>
79    </div>
80
81    <div class="entries" [class.padded]="padEntries">
82      <div class="headers table-header" *ngIf="headers.length > 0">
83        <div *ngIf="showTraceEntryTimes" class="time">
84          <button
85              color="primary"
86              mat-button
87              class="time-button go-to-current-time"
88              *ngIf="showCurrentTimeButton"
89              (click)="onGoToCurrentTimeClick()">
90            Go to Current Time
91          </button>
92        </div>
93
94        <ng-container *ngFor="let header of headers">
95          <div
96            *ngIf="!isHeaderWithFilter(header)"
97            class="mat-body-2 header"
98            [class]="header.spec.cssClass">
99          {{header.spec.name}}</div>
100
101          <div
102            *ngIf="isHeaderWithFilter(header) && !showFiltersInTitle"
103            class="filter mat-body-2"
104            [class]="header.spec.cssClass">
105            <select-with-filter
106                *ngIf="(header.filter.options?.length ?? 0) > 0"
107                [label]="header.spec.name"
108                [options]="header.filter.options"
109                [outerFilterWidth]="header.filter.outerFilterWidthCss"
110                [innerFilterWidth]="header.filter.innerFilterWidthCss"
111                appearance="none"
112                formFieldClass="no-padding-field"
113                (selectChange)="onFilterChange($event, header)">
114            </select-with-filter>
115
116            <search-box
117              *ngIf="header.filter.textFilter"
118              [textFilter]="header.filter.textFilter"
119              [label]="header.spec.name"
120              [filterName]="header.spec.name"
121              appearance="none"
122              [formFieldClass]="
123                'wide-field no-padding-field center-field '
124                 + header.spec.cssClass
125                 + (header.filter.textFilter.filterString?.length === 0 ? ' mat-body-2' : '')
126              "
127              height="fit-content"
128              (filterChange)="onSearchBoxChange($event, header)"></search-box>
129          </div>
130        </ng-container>
131      </div>
132
133      <div class="placeholder-text mat-body-1" *ngIf="!isFetchingData && entries.length === 0"> No entries found. </div>
134
135      <div class="fetching-data mat-body-1" *ngIf="isFetchingData">
136        <span class="message-with-spinner">
137          <span>Fetching all data</span>
138          <mat-spinner [diameter]="20"></mat-spinner>
139        </span>
140      </div>
141
142      <cdk-virtual-scroll-viewport
143          *ngIf="isTransactions()"
144          transactionsVirtualScroll
145          class="scroll"
146          [scrollItems]="entries">
147        <ng-container
148            *cdkVirtualFor="let entry of entries; let i = index"
149            [ngTemplateOutlet]="content"
150            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
151      </cdk-virtual-scroll-viewport>
152
153      <cdk-virtual-scroll-viewport
154          *ngIf="isProtolog()"
155          protologVirtualScroll
156          class="scroll"
157          [scrollItems]="entries">
158        <ng-container
159            *cdkVirtualFor="let entry of entries; let i = index"
160            [ngTemplateOutlet]="content"
161            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
162      </cdk-virtual-scroll-viewport>
163
164      <cdk-virtual-scroll-viewport
165          *ngIf="isTransitions()"
166          transitionsVirtualScroll
167          class="scroll"
168          [scrollItems]="entries">
169        <ng-container
170            *cdkVirtualFor="let entry of entries; let i = index"
171            [ngTemplateOutlet]="content"
172            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
173      </cdk-virtual-scroll-viewport>
174
175      <cdk-virtual-scroll-viewport
176          *ngIf="isFixedSizeScrollViewport()"
177          [itemSize]="36"
178          [minBufferPx]="1000"
179          [maxBufferPx]="2000"
180          class="scroll">
181        <ng-container
182            *cdkVirtualFor="let entry of entries; let i = index"
183            [ngTemplateOutlet]="content"
184            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
185      </cdk-virtual-scroll-viewport>
186
187      <ng-template #content let-entry="entry" let-i="i">
188        <div
189            class="entry"
190            [attr.item-id]="i"
191            [class.current]="isCurrentEntry(i)"
192            [class.selected]="isSelectedEntry(i)"
193            (click)="onEntryClicked(i)">
194          <div *ngIf="showTraceEntryTimes" class="time">
195            <button
196                mat-button
197                class="time-button"
198                color="primary"
199                (click)="onTraceEntryTimestampClick($event, entry)"
200                [disabled]="!entry.traceEntry.hasValidTimestamp()">
201              {{ formatTimestamp(entry.traceEntry.getTimestamp()) }}
202            </button>
203          </div>
204
205          <div [class]="field.spec.cssClass" *ngFor="let field of entry.fields; index as i">
206            <span class="mat-body-1" *ngIf="!showFieldButton(entry, field)">{{ field.value }}</span>
207            <button
208                *ngIf="showFieldButton(entry, field)"
209                mat-button
210                class="time-button"
211                color="primary"
212                (click)="onFieldButtonClick($event, entry, field)">
213              {{ formatFieldButton(field) }}
214            </button>
215            <mat-icon
216                *ngIf="field.icon"
217                aria-hidden="false"
218                [style]="{color: field.iconColor}"> {{field.icon}} </mat-icon>
219          </div>
220        </div>
221      </ng-template>
222    </div>
223  `,
224  styles: [
225    `
226      .view-header {
227        display: flex;
228        flex-direction: column;
229        flex: 0 0 auto
230      }
231      .message-with-spinner {
232        display: flex;
233        flex-direction: row;
234        align-items: center;
235        justify-content: center;
236      }
237    `,
238    selectedElementStyle,
239    currentElementStyle,
240    timeButtonStyle,
241    inlineButtonStyle,
242    viewerCardStyle,
243    viewerCardInnerStyle,
244    logComponentStyles,
245  ],
246})
247export class LogComponent {
248  emptyFilterValue = '';
249  private lastClickedTimestamp: Timestamp | undefined;
250
251  @Input() title: string | undefined;
252  @Input() selectedIndex: number | undefined;
253  @Input() scrollToIndex: number | undefined;
254  @Input() currentIndex: number | undefined;
255  @Input() headers: LogHeader[] = [];
256  @Input() entries: LogEntry[] = [];
257  @Input() showCurrentTimeButton = true;
258  @Input() traceType: TraceType | undefined;
259  @Input() showTraceEntryTimes = true;
260  @Input() showFiltersInTitle = false;
261  @Input() padEntries = true;
262  @Input() isFetchingData = false;
263
264  @Output() collapseButtonClicked = new EventEmitter();
265
266  @ViewChild(CdkVirtualScrollViewport)
267  scrollComponent?: CdkVirtualScrollViewport;
268
269  constructor(
270    @Inject(ElementRef) private elementRef: ElementRef<HTMLElement>,
271  ) {}
272
273  getHeadersWithFilters() {
274    return this.headers.filter((header) => this.isHeaderWithFilter(header));
275  }
276
277  isHeaderWithFilter(header: LogHeader): boolean {
278    return header.filter !== undefined;
279  }
280
281  showFieldButton(entry: LogEntry, field: LogField): boolean {
282    const propagateEntryTimestamp =
283      !!field.propagateEntryTimestamp && entry.traceEntry.hasValidTimestamp();
284    return field.value instanceof Timestamp || propagateEntryTimestamp;
285  }
286
287  formatFieldButton(field: LogField): string | number {
288    return field.value instanceof Timestamp
289      ? this.formatTimestamp(field.value)
290      : field.value;
291  }
292
293  areMultipleDatesPresent(): boolean {
294    return (
295      this.entries.at(0)?.traceEntry.getFullTrace().spansMultipleDates() ??
296      false
297    );
298  }
299
300  formatTimestamp(timestamp: Timestamp) {
301    if (!this.areMultipleDatesPresent()) {
302      return timestamp.format(TimestampFormatType.DROP_DATE);
303    }
304    return timestamp.format();
305  }
306
307  ngOnChanges() {
308    if (
309      this.scrollToIndex !== undefined &&
310      this.lastClickedTimestamp !==
311        this.entries.at(this.scrollToIndex)?.traceEntry.getTimestamp()
312    ) {
313      this.scrollComponent?.scrollToIndex(Math.max(0, this.scrollToIndex - 1));
314    }
315  }
316
317  async ngAfterContentInit() {
318    await TimeUtils.sleepMs(10);
319    this.updateTableMarginEnd();
320  }
321
322  @HostListener('window:resize', ['$event'])
323  onResize(event: Event) {
324    this.updateTableMarginEnd();
325  }
326
327  onFilterChange(event: MatSelectChange, header: LogHeader) {
328    this.emitEvent(
329      ViewerEvents.LogFilterChange,
330      new LogFilterChangeDetail(header, event.value),
331    );
332  }
333
334  onSearchBoxChange(detail: TextFilter, header: LogHeader) {
335    this.emitEvent(
336      ViewerEvents.LogTextFilterChange,
337      new LogTextFilterChangeDetail(header, detail),
338    );
339  }
340
341  onEntryClicked(index: number) {
342    this.emitEvent(ViewerEvents.LogEntryClick, index);
343  }
344
345  onGoToCurrentTimeClick() {
346    if (this.currentIndex !== undefined && this.scrollComponent) {
347      this.scrollComponent.scrollToIndex(this.currentIndex);
348    }
349  }
350
351  onTraceEntryTimestampClick(event: MouseEvent, entry: LogEntry) {
352    event.stopPropagation();
353    this.lastClickedTimestamp = entry.traceEntry.getTimestamp();
354    this.emitEvent(
355      ViewerEvents.TimestampClick,
356      new TimestampClickDetail(entry.traceEntry),
357    );
358  }
359
360  onFieldButtonClick(event: MouseEvent, entry: LogEntry, field: LogField) {
361    event.stopPropagation();
362    if (field.propagateEntryTimestamp) {
363      this.onTraceEntryTimestampClick(event, entry);
364    } else if (field.value instanceof Timestamp) {
365      this.onRawTimestampClick(field.value as Timestamp);
366    }
367  }
368
369  @HostListener('document:keydown', ['$event'])
370  async handleKeyboardEvent(event: KeyboardEvent) {
371    const logComponentVisible = DOMUtils.isElementVisible(
372      this.elementRef.nativeElement,
373    );
374    if (event.key === 'ArrowDown' && logComponentVisible) {
375      event.stopPropagation();
376      event.preventDefault();
377      this.emitEvent(ViewerEvents.ArrowDownPress);
378    }
379    if (event.key === 'ArrowUp' && logComponentVisible) {
380      event.stopPropagation();
381      event.preventDefault();
382      this.emitEvent(ViewerEvents.ArrowUpPress);
383    }
384  }
385
386  isCurrentEntry(index: number): boolean {
387    return index === this.currentIndex;
388  }
389
390  isSelectedEntry(index: number): boolean {
391    return index === this.selectedIndex;
392  }
393
394  isTransactions() {
395    return this.traceType === TraceType.TRANSACTIONS;
396  }
397
398  isProtolog() {
399    return this.traceType === TraceType.PROTO_LOG;
400  }
401
402  isTransitions() {
403    return this.traceType === TraceType.TRANSITION;
404  }
405
406  isFixedSizeScrollViewport() {
407    return !(
408      this.isTransactions() ||
409      this.isProtolog() ||
410      this.isTransitions()
411    );
412  }
413
414  updateTableMarginEnd() {
415    const tableHeader =
416      this.elementRef.nativeElement.querySelector<HTMLElement>('.table-header');
417    if (!tableHeader) {
418      return;
419    }
420    const el = this.scrollComponent?.elementRef.nativeElement;
421    if (el && el.scrollHeight > el.offsetHeight) {
422      tableHeader.style.marginInlineEnd =
423        el.offsetWidth - el.scrollWidth + 'px';
424    } else {
425      tableHeader.style.marginInlineEnd = '';
426    }
427  }
428
429  private onRawTimestampClick(value: Timestamp) {
430    this.emitEvent(
431      ViewerEvents.TimestampClick,
432      new TimestampClickDetail(undefined, value),
433    );
434  }
435
436  private emitEvent(event: ViewerEvents, data?: object | number) {
437    const customEvent = new CustomEvent(event, {
438      bubbles: true,
439      detail: data,
440    });
441    this.elementRef.nativeElement.dispatchEvent(customEvent);
442  }
443}
444