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 { 17 ChangeDetectorRef, 18 Component, 19 EventEmitter, 20 Inject, 21 Input, 22 NgZone, 23 Output, 24} from '@angular/core'; 25import {TracePipeline} from 'app/trace_pipeline'; 26import {ProgressListener} from 'messaging/progress_listener'; 27import {Trace} from 'trace/trace'; 28import {TRACE_INFO} from 'trace/trace_info'; 29import {TraceTypeUtils} from 'trace/trace_type'; 30import {LoadProgressComponent} from './load_progress_component'; 31 32@Component({ 33 selector: 'upload-traces', 34 template: ` 35 <mat-card class="upload-card"> 36 <div class="card-header"> 37 <mat-card-title class="title">Upload Traces</mat-card-title> 38 <div 39 *ngIf="!isLoadingFiles && tracePipeline.getTraces().getSize() > 0" 40 class="trace-actions-container"> 41 <button 42 color="primary" 43 mat-raised-button 44 class="load-btn" 45 matTooltip="Upload trace with an associated viewer to visualize" 46 [matTooltipDisabled]="hasLoadedFilesWithViewers()" 47 [disabled]="!hasLoadedFilesWithViewers()" 48 (click)="onViewTracesButtonClick()"> 49 View traces 50 </button> 51 52 <button class="download-btn" color="primary" mat-stroked-button (click)="downloadTracesClick.emit()"> 53 Download all 54 </button> 55 56 <button color="primary" mat-stroked-button for="fileDropRef" (click)="fileDropRef.click()"> 57 Upload another file 58 </button> 59 60 <button 61 class="clear-all-btn" 62 color="primary" 63 mat-stroked-button 64 (click)="onClearButtonClick()"> 65 Clear all 66 </button> 67 </div> 68 </div> 69 70 <mat-card-content 71 class="drop-box" 72 ref="drop-box" 73 (dragleave)="onFileDragOut($event)" 74 (dragover)="onFileDragIn($event)" 75 (drop)="onFileDrop($event)" 76 (click)="fileDropRef.click()"> 77 <input 78 id="fileDropRef" 79 hidden 80 type="file" 81 multiple 82 onclick="this.value = null" 83 #fileDropRef 84 (change)="onInputFiles($event)" /> 85 86 <load-progress 87 *ngIf="isLoadingFiles" 88 [progressPercentage]="progressPercentage" 89 [message]="progressMessage"> 90 </load-progress> 91 92 <mat-list 93 *ngIf="!isLoadingFiles && tracePipeline.getTraces().getSize() > 0" 94 class="uploaded-files"> 95 <mat-list-item [class.no-visualization]="!canVisualizeTrace(trace)" [class.trace-error]="trace.isCorrupted()" *ngFor="let trace of tracePipeline.getTraces()"> 96 <mat-icon 97 matListIcon 98 [style]="{color: TRACE_INFO[trace.type].color}"> 99 {{ TRACE_INFO[trace.type].icon }} 100 </mat-icon> 101 102 <p matLine>{{ TRACE_INFO[trace.type].name }}</p> 103 <p matLine *ngFor="let descriptor of trace.getDescriptors()">{{ descriptor }}</p> 104 105 <mat-icon class="warning-icon" *ngIf="!canVisualizeTrace(trace)" [matTooltip]="cannotVisualizeTraceTooltip(trace)"> 106 warning 107 </mat-icon> 108 <mat-icon class="error-icon" *ngIf="trace.isCorrupted()" [matTooltip]="traceErrorTooltip(trace)"> 109 error 110 </mat-icon> 111 <button mat-icon-button (click)="onRemoveTrace($event, trace)"> 112 <mat-icon>close</mat-icon> 113 </button> 114 </mat-list-item> 115 </mat-list> 116 117 <div *ngIf="!isLoadingFiles && tracePipeline.getTraces().getSize() === 0" class="drop-info"> 118 <p class="mat-body-3 icon"> 119 <mat-icon inline fontIcon="upload"></mat-icon> 120 </p> 121 <p class="mat-body-1">Drag your .winscope file(s) or click to upload</p> 122 </div> 123 </mat-card-content> 124 </mat-card> 125 `, 126 styles: [ 127 ` 128 .upload-card { 129 height: 100%; 130 display: flex; 131 flex-direction: column; 132 overflow: auto; 133 margin: 10px; 134 padding-top: 0px; 135 } 136 .card-header { 137 justify-content: space-between; 138 align-items: center; 139 display: flex; 140 flex-direction: row; 141 } 142 .title { 143 padding-top: 16px; 144 text-align: center; 145 } 146 .trace-actions-container { 147 display: flex; 148 flex-direction: row; 149 flex-wrap: wrap; 150 gap: 10px; 151 padding: 4px 0px; 152 } 153 .drop-box { 154 display: flex; 155 flex-direction: column; 156 overflow: auto; 157 border: 2px dashed var(--border-color); 158 cursor: pointer; 159 } 160 .uploaded-files { 161 flex: 400px; 162 padding: 0; 163 } 164 .drop-info { 165 flex: 400px; 166 display: flex; 167 flex-direction: column; 168 justify-content: center; 169 align-items: center; 170 pointer-events: none; 171 } 172 .drop-info p { 173 opacity: 0.6; 174 font-size: 1.2rem; 175 } 176 .drop-info .icon { 177 font-size: 3rem; 178 margin: 0; 179 } 180 .div-progress { 181 display: flex; 182 height: 100%; 183 flex-direction: column; 184 justify-content: center; 185 align-content: center; 186 align-items: center; 187 } 188 .div-progress p { 189 opacity: 0.6; 190 } 191 .div-progress mat-icon { 192 font-size: 3rem; 193 width: unset; 194 height: unset; 195 } 196 .div-progress mat-progress-bar { 197 max-width: 250px; 198 } 199 mat-card-content { 200 flex-grow: 1; 201 } 202 .no-visualization { 203 background-color: var(--warning-background-color); 204 } 205 .trace-error { 206 background-color: var(--error-background-color); 207 } 208 .warning-icon, .error-icon { 209 flex-shrink: 0; 210 } 211 `, 212 ], 213}) 214export class UploadTracesComponent implements ProgressListener { 215 TRACE_INFO = TRACE_INFO; 216 isLoadingFiles = false; 217 progressMessage = ''; 218 progressPercentage?: number; 219 lastUiProgressUpdateTimeMs?: number; 220 221 @Input() tracePipeline: TracePipeline | undefined; 222 @Output() filesUploaded = new EventEmitter<File[]>(); 223 @Output() viewTracesButtonClick = new EventEmitter<void>(); 224 @Output() downloadTracesClick = new EventEmitter<void>(); 225 226 constructor( 227 @Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef, 228 @Inject(NgZone) private ngZone: NgZone, 229 ) {} 230 231 ngOnInit() { 232 this.tracePipeline?.clear(); 233 } 234 235 onProgressUpdate( 236 message: string | undefined, 237 progressPercentage: number | undefined, 238 ) { 239 if ( 240 !LoadProgressComponent.canUpdateComponent(this.lastUiProgressUpdateTimeMs) 241 ) { 242 return; 243 } 244 this.isLoadingFiles = true; 245 this.progressMessage = message ? message : 'Loading...'; 246 this.progressPercentage = progressPercentage; 247 this.lastUiProgressUpdateTimeMs = Date.now(); 248 this.changeDetectorRef.detectChanges(); 249 } 250 251 onOperationFinished() { 252 this.isLoadingFiles = false; 253 this.lastUiProgressUpdateTimeMs = undefined; 254 this.changeDetectorRef.detectChanges(); 255 } 256 257 onInputFiles(event: Event) { 258 const files = this.getInputFiles(event); 259 this.filesUploaded.emit(files); 260 } 261 262 onViewTracesButtonClick() { 263 this.viewTracesButtonClick.emit(); 264 } 265 266 onClearButtonClick() { 267 this.tracePipeline?.clear(); 268 this.onOperationFinished(); 269 } 270 271 onFileDragIn(e: DragEvent) { 272 e.preventDefault(); 273 e.stopPropagation(); 274 } 275 276 onFileDragOut(e: DragEvent) { 277 e.preventDefault(); 278 e.stopPropagation(); 279 } 280 281 onFileDrop(e: DragEvent) { 282 e.preventDefault(); 283 e.stopPropagation(); 284 const droppedFiles = e.dataTransfer?.files; 285 if (!droppedFiles) return; 286 this.filesUploaded.emit(Array.from(droppedFiles)); 287 } 288 289 onRemoveTrace(event: MouseEvent, trace: Trace<object>) { 290 event.preventDefault(); 291 event.stopPropagation(); 292 this.tracePipeline?.removeTrace(trace); 293 this.onOperationFinished(); 294 } 295 296 hasLoadedFilesWithViewers(): boolean { 297 return this.ngZone.run(() => { 298 let hasFilesWithViewers = false; 299 this.tracePipeline?.getTraces().forEachTrace((trace) => { 300 if ( 301 !trace.isCorrupted() && 302 TraceTypeUtils.isTraceTypeWithViewer(trace.type) 303 ) { 304 hasFilesWithViewers = true; 305 } 306 }); 307 308 return hasFilesWithViewers; 309 }); 310 } 311 312 canVisualizeTrace(trace: Trace<object>): boolean { 313 return TraceTypeUtils.isTraceTypeWithViewer(trace.type); 314 } 315 316 cannotVisualizeTraceTooltip(trace: Trace<object>): string { 317 return TraceTypeUtils.getReasonForNoTraceVisualization(trace.type); 318 } 319 320 traceErrorTooltip(trace: Trace<object>): string { 321 const reason = trace.getCorruptedReason() ?? 'Trace is corrupted.'; 322 return 'Cannot visualize trace. ' + reason; 323 } 324 325 private getInputFiles(event: Event): File[] { 326 const files: FileList | null = (event?.target as HTMLInputElement)?.files; 327 if (!files || !files[0]) { 328 return []; 329 } 330 return Array.from(files); 331 } 332} 333