1/* 2 * Copyright (C) 2022 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 */ 16import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling'; 17import {Component, ElementRef, Inject, Input, ViewChild} from '@angular/core'; 18import {MatSelectChange} from '@angular/material/select'; 19import {TimestampClickDetail, ViewerEvents} from 'viewers/common/viewer_events'; 20import {timeButtonStyle} from 'viewers/components/styles/clickable_property.styles'; 21import {currentElementStyle} from 'viewers/components/styles/current_element.styles'; 22import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles'; 23import {viewerCardStyle} from 'viewers/components/styles/viewer_card.styles'; 24import {UiData, UiDataMessage} from './ui_data'; 25 26@Component({ 27 selector: 'viewer-protolog', 28 template: ` 29 <div class="card-grid container"> 30 <div class="log-view"> 31 <div class="filters"> 32 <div class="log-level"> 33 <select-with-filter 34 label="Log level" 35 flex="3" 36 [options]="uiData.allLogLevels" 37 outerFilterWidth="225" 38 (selectChange)="onLogLevelsChange($event)"> 39 </select-with-filter> 40 </div> 41 <div class="tag"> 42 <select-with-filter 43 label="Tags" 44 flex="2" 45 [options]="uiData.allTags" 46 outerFilterWidth="150" 47 innerFilterWidth="150" 48 (selectChange)="onTagsChange($event)"> 49 </select-with-filter> 50 </div> 51 <div class="source-file"> 52 <select-with-filter 53 label="Source files" 54 flex="4" 55 [options]="uiData.allSourceFiles" 56 outerFilterWidth="300" 57 innerFilterWidth="300" 58 (selectChange)="onSourceFilesChange($event)"> 59 </select-with-filter> 60 </div> 61 <div class="text"> 62 <mat-form-field appearance="fill" (keydown.enter)="$event.target.blur()"> 63 <mat-label>Search text</mat-label> 64 <input matInput name="protologTextInput" [(ngModel)]="searchString" (input)="onSearchStringChange()" /> 65 </mat-form-field> 66 </div> 67 68 <button 69 color="primary" 70 mat-stroked-button 71 class="go-to-current-time" 72 (click)="onGoToCurrentTimeClick()"> 73 Go to Current Time 74 </button> 75 </div> 76 <cdk-virtual-scroll-viewport 77 protologVirtualScroll 78 class="scroll-messages" 79 [scrollItems]="uiData.messages"> 80 <div 81 *cdkVirtualFor="let message of uiData.messages; let i = index" 82 class="message" 83 [attr.item-id]="i" 84 [class.current]="isCurrentMessage(i)" 85 [class.selected]="isSelectedMessage(i)" 86 (click)="onMessageClicked(i)"> 87 <div class="time"> 88 <button 89 mat-button 90 color="primary" 91 (click)="onTimestampClicked(message)"> 92 {{ message.time.formattedValue() }} 93 </button> 94 </div> 95 <div class="log-level"> 96 <span class="mat-body-1">{{ message.level }}</span> 97 </div> 98 <div class="tag"> 99 <span class="mat-body-1">{{ message.tag }}</span> 100 </div> 101 <div class="source-file"> 102 <span class="mat-body-1">{{ message.at }}</span> 103 </div> 104 <div class="text"> 105 <span class="mat-body-1">{{ message.text }}</span> 106 </div> 107 </div> 108 </cdk-virtual-scroll-viewport> 109 </div> 110 </div> 111 `, 112 styles: [ 113 ` 114 .container { 115 display: flex; 116 flex-direction: column; 117 } 118 119 .filters { 120 display: flex; 121 flex-direction: row; 122 margin-top: 16px; 123 } 124 125 .scroll-messages { 126 height: 100%; 127 flex: 1; 128 } 129 130 .message { 131 display: flex; 132 flex-direction: row; 133 overflow-wrap: anywhere; 134 } 135 136 .time { 137 flex: 2; 138 } 139 140 .log-level { 141 flex: 1; 142 } 143 144 .filters .log-level { 145 flex: 3; 146 } 147 148 .tag { 149 flex: 2; 150 } 151 152 .source-file { 153 flex: 4; 154 } 155 156 .text { 157 flex: 10; 158 } 159 160 .filters .text mat-form-field { 161 width: 80%; 162 } 163 164 .go-to-current-time { 165 margin-top: 4px; 166 font-size: 12px; 167 height: 65%; 168 width: fit-content; 169 } 170 171 .filters div { 172 margin: 4px; 173 } 174 175 .message div { 176 margin: 4px; 177 } 178 179 mat-form-field { 180 width: 100%; 181 font-size: 12px; 182 } 183 `, 184 selectedElementStyle, 185 currentElementStyle, 186 timeButtonStyle, 187 viewerCardStyle, 188 ], 189}) 190export class ViewerProtologComponent { 191 uiData: UiData = UiData.EMPTY; 192 193 private searchString = ''; 194 private lastClicked = ''; 195 private lastSelectedMessage: undefined | number; 196 197 @ViewChild(CdkVirtualScrollViewport) 198 scrollComponent?: CdkVirtualScrollViewport; 199 200 constructor(@Inject(ElementRef) private elementRef: ElementRef) {} 201 202 @Input() 203 set inputData(data: UiData) { 204 this.uiData = data; 205 if ( 206 this.lastSelectedMessage === undefined && 207 this.uiData.currentMessageIndex !== undefined && 208 this.scrollComponent && 209 this.lastClicked !== 210 this.uiData.messages[ 211 this.uiData.currentMessageIndex 212 ].time.formattedValue() 213 ) { 214 this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex); 215 } 216 217 this.lastSelectedMessage = undefined; 218 } 219 220 onLogLevelsChange(event: MatSelectChange) { 221 this.emitEvent(ViewerEvents.LogLevelsFilterChanged, event.value); 222 } 223 224 onTagsChange(event: MatSelectChange) { 225 this.emitEvent(ViewerEvents.TagsFilterChanged, event.value); 226 } 227 228 onSourceFilesChange(event: MatSelectChange) { 229 this.emitEvent(ViewerEvents.SourceFilesFilterChanged, event.value); 230 } 231 232 onSearchStringChange() { 233 this.emitEvent(ViewerEvents.SearchStringFilterChanged, this.searchString); 234 } 235 236 onGoToCurrentTimeClick() { 237 if (this.uiData.currentMessageIndex !== undefined && this.scrollComponent) { 238 this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex); 239 } 240 } 241 242 onTimestampClicked(message: UiDataMessage) { 243 this.emitEvent( 244 ViewerEvents.TimestampClick, 245 new TimestampClickDetail(message.time.getValue(), message.traceIndex), 246 ); 247 } 248 249 onMessageClicked(index: number) { 250 this.lastSelectedMessage = index; 251 this.emitEvent(ViewerEvents.LogClicked, index); 252 } 253 254 isCurrentMessage(index: number): boolean { 255 return index === this.uiData.currentMessageIndex; 256 } 257 258 isSelectedMessage(index: number): boolean { 259 return index === this.uiData.selectedMessageIndex; 260 } 261 262 private emitEvent(event: string, data: any) { 263 const customEvent = new CustomEvent(event, { 264 bubbles: true, 265 detail: data, 266 }); 267 this.elementRef.nativeElement.dispatchEvent(customEvent); 268 } 269} 270