/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling'; import { Component, ElementRef, EventEmitter, HostListener, Inject, Input, Output, ViewChild, } from '@angular/core'; import {MatSelectChange} from '@angular/material/select'; import {DOMUtils} from 'common/dom_utils'; import {Timestamp, TimestampFormatType} from 'common/time/time'; import {TimeUtils} from 'common/time/time_utils'; import {TraceType} from 'trace/trace_type'; import {TextFilter} from 'viewers/common/text_filter'; import {LogEntry, LogField, LogHeader} from 'viewers/common/ui_data_log'; import { LogFilterChangeDetail, LogTextFilterChangeDetail, TimestampClickDetail, ViewerEvents, } from 'viewers/common/viewer_events'; import { inlineButtonStyle, timeButtonStyle, } from 'viewers/components/styles/clickable_property.styles'; import {currentElementStyle} from 'viewers/components/styles/current_element.styles'; import {logComponentStyles} from 'viewers/components/styles/log_component.styles'; import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles'; import { viewerCardInnerStyle, viewerCardStyle, } from 'viewers/components/styles/viewer_card.styles'; @Component({ selector: 'log-view', template: `
{{header.spec.name}}
No entries found.
Fetching all data
{{ field.value }} {{field.icon}}
`, styles: [ ` .view-header { display: flex; flex-direction: column; flex: 0 0 auto } .message-with-spinner { display: flex; flex-direction: row; align-items: center; justify-content: center; } `, selectedElementStyle, currentElementStyle, timeButtonStyle, inlineButtonStyle, viewerCardStyle, viewerCardInnerStyle, logComponentStyles, ], }) export class LogComponent { emptyFilterValue = ''; private lastClickedTimestamp: Timestamp | undefined; @Input() title: string | undefined; @Input() selectedIndex: number | undefined; @Input() scrollToIndex: number | undefined; @Input() currentIndex: number | undefined; @Input() headers: LogHeader[] = []; @Input() entries: LogEntry[] = []; @Input() showCurrentTimeButton = true; @Input() traceType: TraceType | undefined; @Input() showTraceEntryTimes = true; @Input() showFiltersInTitle = false; @Input() padEntries = true; @Input() isFetchingData = false; @Output() collapseButtonClicked = new EventEmitter(); @ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport; constructor( @Inject(ElementRef) private elementRef: ElementRef, ) {} getHeadersWithFilters() { return this.headers.filter((header) => this.isHeaderWithFilter(header)); } isHeaderWithFilter(header: LogHeader): boolean { return header.filter !== undefined; } showFieldButton(entry: LogEntry, field: LogField): boolean { const propagateEntryTimestamp = !!field.propagateEntryTimestamp && entry.traceEntry.hasValidTimestamp(); return field.value instanceof Timestamp || propagateEntryTimestamp; } formatFieldButton(field: LogField): string | number { return field.value instanceof Timestamp ? this.formatTimestamp(field.value) : field.value; } areMultipleDatesPresent(): boolean { return ( this.entries.at(0)?.traceEntry.getFullTrace().spansMultipleDates() ?? false ); } formatTimestamp(timestamp: Timestamp) { if (!this.areMultipleDatesPresent()) { return timestamp.format(TimestampFormatType.DROP_DATE); } return timestamp.format(); } ngOnChanges() { if ( this.scrollToIndex !== undefined && this.lastClickedTimestamp !== this.entries.at(this.scrollToIndex)?.traceEntry.getTimestamp() ) { this.scrollComponent?.scrollToIndex(Math.max(0, this.scrollToIndex - 1)); } } async ngAfterContentInit() { await TimeUtils.sleepMs(10); this.updateTableMarginEnd(); } @HostListener('window:resize', ['$event']) onResize(event: Event) { this.updateTableMarginEnd(); } onFilterChange(event: MatSelectChange, header: LogHeader) { this.emitEvent( ViewerEvents.LogFilterChange, new LogFilterChangeDetail(header, event.value), ); } onSearchBoxChange(detail: TextFilter, header: LogHeader) { this.emitEvent( ViewerEvents.LogTextFilterChange, new LogTextFilterChangeDetail(header, detail), ); } onEntryClicked(index: number) { this.emitEvent(ViewerEvents.LogEntryClick, index); } onGoToCurrentTimeClick() { if (this.currentIndex !== undefined && this.scrollComponent) { this.scrollComponent.scrollToIndex(this.currentIndex); } } onTraceEntryTimestampClick(event: MouseEvent, entry: LogEntry) { event.stopPropagation(); this.lastClickedTimestamp = entry.traceEntry.getTimestamp(); this.emitEvent( ViewerEvents.TimestampClick, new TimestampClickDetail(entry.traceEntry), ); } onFieldButtonClick(event: MouseEvent, entry: LogEntry, field: LogField) { event.stopPropagation(); if (field.propagateEntryTimestamp) { this.onTraceEntryTimestampClick(event, entry); } else if (field.value instanceof Timestamp) { this.onRawTimestampClick(field.value as Timestamp); } } @HostListener('document:keydown', ['$event']) async handleKeyboardEvent(event: KeyboardEvent) { const logComponentVisible = DOMUtils.isElementVisible( this.elementRef.nativeElement, ); if (event.key === 'ArrowDown' && logComponentVisible) { event.stopPropagation(); event.preventDefault(); this.emitEvent(ViewerEvents.ArrowDownPress); } if (event.key === 'ArrowUp' && logComponentVisible) { event.stopPropagation(); event.preventDefault(); this.emitEvent(ViewerEvents.ArrowUpPress); } } isCurrentEntry(index: number): boolean { return index === this.currentIndex; } isSelectedEntry(index: number): boolean { return index === this.selectedIndex; } isTransactions() { return this.traceType === TraceType.TRANSACTIONS; } isProtolog() { return this.traceType === TraceType.PROTO_LOG; } isTransitions() { return this.traceType === TraceType.TRANSITION; } isFixedSizeScrollViewport() { return !( this.isTransactions() || this.isProtolog() || this.isTransitions() ); } updateTableMarginEnd() { const tableHeader = this.elementRef.nativeElement.querySelector('.table-header'); if (!tableHeader) { return; } const el = this.scrollComponent?.elementRef.nativeElement; if (el && el.scrollHeight > el.offsetHeight) { tableHeader.style.marginInlineEnd = el.offsetWidth - el.scrollWidth + 'px'; } else { tableHeader.style.marginInlineEnd = ''; } } private onRawTimestampClick(value: Timestamp) { this.emitEvent( ViewerEvents.TimestampClick, new TimestampClickDetail(undefined, value), ); } private emitEvent(event: ViewerEvents, data?: object | number) { const customEvent = new CustomEvent(event, { bubbles: true, detail: data, }); this.elementRef.nativeElement.dispatchEvent(customEvent); } }