xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/log_component.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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';
32import {TimeUtils} from 'common/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">
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="entries.length === 0"> No entries found. </div>
134
135      <cdk-virtual-scroll-viewport
136          *ngIf="isTransactions()"
137          transactionsVirtualScroll
138          class="scroll"
139          [scrollItems]="entries">
140        <ng-container
141            *cdkVirtualFor="let entry of entries; let i = index"
142            [ngTemplateOutlet]="content"
143            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
144      </cdk-virtual-scroll-viewport>
145
146      <cdk-virtual-scroll-viewport
147          *ngIf="isProtolog()"
148          protologVirtualScroll
149          class="scroll"
150          [scrollItems]="entries">
151        <ng-container
152            *cdkVirtualFor="let entry of entries; let i = index"
153            [ngTemplateOutlet]="content"
154            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
155      </cdk-virtual-scroll-viewport>
156
157      <cdk-virtual-scroll-viewport
158          *ngIf="isTransitions()"
159          transitionsVirtualScroll
160          class="scroll"
161          [scrollItems]="entries">
162        <ng-container
163            *cdkVirtualFor="let entry of entries; let i = index"
164            [ngTemplateOutlet]="content"
165            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
166      </cdk-virtual-scroll-viewport>
167
168      <cdk-virtual-scroll-viewport
169          *ngIf="isFixedSizeScrollViewport()"
170          itemSize="36"
171          class="scroll">
172        <ng-container
173            *cdkVirtualFor="let entry of entries; let i = index"
174            [ngTemplateOutlet]="content"
175            [ngTemplateOutletContext]="{entry: entry, i: i}"> </ng-container>
176      </cdk-virtual-scroll-viewport>
177
178      <ng-template #content let-entry="entry" let-i="i">
179        <div
180            class="entry"
181            [attr.item-id]="i"
182            [class.current]="isCurrentEntry(i)"
183            [class.selected]="isSelectedEntry(i)"
184            (click)="onEntryClicked(i)">
185          <div *ngIf="showTraceEntryTimes" class="time">
186            <button
187                mat-button
188                class="time-button"
189                color="primary"
190                (click)="onTraceEntryTimestampClick($event, entry)"
191                [disabled]="!entry.traceEntry.hasValidTimestamp()">
192              {{ formatTimestamp(entry.traceEntry.getTimestamp()) }}
193            </button>
194          </div>
195
196          <div [class]="field.spec.cssClass" *ngFor="let field of entry.fields; index as i">
197            <span class="mat-body-1" *ngIf="!showFieldButton(field)">{{ field.value }}</span>
198            <button
199                *ngIf="showFieldButton(field)"
200                mat-button
201                class="time-button"
202                color="primary"
203                (click)="onFieldButtonClick($event, entry, field)">
204              {{ formatFieldButton(field) }}
205            </button>
206            <mat-icon
207                *ngIf="field.icon"
208                aria-hidden="false"
209                [style]="{color: field.iconColor}"> {{field.icon}} </mat-icon>
210          </div>
211        </div>
212      </ng-template>
213    </div>
214  `,
215  styles: [
216    `
217      .view-header {
218        display: flex;
219        flex-direction: column;
220        flex: 0 0 auto
221      }
222    `,
223    selectedElementStyle,
224    currentElementStyle,
225    timeButtonStyle,
226    inlineButtonStyle,
227    viewerCardStyle,
228    viewerCardInnerStyle,
229    logComponentStyles,
230  ],
231})
232export class LogComponent {
233  emptyFilterValue = '';
234  private lastClickedTimestamp: Timestamp | undefined;
235
236  @Input() title: string | undefined;
237  @Input() selectedIndex: number | undefined;
238  @Input() scrollToIndex: number | undefined;
239  @Input() currentIndex: number | undefined;
240  @Input() headers: LogHeader[] = [];
241  @Input() entries: LogEntry[] = [];
242  @Input() showCurrentTimeButton = true;
243  @Input() traceType: TraceType | undefined;
244  @Input() showTraceEntryTimes = true;
245  @Input() showFiltersInTitle = false;
246
247  @Output() collapseButtonClicked = new EventEmitter();
248
249  @ViewChild(CdkVirtualScrollViewport)
250  scrollComponent?: CdkVirtualScrollViewport;
251
252  constructor(
253    @Inject(ElementRef) private elementRef: ElementRef<HTMLElement>,
254  ) {}
255
256  getHeadersWithFilters() {
257    return this.headers.filter((header) => this.isHeaderWithFilter(header));
258  }
259
260  isHeaderWithFilter(header: LogHeader): boolean {
261    return header.filter !== undefined;
262  }
263
264  showFieldButton(field: LogField) {
265    return field.value instanceof Timestamp || field.propagateEntryTimestamp;
266  }
267
268  formatFieldButton(field: LogField): string | number {
269    return field.value instanceof Timestamp
270      ? this.formatTimestamp(field.value)
271      : field.value;
272  }
273
274  areMultipleDatesPresent(): boolean {
275    return (
276      this.entries.at(0)?.traceEntry.getFullTrace().spansMultipleDates() ??
277      false
278    );
279  }
280
281  formatTimestamp(timestamp: Timestamp) {
282    if (!this.areMultipleDatesPresent()) {
283      return timestamp.format(TimestampFormatType.DROP_DATE);
284    }
285    return timestamp.format();
286  }
287
288  ngOnChanges() {
289    if (
290      this.scrollToIndex !== undefined &&
291      this.lastClickedTimestamp !==
292        this.entries.at(this.scrollToIndex)?.traceEntry.getTimestamp()
293    ) {
294      this.scrollComponent?.scrollToIndex(Math.max(0, this.scrollToIndex - 1));
295    }
296  }
297
298  async ngAfterContentInit() {
299    await TimeUtils.sleepMs(10);
300    this.updateTableMarginEnd();
301  }
302
303  @HostListener('window:resize', ['$event'])
304  onResize(event: Event) {
305    this.updateTableMarginEnd();
306  }
307
308  onFilterChange(event: MatSelectChange, header: LogHeader) {
309    this.emitEvent(
310      ViewerEvents.LogFilterChange,
311      new LogFilterChangeDetail(header, event.value),
312    );
313  }
314
315  onSearchBoxChange(detail: TextFilter, header: LogHeader) {
316    this.emitEvent(
317      ViewerEvents.LogTextFilterChange,
318      new LogTextFilterChangeDetail(header, detail),
319    );
320  }
321
322  onEntryClicked(index: number) {
323    this.emitEvent(ViewerEvents.LogEntryClick, index);
324  }
325
326  onGoToCurrentTimeClick() {
327    if (this.currentIndex !== undefined && this.scrollComponent) {
328      this.scrollComponent.scrollToIndex(this.currentIndex);
329    }
330  }
331
332  onTraceEntryTimestampClick(event: MouseEvent, entry: LogEntry) {
333    event.stopPropagation();
334    this.lastClickedTimestamp = entry.traceEntry.getTimestamp();
335    this.emitEvent(
336      ViewerEvents.TimestampClick,
337      new TimestampClickDetail(entry.traceEntry),
338    );
339  }
340
341  onFieldButtonClick(event: MouseEvent, entry: LogEntry, field: LogField) {
342    event.stopPropagation();
343    if (field.propagateEntryTimestamp) {
344      this.onTraceEntryTimestampClick(event, entry);
345    } else if (field.value instanceof Timestamp) {
346      this.onRawTimestampClick(field.value as Timestamp);
347    }
348  }
349
350  @HostListener('document:keydown', ['$event'])
351  async handleKeyboardEvent(event: KeyboardEvent) {
352    const logComponentVisible = DOMUtils.isElementVisible(
353      this.elementRef.nativeElement,
354    );
355    if (event.key === 'ArrowDown' && logComponentVisible) {
356      event.stopPropagation();
357      event.preventDefault();
358      this.emitEvent(ViewerEvents.ArrowDownPress);
359    }
360    if (event.key === 'ArrowUp' && logComponentVisible) {
361      event.stopPropagation();
362      event.preventDefault();
363      this.emitEvent(ViewerEvents.ArrowUpPress);
364    }
365  }
366
367  isCurrentEntry(index: number): boolean {
368    return index === this.currentIndex;
369  }
370
371  isSelectedEntry(index: number): boolean {
372    return index === this.selectedIndex;
373  }
374
375  isTransactions() {
376    return this.traceType === TraceType.TRANSACTIONS;
377  }
378
379  isProtolog() {
380    return this.traceType === TraceType.PROTO_LOG;
381  }
382
383  isTransitions() {
384    return this.traceType === TraceType.TRANSITION;
385  }
386
387  isFixedSizeScrollViewport() {
388    return !(
389      this.isTransactions() ||
390      this.isProtolog() ||
391      this.isTransitions()
392    );
393  }
394
395  updateTableMarginEnd() {
396    const tableHeader =
397      this.elementRef.nativeElement.querySelector<HTMLElement>('.table-header');
398    if (!tableHeader) {
399      return;
400    }
401    const el = this.scrollComponent?.elementRef.nativeElement;
402    if (el && el.scrollHeight > el.offsetHeight) {
403      tableHeader.style.marginInlineEnd =
404        el.offsetWidth - el.scrollWidth + 'px';
405    } else {
406      tableHeader.style.marginInlineEnd = '';
407    }
408  }
409
410  private onRawTimestampClick(value: Timestamp) {
411    this.emitEvent(
412      ViewerEvents.TimestampClick,
413      new TimestampClickDetail(undefined, value),
414    );
415  }
416
417  private emitEvent(event: ViewerEvents, data?: any) {
418    const customEvent = new CustomEvent(event, {
419      bubbles: true,
420      detail: data,
421    });
422    this.elementRef.nativeElement.dispatchEvent(customEvent);
423  }
424}
425