diff --git a/src/app/core/app.component.html b/src/app/core/app.component.html index 6c46b1de8..9cdc24699 100644 --- a/src/app/core/app.component.html +++ b/src/app/core/app.component.html @@ -1 +1,2 @@ + diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 2d354cdb4..34ec4d7cd 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,8 +1,8 @@ import { Injectable, PLATFORM_ID, Inject } from '@angular/core'; import { ConfigService } from '../config/config.service'; import { Auth, Role, UserGroup } from './auth.model'; -import { Observable, Subject, of } from 'rxjs'; -import { map, take, catchError } from 'rxjs/operators'; +import { interval, Observable, Subject, of } from 'rxjs'; +import { catchError, concatMap, filter, map, take, takeWhile } from 'rxjs/operators'; import { HttpClient, HttpParams } from '@angular/common/http'; import { isPlatformBrowser } from '@angular/common'; import { UserDownload, AllUserDownloads } from '@gsrs-core/auth/user-downloads/download.model'; @@ -96,6 +96,36 @@ export class AuthService { ); } + // Helper function to create an Observable that emits when the popup login window closes + private waitForPopupToClose(popupWindow: Window): Observable { + return interval(1000).pipe( + takeWhile(() => !popupWindow.closed, true), + filter(() => popupWindow.closed) + ); + } + + // Method to handle pFDA login (using popup window) and return success/unsuccess flag + pfdaLogin(): Observable { + const height = 700; + const width = 700; + const left = (screen.width / 2) - (width / 2); + const top = (screen.height / 2) - (height / 2); + const loginWindow = window.open( + '/login?force_fda_sso_login=true&user_return_to=%2Fginas%2Fclose-pfda-login-window', + 'pFDA Login', + `height=${height},width=${width},top=${top},left=${left}` + ); + + return this.waitForPopupToClose(loginWindow).pipe( + concatMap(() => + this.getAuth().pipe( + map(authAfterLogin => !!authAfterLogin), // Convert to boolean (true = success) + catchError(() => of(false)) // Return false if there's an error + ) + ) + ); + } + getAuth(): Observable { return new Observable(observer => { @@ -143,6 +173,10 @@ export class AuthService { }); } + private deleteCookie(name: string) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + } + logout(): void { // if ( // !this.configService.configData @@ -161,13 +195,22 @@ export class AuthService { document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; } } - const url = `${this.configService.configData.apiBaseUrl}logout`; - this.http.get(url).subscribe(() => { + let url = `${this.configService.configData.apiBaseUrl}logout`; + let method = 'GET'; + + if (this.configService.configData.isPfdaVersion) { + url = '/logout'; + method = 'DELETE'; + } + + this.http.request(method, url).subscribe(() => { this._auth = null; this._authUpdate.next(null); + this.deleteCookie('sessionExpiredAt'); }, error => { this._auth = null; this._authUpdate.next(null); + this.deleteCookie('sessionExpiredAt'); }); } @@ -307,11 +350,11 @@ export class AuthService { if (this.configService.configData && this.configService.configData.dummyWhoami) { observer.next(this.configService.configData.dummyWhoami); } else { - this.http.get(`${url}whoami`) + this.http.get(`${url}whoami`) .subscribe( auth => { - // console.log("Authorized as"); - // console.log(auth); + // console.log("Authorized as"); + // console.log(auth); observer.next(auth); }, err => { diff --git a/src/app/core/auth/csrf-token.interceptor.ts b/src/app/core/auth/csrf-token.interceptor.ts index 597f28061..412d3b5c1 100644 --- a/src/app/core/auth/csrf-token.interceptor.ts +++ b/src/app/core/auth/csrf-token.interceptor.ts @@ -1,35 +1,37 @@ import { Injectable } from '@angular/core'; -import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpClient } from '@angular/common/http'; +import { from, Observable, switchMap } from 'rxjs'; +import { ConfigService } from "@gsrs-core/config"; @Injectable() export class CsrfTokenInterceptor implements HttpInterceptor { - - constructor() {} + constructor(private http: HttpClient, private configService: ConfigService) {} intercept(request: HttpRequest, next: HttpHandler): Observable> { - // CSRF token for GET and HEAD is not needed - if (['GET', 'HEAD'].includes(request.method)) { + // CSRF token request needed in pFDA version only + if (['GET', 'HEAD'].includes(request.method) + || !(this.configService.configData?.isPfdaVersion)) { return next.handle(request); } - // Parse CSRF token from HTML meta tag - const metaTag: HTMLMetaElement | null = document.querySelector('meta[name=csrf-token]'); - let csrfToken = metaTag?.content; - if (csrfToken === undefined) { - csrfToken = 'CSRF-TOKEN-NOT-PARSED'; - } + return from(this.fetchCsrfToken()).pipe( + switchMap((token: string) => { + const modifiedRequest = this.addCsrfToken(request, token); + return next.handle(modifiedRequest); + }) + ); + } - // Clone the request and add the CSRF token to the headers - const modifiedRequest = request.clone({ + private fetchCsrfToken(): Promise { + return this.http.get(`/csrf-token`, { responseType: 'text' }).toPromise(); + } + + private addCsrfToken(request: HttpRequest, token: string): HttpRequest { + return request.clone({ setHeaders: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'X-CSRF-Token': csrfToken + 'X-CSRF-Token': token } }); - - // Pass the modified request to the next handler - return next.handle(modifiedRequest); } } diff --git a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html index c35a6869c..2903ea40f 100644 --- a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.html @@ -6,6 +6,7 @@

