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