blob: f53e696f8973707d0d494c7ba775463a7ea76f3b [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
Input,
NgZone,
Output,
} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';
import {TRACE_INFO} from 'app/trace_info';
import {TracePipeline} from 'app/trace_pipeline';
import {FileUtils, OnFile} from 'common/file_utils';
import {FilesDownloadListener} from 'interfaces/files_download_listener';
import {LoadedTraceFile, TraceFile} from 'trace/trace_file';
import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
@Component({
selector: 'upload-traces',
template: `
<mat-card class="upload-card">
<mat-card-title class="title">Upload Traces</mat-card-title>
<mat-card-content
class="drop-box"
ref="drop-box"
(dragleave)="onFileDragOut($event)"
(dragover)="onFileDragIn($event)"
(drop)="onHandleFileDrop($event)"
(click)="fileDropRef.click()">
<input
id="fileDropRef"
hidden
type="file"
multiple
#fileDropRef
(change)="onInputFiles($event)" />
<load-progress
*ngIf="isLoadingFiles"
[progressPercentage]="progressPercentage"
[message]="progressMessage">
</load-progress>
<mat-list
*ngIf="!isLoadingFiles && this.tracePipeline.getLoadedTraceFiles().length > 0"
class="uploaded-files">
<mat-list-item *ngFor="let trace of this.tracePipeline.getLoadedTraceFiles()">
<mat-icon matListIcon>
{{ TRACE_INFO[trace.type].icon }}
</mat-icon>
<p matLine>{{ trace.traceFile.file.name }} ({{ TRACE_INFO[trace.type].name }})</p>
<button color="primary" mat-icon-button (click)="onRemoveTrace($event, trace)">
<mat-icon>close</mat-icon>
</button>
</mat-list-item>
</mat-list>
<div
*ngIf="!isLoadingFiles && tracePipeline.getLoadedTraceFiles().length === 0"
class="drop-info">
<p class="mat-body-3 icon">
<mat-icon inline fontIcon="upload"></mat-icon>
</p>
<p class="mat-body-1">Drag your .winscope file(s) or click to upload</p>
</div>
</mat-card-content>
<div
*ngIf="!isLoadingFiles && tracePipeline.getLoadedTraceFiles().length > 0"
class="trace-actions-container">
<button
color="primary"
mat-raised-button
class="load-btn"
(click)="onViewTracesButtonClick()">
View traces
</button>
<button color="primary" mat-stroked-button for="fileDropRef" (click)="fileDropRef.click()">
Upload another file
</button>
<button color="primary" mat-stroked-button (click)="onClearButtonClick()">Clear all</button>
</div>
</mat-card>
`,
styles: [
`
.upload-card {
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
margin: 10px;
}
.drop-box {
display: flex;
flex-direction: column;
overflow: auto;
border: 2px dashed var(--border-color);
cursor: pointer;
}
.uploaded-files {
flex: 400px;
padding: 0;
}
.drop-info {
flex: 400px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: none;
}
.drop-info p {
opacity: 0.6;
font-size: 1.2rem;
}
.drop-info .icon {
font-size: 3rem;
margin: 0;
}
.trace-actions-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
}
.div-progress {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
}
.div-progress p {
opacity: 0.6;
}
.div-progress mat-icon {
font-size: 3rem;
width: unset;
height: unset;
}
.div-progress mat-progress-bar {
max-width: 250px;
}
mat-card-content {
flex-grow: 1;
}
`,
],
})
export class UploadTracesComponent implements FilesDownloadListener {
TRACE_INFO = TRACE_INFO;
isLoadingFiles = false;
progressMessage = '';
progressPercentage?: number;
@Input() tracePipeline!: TracePipeline;
@Output() traceDataLoaded = new EventEmitter<void>();
constructor(
@Inject(ChangeDetectorRef) private changeDetectorRef: ChangeDetectorRef,
@Inject(MatSnackBar) private snackBar: MatSnackBar,
@Inject(NgZone) private ngZone: NgZone
) {}
ngOnInit() {
this.tracePipeline.clear();
}
onFilesDownloadStart() {
this.isLoadingFiles = true;
this.progressMessage = 'Downloading files...';
this.progressPercentage = undefined;
this.changeDetectorRef.detectChanges();
}
async onFilesDownloaded(files: File[]) {
await this.processFiles(files);
}
async onInputFiles(event: Event) {
const files = this.getInputFiles(event);
await this.processFiles(files);
}
onViewTracesButtonClick() {
this.traceDataLoaded.emit();
}
onClearButtonClick() {
this.tracePipeline.clear();
this.changeDetectorRef.detectChanges();
}
onFileDragIn(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
}
onFileDragOut(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
}
async onHandleFileDrop(e: DragEvent) {
e.preventDefault();
e.stopPropagation();
const droppedFiles = e.dataTransfer?.files;
if (!droppedFiles) return;
await this.processFiles(Array.from(droppedFiles));
}
onRemoveTrace(event: MouseEvent, trace: LoadedTraceFile) {
event.preventDefault();
event.stopPropagation();
this.tracePipeline.removeTraceFile(trace.type);
this.changeDetectorRef.detectChanges();
}
private async processFiles(files: File[]) {
const UI_PROGRESS_UPDATE_PERIOD_MS = 200;
let lastUiProgressUpdate = Date.now();
const onProgressUpdate = (progress: number) => {
const now = Date.now();
if (Date.now() - lastUiProgressUpdate < UI_PROGRESS_UPDATE_PERIOD_MS) {
// Let's limit the amount of UI updates, because the progress bar component
// renders weird stuff when updated too frequently
return;
}
lastUiProgressUpdate = now;
this.progressPercentage = progress;
this.changeDetectorRef.detectChanges();
};
const traceFiles: TraceFile[] = [];
const onFile: OnFile = (file: File, parentArchive?: File) => {
traceFiles.push(new TraceFile(file, parentArchive));
};
this.isLoadingFiles = true;
this.progressMessage = 'Unzipping files...';
this.changeDetectorRef.detectChanges();
await FileUtils.unzipFilesIfNeeded(files, onFile, onProgressUpdate);
this.progressMessage = 'Parsing files...';
this.changeDetectorRef.detectChanges();
const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate);
this.isLoadingFiles = false;
this.changeDetectorRef.detectChanges();
ParserErrorSnackBarComponent.showIfNeeded(this.ngZone, this.snackBar, parserErrors);
}
private getInputFiles(event: Event): File[] {
const files: FileList | null = (event?.target as HTMLInputElement)?.files;
if (!files || !files[0]) {
return [];
}
return Array.from(files);
}
}