+
diff --git a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts index 3dbae841e..071c55c64 100644 --- a/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration-dialog/session-expiration-dialog.component.ts @@ -2,8 +2,9 @@ import { Component, OnInit, Inject } from '@angular/core'; import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { ConfigService, SessionExpirationWarning } from '@gsrs-core/config'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { AnyNsRecord } from 'dns'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { AuthService } from '@gsrs-core/auth'; +import { concatMap } from "rxjs" @Component({ selector: 'app-session-expiration-dialog', @@ -23,7 +24,9 @@ export class SessionExpirationDialogComponent implements OnInit { @Inject(MAT_DIALOG_DATA) public data: any, // N.B. injected services has to come after data private router: Router, - private http: HttpClient + private http: HttpClient, + private authService: AuthService, + public configService: ConfigService ) { this.sessionExpirationWarning = data.sessionExpirationWarning; this.sessionExpiringAt = data.sessionExpiringAt; @@ -49,11 +52,10 @@ export class SessionExpirationDialogComponent implements OnInit { if (this.timeRemainingSeconds > 0) { const remainingMinutes = Math.floor(this.timeRemainingSeconds / 60); - const reminaingSeconds = String(this.timeRemainingSeconds % 60).padStart(2, '0'); + const remainingSeconds = String(this.timeRemainingSeconds % 60).padStart(2, '0'); this.dialogTitle = "Session Ending Soon" - this.dialogMessage = `You will be logged out in ${remainingMinutes}:${reminaingSeconds}` - } - else { + this.dialogMessage = `You will be logged out in ${remainingMinutes}:${remainingSeconds}` + } else { this.dialogTitle = "Session Ended" this.dialogMessage = "Your session has expired, please login again." } @@ -75,6 +77,23 @@ export class SessionExpirationDialogComponent implements OnInit { } login() { - window.location.assign('/login'); + if (this.configService.configData.isPfdaVersion) { + this.authService.pfdaLogin().pipe( + concatMap(success => { + console.log('success: ', success); + if (success) { + this.closeDialog(); + return this.authService.getAuth(); + } + })).subscribe(); + } else { + window.location.assign('/login'); + } + } + + proceedAsGuest() { + clearInterval(this.updateDialogInterval); + this.authService.logout(); + this.closeDialog(); } } diff --git a/src/app/core/auth/session-expiration/session-expiration.component.ts b/src/app/core/auth/session-expiration/session-expiration.component.ts index 14fb287a5..16f4b7967 100644 --- a/src/app/core/auth/session-expiration/session-expiration.component.ts +++ b/src/app/core/auth/session-expiration/session-expiration.component.ts @@ -1,12 +1,10 @@ -import { Router, Event as NavigationEvent, NavigationStart } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { OverlayContainer } from '@angular/cdk/overlay'; -import { HttpClient } from '@angular/common/http'; import { ConfigService, SessionExpirationWarning } from '@gsrs-core/config'; import { AuthService } from '../auth.service'; import { SessionExpirationDialogComponent } from './session-expiration-dialog/session-expiration-dialog.component' -import { Subscription } from 'rxjs'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogRef, MatDialogState } from '@angular/material/dialog'; +import { UtilsService } from "@gsrs-core/utils"; @Component({ selector: 'app-session-expiration', @@ -16,85 +14,133 @@ export class SessionExpirationComponent implements OnInit { sessionExpirationWarning: SessionExpirationWarning = null; sessionExpiringAt: number; private overlayContainer: HTMLElement; - private subscriptions: Array = []; - private expirationTimer: any; + private refreshInterval: any; + private activityRefreshInterval: any; + private userActive: boolean = false; + private baseHref: string = '/ginas/app/'; + private extendSessionDialog: MatDialogRef; + + private static instance?: SessionExpirationComponent = undefined; + private static sessionExpirationCheckInterval = null; constructor( - private router: Router, private configService: ConfigService, private authService: AuthService, - private http: HttpClient, - private dialog: MatDialog, - private overlayContainerService: OverlayContainer + private matDialog: MatDialog, + private overlayContainerService: OverlayContainer, + private utilsService: UtilsService ) { + if (SessionExpirationComponent.instance !== undefined) { + return SessionExpirationComponent.instance; + } this.sessionExpirationWarning = configService.configData.sessionExpirationWarning; this.overlayContainer = this.overlayContainerService.getContainerElement(); } ngOnInit() { - // If SessionExpirationWarning is not found in configData, the intervals are never set - // and this component is inert - const authSubscription = this.authService.getAuth().subscribe(auth => { - if (this.sessionExpirationWarning) { - if (auth) { - this.resetExpirationTimer(); - } - else { - // User has logged out while timeout is active - this.clearExpirationTimer(); - } + if (SessionExpirationComponent.instance !== undefined) { + return; + } + SessionExpirationComponent.instance = this; + + const homeBaseUrl = this.configService.configData && this.configService.configData.gsrsHomeBaseUrl || null; + if (homeBaseUrl) { + this.baseHref = homeBaseUrl; + } + + this.startSessionTimeoutInterval(); + } + + setup() { + this.configService.afterLoad().then(cd => { + // If enabled in config file, this functionality periodically checks whether there was a user activity (mouse or keyboard) or not + // In case there was some activity, the session is refreshed (otherwise the session is not refreshed and may eventually expire) + if (this.configService.configData.sessionRefreshOnActiveUserOnly) { + const page = document.getElementsByTagName('body')[0]; + page.addEventListener('mousemove', (e) => { + if (e instanceof MouseEvent) { + this.userActive = true; + } + }); + page.addEventListener('keydown', (e) => { + if (e instanceof KeyboardEvent) { + this.userActive = true; + } + }); + clearInterval(this.activityRefreshInterval); + this.activityRefreshInterval = setInterval(() => { + if (this.userActive) { + this.refreshSession(); + this.userActive = false; + } + }, 10000); + } else { + clearInterval(this.refreshInterval); + this.refreshInterval = setInterval(() => { + this.refreshSession(); + }, 600000); } }); - this.subscriptions.push(authSubscription); - - // This component seems to be destroyed and recreated on route change, so maybe - // the following isn't necessary: - // const routerSubscription = this.router.events.subscribe((event: NavigationEvent) => { - // if (event instanceof NavigationStart && this.expirationTimer) { - // this.extendSession(); - // } - // }); - // this.subscriptions.push(routerSubscription); } - ngOnDestroy() { - this.subscriptions.forEach(subscription => { - subscription.unsubscribe(); - }); - this.clearExpirationTimer(); + refreshSession(): any { + fetch(`${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}`); } - getCurrentTime() { - return Math.floor((new Date()).getTime() / 1000); + startSessionTimeoutInterval() { + this.authService.getAuth().subscribe(auth => { + if (auth != null && this.refreshInterval == null) { + this.setup(); + } else if (auth === null) { + clearInterval(this.refreshInterval); + this.refreshInterval = null; + } + }); + + clearInterval(SessionExpirationComponent.sessionExpirationCheckInterval); + SessionExpirationComponent.sessionExpirationCheckInterval = setInterval(() => { + this.sessionExpiringAt = this.getSessionExpiredAt(); + const currentTime = this.getCurrentTime(); + const sessionTtl = this.sessionExpiringAt - currentTime; + // If session is about to expire in less than 60 seconds, show dialog window + if (sessionTtl > 0 && sessionTtl < 60) { + if (!this.isDialogOpened()) { + this.openDialog(); + } + // Do not automatically (mouse/keyboard event) extend session when the dialog is opened + clearInterval(this.activityRefreshInterval); + } else if (this.sessionExpiringAt !== null && sessionTtl > 0) { + // The session was externally extended (eg. in pfda) -> close the session dialog + if (this.isDialogOpened()) { + this.extendSessionDialog.close(); + } + } + }, 5000) } - clearExpirationTimer() { - if (this.expirationTimer) { - clearTimeout(this.expirationTimer); - this.expirationTimer = null; + private getCookie(name: string) { + const cookieArr = document.cookie.split(';') + for (let i = 0; i < cookieArr.length; i++) { + const cookiePair = cookieArr[i].split('=') + if (name === cookiePair[0].trim()) { + return decodeURIComponent(cookiePair[1]) + } } + return null } - resetExpirationTimer() { - this.clearExpirationTimer(); - - const currentTime = this.getCurrentTime() - this.sessionExpiringAt = currentTime + this.sessionExpirationWarning.maxSessionDurationMinutes * 60; + private getSessionExpiredAt() { + const cookie = this.getCookie('sessionExpiredAt') + if (!cookie) return null + return parseInt(cookie) + } - const timeRemainingSeconds = this.sessionExpiringAt - currentTime; - const timeBeforeDisplayingDialogMs = (timeRemainingSeconds - 61) * 1000; - if (timeBeforeDisplayingDialogMs > 0) { - this.expirationTimer = setTimeout( () => { - this.openDialog(); - }, timeBeforeDisplayingDialogMs); - } - else { - this.login(); - } + getCurrentTime() { + return Math.floor((new Date()).getTime() / 1000); } openDialog() { - const dialogRef = this.dialog.open(SessionExpirationDialogComponent, { + this.extendSessionDialog = this.matDialog.open(SessionExpirationDialogComponent, { data: { 'sessionExpirationWarning': this.sessionExpirationWarning, 'sessionExpiringAt': this.sessionExpiringAt @@ -104,27 +150,17 @@ export class SessionExpirationComponent implements OnInit { disableClose: true }); this.overlayContainer.style.zIndex = '1501'; - const dialogSubscription = dialogRef.afterClosed().subscribe(response => { + this.extendSessionDialog.afterClosed().subscribe(response => { this.overlayContainer.style.zIndex = null; - if (response) { - // Session was extended - this.resetExpirationTimer(); - } + this.startSessionTimeoutInterval(); }); } - extendSession() { - const url = this.sessionExpirationWarning.extendSessionApiUrl; - this.http.get(url).subscribe( - data => { - this.resetExpirationTimer(); - }, - err => { console.log("Error extending session: ", err) }, - () => { } - ); - } - login() { window.location.assign('/login'); } + + isDialogOpened(): boolean { + return this.extendSessionDialog && this.extendSessionDialog.getState() === MatDialogState.OPEN; + } } diff --git a/src/app/core/base/base.component.html b/src/app/core/base/base.component.html index 4194f0be6..b6d54f683 100644 --- a/src/app/core/base/base.component.html +++ b/src/app/core/base/base.component.html @@ -1,4 +1,4 @@ - + -
+
- diff --git a/src/app/core/base/base.component.ts b/src/app/core/base/base.component.ts index 1d12fe9bb..186303abb 100644 --- a/src/app/core/base/base.component.ts +++ b/src/app/core/base/base.component.ts @@ -46,7 +46,7 @@ export class BaseComponent implements OnInit, OnDestroy { appId: string; clasicBaseHref: string; navItems: Array; - customToolbarComponent: string = ''; + isPfdaVersion: boolean = false; canRegister = false; registerNav: Array; searchNav: Array; @@ -75,7 +75,7 @@ export class BaseComponent implements OnInit, OnDestroy { private utilsService: UtilsService, private wildCardService: WildcardService ) { - this.customToolbarComponent = this.configService.configData.customToolbarComponent; + this.isPfdaVersion = this.configService.configData.isPfdaVersion === true; this.wildCardService.wildCardObservable.subscribe((data) => { this.wildCardText = data; }); diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html index 3e2bd168b..7f867f2b4 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.html @@ -61,12 +61,12 @@
- +
Login
-
+ diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss index 8a863ed32..d5e1fc63f 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.scss @@ -47,6 +47,7 @@ $screenMedium: 1045px; align-items: center; justify-content: center; padding: 10px 6px; + cursor: pointer; &:hover { color: $pfda-navbar-item-hover; diff --git a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts index f05646868..7e922b555 100644 --- a/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts +++ b/src/app/core/base/pfda-toolbar/pfda-toolbar.component.ts @@ -5,7 +5,7 @@ import { OverlayContainer } from '@angular/cdk/overlay'; import { AuthService } from '../../auth/auth.service'; import { SubstanceTextSearchService } from '@gsrs-core/substance-text-search/substance-text-search.service'; import { Auth } from '../../auth/auth.model'; -import { Subscription } from 'rxjs'; +import { concatMap, Subscription } from 'rxjs'; import { NavItem } from '@gsrs-core/config'; @Component({ @@ -40,7 +40,7 @@ export class PfdaToolbarComponent implements OnInit { ngOnInit() { this.pfdaBaseUrl = this.configService.configData.pfdaBaseUrl || '/'; - const baseHref = this.configService.environment.baseHref || '/' + const baseHref = this.configService.environment.baseHref || '/ginas/app/beta/'; this.logoSrcPath = `${baseHref}assets/images/pfda/pfda-logo.png`; this.homeIconPath = `${baseHref}assets/images/pfda/home.svg`; @@ -87,4 +87,15 @@ export class PfdaToolbarComponent implements OnInit { removeZindex(): void { this.overlayContainer.style.zIndex = null; } + + login(): void { + this.authService.pfdaLogin().pipe( + concatMap(success => { + return this.authService.getAuth(); + })).subscribe(); + } + + logout(): void { + this.authService.logout(); + } } diff --git a/src/app/core/config/config.model.ts b/src/app/core/config/config.model.ts index b546aca89..b5e8c4aba 100644 --- a/src/app/core/config/config.model.ts +++ b/src/app/core/config/config.model.ts @@ -37,8 +37,8 @@ export interface Config { advancedSearchFacetDisplay?: boolean; facetDisplay?: Array; relationshipsVisualizationUri?: string; - customToolbarComponent?: string; - disableSessionRefresh?: boolean; + isPfdaVersion?: boolean; + sessionRefreshOnActiveUserOnly?: boolean; sessionExpirationWarning?: SessionExpirationWarning; disableReferenceDocumentUpload?: boolean; externalSiteWarning?: ExternalSiteWarning; @@ -180,4 +180,4 @@ export interface DownloadAsPDF { buttonName?:string; companyName?:string; proprietaryNote?:string; -} \ No newline at end of file +} diff --git a/src/app/core/config/config.pfda.json b/src/app/core/config/config.pfda.json index dc35f4eb0..54955437b 100644 --- a/src/app/core/config/config.pfda.json +++ b/src/app/core/config/config.pfda.json @@ -544,5 +544,5 @@ "dialogMessage" : "You will be making an API call outside of the precisionFDA boundary. Do you want to continue?" }, "googleAnalyticsId": "", - "customToolbarComponent": "precisionFDA" -} \ No newline at end of file + "isPfdaVersion": true +} diff --git a/src/app/core/home/home.component.html b/src/app/core/home/home.component.html index e7309c757..fa1c69019 100644 --- a/src/app/core/home/home.component.html +++ b/src/app/core/home/home.component.html @@ -341,25 +341,30 @@

{{homeHeader}}

-
-
- Total substances: {{total | number:'1.0':'en-US'}} -
+
+
+ Total substances: +
+ + {{total | number:'1.0':'en-US'}} +
+
-
- {{link.display}} -
-
- {{link.total | number:'1.0':'en-US'}} -
-
+
+ {{link.display}} +
+ +
+ {{link.total | number:'1.0':'en-US'}} +
+
-
-
-
+
+
+
@@ -392,4 +397,4 @@
- \ No newline at end of file + diff --git a/src/app/core/home/home.component.ts b/src/app/core/home/home.component.ts index e8c9a9c11..2d28c35bb 100644 --- a/src/app/core/home/home.component.ts +++ b/src/app/core/home/home.component.ts @@ -12,7 +12,7 @@ import { OverlayContainer } from '@angular/cdk/overlay'; import { UtilsService } from '@gsrs-core/utils'; import { UsefulLink } from '../config/config.model'; - + @Component({ selector: 'app-home', templateUrl: './home.component.html', @@ -29,19 +29,19 @@ export class HomeComponent implements OnInit, AfterViewInit { imageLoc: any; appId: string; customLinks: Array; - total: string; + total: number; isCollapsed = true; hasBackdrop = false; bannerMessage?: string; usefulLinks?: Array; - - + + // these may be necessary due to a strange quirk // of angular and ngif searchValue: string; loadedComponents: LoadedComponents; - - + + private overlayContainer: HTMLElement; @ViewChild('matSideNavInstance', { static: true }) matSideNav: MatSidenav; @@ -133,7 +133,7 @@ export class HomeComponent implements OnInit, AfterViewInit { }); }); this.substanceService.getRecordCount().subscribe( response => { - this.total = response; + this.total = parseInt(response); }); // this.isClosedWelcomeMessage = localStorage.getItem('isClosedWelcomeMessage') === 'false'; this.isClosedWelcomeMessage = false; diff --git a/src/app/core/main-notification/main-notification.service.ts b/src/app/core/main-notification/main-notification.service.ts index 7531b5c39..26dcad2c7 100644 --- a/src/app/core/main-notification/main-notification.service.ts +++ b/src/app/core/main-notification/main-notification.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; -import { AppNotification } from './notification.model'; +import { AppNotification, NotificationType } from './notification.model'; @Injectable() export class MainNotificationService { @@ -11,4 +11,20 @@ export class MainNotificationService { setNotification(notification: AppNotification): void { this.notificationEvent.next(notification); } + + setSuccessNotification(message: string, duration?: number): void { + this.setNotification({ + message: message, + type: NotificationType.success, + milisecondsToShow: duration ? duration : 4000 + }) + } + + setErrorNotification(message: string, duration?: number): void { + this.setNotification({ + message: message, + type: NotificationType.error, + milisecondsToShow: duration ? duration : 0 + }) + } } diff --git a/src/app/core/main-notification/main-notification/main-notification.component.html b/src/app/core/main-notification/main-notification/main-notification.component.html index db47d4519..ced635246 100644 --- a/src/app/core/main-notification/main-notification/main-notification.component.html +++ b/src/app/core/main-notification/main-notification/main-notification.component.html @@ -1,3 +1,4 @@ \ No newline at end of file + diff --git a/src/app/core/main-notification/main-notification/main-notification.component.scss b/src/app/core/main-notification/main-notification/main-notification.component.scss index c4362dcc2..cccfa90a9 100644 --- a/src/app/core/main-notification/main-notification/main-notification.component.scss +++ b/src/app/core/main-notification/main-notification/main-notification.component.scss @@ -4,7 +4,7 @@ width: 100%; text-align: center; position: fixed; - z-index: 100; + z-index: 2000; transition: all 200ms ease; overflow: hidden; box-sizing: border-box; @@ -16,7 +16,7 @@ height: 0; padding: 0; } - + &.showing { height: auto; box-shadow: 0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14); @@ -36,4 +36,12 @@ background-color: var(--notif-error-bg-color); color: var(--regular-black-color); } + + >.close-link { + float: right; + text-decoration: underline; + font-style: italic; + font-size: 0.9em; + margin: 3px 10px; + } } diff --git a/src/app/core/main-notification/main-notification/main-notification.component.ts b/src/app/core/main-notification/main-notification/main-notification.component.ts index f2780a1d3..b478642e8 100644 --- a/src/app/core/main-notification/main-notification/main-notification.component.ts +++ b/src/app/core/main-notification/main-notification/main-notification.component.ts @@ -34,20 +34,28 @@ export class MainNotificationComponent implements OnInit, OnDestroy { clearTimeout(this.notificationTimer); } + // If notification.milisecondsToShow === 0, the notification is permanent (until closed by user) setNotification(notification: AppNotification): void { this.notifcationType = notification.type || NotificationType.default; this.notificationMessage = notification.message; this.appNotification.nativeElement.classList.remove('hidden'); this.appNotification.nativeElement.classList.add(NotificationType[this.notifcationType]); this.appNotification.nativeElement.classList.add('showing'); - const timeout = notification.milisecondsToShow || 5000; - this.notificationTimer = setTimeout(() => { - this.removeNotification(notification.type); + if (notification.milisecondsToShow === 0) { + if (this.notificationTimer != null) { + clearTimeout(this.notificationTimer); + } this.notificationTimer = null; - }, timeout); + } else { + const timeout = notification.milisecondsToShow || 5000; + this.notificationTimer = setTimeout(() => { + this.removeNotification(); + this.notificationTimer = null; + }, timeout); + } } - removeNotification(notificationType: NotificationType): void { + removeNotification(): void { if (this.notificationTimer != null) { clearTimeout(this.notificationTimer); } @@ -55,5 +63,4 @@ export class MainNotificationComponent implements OnInit, OnDestroy { this.appNotification.nativeElement.classList.add('hidden'); this.appNotification.nativeElement.classList.remove(NotificationType[this.notifcationType]); } - } diff --git a/src/app/core/substance-details/substance-codes/substance-codes.component.html b/src/app/core/substance-details/substance-codes/substance-codes.component.html index fae32366b..282837881 100644 --- a/src/app/core/substance-details/substance-codes/substance-codes.component.html +++ b/src/app/core/substance-details/substance-codes/substance-codes.component.html @@ -93,7 +93,7 @@ + [disabled] = "!code.comments && !code.codeText" >{{!code.comments && !code.codeText ? 'None' : 'View'}}

Code Comments

@@ -110,17 +110,17 @@

Code Comments

- +
- + References - diff --git a/src/app/core/substance-details/substance-names/substance-names.component.html b/src/app/core/substance-details/substance-names/substance-names.component.html index 2dc7a32c0..b8b5876b1 100644 --- a/src/app/core/substance-details/substance-names/substance-names.component.html +++ b/src/app/core/substance-details/substance-names/substance-names.component.html @@ -14,7 +14,7 @@ '>Both - + {{showHideFilterText}} @@ -162,22 +162,22 @@ Details -

Details

- + -
- Naming organizations: + Naming organizations:
- {{org.nameOrg}}{{!last? ', ':''}} + {{org.nameOrg}}{{!last? ', ':''}}
diff --git a/src/app/core/substance-details/substance-references/substance-references.component.html b/src/app/core/substance-details/substance-references/substance-references.component.html index e33d554d8..9ec599324 100644 --- a/src/app/core/substance-details/substance-references/substance-references.component.html +++ b/src/app/core/substance-details/substance-references/substance-references.component.html @@ -97,7 +97,7 @@
Access + diff --git a/src/app/core/substance-details/substance-relationships/substance-relationships.component.html b/src/app/core/substance-details/substance-relationships/substance-relationships.component.html index d213b4b8b..3798f0ea2 100644 --- a/src/app/core/substance-details/substance-relationships/substance-relationships.component.html +++ b/src/app/core/substance-details/substance-relationships/substance-relationships.component.html @@ -33,12 +33,12 @@ Details -
{{filename? filename: 'no file chosen'}}
- - -
-
Or paste JSON here:
- -
-
- {{message}} -
+ +
+
+
{{filename? filename: 'no file chosen'}}
+ +
+ + +
+
Paste JSON here:
+ +
+
+ +
+
URL:
+ +
Note: The URL needs to be publicly accessible
+
+
+ +
+ {{message}} +

- -
\ No newline at end of file + + diff --git a/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts b/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts index dd28c539b..774d23b60 100644 --- a/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts +++ b/src/app/core/substance-edit-import-dialog/substance-edit-import-dialog.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Router } from '@angular/router'; +import { MatTabChangeEvent } from '@angular/material/tabs'; +import { ConfigService } from '@gsrs-core/config'; @Component({ selector: 'app-substance-edit-import-dialog', @@ -14,15 +16,21 @@ export class SubstanceEditImportDialogComponent implements OnInit { record: any; filename: string; pastedJSON: string; - uploaded = false; + pastedUrl: string; title = 'Substance Import'; entity = 'Substance'; + currentTab: number = 0; + urlImportEnabled: boolean = false; + constructor( private router: Router, + private configService: ConfigService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: any - ) { } + ) { + this.urlImportEnabled = this.configService.configData.isPfdaVersion; + } ngOnInit() { if (this.data) { @@ -59,36 +67,70 @@ export class SubstanceEditImportDialogComponent implements OnInit { } }; reader.readAsText(event.target.files[0]); - this.uploaded = true; } } - useFile() { - if (!this.uploaded && this.pastedJSON) { - const read = JSON.parse(this.pastedJSON); - if (!read['substanceClass']) { - this.message = 'Error: Invalid JSON format'; - this.loaded = false; + importSubstance() { + if (this.currentTab === 0) { + // Nothing + this.dialogRef.close(this.record); + } else if (this.currentTab === 1) { + const read = JSON.parse(this.pastedJSON); + if (!read['substanceClass']) { + this.message = 'Error: Invalid JSON format'; + this.loaded = false; + } else { + this.loaded = true; + this.record = this.pastedJSON; + this.message = ''; + this.dialogRef.close(this.record); + } + } else if (this.currentTab === 2) { + fetch(`/reverse-proxy?url=${this.pastedUrl}`).then(r => { + if (r.status !== 200) { + r.json().then(data => { + this.message = data.message ? data.message : 'Error while loading given URL'; + }).catch(_e => { + this.message = 'Error while loading given URL'; + }) } else { - this.loaded = true; - this.record = this.pastedJSON; - this.message = ''; + const json = r.text().then(data => { + try { + JSON.parse(data); + this.record = data; + this.dialogRef.close(this.record); + } catch (_e) { + this.message = 'Error: The URL does not point to a valid JSON file' + } + }); } + }).catch(e => { + this.message = `Error: ${e.message}`; + }) } - this.dialogRef.close(this.record); } - checkLoaded() { this.loaded = true; try { JSON.parse(this.pastedJSON); this.message = ''; - } catch (e) { - this.message = 'Error: Invalid JSON format in pasted string'; - this.loaded = false; + } catch (e) { + this.message = 'Error: Invalid JSON format in pasted string'; + this.loaded = false; + } + } + + checkUrl() { + try { + new URL(this.pastedUrl); + this.loaded = true; + this.message = ''; + } catch (_e) { + this.message = 'Invalid URL'; + this.loaded = false; + } } -} openInput(): void { @@ -104,4 +146,15 @@ export class SubstanceEditImportDialogComponent implements OnInit { return true; } + tabChanged(tabChangeEvent: MatTabChangeEvent) { + if (this.currentTab !== tabChangeEvent.index) { + this.currentTab = tabChangeEvent.index; + this.message = ''; + this.loaded = false; + this.record = ''; + this.pastedJSON = ''; + this.pastedUrl = ''; + this.filename = ''; + } + } } diff --git a/src/app/core/substance-form/can-register-substance-form.ts b/src/app/core/substance-form/can-register-substance-form.ts index 8f6ce3ba4..a4cad22a6 100644 --- a/src/app/core/substance-form/can-register-substance-form.ts +++ b/src/app/core/substance-form/can-register-substance-form.ts @@ -3,13 +3,15 @@ import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Navig import { AuthService } from '../auth/auth.service'; import { Observable } from 'rxjs'; import {Role} from '@gsrs-core/auth/auth.model'; +import { ConfigService } from "@gsrs-core/config"; @Injectable() export class CanRegisterSubstanceForm implements CanActivate { constructor( private router: Router, - private authService: AuthService + private authService: AuthService, + private configService: ConfigService ) {} canActivate( @@ -17,27 +19,32 @@ export class CanRegisterSubstanceForm implements CanActivate { state: RouterStateSnapshot ): Observable | Promise | (boolean | UrlTree) { return new Observable(observer => { - this.authService.getAuth().subscribe(auth => { - if (auth) { - this.authService.hasAnyRolesAsync('DataEntry', 'SuperDataEntry').subscribe(response => { - if (response) { - observer.next(true); - observer.complete(); - } else { - observer.next(this.router.parseUrl('/browse-substance')); - observer.complete(); - } - }); - } else { - const navigationExtras: NavigationExtras = { - queryParams: { - path: state.url - } - }; - observer.next(this.router.createUrlTree(['/login'], navigationExtras)); - observer.complete(); - } - }); + if (this.configService.configData.isPfdaVersion) { + observer.next(true); + observer.complete(); + } else { + this.authService.getAuth().subscribe(auth => { + if (auth) { + this.authService.hasAnyRolesAsync('DataEntry', 'SuperDataEntry').subscribe(response => { + if (response) { + observer.next(true); + observer.complete(); + } else { + observer.next(this.router.parseUrl('/browse-substance')); + observer.complete(); + } + }); + } else { + const navigationExtras: NavigationExtras = { + queryParams: { + path: state.url + } + }; + observer.next(this.router.createUrlTree(['/login'], navigationExtras)); + observer.complete(); + } + }); + } }); } } diff --git a/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts b/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts index a0a25052a..4b6b092e8 100644 --- a/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts +++ b/src/app/core/substance-form/constituents/substance-form-constituents-card.component.ts @@ -78,7 +78,7 @@ export class SubstanceFormConstituentsCardComponent extends SubstanceCardBaseFil this.formulationPercent = 0; this.components = 0; this.constituents.forEach(constituent => { - if(constituent && constituent.amount && constituent.amount.type === "WEIGHT PERCENT" + if(constituent && constituent.amount && constituent.amount.type === "WEIGHT PERCENT" && constituent.amount.units === "%" && constituent.amount.average) { this.formulationPercent = parseFloat(this.formulationPercent.toString()) + parseFloat(constituent.amount.average.toString()); this.components++; diff --git a/src/app/core/substance-form/substance-form.component.html b/src/app/core/substance-form/substance-form.component.html index bec1f84eb..2d7f3b82e 100644 --- a/src/app/core/substance-form/substance-form.component.html +++ b/src/app/core/substance-form/substance-form.component.html @@ -212,8 +212,7 @@

{{ section.menuLabel }}

- - diff --git a/src/app/core/substances-browse/substances-browse.component.scss b/src/app/core/substances-browse/substances-browse.component.scss index a342a6572..6011c967b 100644 --- a/src/app/core/substances-browse/substances-browse.component.scss +++ b/src/app/core/substances-browse/substances-browse.component.scss @@ -346,7 +346,7 @@ display: flex; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; color: var(--pink-span-color); } - + .similarity-label { font-style: italic; } @@ -626,7 +626,7 @@ display: flex; ::ng-deep .mat-select-value { max-width: 100%; width: auto; - } + } } .page-label { @@ -774,4 +774,3 @@ margin-left: 20px; line-height: 28px; margin-left: 20px; } - diff --git a/src/app/core/substances-browse/substances-browse.component.ts b/src/app/core/substances-browse/substances-browse.component.ts index bed97c53c..806094534 100644 --- a/src/app/core/substances-browse/substances-browse.component.ts +++ b/src/app/core/substances-browse/substances-browse.component.ts @@ -219,7 +219,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr }); }); - + this.title.setTitle('Browse Substances'); this.pageSize = 10; @@ -248,7 +248,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr this.privateSearchSeqType = this.activatedRoute.snapshot.queryParams['seq_type'] || ''; this.smiles = this.activatedRoute.snapshot.queryParams['smiles'] || ''; // the sort order should be set to default (similarity) for structure searches, last edited for all others - this.order = this.activatedRoute.snapshot.queryParams['order'] || + this.order = this.activatedRoute.snapshot.queryParams['order'] || (this.privateStructureSearchTerm && this.privateStructureSearchTerm !== '' ? 'default':'$root_lastEdited'); this.view = this.activatedRoute.snapshot.queryParams['view'] || 'cards'; this.pageSize = parseInt(this.activatedRoute.snapshot.queryParams['pageSize'], null) || 10; @@ -516,7 +516,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr id:'structure-dialog' }); this.overlayContainer.style.zIndex = '1002'; - + this.structureSearchDialog.afterClosed().subscribe(result => { this.overlayContainer.style.zIndex = null; this.loadingService.setLoading(false); @@ -525,7 +525,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr }); this.structureDialogOpened = true; } - + } searchSubstances() { @@ -580,7 +580,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr // this.pauseStructureSearch = true; iterations++; } - + this.privateBulkSearchStatusKey = pagingResponse.statusKey; this.isError = false; @@ -620,7 +620,7 @@ export class SubstancesBrowseComponent implements OnInit, AfterViewInit, OnDestr this.etag = pagingResponse.etag; if (pagingResponse.facets && pagingResponse.facets.length > 0) { this.rawFacets = pagingResponse.facets; - + } this.narrowSearchSuggestions = {}; this.matchTypes = []; @@ -760,7 +760,7 @@ searchTermOkforBeginsWithSearch(): boolean { maxHeight: '85%', width: '60%', - + data: { 'extension': extension } }); @@ -1287,21 +1287,21 @@ searchTermOkforBeginsWithSearch(): boolean { addToList(): void { let data = {view: 'add', etag: this.etag, lists: this.userLists}; - + const dialogRef = this.dialog.open(UserQueryListDialogComponent, { width: '800px', autoFocus: false, data: data - + }); this.overlayContainer.style.zIndex = '1002'; - + const dialogSubscription = dialogRef.afterClosed().pipe(take(1)).subscribe(response => { if (response) { this.overlayContainer.style.zIndex = null; } }); } - + } diff --git a/src/app/fda/fda.module.ts b/src/app/fda/fda.module.ts index ad4410cd5..7f83a7751 100644 --- a/src/app/fda/fda.module.ts +++ b/src/app/fda/fda.module.ts @@ -27,7 +27,6 @@ import { SubstanceApplicationMatchListComponent} from './substance-browse/substa import { ApplicationsBrowseComponent } from './application/applications-browse/applications-browse.component'; import { ClinicalTrialsBrowseComponent } from './clinical-trials/clinical-trials-browse/clinical-trials-browse.component'; import { fdaSubstanceCardsFilters } from './substance-details/fda-substance-cards-filters.constant'; -import { SsoRefreshService } from './service/sso-refresh.service'; import { ProductService } from './product/service/product.service'; import { GeneralService} from './service/general.service'; import { ShowApplicationToggleComponent } from './substance-browse/show-application-toggle/show-application-toggle.component'; @@ -57,12 +56,6 @@ const fdaRoutes: Routes = [ } ]; -export function init_sso_refresh_service(ssoService: SsoRefreshService) { - return() => { - ssoService.init(); - }; -} - @NgModule({ imports: [ CommonModule, @@ -100,15 +93,6 @@ export function init_sso_refresh_service(ssoService: SsoRefreshService) { SubstanceCountsComponent, ShowApplicationToggleComponent - ], - providers: [ - SsoRefreshService, - { - provide: APP_INITIALIZER, - useFactory: init_sso_refresh_service, - deps: [SsoRefreshService], - multi: true - } ] }) export class FdaModule { diff --git a/src/app/fda/service/sso-refresh.service.spec.ts b/src/app/fda/service/sso-refresh.service.spec.ts deleted file mode 100644 index ca2f068f5..000000000 --- a/src/app/fda/service/sso-refresh.service.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { SsoRefreshService } from './sso-refresh.service'; - -describe('SsoRefreshService', () => { - beforeEach(() => TestBed.configureTestingModule({})); - - it('should be created', () => { - const service: SsoRefreshService = TestBed.get(SsoRefreshService); - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/fda/service/sso-refresh.service.ts b/src/app/fda/service/sso-refresh.service.ts deleted file mode 100644 index aa97fb0a4..000000000 --- a/src/app/fda/service/sso-refresh.service.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Injectable, Inject, PLATFORM_ID, OnDestroy } from '@angular/core'; -import { Router, NavigationExtras, ActivatedRoute } from '@angular/router'; -import { isPlatformBrowser } from '@angular/common'; -import { take } from 'rxjs/operators'; -import { AuthService } from '@gsrs-core/auth'; -import { UtilsService } from '@gsrs-core/utils'; -import { ConfigService } from '@gsrs-core/config/config.service'; - -@Injectable() -export class SsoRefreshService implements OnDestroy { - private iframe: HTMLIFrameElement; - private refreshInterval: any; - private baseHref: string; - private showHeaderBar = 'true'; - - constructor( - @Inject(PLATFORM_ID) private platformId: Object, - private utilsService: UtilsService, - private configService: ConfigService, - private authService: AuthService, - private activatedRoute: ActivatedRoute - ) { - if (isPlatformBrowser(this.platformId)) { - - if (window.location.pathname.indexOf('/ginas/app/ui/') > -1) { - this.baseHref = '/ginas/app/'; - } - } - } - - updateIframe(): any { - if (!this.iframe) { - this.iframe = document.createElement('IFRAME') as HTMLIFrameElement; - this.iframe.title = 'page refresher'; - this.iframe.name = 'refresher'; - this.iframe.style.height = '0'; - this.iframe.style.opacity = '0'; - this.iframe.src = `${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}&noWarningBox=true`; - document.body.appendChild(this.iframe); - } else { - this.iframe.src = `${this.baseHref || ''}api/v1/whoami?key=${this.utilsService.newUUID()}&noWarningBox=true`; - } - } - - setup() { - this.configService.afterLoad().then(cd => { - // Session auto refresh can be explicitly disabled in config file - if (this.configService.configData.disableSessionRefresh) { - return; - } - const homeBaseUrl = this.configService.configData && this.configService.configData.gsrsHomeBaseUrl || null; - if (homeBaseUrl) { - this.baseHref = homeBaseUrl; - this.updateIframe(); - } - clearInterval(this.refreshInterval); - this.refreshInterval = setInterval(() => { - console.log("REFRESHING iFrame"); - this.updateIframe(); - }, 600000); - }); - } - - init(): any { - if(new URLSearchParams(window.location.search).get("noWarningBox") === 'true'){ - //do not do sso refresher recursively - return; - } - if (new URLSearchParams(window.location.search).get("header") === 'false') { - this.setup(); - } else { - this.authService.getAuth().subscribe(auth => { - if (auth != null && this.refreshInterval == null) { - this.setup(); - } else if (auth === null){ - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } - }); - } //else - } - - ngOnDestroy() { - clearInterval(this.refreshInterval); - this.refreshInterval = null; - } -}