/* * Copyright (C) 2022 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, Inject, Input, ViewChild} from '@angular/core'; import {MatSelectChange} from '@angular/material/select'; import {TimestampClickDetail, ViewerEvents} from 'viewers/common/viewer_events'; import {timeButtonStyle} from 'viewers/components/styles/clickable_property.styles'; import {currentElementStyle} from 'viewers/components/styles/current_element.styles'; import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles'; import {viewerCardStyle} from 'viewers/components/styles/viewer_card.styles'; import {UiData, UiDataMessage} from './ui_data'; @Component({ selector: 'viewer-protolog', template: ` <div class="card-grid container"> <div class="log-view"> <div class="filters"> <div class="log-level"> <select-with-filter label="Log level" flex="3" [options]="uiData.allLogLevels" outerFilterWidth="225" (selectChange)="onLogLevelsChange($event)"> </select-with-filter> </div> <div class="tag"> <select-with-filter label="Tags" flex="2" [options]="uiData.allTags" outerFilterWidth="150" innerFilterWidth="150" (selectChange)="onTagsChange($event)"> </select-with-filter> </div> <div class="source-file"> <select-with-filter label="Source files" flex="4" [options]="uiData.allSourceFiles" outerFilterWidth="300" innerFilterWidth="300" (selectChange)="onSourceFilesChange($event)"> </select-with-filter> </div> <div class="text"> <mat-form-field appearance="fill" (keydown.enter)="$event.target.blur()"> <mat-label>Search text</mat-label> <input matInput name="protologTextInput" [(ngModel)]="searchString" (input)="onSearchStringChange()" /> </mat-form-field> </div> <button color="primary" mat-stroked-button class="go-to-current-time" (click)="onGoToCurrentTimeClick()"> Go to Current Time </button> </div> <cdk-virtual-scroll-viewport protologVirtualScroll class="scroll-messages" [scrollItems]="uiData.messages"> <div *cdkVirtualFor="let message of uiData.messages; let i = index" class="message" [attr.item-id]="i" [class.current]="isCurrentMessage(i)" [class.selected]="isSelectedMessage(i)" (click)="onMessageClicked(i)"> <div class="time"> <button mat-button color="primary" (click)="onTimestampClicked(message)"> {{ message.time.formattedValue() }} </button> </div> <div class="log-level"> <span class="mat-body-1">{{ message.level }}</span> </div> <div class="tag"> <span class="mat-body-1">{{ message.tag }}</span> </div> <div class="source-file"> <span class="mat-body-1">{{ message.at }}</span> </div> <div class="text"> <span class="mat-body-1">{{ message.text }}</span> </div> </div> </cdk-virtual-scroll-viewport> </div> </div> `, styles: [ ` .container { display: flex; flex-direction: column; } .filters { display: flex; flex-direction: row; margin-top: 16px; } .scroll-messages { height: 100%; flex: 1; } .message { display: flex; flex-direction: row; overflow-wrap: anywhere; } .time { flex: 2; } .log-level { flex: 1; } .filters .log-level { flex: 3; } .tag { flex: 2; } .source-file { flex: 4; } .text { flex: 10; } .filters .text mat-form-field { width: 80%; } .go-to-current-time { margin-top: 4px; font-size: 12px; height: 65%; width: fit-content; } .filters div { margin: 4px; } .message div { margin: 4px; } mat-form-field { width: 100%; font-size: 12px; } `, selectedElementStyle, currentElementStyle, timeButtonStyle, viewerCardStyle, ], }) export class ViewerProtologComponent { uiData: UiData = UiData.EMPTY; private searchString = ''; private lastClicked = ''; private lastSelectedMessage: undefined | number; @ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport; constructor(@Inject(ElementRef) private elementRef: ElementRef) {} @Input() set inputData(data: UiData) { this.uiData = data; if ( this.lastSelectedMessage === undefined && this.uiData.currentMessageIndex !== undefined && this.scrollComponent && this.lastClicked !== this.uiData.messages[ this.uiData.currentMessageIndex ].time.formattedValue() ) { this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex); } this.lastSelectedMessage = undefined; } onLogLevelsChange(event: MatSelectChange) { this.emitEvent(ViewerEvents.LogLevelsFilterChanged, event.value); } onTagsChange(event: MatSelectChange) { this.emitEvent(ViewerEvents.TagsFilterChanged, event.value); } onSourceFilesChange(event: MatSelectChange) { this.emitEvent(ViewerEvents.SourceFilesFilterChanged, event.value); } onSearchStringChange() { this.emitEvent(ViewerEvents.SearchStringFilterChanged, this.searchString); } onGoToCurrentTimeClick() { if (this.uiData.currentMessageIndex !== undefined && this.scrollComponent) { this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex); } } onTimestampClicked(message: UiDataMessage) { this.emitEvent( ViewerEvents.TimestampClick, new TimestampClickDetail(message.time.getValue(), message.traceIndex), ); } onMessageClicked(index: number) { this.lastSelectedMessage = index; this.emitEvent(ViewerEvents.LogClicked, index); } isCurrentMessage(index: number): boolean { return index === this.uiData.currentMessageIndex; } isSelectedMessage(index: number): boolean { return index === this.uiData.selectedMessageIndex; } private emitEvent(event: string, data: any) { const customEvent = new CustomEvent(event, { bubbles: true, detail: data, }); this.elementRef.nativeElement.dispatchEvent(customEvent); } }