import {
Component,
ElementRef,
OnInit,
ViewChild,
ChangeDetectionStrategy,
ChangeDetectorRef
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DataSource } from '@angular/cdk/collections';
import {
BehaviorSubject,
fromEvent,
merge,
Observable,
Subscription
} from 'rxjs';
import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { NotificationService } from '@core/core.module';
import { FileService } from '@file-manager/services/file-manager.service';
import { File, FileAdapter } from '@app/shared/models/file';
import { AddFileDialogComponent } from '@app/features/file-manager/components/dialogs/add-file-dialog/add-file-dialog.component';
import { EditFileDialogComponent } from '@app/features/file-manager/components/dialogs/edit-file-dialog/edit-file-dialog.component';
import { DeleteFileDialogComponent } from '@app/features/file-manager/components/dialogs/delete-file-dialog/delete-file-dialog.component';
import { Dropbox, DropboxChooseOptions } from '@file-manager/models/dropbox';
import { AnalysesService } from '@app/features/analysis/services/analysis.service';
import { PreviewDialogComponent } from '@app/features/workspace-detail/components/dialogs/preview-dialog/preview-dialog.component';
import { ViewFileTypesDialogComponent } from '../dialogs/view-file-types-dialog/view-file-types-dialog.component';
declare var Dropbox: Dropbox;
/**
* View File List Component
*
* Used to display the list of uploaded files
*/
@Component({
selector: 'mev-file-list',
templateUrl: './file-list.component.html',
styleUrls: ['./file-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileListComponent implements OnInit {
dropboxUploadInProgressMsg = '';
dropboxUploadCompleteMsg =
'File(s) uploaded successfully. Please assign the specific type for the file(s) uploaded.';
uploadInProgressMsg = '';
displayedColumns = [
'name',
'resource_type',
'status',
'size',
'created',
'workspace',
'is_active',
'is_public',
'actions'
];
exampleDatabase: FileService | null;
dataSource: ExampleDataSource | null;
id: string;
uploadProgressData: Map<string, object>;
isWait = false;
Object = Object;
private fileUploadProgressSubscription: Subscription = new Subscription();
constructor(
public httpClient: HttpClient,
public dialog: MatDialog,
public fileService: FileService,
private adapter: FileAdapter,
private readonly notificationService: NotificationService,
private readonly analysesService: AnalysesService,
private ref: ChangeDetectorRef
) {}
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild('filter', { static: true }) filter: ElementRef;
ngOnInit() {
this.loadData();
this.fileUploadProgressSubscription = this.fileService.fileUploadsProgress.subscribe(
uploadProgressData => {
this.uploadProgressData = uploadProgressData;
// show % of upload
let txt = '';
for (const key of Object.keys(uploadProgressData)) {
txt += `File ${key} is ${uploadProgressData[key].percent}% uploaded. \n`;
}
this.uploadInProgressMsg = txt;
this.ref.markForCheck();
// refresh table if all files are uploaded
const allFilesUploaded = Object.keys(uploadProgressData).every(
key => uploadProgressData[key].isUploaded
);
if (allFilesUploaded) {
this.refresh();
this.uploadInProgressMsg = '';
this.ref.markForCheck();
}
}
);
}
public ngOnDestroy(): void {
this.fileUploadProgressSubscription.unsubscribe();
}
refresh() {
this.loadData();
}
/**
* Open a modal dialog to upload files
*
*/
addItem() {
const dialogRef = this.dialog.open(AddFileDialogComponent, {
data: { file: File }
});
dialogRef.afterClosed().subscribe(result => {
if (result === 1) {
// After dialog is closed we're doing frontend updates
// For add we're just pushing a new row inside FileService
this.exampleDatabase.dataChange.value.push(
this.fileService.getDialogData()
);
}
});
}
/**
* Open Dropbox pop-up window to add files from Dropbox
*
*/
addDropBoxItem() {
const options: DropboxChooseOptions = {
success: files => {
const fileNames = files.map(file => file.name).join("' ,'");
this.dropboxUploadInProgressMsg = `Uploading file(s) '${fileNames}' from Dropbox...`;
this.ref.markForCheck();
const filesToUpload = files.map(file => ({
download_link: file.link,
filename: file.name
}));
this.fileService.addDropboxFile(filesToUpload).subscribe(data => {
this.notificationService.success(this.dropboxUploadCompleteMsg);
this.dropboxUploadInProgressMsg = '';
this.ref.markForCheck();
this.refresh();
});
},
cancel: () => {},
linkType: 'direct',
multiselect: true,
folderselect: false
};
Dropbox.choose(options);
}
/**
* Open a modal dialog to edit file properties
*
*/
editItem(i: number, id: string, file_name: string, resource_type: string) {
this.id = id;
const dialogRef = this.dialog.open(EditFileDialogComponent, {
data: { id: id, name: file_name, resource_type: resource_type }
});
dialogRef.afterClosed().subscribe(result => {
if (result === 1) {
// When using an edit things are little different, firstly we find record inside FileService by id
const foundIndex = this.exampleDatabase.dataChange.value.findIndex(
x => x.id === this.id
);
// Then you update that record using data from dialogData (values you entered)
this.exampleDatabase.dataChange.value[
foundIndex
] = this.fileService.getDialogData();
// And lastly refresh table
this.refresh();
}
});
}
/**
* Open a modal dialog to view detailed information about the available file types and their formats
*
*/
viewFileTypes() {
this.dialog.open(ViewFileTypesDialogComponent);
}
/**
* Open a modal dialog to preview file
*
*/
previewItem(fileId: string) {
this.isWait = true;
this.fileService.getFilePreview(fileId).subscribe(data => {
const previewData = {};
if (data?.results?.length && 'rowname' in data.results[0]) {
const minN = Math.min(data.results.length, 10);
let slicedData = data.results.slice(0, minN);
const columns = Object.keys(slicedData[0].values);
const rows = slicedData.map(elem => elem.rowname);
const values = slicedData.map(elem => {
let rowValues = [];
const elemValues = elem.values;
columns.forEach(col => rowValues.push(elemValues[col]));
return rowValues;
});
previewData['columns'] = columns;
previewData['rows'] = rows;
previewData['values'] = values;
}
this.isWait = false;
this.ref.markForCheck();
this.dialog.open(PreviewDialogComponent, {
data: {
previewData: previewData
}
});
});
}
/**
* Open a modal dialog to delete a file
*
*/
deleteItem(
i: number,
id: string,
file_name: string,
readable_resource_type: string
) {
this.id = id;
const dialogRef = this.dialog.open(DeleteFileDialogComponent, {
data: {
id: id,
name: file_name,
readable_resource_type: readable_resource_type
}
});
dialogRef.afterClosed().subscribe(result => {
if (result === 1) {
const foundIndex = this.exampleDatabase.dataChange.value.findIndex(
x => x.id === this.id
);
// for delete we use splice in order to remove single object from FileService
this.exampleDatabase.dataChange.value.splice(foundIndex, 1);
this.refresh();
}
});
}
public loadData() {
this.exampleDatabase = new FileService(
this.httpClient,
this.adapter,
this.analysesService
);
this.dataSource = new ExampleDataSource(
this.exampleDatabase,
this.paginator,
this.sort
);
fromEvent(this.filter.nativeElement, 'keyup')
.pipe(debounceTime(150), distinctUntilChanged())
.subscribe(() => {
if (!this.dataSource) {
return;
}
this.dataSource.filter = this.filter.nativeElement.value;
});
}
}
export class ExampleDataSource extends DataSource<File> {
_filterChange = new BehaviorSubject('');
get filter(): string {
return this._filterChange.value;
}
set filter(filter: string) {
this._filterChange.next(filter);
}
filteredData: File[] = [];
renderedData: File[] = [];
constructor(
public _exampleDatabase: FileService,
public _paginator: MatPaginator,
public _sort: MatSort
) {
super();
// Reset to the first page when the user changes the filter.
this._filterChange.subscribe(() => (this._paginator.pageIndex = 0));
}
/**
* Connect function called by the table to retrieve one stream containing the data to render.
*/
connect(): Observable<File[]> {
// Listen for any changes in the base data, sorting, filtering, or pagination
const displayDataChanges = [
this._exampleDatabase.dataChange,
this._sort.sortChange,
this._filterChange,
this._paginator.page
];
this._exampleDatabase.getAllFiles();
return merge(...displayDataChanges).pipe(
map(() => {
// Filter data
this.filteredData = this._exampleDatabase.data
.slice()
.filter((file: File) => {
const searchStr = (file.name + file.workspaces).toLowerCase();
return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
});
// Sort filtered data
const sortedData = this.sortData(this.filteredData.slice());
// Grab the page's slice of the filtered sorted data.
const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
this.renderedData = sortedData.splice(
startIndex,
this._paginator.pageSize
);
return this.renderedData;
})
);
}
disconnect() {}
/**
* Returns a sorted copy of the database data.
*
*/
sortData(data: File[]): File[] {
if (!this._sort.active || this._sort.direction === '') {
return data;
}
return data.sort((a, b) => {
let propertyA: any = '';
let propertyB: any = '';
switch (this._sort.active) {
case 'id':
[propertyA, propertyB] = [a.id, b.id];
break;
case 'name':
[propertyA, propertyB] = [a.name, b.name];
break;
case 'resource_type':
[propertyA, propertyB] = [a.resource_type, b.resource_type];
break;
case 'status':
[propertyA, propertyB] = [a.status, b.status];
break;
case 'workspace':
[propertyA, propertyB] = [a.workspaces, b.workspaces];
break;
case 'url':
[propertyA, propertyB] = [a.url, b.url];
break;
case 'created':
[propertyA, propertyB] = [new Date(a.created), new Date(b.created)];
break;
case 'size':
[propertyA, propertyB] = [a.size, b.size];
break;
}
const valueA = isNaN(+propertyA) ? propertyA : +propertyA;
const valueB = isNaN(+propertyB) ? propertyB : +propertyB;
return (
(valueA < valueB ? -1 : 1) * (this._sort.direction === 'asc' ? 1 : -1)
);
});
}
}
<mev-spinner-overlay *ngIf="isWait"></mev-spinner-overlay>
<div class="instruction">
Upload and manage your files here. The table below shows all files associated with your
account. After your files have been uploaded and validated, you can add them to
workspaces to conduct your analysis.
Note that we need to know what type of file we are working with. Hence, after upload, we
ask that you specify a file type. To learn more about the available file types and their
formats, <a (click)="viewFileTypes()">click here</a>. If applicable, you can preview the file to check that we have parsed it correctly. If
something does not appear to be correct, you may have to edit your file offline and
upload again.
</div>
<div class="file-btn-group">
<div>
<button mat-raised-button color="accent" (click)="addItem()">
<mat-icon aria-label="Upload a file from computer">add</mat-icon>
Upload a file from computer
</button>
<button mat-raised-button color="accent" (click)="addDropBoxItem()">
<mat-icon fontSet="material-icons-outlined" aria-label="Upload from Dropbox">backup</mat-icon>
Upload from Dropbox
</button>
</div>
<button mat-raised-button color="accent" (click)="refresh()">
<mat-icon aria-label="Refresh">refresh</mat-icon>
Refresh
</button>
</div>
<div class="file-list-container">
<div class="form">
<mat-form-field floatPlaceholder="never" color="accent">
<input matInput #filter placeholder="Filter files">
</mat-form-field>
</div>
<div *ngIf="uploadInProgressMsg" class="process-status">{{ uploadInProgressMsg }}</div>
<div *ngIf="dropboxUploadInProgressMsg" class="process-status">{{ dropboxUploadInProgressMsg }}</div>
<mat-table #table [dataSource]="dataSource" matSort matSortActive="created" matSortDirection="desc" class="mat-cell">
<ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef mat-sort-header>Id</mat-header-cell>
<mat-cell *matCellDef="let row">{{row.id}}</mat-cell>
</ng-container>
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">File name</mat-header-cell>
<mat-cell *matCellDef="let row" [class.warning-cell]="!row.resource_type"> {{row.name}}</mat-cell>
</ng-container>
<ng-container matColumnDef="resource_type">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">File type</mat-header-cell>
<mat-cell *matCellDef="let row"
[ngClass]="{'warning-cell': !row.resource_type, 'warning-cell--asterisk ': !row.resource_type}">
{{ row.readable_resource_type || 'Please specify file type' }}</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">Status</mat-header-cell>
<mat-cell *matCellDef="let row"> {{ row.status}}</mat-cell>
</ng-container>
<ng-container matColumnDef="size">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">File size</mat-header-cell>
<mat-cell *matCellDef="let row"> {{ row.size | byteName }}</mat-cell>
</ng-container>
<ng-container matColumnDef="created">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">Creation date</mat-header-cell>
<mat-cell *matCellDef="let row"> {{ row.created | date :'medium' }}</mat-cell>
</ng-container>
<ng-container matColumnDef="is_active">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">Active</mat-header-cell>
<mat-cell *matCellDef="let row"> {{ row.is_active ? 'Active' : 'Not active' }} </mat-cell>
</ng-container>
<ng-container matColumnDef="is_public">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">Public</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.is_public ? 'Public' : 'Private'}}</mat-cell>
</ng-container>
<ng-container matColumnDef="workspace">
<mat-header-cell *matHeaderCellDef mat-sort-header class="file-table-header">Workspace(s)</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.workspaces}}</mat-cell>
</ng-container>
<!-- actions -->
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef class="file-table-header">Actions</mat-header-cell>
<mat-cell *matCellDef="let row; let i=index;">
<button mat-icon-button color="accent" title="Preview"
(click)="previewItem(row.id)">
<mat-icon aria-label="Preview">visibility</mat-icon>
</button>
<button mat-icon-button color="accent" title="Edit" [disabled]="!row.is_active"
(click)="editItem(i, row.id, row.name, row.resource_type)">
<mat-icon aria-label="Edit">edit</mat-icon>
</button>
<button mat-icon-button color="accent" title="Delete file" [disabled]="!row.is_active"
(click)="deleteItem(i, row.id, row.name, row.readable_resource_type)">
<mat-icon aria-label="Delete">delete</mat-icon>
</button>
<button mat-icon-button color="accent" title="Download" disabled>
<mat-icon aria-label="Download">get_app</mat-icon>
</button>
<button mat-icon-button color="accent" title="Download to DropBox" disabled>
<mat-icon aria-label="Download">cloud_download</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</mat-table>
<div class="no-results" [style.display]="dataSource.renderedData.length == 0 ? '' : 'none'">
No results are available
</div>
<mat-paginator #paginator [length]="dataSource.filteredData.length" [pageIndex]="0" [pageSize]="10"
[pageSizeOptions]="[10, 25, 100]">
</mat-paginator>
<div class="warning-note" [style.display]="dataSource.renderedData.length >= 0 ? '' : 'none'">
* Please assign file types by clicking the Edit button in the table row
</div>
</div>