Bulk download overlay

This commit is contained in:
Adina Țeudan 2021-01-14 02:11:48 +02:00
parent 7efb2c65a2
commit b66632968d
25 changed files with 444 additions and 182 deletions

View File

@ -37,7 +37,7 @@ import { ToastrModule } from 'ngx-toastr';
import { ServiceWorkerModule } from '@angular/service-worker';
import { environment } from '../environments/environment';
import { AuthModule } from './auth/auth.module';
import { FileUploadModule } from './upload/file-upload.module';
import { FileUploadDownloadModule } from './upload-download/file-upload-download.module';
import { FullPageLoadingIndicatorComponent } from './utils/full-page-loading-indicator/full-page-loading-indicator.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { InitialsAvatarComponent } from './common/initials-avatar/initials-avatar.component';
@ -357,7 +357,7 @@ const matImports = [
enableHtml: true,
toastComponent: ToastComponent
}),
FileUploadModule,
FileUploadDownloadModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
ColorPickerModule,
AceEditorModule

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core';
import { AppStateService } from '../../../state/app-state.service';
import { UserService } from '../../../user/user.service';
import { FileManagementControllerService, ReanalysisControllerService } from '@redaction/red-ui-http';
@ -6,9 +6,9 @@ import { DialogService } from '../../../dialogs/dialog.service';
import { PermissionsService } from '../../../common/service/permissions.service';
import { FileStatusWrapper } from '../../file/model/file-status.wrapper';
import { FileActionService } from '../../file/service/file-action.service';
import { download } from '../../../utils/file-download-utils';
import { computerize } from '../../../utils/functions';
import { Observable } from 'rxjs';
import { StatusOverlayService } from '../../../upload-download/status-overlay.service';
import { FileDownloadService } from '../../../upload-download/file-download.service';
@Component({
selector: 'redaction-bulk-actions',
@ -27,7 +27,10 @@ export class BulkActionsComponent {
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _reanalysisControllerService: ReanalysisControllerService,
private readonly _permissionsService: PermissionsService,
private readonly _fileActionService: FileActionService
private readonly _fileActionService: FileActionService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _fileDownloadService: FileDownloadService
) {}
private get selectedFiles(): FileStatusWrapper[] {
@ -118,15 +121,12 @@ export class BulkActionsComponent {
// Bulk Download
downloadRedactedFiles() {
this.loading = true;
this._fileManagementControllerService
.downloadRedactedFiles({ fileIds: this.selectedFiles.map((file) => file.fileId) }, this._appStateService.activeProjectId, false, 'response')
.subscribe((data) => {
download(data, 'redacted_files_' + computerize(this._appStateService.activeProject.name) + '.zip');
})
.add(() => {
this.loading = false;
});
this._fileDownloadService.downloadProjectFiles(
this.selectedFiles.map((file) => file.fileId),
this._appStateService.activeProject
);
this._statusOverlayService.openDownloadStatusOverlay();
this._changeDetectorRef.detectChanges();
}
private _performBulkAction(obs: Observable<any>) {

View File

@ -2,10 +2,10 @@ import { ChangeDetectorRef, Component, HostListener, OnDestroy, OnInit, ViewChil
import { ActivatedRoute, Router } from '@angular/router';
import { NotificationService, NotificationType } from '../../notification/notification.service';
import { AppStateService } from '../../state/app-state.service';
import { FileDropOverlayService } from '../../upload/file-drop/service/file-drop-overlay.service';
import { FileUploadModel } from '../../upload/model/file-upload.model';
import { FileUploadService } from '../../upload/file-upload.service';
import { UploadStatusOverlayService } from '../../upload/upload-status-dialog/service/upload-status-overlay.service';
import { FileDropOverlayService } from '../../upload-download/file-drop/service/file-drop-overlay.service';
import { FileUploadModel } from '../../upload-download/model/file-upload.model';
import { FileUploadService } from '../../upload-download/file-upload.service';
import { StatusOverlayService } from '../../upload-download/status-overlay.service';
import { computerize, humanize } from '../../utils/functions';
import { DialogService } from '../../dialogs/dialog.service';
import { TranslateService } from '@ngx-translate/core';
@ -28,6 +28,7 @@ import { debounce } from '../../utils/debounce';
import { download } from '../../utils/file-download-utils';
import { convertFiles, handleFileDrop } from '../../utils/file-drop-utils';
import { FilterComponent } from '../../common/filter/filter.component';
import { FileDownloadService } from '../../upload-download/file-download.service';
@Component({
selector: 'redaction-project-overview-screen',
@ -67,7 +68,8 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private readonly _dialogService: DialogService,
private readonly _fileActionService: FileActionService,
private readonly _fileUploadService: FileUploadService,
private readonly _uploadStatusOverlayService: UploadStatusOverlayService,
private readonly _fileDownloadService: FileDownloadService,
private readonly _statusOverlayService: StatusOverlayService,
private readonly _router: Router,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _translateService: TranslateService,
@ -239,7 +241,7 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
private async _uploadFiles(files: FileUploadModel[]) {
const fileCount = await this._fileUploadService.uploadFiles(files);
if (fileCount) {
this._uploadStatusOverlayService.openStatusOverlay();
this._statusOverlayService.openUploadStatusOverlay();
}
this._changeDetectorRef.detectChanges();
}
@ -379,16 +381,12 @@ export class ProjectOverviewScreenComponent implements OnInit, OnDestroy {
// Download Files
public downloadRedactedFiles() {
this._fileManagementControllerService
.downloadRedactedFiles(
{ fileIds: this.appStateService.activeProject.files.map((file) => file.fileId) },
this.appStateService.activeProjectId,
false,
'response'
)
.subscribe((data) => {
download(data, 'redacted_files_' + computerize(this.appStateService.activeProject.name) + '.zip');
});
this._fileDownloadService.downloadProjectFiles(
this.appStateService.activeProject.files.map((file) => file.fileId),
this.appStateService.activeProject
);
this._statusOverlayService.openDownloadStatusOverlay();
this._changeDetectorRef.detectChanges();
}
public get canDownloadRedactedFiles() {

View File

@ -0,0 +1,62 @@
<section class="red-upload-download-overlay mat-elevation-z4">
<div (click)="collapsed = !collapsed" class="red-upload-download-header">
<div class="title">
{{ 'download-status.dialog.title' | translate: { len: downloadService.downloads.length } }}
</div>
<div *ngIf="!collapsed" class="collapse-icon">
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</div>
<div *ngIf="collapsed" class="collapse-icon">
<mat-icon svgIcon="red:arrow-up"></mat-icon>
</div>
<div (click)="closeDialog()" class="close-icon">
<mat-icon svgIcon="red:close"></mat-icon>
</div>
</div>
<div [hidden]="collapsed">
<div class="upload-download-list">
<div *ngFor="let model of downloadService.downloads" class="upload-download-list-item">
<div class="upload-download-line">
<div
matTooltipPosition="above"
[matTooltip]="'download-status.dialog.tooltip' | translate: { len: model.fileIds.length }"
class="upload-download-item-name"
>
{{ model.project.name }}
</div>
<mat-spinner *ngIf="!model.completed" class="upload-download-progress" diameter="15" color="primary"></mat-spinner>
<div *ngIf="model.completed && model.error" class="upload-download-progress error">
<mat-icon svgIcon="red:error"></mat-icon>
</div>
<div *ngIf="model.completed && !model.error" class="upload-download-progress ok">
<mat-icon svgIcon="red:check"></mat-icon>
</div>
</div>
<div *ngIf="model.completed && model.error" class="upload-download-line">
<div matTooltipPosition="above" [matTooltip]="model.error.message" class="upload-download-item-name error">
{{ model.error.message }}
</div>
<div class="upload-download-progress">
<div
(click)="downloadItem(model)"
[matTooltip]="'download-status.dialog.actions.re-download' | translate"
matTooltipPosition="above"
class="error-action pointer"
>
<mat-icon svgIcon="red:refresh"></mat-icon>
</div>
<div
(click)="cancelItem(model)"
[matTooltip]="'download-status.dialog.actions.cancel' | translate"
matTooltipPosition="above"
class="error-action pointer"
>
<mat-icon svgIcon="red:close"></mat-icon>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,3 @@
.red-upload-download-overlay {
left: 10px;
}

View File

@ -0,0 +1,49 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { OverlayRef } from '@angular/cdk/overlay';
import { FileDownloadService } from '../file-download.service';
import { ProjectDownloadModel } from '../model/project-download.model';
@Component({
selector: 'redaction-download-status-overlay',
templateUrl: './download-status-overlay.component.html',
styleUrls: ['./download-status-overlay.component.scss']
})
export class DownloadStatusOverlay implements OnInit {
collapsed = false;
downloadStatusInterval: number;
constructor(
public readonly downloadService: FileDownloadService,
private readonly _overlayRef: OverlayRef,
private readonly _changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
this.downloadStatusInterval = setInterval(() => {
// keep only errors
this.downloadService.downloads = this.downloadService.downloads.filter((projectDownload) => !projectDownload.completed || projectDownload.error);
if (this.downloadService.downloads.length === 0) {
this.closeDialog();
}
}, 2500);
}
cancelItem(item: ProjectDownloadModel) {
this.downloadService.removeProject(item);
}
downloadItem(item: ProjectDownloadModel) {
this.downloadService.scheduleDownload(item);
this._changeDetectorRef.detectChanges();
}
closeDialog() {
if (this.downloadStatusInterval) {
clearInterval(this.downloadStatusInterval);
this.downloadStatusInterval = null;
}
this.downloadService.stopAllDownloads();
this._overlayRef.detach();
}
}

View File

@ -0,0 +1,113 @@
import { ApplicationRef, Injectable } from '@angular/core';
import { AppStateService } from '../state/app-state.service';
import { FileManagementControllerService } from '@redaction/red-ui-http';
import { interval, Subscription } from 'rxjs';
import { AppConfigService } from '../app-config/app-config.service';
import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '../dialogs/dialog.service';
import { download } from '../utils/file-download-utils';
import { computerize } from '../utils/functions';
import { ProjectDownloadModel } from './model/project-download.model';
import { ProjectWrapper } from '../state/model/project.wrapper';
@Injectable({
providedIn: 'root'
})
export class FileDownloadService {
static readonly MAX_PARALLEL_DOWNLOADS = 2;
activeDownloadsCnt = 0;
public downloads: ProjectDownloadModel[] = [];
private _pendingDownloads: ProjectDownloadModel[] = [];
private _activeDownloads: Subscription[] = [];
constructor(
private readonly _applicationRef: ApplicationRef,
private readonly _translateService: TranslateService,
private readonly _appConfigService: AppConfigService,
private readonly _fileManagementControllerService: FileManagementControllerService,
private readonly _dialogService: DialogService
) {
interval(2500).subscribe((val) => {
this._handleDownloads();
});
}
public downloadProjectFiles(fileIds: string[], project: ProjectWrapper): void {
const item = { project, fileIds, completed: false, error: null };
this.downloads.push(item);
this.scheduleDownload(item);
}
stopAllDownloads() {
this.downloads = [];
}
public scheduleDownload(item: ProjectDownloadModel) {
item.completed = false;
item.error = null;
this._pendingDownloads.push(item);
}
private _handleDownloads() {
if (this._activeDownloads.length < FileDownloadService.MAX_PARALLEL_DOWNLOADS && this._pendingDownloads.length > 0) {
let cnt = FileDownloadService.MAX_PARALLEL_DOWNLOADS - this._activeDownloads.length;
while (cnt > 0) {
cnt--;
const popped = this._pendingDownloads.shift();
if (popped) {
const sub = this._createSubscription(popped);
this._activeDownloads.push(sub);
} else {
return;
}
}
}
}
private _createSubscription(downloadModel: ProjectDownloadModel) {
this.activeDownloadsCnt++;
const obs = this._fileManagementControllerService.downloadRedactedFiles(
{ fileIds: downloadModel.fileIds },
downloadModel.project.projectId,
false,
'response'
);
const subscription = obs.subscribe(
async (event) => {
if (event.status < 300) {
downloadModel.completed = true;
if (this.downloads.indexOf(downloadModel) !== -1) {
download(event, 'redacted_files_' + computerize(downloadModel.project.name) + '.zip');
}
} else {
downloadModel.completed = true;
downloadModel.error = { message: this._translateService.instant('download-status.error.generic') };
}
this._removeDownload(subscription);
},
() => {
downloadModel.completed = true;
downloadModel.error = { message: this._translateService.instant('upload-status.error.generic') };
this._removeDownload(subscription);
}
);
return subscription;
}
private _removeDownload(subscription: Subscription) {
const index = this._activeDownloads.indexOf(subscription);
if (index > -1) {
this._activeDownloads.splice(index, 1);
this.activeDownloadsCnt--;
}
}
removeProject(item: ProjectDownloadModel) {
const index = this.downloads.indexOf(item);
if (index > -1) {
this.downloads.splice(index, 1);
}
}
}

View File

@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/cor
import { FileUploadService } from '../file-upload.service';
import { FileUploadModel } from '../model/file-upload.model';
import { OverlayRef } from '@angular/cdk/overlay';
import { UploadStatusOverlayService } from '../upload-status-dialog/service/upload-status-overlay.service';
import { StatusOverlayService } from '../status-overlay.service';
import { handleFileDrop } from '../../utils/file-drop-utils';
import { AppStateService } from '../../state/app-state.service';
@ -17,7 +17,7 @@ export class FileDropComponent implements OnInit {
private readonly _fileUploadService: FileUploadService,
private readonly _appStateService: AppStateService,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _uploadStatusOverlayService: UploadStatusOverlayService
private readonly _statusOverlayService: StatusOverlayService
) {}
ngOnInit() {}
@ -47,7 +47,7 @@ export class FileDropComponent implements OnInit {
async uploadFiles(files: FileUploadModel[]) {
const fileCount = await this._fileUploadService.uploadFiles(files);
if (fileCount) {
this._uploadStatusOverlayService.openStatusOverlay();
this._statusOverlayService.openUploadStatusOverlay();
}
this._dialogRef.detach();
this._changeDetectorRef.detectChanges();

View File

@ -6,10 +6,12 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FileDropComponent } from './file-drop/file-drop.component';
import { OverlayModule } from '@angular/cdk/overlay';
import { UploadStatusOverlay } from './upload-status-dialog/upload-status-overlay.component';
import { UploadStatusOverlay } from './upload-status-overlay/upload-status-overlay.component';
import { NgxDropzoneModule } from 'ngx-dropzone';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { DownloadStatusOverlay } from './download-status-overlay/download-status-overlay.component';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@NgModule({
imports: [
@ -20,12 +22,13 @@ import { MatButtonModule } from '@angular/material/button';
MatListModule,
NgxDropzoneModule,
MatProgressBarModule,
MatProgressSpinnerModule,
OverlayModule,
MatButtonModule
],
declarations: [FileDropComponent, UploadStatusOverlay],
declarations: [FileDropComponent, UploadStatusOverlay, DownloadStatusOverlay],
providers: [],
entryComponents: [FileDropComponent, UploadStatusOverlay],
exports: [FileDropComponent, UploadStatusOverlay]
entryComponents: [FileDropComponent, UploadStatusOverlay, DownloadStatusOverlay],
exports: [FileDropComponent, UploadStatusOverlay, DownloadStatusOverlay]
})
export class FileUploadModule {}
export class FileUploadDownloadModule {}

View File

@ -0,0 +1,8 @@
import { ProjectWrapper } from '../../state/model/project.wrapper';
export interface ProjectDownloadModel {
fileIds: string[];
project: ProjectWrapper;
completed: boolean;
error: any;
}

View File

@ -0,0 +1,46 @@
import { Injectable, Injector } from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { UploadStatusOverlay } from './upload-status-overlay/upload-status-overlay.component';
import { DownloadStatusOverlay } from './download-status-overlay/download-status-overlay.component';
@Injectable({
providedIn: 'root'
})
export class StatusOverlayService {
private readonly _uploadStatusOverlayRef: OverlayRef;
private readonly _downloadStatusOverlayRef: OverlayRef;
constructor(private overlay: Overlay, private readonly _injector: Injector) {
this._uploadStatusOverlayRef = this.overlay.create();
this._downloadStatusOverlayRef = this.overlay.create();
}
private _createUploadInjector() {
return Injector.create({
providers: [{ provide: OverlayRef, useValue: this._uploadStatusOverlayRef }],
parent: this._injector
});
}
private _createDownloadInjector() {
return Injector.create({
providers: [{ provide: OverlayRef, useValue: this._downloadStatusOverlayRef }],
parent: this._injector
});
}
openUploadStatusOverlay() {
const component = new ComponentPortal(UploadStatusOverlay, null, this._createUploadInjector());
if (!this._uploadStatusOverlayRef.hasAttached()) {
this._uploadStatusOverlayRef.attach(component);
}
}
openDownloadStatusOverlay() {
const component = new ComponentPortal(DownloadStatusOverlay, null, this._createDownloadInjector());
if (!this._downloadStatusOverlayRef.hasAttached()) {
this._downloadStatusOverlayRef.attach(component);
}
}
}

View File

@ -1,8 +1,6 @@
<section class="red-upload-overlay mat-elevation-z4">
<div (click)="collapsed = !collapsed" class="red-upload-header">
<div class="title">
{{ 'upload-status.dialog.title' | translate: { p1: uploadService.files.length } }}
</div>
<section class="red-upload-download-overlay mat-elevation-z4">
<div (click)="collapsed = !collapsed" class="red-upload-download-header">
<div class="title" translate="upload-status.dialog.title"></div>
<div *ngIf="!collapsed" class="collapse-icon">
<mat-icon svgIcon="red:arrow-down"></mat-icon>
</div>
@ -14,26 +12,26 @@
</div>
</div>
<div [hidden]="collapsed">
<div class="upload-list">
<div *ngFor="let model of uploadService.files" class="upload-list-item">
<div class="upload-line">
<div matTooltipPosition="above" [matTooltip]="model.file?.name" class="upload-file-name">
<div class="upload-download-list">
<div *ngFor="let model of uploadService.files" class="upload-download-list-item">
<div class="upload-download-line">
<div matTooltipPosition="above" [matTooltip]="model.file?.name" class="upload-download-item-name">
{{ model.file?.name }}
</div>
<div *ngIf="!model.completed && model.progress < 100" class="upload-progress">{{ model.progress }}%</div>
<div *ngIf="model.completed && model.error" class="upload-progress error">
<div *ngIf="!model.completed && model.progress < 100" class="upload-download-progress">{{ model.progress }}%</div>
<div *ngIf="model.completed && model.error" class="upload-download-progress error">
<mat-icon svgIcon="red:error"></mat-icon>
</div>
<div *ngIf="model.completed && !model.error" class="upload-progress ok">
<div *ngIf="model.completed && !model.error" class="upload-download-progress ok">
<mat-icon svgIcon="red:check"></mat-icon>
</div>
</div>
<div *ngIf="model.completed && model.error" class="upload-line">
<div matTooltipPosition="above" [matTooltip]="model.error.message" class="upload-file-name error">
<div *ngIf="model.completed && model.error" class="upload-download-line">
<div matTooltipPosition="above" [matTooltip]="model.error.message" class="upload-download-item-name error">
{{ model.error.message }}
</div>
<div class="upload-progress">
<div class="upload-download-progress">
<div
*ngIf="!model.sizeError"
(click)="uploadItem(model)"
@ -53,7 +51,7 @@
</div>
</div>
</div>
<div *ngIf="!model.completed" class="upload-progress" mat-line>
<div *ngIf="!model.completed" class="upload-download-progress" mat-line>
<mat-progress-bar *ngIf="model.progress !== 100" [value]="model.progress" color="primary" mode="determinate"></mat-progress-bar>
</div>
</div>

View File

@ -0,0 +1,3 @@
.red-upload-download-overlay {
right: 10px;
}

View File

@ -1,35 +0,0 @@
import { Injectable, Injector } from '@angular/core';
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { UploadStatusOverlay } from '../upload-status-overlay.component';
@Injectable({
providedIn: 'root'
})
export class UploadStatusOverlayService {
private readonly _statusOverlayRef: OverlayRef;
constructor(private overlay: Overlay, private readonly _injector: Injector) {
this._statusOverlayRef = this.overlay.create();
}
private _createInjector() {
return Injector.create({
providers: [{ provide: OverlayRef, useValue: this._statusOverlayRef }],
parent: this._injector
});
}
openStatusOverlay() {
const component = new ComponentPortal(UploadStatusOverlay, null, this._createInjector());
if (!this._statusOverlayRef.hasAttached()) {
this._statusOverlayRef.attach(component);
}
}
closeStatusOverlay() {
if (this._statusOverlayRef) {
this._statusOverlayRef.detach();
}
}
}

View File

@ -1,90 +0,0 @@
@import '../../../assets/styles/red-variables';
section {
background: white;
position: fixed;
bottom: 10px;
right: 10px;
border: 2px solid $grey-1;
}
.upload-list {
max-height: 216px;
max-width: 400px;
overflow: auto;
}
.red-upload-header {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
padding: 10px;
background: $grey-1;
color: $white;
width: 380px;
cursor: pointer;
mat-icon {
color: $white;
}
}
.collapse-icon {
transform: translate(0, 2px);
mat-icon {
width: 20px;
}
}
.close-icon {
position: absolute;
right: 10px;
color: $white;
}
.upload-list-item {
padding: 8px;
mat-icon {
width: 16px;
}
.upload-line {
display: flex !important;
height: 20px;
position: relative;
justify-content: flex-start;
.upload-file-name {
text-overflow: ellipsis;
overflow: hidden;
display: block;
white-space: nowrap;
padding-right: 50px;
&.error {
color: $red-1;
padding-right: 60px;
padding-top: 4px;
}
}
.upload-progress {
position: absolute;
right: 0;
width: 50px;
display: flex;
justify-content: space-evenly;
&.error {
color: $red-1;
}
&.ok {
color: $blue-1;
}
}
}
}

View File

@ -1,4 +1,4 @@
import { FileUploadModel } from '../upload/model/file-upload.model';
import { FileUploadModel } from '../upload-download/model/file-upload.model';
export function handleFileDrop(event: DragEvent, projectId: string, uploadFiles: (files: FileUploadModel[]) => void) {
event.preventDefault();

View File

@ -11,7 +11,7 @@
"upload-status": {
"error": {
"file-size": "File to large. Limit is {{size}}MB.",
"generic": "Failed to upload file ... "
"generic": "Failed to upload file... "
},
"dialog": {
"title": "File Upload",
@ -21,6 +21,19 @@
}
}
},
"download-status": {
"error": {
"generic": "Failed to download project."
},
"dialog": {
"title": "Downloads ({{len}})",
"tooltip": "{{len}} files",
"actions": {
"re-download": "Retry Download",
"cancel": "Cancel Download"
}
}
},
"pdf-viewer": {
"text-popup": {
"actions": {

View File

@ -24,3 +24,4 @@
@import 'red-breadcrumbs';
@import 'red-editor';
@import 'red-slider';
@import 'red-upload-download-overlay';

View File

@ -0,0 +1,90 @@
@import 'red-variables';
.red-upload-download-overlay {
background: white;
position: fixed;
bottom: 10px;
border: 2px solid $grey-1;
.upload-download-list {
max-height: 216px;
max-width: 400px;
overflow: auto;
}
.red-upload-download-header {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
padding: 10px;
background: $grey-1;
color: $white;
width: 380px;
cursor: pointer;
mat-icon {
color: $white;
}
}
.collapse-icon {
transform: translate(0, 2px);
mat-icon {
width: 20px;
}
}
.close-icon {
position: absolute;
right: 10px;
color: $white;
}
.upload-download-list-item {
padding: 8px;
mat-icon {
width: 16px;
}
.upload-download-line {
display: flex !important;
height: 20px;
position: relative;
justify-content: flex-start;
align-items: center;
.upload-download-item-name {
text-overflow: ellipsis;
overflow: hidden;
display: block;
white-space: nowrap;
padding-right: 50px;
&.error {
color: $red-1;
padding-right: 60px;
padding-top: 4px;
}
}
.upload-download-progress {
position: absolute;
right: 0;
width: 50px;
display: flex;
justify-content: space-evenly;
&.error {
color: $red-1;
}
&.ok {
color: $blue-1;
}
}
}
}
}