Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First-pass at a pattern for handling state updates from SSE #139

Merged
merged 10 commits into from
Apr 1, 2021
131 changes: 122 additions & 9 deletions src/app/+run-tale/run-tale/run-tale.component.ts
@@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, NgZone, OnChanges, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, NgZone, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiConfiguration } from '@api/api-configuration';
Expand All @@ -17,9 +17,12 @@ import { WindowService } from '@framework/core/window.service';
import { enterZone } from '@framework/ngrx/enter-zone.operator';
import { TaleAuthor } from '@tales/models/tale-author';
import { routeAnimation } from '~/app/shared';
import { SyncService } from '@tales/sync.service';
import { Subscription } from 'rxjs';

import { ConnectGitRepoDialogComponent } from './modals/connect-git-repo-dialog/connect-git-repo-dialog.component';
import { PublishTaleDialogComponent } from './modals/publish-tale-dialog/publish-tale-dialog.component';
import { AlertModalComponent } from '@shared/common/components/alert-modal/alert-modal.component';

// import * as $ from 'jquery';
declare var $: any;
Expand All @@ -34,7 +37,7 @@ enum TaleExportFormat {
styleUrls: ['./run-tale.component.scss'],
animations: [routeAnimation]
})
export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges {
export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges, OnDestroy {
AccessLevel: any = AccessLevel;

taleId: string;
Expand All @@ -43,9 +46,16 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
creator: User;
currentTab = 'metadata';
showVersionsPanel = false;
fetching = false;

collaborators: { users: Array<User>, groups: Array<User> } = { users: [], groups: [] };

removeSubscription:Subscription;
subscription: Subscription;
taleUnsharedSubscription: Subscription;
taleInstanceLaunchingSubscription: Subscription;
taleInstanceRunningSubscription: Subscription;

constructor(
private ref: ChangeDetectorRef,
private zone: NgZone,
Expand All @@ -58,6 +68,7 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
private userService: UserService,
private tokenService: TokenService,
private versionService: VersionService,
private syncService: SyncService,
private config: ApiConfiguration,
private dialog: MatDialog
) {
Expand Down Expand Up @@ -86,10 +97,10 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
}

get dashboardLink(): string {
if (!this.tale || this.tale._accessLevel === AccessLevel.None) {
return '/public';
} else if (this.tale._accessLevel === AccessLevel.Admin) {
if (!this.tale || this.tale._accessLevel === AccessLevel.Admin) {
return '/mine';
} else if (this.tale._accessLevel === AccessLevel.None) {
return '/public';
} else if (this.tale._accessLevel === AccessLevel.Read || this.tale._accessLevel === AccessLevel.Write) {
return '/shared';
}
Expand All @@ -116,11 +127,13 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
return this.currentTab === tab;
}

refreshCollaborators(): void {
this.taleService.taleGetTaleAccess(this.taleId).subscribe(resp => {
refreshCollaborators(): Promise<any> {
return this.taleService.taleGetTaleAccess(this.taleId).toPromise().then((resp: any) => {
this.logger.info("Fetched collaborators:", resp);
this.collaborators = resp;
this.ref.detectChanges();

return resp;
});
}

Expand All @@ -145,7 +158,7 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
.subscribe((tale: Tale) => {
if (!tale) {
this.logger.error("Tale is null, something went horribly wrong");
this.router.navigate(['public']);
this.router.navigate(['mine']);

return;
}
Expand All @@ -171,7 +184,7 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
});
}, err => {
this.logger.error("Failed to fetch tale:", err);
this.router.navigate(['public']);
this.router.navigate(['mine']);
});
}

Expand All @@ -185,13 +198,113 @@ export class RunTaleComponent extends BaseComponent implements OnInit, OnChanges
ngOnInit(): void {
this.detectTaleId();
this.detectCurrentTab();

this.taleInstanceLaunchingSubscription = this.syncService.instanceLaunchingSubject.subscribe((resource: {taleId: string, instanceId: string}) => {
if (resource.taleId === this.taleId) {
this.instanceService.instanceGetInstance(resource.instanceId).subscribe((instance: Instance) => {
this.instance = instance;
this.ref.detectChanges();
});
}
});

this.taleInstanceRunningSubscription = this.syncService.instanceRunningSubject.subscribe((resource: {taleId: string, instanceId: string}) => {
if (resource.taleId === this.taleId) {
this.instanceService.instanceGetInstance(resource.instanceId).subscribe((instance: Instance) => {
this.instance = instance;
this.ref.detectChanges();
});
}
});

this.taleUnsharedSubscription = this.syncService.taleUnsharedSubject.subscribe((taleId) => {
if (taleId === this.taleId) {
// Update current collaborators list
this.refreshCollaborators();

this.taleService.taleGetTale(taleId).subscribe((tale: Tale) => {
// Update the current access level
this.tale._accessLevel = tale._accessLevel;
this.ref.detectChanges();

// If we can no longer view this Tale, redirect to the Catalog
if (tale._accessLevel < AccessLevel.Read) {
const returnRoute = this.tale ? this.dashboardLink : '/mine';
const dialogRef = this.dialog.open(AlertModalComponent, { data: {
title: 'Tale was unshared',
content: [
'This Tale is no longer being shared with you.',
'You will be redirected back to the catalog.'
]
}});
dialogRef.afterClosed().subscribe((result: boolean) => {
this.router.navigate([returnRoute]);
});
}
},
(err: any) => {
this.logger.error("Failed to fetch Tale:", err);
if (err.status === 403) {
const returnRoute = this.tale ? this.dashboardLink : '/mine';
const dialogRef = this.dialog.open(AlertModalComponent, { data: {
title: 'Tale was unshared',
content: [
'This Tale is no longer being shared with you.',
'You will be redirected back to the catalog.'
]
}});
dialogRef.afterClosed().subscribe((result: boolean) => {
this.router.navigate([returnRoute]);
});
}
});
}
});

this.removeSubscription = this.syncService.taleRemovedSubject.subscribe((taleId) => {
if (taleId === this.taleId) {
const returnRoute = this.tale ? this.dashboardLink : '/mine';
const dialogRef = this.dialog.open(AlertModalComponent, { data: {
title: 'Tale was deleted',
content: [
'The Tale was removed by another user.',
'You will be redirected back to the catalog.'
]
}});
dialogRef.afterClosed().subscribe((result: boolean) => {
this.router.navigate([returnRoute]);
});

return;
}
});

this.subscription = this.syncService.taleUpdatedSubject.subscribe((taleId) => {
this.logger.info("Tale update received from SyncService: ", taleId);
if (taleId === this.taleId && !this.fetching) {
this.fetching = true;
setTimeout(() => {
this.logger.info("Tale update applied via SyncService: ", taleId);
this.refresh();
this.fetching = false;
}, 1000);
}
});
}

ngOnChanges(): void {
this.detectTaleId();
this.detectCurrentTab();
}

ngOnDestroy(): void {
this.subscription.unsubscribe();
this.removeSubscription.unsubscribe();
this.taleUnsharedSubscription.unsubscribe();
this.taleInstanceLaunchingSubscription.unsubscribe();
this.taleInstanceRunningSubscription.unsubscribe();
}

performRecordedRun(): void {
this.logger.debug('Performing recorded run');
}
Expand Down
Expand Up @@ -8,7 +8,7 @@
<div class="inline fields ui grid">
<label class="two wide column right aligned">Title</label>
<div class="field thirteen wide column">
<input placeholder="Title is required." type="text" name="title" [(ngModel)]="tale.title" required>
<input placeholder="Title is required." type="text" name="title" [(ngModel)]="_editState.title" required>
</div>
</div>

Expand All @@ -17,15 +17,15 @@
<div class="thirteen wide column" style="padding-left:0;">
<h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator.firstName }} {{ creator.lastName }}</span></h4>

<form id="taleAuthorsSubForm" #taleAuthorsSubForm="ngForm" class="ui form" *ngIf="tale.authors.length">
<form id="taleAuthorsSubForm" #taleAuthorsSubForm="ngForm" class="ui form" *ngIf="_editState.authors.length">
<table class="ui table striped condensed">
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>ORCID</th>
<th></th>
</tr>
<tr *ngFor="let author of tale.authors; index as i; trackBy: trackByAuthorHash">
<tr *ngFor="let author of _editState.authors; index as i; trackBy: trackByAuthorHash">
<td><input type="text" name="firstName_{{i}}" placeholder="First name is required." [(ngModel)]="author.firstName" required></td>
<td><input type="text" name="lastName_{{i}}" placeholder="Last name is required." [(ngModel)]="author.lastName" required></td>
<td><input type="text" name="orcid_{{i}}" placeholder="ORCID is required." [(ngModel)]="author.orcid" required></td>
Expand All @@ -40,14 +40,14 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator
<div class="inline fields ui grid">
<label class="two wide column right aligned">Category</label>
<div class="field thirteen wide column">
<input placeholder="Category is required." type="text" name="category" [(ngModel)]="tale.category" required>
<input placeholder="Category is required." type="text" name="category" [(ngModel)]="_editState.category" required>
</div>
</div>

<div class="inline fields ui grid">
<label class="two wide column right aligned">Environment</label>
<div class="field thirteen wide column">
<select id="environmentDropdown" class="ui labeled icon fluid dropdown" name="imageId" [(ngModel)]="tale.imageId">
<select id="environmentDropdown" class="ui labeled icon fluid dropdown" name="imageId" [(ngModel)]="_editState.imageId">
<option class="item" [ngValue]="env._id" *ngFor="let env of (environments | async); index as i; trackBy: trackById">
<img class="ui avatar image" [src]="env.icon | safe:'url'" />
{{ env.name }}
Expand All @@ -60,8 +60,8 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator
<label class="two wide column right aligned">Datasets used</label>
<span *ngIf="!tale.dataSetCitation">No citable data</span>
<ul *ngIf="tale.dataSetCitation" style="max-width:60vw">
<li *ngFor="let citation of tale.dataSetCitation">
<a routerLink="/run/{{ tale._id }}" [queryParams]="{ tab: 'files' }" routerLinkActive="active" queryParamsHandling="merge">
<li *ngFor="let citation of _editState.dataSetCitation">
<a routerLink="/run/{{ _editState._id }}" [queryParams]="{ tab: 'files' }" routerLinkActive="active" queryParamsHandling="merge">
{{ citation }}
</a>
</li>
Expand All @@ -71,7 +71,7 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator
<div class="inline fields ui grid">
<label class="two wide column right aligned">License</label>
<div class="field thirteen wide column">
<select id="licenseDropdown" class="ui labeled icon fluid dropdown" name="licenseSPDX" [(ngModel)]="tale.licenseSPDX">
<select id="licenseDropdown" class="ui labeled icon fluid dropdown" name="licenseSPDX" [(ngModel)]="_editState.licenseSPDX">
<option class="item" [ngValue]="license.spdx" *ngFor="let license of (licenses | async); index as i; trackBy: trackBySpdx">
{{ license.name }}
</option>
Expand All @@ -82,14 +82,14 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator
<div class="inline fields ui grid">
<label class="two wide column right aligned">Date created</label>
<div class="field thirteen wide column">
<span>{{ tale.created | date:'full' }}</span>
<span>{{ _editState.created | date:'full' }}</span>
</div>
</div>

<div class="inline fields ui grid">
<label class="two wide column right aligned">Last updated</label>
<div class="field thirteen wide column">
<span>{{ tale.updated | date:'full' }}</span>
<span>{{ _editState.updated | date:'full' }}</span>
</div>
</div>

Expand All @@ -107,10 +107,10 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator
</a>
</div>
<div class="ui bottom attached tab segment" [ngClass]="{ 'active': !previewMarkdown }">
<textarea rows="5" type="text" name="description" placeholder="Description is required." [(ngModel)]="tale.description" required></textarea>
<textarea rows="5" type="text" name="description" placeholder="Description is required." [(ngModel)]="_editState.description" required></textarea>
</div>
<div class="ui bottom attached tab segment" [ngClass]="{ 'active': previewMarkdown }">
<markdown ngPreserveWhitespaces [data]="tale.description"></markdown>
<markdown ngPreserveWhitespaces [data]="_editState.description"></markdown>
</div>
</div>
</div>
Expand All @@ -119,7 +119,7 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator
<div class="inline fields ui grid">
<label class="two wide column right aligned">Illustration</label>
<div class="ui action field thirteen wide column">
<input placeholder="http://" type="text" name="icon" [(ngModel)]="tale.illustration">
<input placeholder="http://" type="text" name="icon" [(ngModel)]="_editState.illustration">
<div class="or"></div>
<button class="ui blue button" (click)="generateIcon()">Generate Illustration</button>
</div>
Expand All @@ -145,8 +145,8 @@ <h4 style="text-align: center">Created by <span style="color:#67c096">{{ creator

<div class="field toggle ui checkbox four wide column">
<input type="checkbox" name="public"
[checked]="tale.public"
(change)="tale.public=!tale.public">
[checked]="_editState.public"
(change)="_editState.public=!_editState.public">
<label>Public?</label>
</div>
</div>
Expand Down