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