xref: /aosp_15_r20/development/tools/winscope/src/app/components/upload_traces_component.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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