Skip to content

Commit

Permalink
chore(reports): use CryostatContext url and headers for reportJson (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
aptmac authored Jan 31, 2025
1 parent 6727eab commit b122e0e
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 104 deletions.
206 changes: 107 additions & 99 deletions src/app/Shared/Services/Report.service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand All @@ -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<AnalysisResult[]>();
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<AnalysisResult[]>();
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 {
Expand Down
2 changes: 1 addition & 1 deletion src/app/Shared/Services/Services.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
8 changes: 4 additions & 4 deletions src/app/utils/fakeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<AnalysisResult[]> {
Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit b122e0e

Please sign in to comment.