import { Component, OnInit, OnDestroy } from '@angular/core';
import { ResourceService, ToasterService, ConfigService, NavigationHelperService } from '@sunbird/shared';
import { timer, Subject, combineLatest } from 'rxjs';
import { switchMap, map, filter, takeUntil } from 'rxjs/operators';
import * as _ from 'lodash-es';
import { ContentManagerService, ElectronDialogService } from '../../services';
import { Router, ActivatedRoute } from '@angular/router';
import { ILogEventInput, TelemetryService } from '@sunbird/telemetry';
import { UserService } from '../../../../../core/services/user/user.service';
@Component({
selector: 'app-content-manager',
templateUrl: './content-manager.component.html',
styleUrls: ['./content-manager.component.scss']
})
export class ContentManagerComponent implements OnInit, OnDestroy {
contentResponse: any = [];
isOpen = true;
callContentList = false;
callContentListTimer = false;
contentStatusObject = {};
subscription: any;
public unsubscribe$ = new Subject<void>();
localStatusArr = ['inProgress', 'inQueue', 'resume', 'resuming', 'pausing', 'canceling'];
cancelId: string;
apiCallTimer = timer(1000, 3000).pipe(filter(data => !data || (this.callContentList)));
apiCallSubject = new Subject();
completedCount: number;
handledFailedList = [];
unHandledFailedList = [];
deletedContents: string [] = [];
isWindows = false;
suggestedDrive: string;
drives: [];
visibility = true;
constructor(public contentManagerService: ContentManagerService,
public resourceService: ResourceService, public toasterService: ToasterService,
public electronDialogService: ElectronDialogService,
public configService: ConfigService,
public activatedRoute: ActivatedRoute,
public router: Router,
private telemetryService: TelemetryService,
public navigationHelperService: NavigationHelperService,
public userService: UserService
) {
this.getList();
document.addEventListener('content:import', (event) => {
this.isOpen = true;
this.apiCallSubject.next();
});
}
ngOnInit() {
// Subscribing to delete event to remove it from content manager list
this.contentManagerService.deletedContent.pipe(takeUntil(this.unsubscribe$)).subscribe((deletedIds: string[]) => {
this.isOpen = true;
this.apiCallSubject.next();
});
// Call download list initially
this.apiCallSubject.next();
// Call content list when clicked on add to library
this.contentManagerService.downloadEvent
.pipe(takeUntil(this.unsubscribe$))
.subscribe(data => {
this.isOpen = true;
this.apiCallSubject.next();
});
this.contentManagerService.downloadFailEvent
.pipe(takeUntil(this.unsubscribe$))
.subscribe(popupInfo => {
this.unHandledFailedList.push({name: popupInfo.failedContentName});
this.isWindows = popupInfo.isWindows;
/* istanbul ignore else */
if (popupInfo.isWindows && popupInfo.drives) {
this.drives = popupInfo.drives;
}
});
this.navigationHelperService.handleCMvisibility.
pipe(takeUntil(this.unsubscribe$)).subscribe(hideCM => {
this.visibility = !hideCM;
});
}
getList() {
// tslint:disable-next-line: deprecation
combineLatest(this.apiCallTimer, this.apiCallSubject, (data1, data2) => true)
.pipe(takeUntil(this.unsubscribe$), filter(() => this.isOpen), switchMap(() => this.contentManagerService.getContentList()),
map((resp: any) => {
this.callContentList = false;
let completedCount = 0;
_.forEach(_.get(resp, 'result.response.contents'), (value) => {
const data = this.contentStatusObject[value.id];
if (data) { value.status = data.currentStatus; }
if (_.includes(this.localStatusArr, value.status)) {
this.callContentList = true;
}
if (value.status === 'completed') {
completedCount += 1;
}
});
if ((completedCount > this.completedCount) && this.completedCount !== undefined) {
this.contentManagerService.completeEvent.emit();
}
this.completedCount = completedCount;
return _.get(resp, 'result.response.contents');
})).subscribe((apiResponse: any) => {
this.handleInsufficientMemoryError(apiResponse);
this.contentResponse = _.filter(apiResponse, (o) => {
if (o.status !== 'canceled' && o.addedUsing === 'download') {
const statusMsg = this.getContentStatus(o.contentDownloadList);
o.status = statusMsg ? statusMsg : o.status;
}
return o.status !== 'canceled';
});
});
}
getContentStatus(content) {
const notCompleted = _.find(content, c => {
return (!_.includes(['COMPLETE', 'EXTRACT'], c.step));
});
if (!notCompleted) {
const extracting = _.find(content, c => {
return c.step === 'EXTRACT';
});
if (extracting) { return 'extract'; }
}
}
async handleInsufficientMemoryError(allContentList) {
const noSpaceContentList = _.filter(allContentList, (content) =>
content.failedCode === 'LOW_DISK_SPACE' && content.status === 'failed');
if (noSpaceContentList.length) {
const failedContent = _.differenceBy(noSpaceContentList, this.handledFailedList, 'id');
if (failedContent[0]) {
this.logTelemetry(failedContent[0]);
}
const popupInfo: any = {
failedContentName: failedContent,
};
try {
const response: any = await this.contentManagerService.getSuggestedDrive(popupInfo);
this.unHandledFailedList = response.failedContentName;
this.isWindows = response.isWindows;
if (response.isWindows && response.drives) {
this.drives = response.drives;
}
} catch (error) {
this.unHandledFailedList = popupInfo.failedContentName;
}
}
}
removeFromHandledFailedList(id) {
this.handledFailedList = _.filter(this.handledFailedList, (content) => content.id !== id);
}
closeModal(event: any) {
this.handledFailedList.push(...this.unHandledFailedList);
this.unHandledFailedList = [];
this.isWindows = false;
/* istanbul ignore else */
if (event.selectedDrive) {
const req = {
request: {
path: event.selectedDrive.name
}
};
this.contentManagerService.changeContentLocation(req).subscribe(response => {
this.toasterService.success(this.resourceService.messages.stmsg.contentLocationChanged);
}, error => {
this.toasterService.error(this.resourceService.messages.fmsg.m0097);
});
}
}
contentManagerActions(type: string, action: string, id: string) {
// Unique download/import Id
switch (`${action.toUpperCase()}_${type.toUpperCase()}`) {
case 'PAUSE_IMPORT':
this.pauseImportContent(id);
break;
case 'RESUME_IMPORT':
this.resumeImportContent(id);
break;
case 'CANCEL_IMPORT':
this.cancelImportContent(id);
break;
case 'RETRY_IMPORT':
this.retryImportContent(id);
break;
case 'PAUSE_DOWNLOAD':
this.pauseDownloadContent(id);
break;
case 'RESUME_DOWNLOAD':
this.resumeDownloadContent(id);
break;
case 'CANCEL_DOWNLOAD':
this.cancelDownloadContent(id);
break;
case 'RETRY_DOWNLOAD':
this.retryDownloadContent(id);
break;
}
}
updateLocalStatus(contentData, currentStatus) {
this.contentStatusObject[contentData.id] = {
currentStatus: currentStatus,
previousState: contentData.status
};
const data = _.find(this.contentResponse, { id: contentData.id });
data.status = currentStatus;
}
private getSubscription(id) {
const _this = this;
return ({
next(apiResponse: any) {
_this.deleteLocalContentStatus(id);
_this.apiCallSubject.next();
_this.removeFromHandledFailedList(id);
},
error(err) {
_this.deleteLocalContentStatus(id);
_this.toasterService.error(_this.resourceService.messages.fmsg.m0097);
_this.apiCallSubject.next();
_this.removeFromHandledFailedList(id);
}
});
}
pauseDownloadContent(id) {
this.contentManagerService.pauseDownloadContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
resumeDownloadContent(id) {
this.contentManagerService.resumeDownloadContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
cancelDownloadContent(id) {
this.contentManagerService.cancelDownloadContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
retryDownloadContent(id) {
this.contentManagerService.retryDownloadContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
pauseImportContent(id) {
this.contentManagerService.pauseImportContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
resumeImportContent(id) {
this.contentManagerService.resumeImportContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
cancelImportContent(id) {
this.contentManagerService.cancelImportContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
retryImportContent(id) {
this.contentManagerService.retryImportContent(id).pipe(takeUntil(this.unsubscribe$)).subscribe(this.getSubscription(id));
}
deleteLocalContentStatus(id) {
delete this.contentStatusObject[id];
}
getContentPercentage(progressSize, totalSize) {
return (progressSize / totalSize) * 100;
}
openContent(data) {
const { contentId, mimeType, status } = data;
if (status === 'completed') {
let path;
if (mimeType === this.configService.appConfig.PLAYER_CONFIG.MIME_TYPE.collection) {
if (typeof _.get(data, 'trackable') === 'string') {
try {
data.trackable = JSON.parse(data.trackable);
} catch (error) {
console.error('Error while json parse', error);
}
}
if (_.toUpper(_.get(data, 'contentType')) === 'COURSE' || _.toUpper(_.get(data, 'trackable.enabled')) === 'YES') {
path = this.userService.loggedIn ? 'learn/course' : 'explore-course/course';
} else {
path = 'play/collection';
}
} else if(mimeType === this.configService.appConfig.PLAYER_CONFIG.MIME_TYPE.questionset) {
path = 'resources/play/questionset';
} else {
path = 'play/content';
}
this.router.navigate([path, contentId]);
}
}
getTelemetryInteractData() {
const pageId = _.get(this.activatedRoute, 'snapshot.root.firstChild.data.telemetry.env') ||
_.get(this.activatedRoute, 'snapshot.data.telemetry.env') ||
_.get(this.activatedRoute.snapshot.firstChild, 'children[0].data.telemetry.env');
return {
id: this.isOpen ? 'content-manager-close' : 'content-manager-open',
type: 'click',
pageid: pageId
};
}
getButtonsInteractData(id, percentage) {
const pageId = _.get(this.activatedRoute, 'snapshot.root.firstChild.data.telemetry.env') ||
_.get(this.activatedRoute, 'snapshot.data.telemetry.env') ||
_.get(this.activatedRoute.snapshot.firstChild, 'children[0].data.telemetry.env');
const interactData = {
id: id,
type: 'click',
pageid: pageId
};
if (percentage) {
interactData['extra'] = {
percentage: percentage
};
}
return interactData;
}
getContentList() {
if (this.isOpen) {
this.apiCallSubject.next();
}
}
logTelemetry(response: any) {
const input: ILogEventInput = {
context: {
env: _.get(this.activatedRoute, 'snapshot.root.firstChild.data.telemetry.env') ||
_.get(this.activatedRoute, 'snapshot.data.telemetry.env') ||
_.get(this.activatedRoute.snapshot.firstChild, 'children[0].data.telemetry.env')
},
object: {
id: response.id,
type: response.contentType || 'content',
ver: `${response.pkgVersion || ''}`
},
edata: {
type: 'api_call',
level: 'WARN',
message: response.failedReason
}
};
this.telemetryService.log(input);
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
<mat-accordion class="sb-mat-accordion sb-desktop-content-manager"
[ngClass]="{'sb-single-pannel-accordion-fixed': isOpen === true, 'hide': visibility === false}"
*ngIf="contentResponse?.length">
<mat-expansion-panel [expanded]="isOpen" (click)="getContentList()">
<mat-expansion-panel-header>
<mat-panel-title class="sb-mat-accordion__title" appTelemetryInteract [telemetryInteractEdata]="getTelemetryInteractData()">
{{resourceService?.frmelmnts?.lbl?.contentManager}}
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="contentResponse" class="sb-mat-accordion__content">
<div class="sb-single-pannel-accordion-list-item flex-ai-center text-left"
*ngFor="let data of contentResponse">
<i class="check circle icon success-icon mr-8 fs-1-429" *ngIf="data.status === 'completed'"></i>
<div class="download-details-container" [ngClass]="{'cursor-pointer': data.status === 'completed'}" tabindex="0"
(click)="openContent(data)">
<div class="d-flex flex-ai-center flex-w-wrap">
<div class="download-text mr-auto">{{data.name}}</div>
<!-- Buttons -->
<button *ngIf="cancelId !== data.id && (data.status === 'pausing' || data.status === 'inProgress')"
type="button" class="sb-btn sb-btn-xs sb-btn-outline-primary ml-8" tabindex="0"
(click)="contentManagerActions(data.addedUsing, 'Pause', data.id); updateLocalStatus(data, 'pausing');"
[disabled]="data.status === 'pausing'" appTelemetryInteract
[telemetryInteractEdata]="getButtonsInteractData('pause', data.downloadedSize)"
[telemetryInteractCdata]="[{ 'type': data.addedUsing, 'id': data.id }]"
[telemetryInteractObject]="{id:data.contentId, type: 'Content', ver:data.pkgVersion ? data.pkgVersion : ''}">
{{resourceService?.frmelmnts?.btn?.pause}}
</button>
<button
*ngIf="cancelId !== data.id && (data.status === 'paused' || data.status === 'resume' || data.status === 'resuming')"
type="button" class="sb-btn sb-btn-xs sb-btn-outline-primary ml-8" tabindex="0"
(click)="contentManagerActions(data.addedUsing, 'Resume', data.id); updateLocalStatus(data, 'resuming');"
[disabled]="data.status === 'resuming' || data.status === 'resume'" appTelemetryInteract
[telemetryInteractEdata]="getButtonsInteractData('resume', data.downloadedSize)"
[telemetryInteractCdata]="[{ 'type': data.addedUsing, 'id': data.id }]"
[telemetryInteractObject]="{id:data.contentId, type: 'Content', ver:data.pkgVersion ? data.pkgVersion : ''}">
{{resourceService?.frmelmnts?.btn?.resume}}
</button>
<button
*ngIf="(data.status === 'inProgress' || data.status === 'inQueue' || data.status === 'paused') && cancelId !== data.id"
type="button" class="sb-btn sb-btn-xs sb-btn-outline-error ml-8" tabindex="0"
(click)="cancelId = data.id;" [disabled]="data.status === 'canceling'" appTelemetryInteract
[telemetryInteractEdata]="getButtonsInteractData('cancel', data.downloadedSize)"
[telemetryInteractCdata]="[{ 'type': data.addedUsing, 'id': data.id }]"
[telemetryInteractObject]="{id:data.contentId, type: 'Content', ver:data.pkgVersion ? data.pkgVersion : ''}">
{{resourceService?.frmelmnts?.btn?.cancel}}
</button>
<button *ngIf="data.status === 'failed' || data.status === 'retrying'" type="button"
class="sb-btn sb-btn-xs sb-btn-outline-warning ml-8" tabindex="0"
(click)="contentManagerActions(data.addedUsing, 'Retry', data.id); updateLocalStatus(data, 'retrying');"
[disabled]="data.status === 'retrying'" appTelemetryInteract
[telemetryInteractEdata]="getButtonsInteractData('retry', data.downloadedSize)"
[telemetryInteractCdata]="[{ 'type': data.addedUsing, 'id': data.id }]"
[telemetryInteractObject]="{id:data.contentId, type: 'Content', ver:data.pkgVersion ? data.pkgVersion : ''}">
{{resourceService?.frmelmnts?.btn?.retry}}
</button>
<!-- Buttons -->
</div>
<!-- File details -->
<div class="info-text" *ngIf="data.totalSize && data.status !== 'inProgress'">{{data.totalSize | filesize}}
</div>
<div *ngIf="data.addedUsing === 'download' && data.status === 'inProgress'" class="info-text">
{{resourceService?.frmelmnts?.lbl?.downloading}} {{data.totalSize | filesize}}</div>
<div *ngIf="data.addedUsing === 'import' && data.status === 'inProgress'" class="info-text">
{{resourceService?.frmelmnts?.lbl?.uploading}}</div>
<!-- File details -->
<!-- Progress bar -->
<mat-progress-bar *ngIf="data.status === 'inProgress' && data.addedUsing === 'download'" mode="determinate" [value]="getContentPercentage(data.downloadedSize, data.totalSize)" class="my-0 mr-0"></mat-progress-bar>
<mat-progress-bar *ngIf="data.status === 'inProgress' && data.addedUsing === 'import'" mode="determinate" [value]="data.downloadedSize" class="my-0 mr-0"></mat-progress-bar>
<!-- Progress bar -->
<!-- Content status -->
<div *ngIf="data.status === 'paused' && data.addedUsing === 'import'" class="fs-0-785 sb-color-primary">
{{resourceService?.frmelmnts?.lbl?.uploadPaused}} </div>
<div *ngIf="data.status === 'retrying' && data.addedUsing === 'download'" class="fs-0-785 sb-color-primary">
{{resourceService?.frmelmnts?.btn?.resuming}} </div>
<div *ngIf="data.status === 'paused' && data.addedUsing === 'download'" class="fs-0-785 sb-color-primary">
{{resourceService?.frmelmnts?.lbl?.downloadPaused}} </div>
<div *ngIf="(data.status === 'resuming' || data.status === 'resume')" class="fs-0-785 sb-color-primary">
{{resourceService?.frmelmnts?.btn?.resuming}} </div>
<div *ngIf="data.status === 'pausing'" class="fs-0-785 sb-color-primary">
{{resourceService?.frmelmnts?.btn?.pausing}} </div>
<div *ngIf="data.status === 'canceling'" class="fs-0-785 sb-color-primary">
{{resourceService?.frmelmnts?.btn?.canceling}} </div>
<div *ngIf="data.status === 'completed'" class="fs-0-785 sb-color-success">
{{resourceService?.frmelmnts?.btn?.completed}} </div>
<div *ngIf="data.status === 'extract'" class="fs-0-785 sb-color-success">
{{resourceService?.frmelmnts?.desktop?.lbl?.completing}} </div>
<div *ngIf="data.addedUsing === 'download' && data.status === 'failed'" class="fs-0-785 sb-color-error">
{{resourceService?.frmelmnts?.lbl?.downloadFailed}} </div>
<div *ngIf="data.addedUsing === 'import' && data.status === 'failed'" class="fs-0-785 sb-color-error">
{{resourceService?.frmelmnts?.lbl?.uploadFailed}} </div>
<div *ngIf="data.addedUsing === 'import' && data.status === 'inQueue'" class="fs-0-785 sb-color-warning">
{{resourceService?.frmelmnts?.lbl?.waitingForUpload}} </div>
<div *ngIf="data.addedUsing === 'download' && data.status === 'inQueue'" class="fs-0-785 sb-color-warning">
{{resourceService?.frmelmnts?.lbl?.waitingForDownload}} </div>
<!-- Content status -->
<!-- Cancel confirmation message -->
<div class="d-flex flex-ai-center mt-8 ml-0"
*ngIf="cancelId === data.id && (data.status !== 'completed' && data.status !== 'failed')">
<div *ngIf="data.addedUsing === 'import'" class="fs-1 font-weight-bold sb-color-error mr-auto">
{{resourceService?.frmelmnts?.lbl?.cancelUpload}}
</div>
<div *ngIf="data.addedUsing === 'download'" class="fs-1 font-weight-bold sb-color-error mr-auto">
{{resourceService?.frmelmnts?.lbl?.cancelDownload}}
</div>
<button tabindex="0"
(click)="contentManagerActions(data.addedUsing, 'Cancel', data.id); updateLocalStatus(data, 'canceling'); cancelId = '';"
class="sb-btn sb-btn-xs sb-btn-outline-primary ml-8" type="button" appTelemetryInteract
[telemetryInteractEdata]="getButtonsInteractData('confirm-cancel', getContentPercentage(data.downloadedSize, data.totalSize))"
[telemetryInteractCdata]="[{ 'type': data.addedUsing, 'id': data.id }]"
[telemetryInteractObject]="{id:data.contentId, type: 'Content', ver:data.pkgVersion ? data.pkgVersion : ''}">
{{resourceService?.frmelmnts?.btn?.yes}} </button>
<button tabindex="0" (click)="cancelId = '';" class="sb-btn sb-btn-xs sb-btn-outline-error ml-8"
type="button" appTelemetryInteract
[telemetryInteractEdata]="getButtonsInteractData('deny-cancel', getContentPercentage(data.downloadedSize, data.totalSize))"
[telemetryInteractCdata]="[{ 'type': data.addedUsing, 'id': data.id }]"
[telemetryInteractObject]="{id:data.contentId, type: 'Content', ver:data.pkgVersion ? data.pkgVersion : ''}">
{{resourceService?.frmelmnts?.btn?.no}} </button>
</div>
<!-- Cancel confirmation message -->
</div>
</div>
</div>
</mat-expansion-panel>
</mat-accordion>
<app-content-manager-info-pop-up *ngIf="unHandledFailedList?.length" [showContentChangeWarning]="isWindows"
[failedList]="unHandledFailedList" [drives]="drives" (dismissed)="closeModal($event)">
</app-content-manager-info-pop-up>