diff --git a/src/app/Shared/Services/Report.service.tsx b/src/app/Shared/Services/Report.service.tsx index 4bf2149ff..343c57482 100644 --- a/src/app/Shared/Services/Report.service.tsx +++ b/src/app/Shared/Services/Report.service.tsx @@ -14,16 +14,18 @@ * limitations under the License. */ import { Base64 } from 'js-base64'; -import { Observable, Subject, from, throwError } from 'rxjs'; +import { Observable, Subject, combineLatest, from, throwError } from 'rxjs'; import { fromFetch } from 'rxjs/fetch'; -import { concatMap, filter, first, tap } from 'rxjs/operators'; +import { concatMap, filter, first, map, tap } from 'rxjs/operators'; import { Recording, CachedReportValue, GenerationError, AnalysisResult, NotificationCategory } from './api.types'; import { isActiveRecording, isQuotaExceededError, isGenerationError } from './api.utils'; import { NotificationChannel } from './NotificationChannel.service'; import type { NotificationService } from './Notifications.service'; +import { CryostatContext } from './Services'; export class ReportService { constructor( + private ctx: CryostatContext, private notifications: NotificationService, channel: NotificationChannel, ) { @@ -43,106 +45,112 @@ export class ReportService { } const headers = new Headers(); headers.append('Accept', 'application/json'); - let url = new URL(recording.reportUrl, document.location.href); - - return fromFetch(url.toString(), { - method: 'GET', - mode: 'cors', - credentials: 'include', - headers, - }).pipe( - concatMap((resp) => { - // 200 indicates that the backend has the report cached and it will return - // the json in the response. - if (resp.ok) { - // 202 indicates that the backend does not have the report cached and will return an - // async job ID in the response immediately, then emit a notification with the same - // job ID later to inform us that the report is now ready and can be retrieved by - // sending a follow-up GET to the Location header value - if (resp.status == 202) { - let subj = new Subject(); - resp.text().then((jobId) => { - this.jobIds.set(jobId, resp.headers.get('Location') || recording.reportUrl); - this._jobCompletion - .asObservable() - .pipe(filter((id) => id === jobId)) - .subscribe((id) => { - const jobUrl = this.jobIds.get(id); - if (!jobUrl) throw new Error(`Unknown job ID: ${id}`); - this.jobIds.delete(id); - fromFetch(jobUrl, { - method: 'GET', - mode: 'cors', - credentials: 'include', - headers, - }).subscribe((resp) => { - if (resp.ok && resp.status === 200) { - resp - .text() - .then(JSON.parse) - .then((obj) => subj.next(Object.values(obj) as AnalysisResult[])); - } else { - const ge: GenerationError = { - name: `Report Failure (${recording.name})`, - message: resp.statusText, - messageDetail: from(resp.text()), - status: resp.status, - }; - subj.error(ge); - } + const req = () => + combineLatest([ + this.ctx.url(recording.reportUrl), + this.ctx.headers(headers).pipe( + map((headers) => { + let cfg: RequestInit = {}; + cfg.method = 'GET'; + cfg.mode = 'cors'; + cfg.credentials = 'include'; + cfg.headers = headers; + return cfg; + }), + ), + ]).pipe( + concatMap((parts) => + fromFetch(parts[0], parts[1]).pipe( + concatMap((resp) => { + // 200 indicates that the backend has the report cached and it will return + // the json in the response. + if (resp.ok) { + // 202 indicates that the backend does not have the report cached and will return an + // async job ID in the response immediately, then emit a notification with the same + // job ID later to inform us that the report is now ready and can be retrieved by + // sending a follow-up GET to the Location header value + if (resp.status == 202) { + let subj = new Subject(); + resp.text().then((jobId) => { + this.jobIds.set(jobId, parts[0]); + this._jobCompletion + .asObservable() + .pipe(filter((id) => id === jobId)) + .subscribe((id) => { + const jobUrl = this.jobIds.get(id); + if (!jobUrl) throw new Error(`Unknown job ID: ${id}`); + this.jobIds.delete(id); + fromFetch(parts[0], parts[1]).subscribe((resp) => { + if (resp.ok && resp.status === 200) { + resp + .text() + .then(JSON.parse) + .then((obj) => subj.next(Object.values(obj) as AnalysisResult[])); + } else { + const ge: GenerationError = { + name: `Report Failure (${recording.name})`, + message: resp.statusText, + messageDetail: from(resp.text()), + status: resp.status, + }; + subj.error(ge); + } + }); + }); }); - }); - }); - return subj.asObservable(); - } - return from( - resp - .text() - .then(JSON.parse) - .then((obj) => Object.values(obj) as AnalysisResult[]), - ); - } else { - const ge: GenerationError = { - name: `Report Failure (${recording.name})`, - message: resp.statusText, - messageDetail: from(resp.text()), - status: resp.status, - }; - throw ge; - } - }), - tap({ - next: (report) => { - if (isActiveRecording(recording)) { - try { - sessionStorage.setItem(this.analysisKey(connectUrl), JSON.stringify(report)); - sessionStorage.setItem(this.analysisKeyTimestamp(connectUrl), Date.now().toString()); - } catch (err) { - if (isQuotaExceededError(err)) { - this.notifications.warning('Report Caching Failed', err.message); - this.delete(recording); + return subj.asObservable(); + } + return from( + resp + .text() + .then(JSON.parse) + .then((obj) => Object.values(obj) as AnalysisResult[]), + ); } else { - // see https://mmazzarolo.com/blog/2022-06-25-local-storage-status/ - this.notifications.warning('Report Caching Failed', 'localStorage is not available'); - this.delete(recording); + const ge: GenerationError = { + name: `Report Failure (${recording.name})`, + message: resp.statusText, + messageDetail: from(resp.text()), + status: resp.status, + }; + throw ge; } - } - } - }, - error: (err) => { - if (isGenerationError(err) && err.status >= 500) { - err.messageDetail.pipe(first()).subscribe((detail) => { - this.notifications.warning(`Report generation failure: ${detail}`); - this.deleteCachedAnalysisReport(connectUrl); - }); - } else if (isGenerationError(err) && err.status == 202) { - this.notifications.info('Report generation in progress', 'Report is being generated'); - } else { - this.notifications.danger(err.name, err.message); - } - }, - }), - ); + }), + tap({ + next: (report) => { + if (isActiveRecording(recording)) { + try { + sessionStorage.setItem(this.analysisKey(connectUrl), JSON.stringify(report)); + sessionStorage.setItem(this.analysisKeyTimestamp(connectUrl), Date.now().toString()); + } catch (err) { + if (isQuotaExceededError(err)) { + this.notifications.warning('Report Caching Failed', err.message); + this.delete(recording); + } else { + // see https://mmazzarolo.com/blog/2022-06-25-local-storage-status/ + this.notifications.warning('Report Caching Failed', 'localStorage is not available'); + this.delete(recording); + } + } + } + }, + error: (err) => { + if (isGenerationError(err) && err.status >= 500) { + err.messageDetail.pipe(first()).subscribe((detail) => { + this.notifications.warning(`Report generation failure: ${detail}`); + this.deleteCachedAnalysisReport(connectUrl); + }); + } else if (isGenerationError(err) && err.status == 202) { + this.notifications.info('Report generation in progress', 'Report is being generated'); + } else { + this.notifications.danger(err.name, err.message); + } + }, + }), + ), + ), + ); + return req(); } getCachedAnalysisReport(connectUrl: string): CachedReportValue { diff --git a/src/app/Shared/Services/Services.tsx b/src/app/Shared/Services/Services.tsx index ff2c8246e..c7ce969c1 100644 --- a/src/app/Shared/Services/Services.tsx +++ b/src/app/Shared/Services/Services.tsx @@ -50,7 +50,7 @@ const settings = new SettingsService(); const login = new LoginService(defaultContext.url, settings); const api = new ApiService(defaultContext, target, NotificationsInstance); const notificationChannel = new NotificationChannel(defaultContext, NotificationsInstance, login); -const reports = new ReportService(NotificationsInstance, notificationChannel); +const reports = new ReportService(defaultContext, NotificationsInstance, notificationChannel); const targets = new TargetsService(api, NotificationsInstance, notificationChannel); const defaultServices: Services = { diff --git a/src/app/utils/fakeData.ts b/src/app/utils/fakeData.ts index aad91498c..24ea682ba 100644 --- a/src/app/utils/fakeData.ts +++ b/src/app/utils/fakeData.ts @@ -40,7 +40,7 @@ import { NotificationChannel } from '@app/Shared/Services/NotificationChannel.se import { NotificationService, NotificationsInstance } from '@app/Shared/Services/Notifications.service'; import { ReportService } from '@app/Shared/Services/Report.service'; import { ChartControllerConfig } from '@app/Shared/Services/service.types'; -import { defaultServices, Services } from '@app/Shared/Services/Services'; +import { CryostatContext, defaultContext, defaultServices, Services } from '@app/Shared/Services/Services'; import { SettingsService } from '@app/Shared/Services/Settings.service'; import { TargetService } from '@app/Shared/Services/Target.service'; import { Observable, of } from 'rxjs'; @@ -220,8 +220,8 @@ class FakeTargetService extends TargetService { } class FakeReportService extends ReportService { - constructor(notifications: NotificationService, channel: NotificationChannel) { - super(notifications, channel); + constructor(ctx: CryostatContext, notifications: NotificationService, channel: NotificationChannel) { + super(ctx, notifications, channel); } reportJson(_recording: Recording, _connectUrl: string): Observable { @@ -394,7 +394,7 @@ class FakeApiService extends ApiService { const target = new FakeTargetService(); const api = new FakeApiService(target, NotificationsInstance); -const reports = new FakeReportService(NotificationsInstance, defaultServices.notificationChannel); +const reports = new FakeReportService(defaultContext, NotificationsInstance, defaultServices.notificationChannel); const settings = new FakeSetting(); export const fakeServices: Services = {