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