diff --git a/.helm/adhoc/certificate-sxplr-ebrains.yml b/.helm/adhoc/certificate-sxplr-ebrains.yml index 19a96659ee504b18efd47a738c7a915cd823e3a9..6e73e4c5eec1d39f9e539dec978f5541fa4add22 100644 --- a/.helm/adhoc/certificate-sxplr-ebrains.yml +++ b/.helm/adhoc/certificate-sxplr-ebrains.yml @@ -1,7 +1,7 @@ apiVersion: cert-manager.io/v1 kind: Certificate metadata: - name: siibra-explorer-certificate + name: siibra-explorer-ebrains-certificate spec: secretName: sxplr-ebrains-secret renewBefore: 120h diff --git a/.helm/adhoc/ingress-main.yml b/.helm/adhoc/ingress-main.yml index 3c69b3b6a279217965fdac5cb60b69eeebf9fdb8..230fcf5aa9ee631a6bfb78535f8204f40a5d51e6 100644 --- a/.helm/adhoc/ingress-main.yml +++ b/.helm/adhoc/ingress-main.yml @@ -13,7 +13,31 @@ spec: path: "/viewer" backend: service: - name: master-siibra-explorer + name: prod-siibra-explorer + port: + number: 8080 + - pathType: Prefix + path: "/viewer-staging" + backend: + service: + name: rc-siibra-explorer + port: + number: 8080 + - pathType: Prefix + path: "/viewer-expmt" + backend: + service: + name: expmt-siibra-explorer + port: + number: 8080 + - host: siibra-explorer.apps.ebrains.eu + http: + paths: + - pathType: Prefix + path: "/viewer" + backend: + service: + name: prod-siibra-explorer port: number: 8080 - pathType: Prefix @@ -34,3 +58,6 @@ spec: - secretName: siibra-explorer-prod-secret hosts: - siibra-explorer.apps.tc.humanbrainproject.eu + - secretName: sxplr-ebrains-secret + hosts: + - siibra-explorer.apps.ebrains.eu diff --git a/docs/releases/v2.14.5.md b/docs/releases/v2.14.5.md index ec6e17c76690b0806fdbe9c47585193465ec4223..243ce4cf33361cdd240846c1b922bb61666e3921 100644 --- a/docs/releases/v2.14.5.md +++ b/docs/releases/v2.14.5.md @@ -10,6 +10,7 @@ - Added legend to region hierarchy - Allow latest queried concept in feature view - Allow experimental flag to be set to be on at runtime (this also shows the button, allow toggling of experimental features) +- Added code snippet to limited panels - (experimental) allow addition of custom linear coordinate space - (experimental) show BigBrain slice number diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts b/src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts new file mode 100644 index 0000000000000000000000000000000000000000..63c4e5c2fb06a3e3a5bd404a1510dd7633f7bc78 --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts @@ -0,0 +1,31 @@ +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; +import { TextareaCopyExportCmp } from "src/components/textareaCopyExport/textareaCopyExport.component"; +import { AngularMaterialModule, Clipboard, MAT_DIALOG_DATA } from "src/sharedModules"; + +@Component({ + templateUrl: './codeSnippet.template.html', + standalone: true, + styleUrls: [ + './codeSnippet.style.scss' + ], + imports: [ + TextareaCopyExportCmp, + AngularMaterialModule, + CommonModule, + ] +}) + +export class CodeSnippetCmp { + constructor( + @Inject(MAT_DIALOG_DATA) + public data: any, + public clipboard: Clipboard, + ){ + + } + + copy(){ + this.clipboard.copy(this.data.code) + } +} diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts b/src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..1967c3cdc1140fbc896ad511c1a9e62d7c6e6046 --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts @@ -0,0 +1,88 @@ +import { Directive, HostListener, Input } from "@angular/core"; +import { RouteParam, SapiRoute } from "../typeV3"; +import { SAPI } from "../sapi.service"; +import { BehaviorSubject, from, of } from "rxjs"; +import { switchMap, take } from "rxjs/operators"; +import { MatDialog } from "src/sharedModules" +import { CodeSnippetCmp } from "./codeSnippet.dialog"; + +type V<T extends SapiRoute> = {route: T, param: RouteParam<T>} + +@Directive({ + selector: '[code-snippet]', + standalone: true, + exportAs: "codeSnippet" +}) + +export class CodeSnippet<T extends SapiRoute>{ + + code$ = this.sapi.sapiEndpoint$.pipe( + switchMap(endpt => this.#path.pipe( + switchMap(path => { + if (!path) { + return of(null) + } + return from(this.#getCode(`${endpt}${path}`)) + }) + )), + ) + + #busy$ = new BehaviorSubject<boolean>(false) + busy$ = this.#busy$.asObservable() + + @HostListener("click") + async handleClick(){ + this.#busy$.next(true) + const code = await this.code$.pipe( + take(1) + ).toPromise() + this.#busy$.next(false) + this.matDialog.open(CodeSnippetCmp, { + data: { code } + }) + } + + @Input() + set routeParam(value: V<T>|null|undefined){ + if (!value) { + return + } + const { param, route } = value + const { params, path } = this.sapi.v3GetRoute(route, param) + + let url = encodeURI(path) + const queryParam = new URLSearchParams() + for (const key in params) { + queryParam.set(key, params[key].toString()) + } + const result = `${url}?${queryParam.toString()}` + this.#path.next(result) + } + + @Input() + set path(value: string) { + this.#path.next(value) + } + #path = new BehaviorSubject<string>(null) + + constructor(private sapi: SAPI, private matDialog: MatDialog){} + + async #getCode(url: string): Promise<string> { + try { + const resp = await fetch(url, { + headers: { + Accept: `text/x-sapi-python` + } + }) + if (!resp.ok){ + console.warn(`${url} returned not ok`) + return null + } + const result = await resp.text() + return result + } catch (e) { + console.warn(`Error: ${e}`) + return null + } + } +} diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss b/src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss new file mode 100644 index 0000000000000000000000000000000000000000..2a159cdfc52f7862a74429dd65ccc0e9b8882e68 --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss @@ -0,0 +1,20 @@ +.textarea +{ + width: 75vw; + +} + +textarea-copy-export +{ + display: block; + width: 75vw; + + ::ng-deep mat-form-field { + width: 100%; + + textarea + { + resize: none; + } + } +} diff --git a/src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html b/src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html new file mode 100644 index 0000000000000000000000000000000000000000..0dbf045b34c2bd7490b29854398e6fc18cd9a586 --- /dev/null +++ b/src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html @@ -0,0 +1,31 @@ +<mat-card class="sxplr-custom-cmp text"> + <mat-card-header> + <mat-card-title> + Code snippet + </mat-card-title> + </mat-card-header> + <mat-card-content> + <textarea-copy-export + textarea-copy-export-label="python" + [textarea-copy-export-text]="data.code" + textarea-copy-export-download-filename="export.py" + [textarea-copy-export-disable]="true" + [textarea-copy-show-suffixes]="false" + [textarea-copy-export-rows]="10" + #textAreaCopyExport="textAreaCopyExport"> + + </textarea-copy-export> + </mat-card-content> + + <mat-card-actions> + <button mat-raised-button + color="primary" + (click)="textAreaCopyExport.copyToClipboard(data.code)"> + <mat-icon fontSet="fas" fontIcon="fa-copy"></mat-icon> + <span>copy</span> + </button> + <button mat-button mat-dialog-close> + <span>close</span> + </button> + </mat-card-actions> +</mat-card> diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 11d68580709f0fc9fc79b4dc66890cf760cfa705..331767f012b02dee3a2b08daffad063d05d732a8 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -103,6 +103,7 @@ export type SimpleCompoundFeature<T extends string|Point=string|Point> = { name: string category?: string indices: { + category?: string id: string index: T name: string diff --git a/src/atlasComponents/sapi/translateV3.ts b/src/atlasComponents/sapi/translateV3.ts index a2abb1375bd95b295af572265047119b9f3ac7f2..03659664a737629229c78ae3a5e6c04c1660fd06 100644 --- a/src/atlasComponents/sapi/translateV3.ts +++ b/src/atlasComponents/sapi/translateV3.ts @@ -649,6 +649,7 @@ class TranslateV3 { id, index: await this.#transformIndex(index), name, + category: feat.category }) ) ), diff --git a/src/atlasComponents/sapiViews/core/region/module.ts b/src/atlasComponents/sapiViews/core/region/module.ts index e72f1b154df19469c66dd35bd7671a6bf2386d03..8bb8f8bf797c33c025e5ad526ccc36252aae009b 100644 --- a/src/atlasComponents/sapiViews/core/region/module.ts +++ b/src/atlasComponents/sapiViews/core/region/module.ts @@ -15,6 +15,7 @@ import { SapiViewsCoreParcellationModule } from "../parcellation"; import { TranslateQualificationPipe } from "./translateQualification.pipe"; import { DedupRelatedRegionPipe } from "./dedupRelatedRegion.pipe"; import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.directive"; +import { CodeSnippet } from "src/atlasComponents/sapi/codeSnippets/codeSnippet.directive"; @NgModule({ imports: [ @@ -30,6 +31,7 @@ import { ExperimentalFlagDirective } from "src/experimental/experimental-flag.di SapiViewsCoreParcellationModule, ExperimentalFlagDirective, + CodeSnippet, ], declarations: [ SapiViewsCoreRegionRegionListItem, diff --git a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html index 25a7643b6e7315249dd34ec480b831d78dec4bdc..33e467679bfdace05a008ec89f1a164999911a18 100644 --- a/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html +++ b/src/atlasComponents/sapiViews/core/region/region/rich/region.rich.template.html @@ -37,6 +37,32 @@ <mat-tab label="Overview"> <mat-action-list class="overview-container"> + + <button mat-list-item + code-snippet + [routeParam]="{ + route: '/regions/{region_id}', + param: { + path: { + region_id: region.name + }, + query: { + parcellation_id: parcellation.id + } + } + }" + #codeSnippet="codeSnippet" + [disabled]="codeSnippet.busy$ | async"> + <mat-icon matListItemIcon fontSet="fas" fontIcon="fa-code"></mat-icon> + <div matListItemTitle> + <ng-template [ngIf]="codeSnippet.busy$ | async"> + loading code ... + </ng-template> + <ng-template [ngIf]="!(codeSnippet.busy$ | async)"> + code + </ng-template> + </div> + </button> <!-- parcellation button --> <button diff --git a/src/atlasComponents/userAnnotations/tools/module.ts b/src/atlasComponents/userAnnotations/tools/module.ts index 31c675db2d9bcf44c0b435047a6ceb0118584157..8085478cca4c911a6fa16a955359f20f06d085f8 100644 --- a/src/atlasComponents/userAnnotations/tools/module.ts +++ b/src/atlasComponents/userAnnotations/tools/module.ts @@ -15,7 +15,7 @@ import { ToolSelect } from "./select"; import { ToolDelete } from "./delete"; import { Polygon, ToolPolygon } from "./poly"; import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; -import { TextareaCopyExportCmp } from "./textareaCopyExport/textareaCopyExport.component"; +import { TextareaCopyExportCmp } from "src/components/textareaCopyExport/textareaCopyExport.component"; @NgModule({ imports: [ @@ -23,13 +23,14 @@ import { TextareaCopyExportCmp } from "./textareaCopyExport/textareaCopyExport.c AngularMaterialModule, UtilModule, ZipFilesOutputModule, + + TextareaCopyExportCmp, ], declarations: [ LineUpdateCmp, PolyUpdateCmp, PointUpdateCmp, ToFormattedStringPipe, - TextareaCopyExportCmp, ], exports: [ LineUpdateCmp, diff --git a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.component.ts b/src/components/textareaCopyExport/textareaCopyExport.component.ts similarity index 71% rename from src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.component.ts rename to src/components/textareaCopyExport/textareaCopyExport.component.ts index f749e39b4380bc99ac4151491c214d7817db0474..b9482ce1f03699b2bcbc633ec1d6b434b38696e2 100644 --- a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.component.ts +++ b/src/components/textareaCopyExport/textareaCopyExport.component.ts @@ -2,13 +2,23 @@ import { Component, Input } from "@angular/core"; import { MatSnackBar } from 'src/sharedModules/angularMaterial.exports' import { ARIA_LABELS } from 'common/constants' import { Clipboard } from "@angular/cdk/clipboard"; +import { AngularMaterialModule } from "src/sharedModules"; +import { CommonModule } from "@angular/common"; +import { ZipFilesOutputModule } from "src/zipFilesOutput/module"; @Component({ selector: 'textarea-copy-export', templateUrl: './textareaCopyExport.template.html', styleUrls: [ './textareaCopyExport.style.css' - ] + ], + standalone: true, + imports: [ + AngularMaterialModule, + CommonModule, + ZipFilesOutputModule, + ], + exportAs: "textAreaCopyExport" }) export class TextareaCopyExportCmp { @@ -31,6 +41,9 @@ export class TextareaCopyExportCmp { @Input('textarea-copy-export-disable') disableFlag: boolean = false + + @Input('textarea-copy-show-suffixes') + showSuffix: boolean = true public ARIA_LABELS = ARIA_LABELS @@ -42,7 +55,7 @@ export class TextareaCopyExportCmp { } copyToClipboard(value: string){ - const success = this.clipboard.copy(`${value}`) + const success = this.clipboard.copy(value) this.snackbar.open( success ? `Copied to clipboard!` : `Failed to copy URL to clipboard!`, null, diff --git a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.style.css b/src/components/textareaCopyExport/textareaCopyExport.style.css similarity index 100% rename from src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.style.css rename to src/components/textareaCopyExport/textareaCopyExport.style.css diff --git a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.template.html b/src/components/textareaCopyExport/textareaCopyExport.template.html similarity index 94% rename from src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.template.html rename to src/components/textareaCopyExport/textareaCopyExport.template.html index 65fe0d11264cf82005dcf795ab3fc19239fa83fb..7e023d9afea7bfd98212dc8fc18a60b3f2ffef8d 100644 --- a/src/atlasComponents/userAnnotations/tools/textareaCopyExport/textareaCopyExport.template.html +++ b/src/components/textareaCopyExport/textareaCopyExport.template.html @@ -10,6 +10,7 @@ #exportTarget>{{ input }}</textarea> <button mat-icon-button + *ngIf="showSuffix" matSuffix iav-stop="click" aria-label="Copy to clipboard" @@ -18,6 +19,7 @@ <i class="fas fa-copy"></i> </button> <button mat-icon-button + *ngIf="showSuffix" matSuffix iav-stop="click" [matTooltip]="ARIA_LABELS.USER_ANNOTATION_EXPORT_SINGLE" diff --git a/src/features/feature-view/feature-view.component.html b/src/features/feature-view/feature-view.component.html index 05bffb881df03a0da88cf1148a1555977512fd52..eea4f10179b7c1aa2c3748d985df35090b7b2603 100644 --- a/src/features/feature-view/feature-view.component.html +++ b/src/features/feature-view/feature-view.component.html @@ -63,6 +63,30 @@ </button> </ng-template> + <!-- code --> + <button mat-list-item + code-snippet + [routeParam]="{ + route: '/feature/{feature_id}', + param: { + path: { + feature_id: view.featureId + } + } + }" + #codeSnippet="codeSnippet" + [disabled]="codeSnippet.busy$ | async"> + <mat-icon matListItemIcon fontSet="fas" fontIcon="fa-code"></mat-icon> + <div matListItemTitle> + <ng-template [ngIf]="codeSnippet.busy$ | async"> + loading code ... + </ng-template> + <ng-template [ngIf]="!(codeSnippet.busy$ | async)"> + code + </ng-template> + </div> + </button> + <!-- anchor --> <ng-template [ngIf]="view.concept"> <button mat-list-item diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index f95b096cd388754725a8842d9893e2f29c7da1d8..bbcbfb28baa1e4a7aa95bd79e4532b320c3a7ae9 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -51,17 +51,26 @@ export class FeatureViewComponent { map(f => f.id) ) - #featureDetail$ = this.#feature$.pipe( - switchMap(f => this.sapi.getV3FeatureDetailWithId(f.id)), - shareReplay(1), + #featureDetail$ = this.#featureId.pipe( + switchMap(fid => this.sapi.getV3FeatureDetailWithId(fid)), ) - #featureDesc$ = this.#feature$.pipe( switchMap(() => concat( of(null as string), this.#featureDetail$.pipe( - map(v => v.desc) + map(v => v?.desc), + catchError((err) => { + let errortext = 'Error fetching feature instance' + + if (err.error instanceof Error) { + errortext += `:\n\n${err.error.toString()}` + } else { + errortext += '!' + } + + return of(errortext) + }), ) )) ) @@ -70,6 +79,7 @@ export class FeatureViewComponent { switchMap(() => concat( of(null), this.#featureDetail$.pipe( + catchError(() => of(null)), map(val => { if (isVoiData(val)) { return val @@ -84,7 +94,8 @@ export class FeatureViewComponent { switchMap(() => concat( of([] as string[]), this.#featureDetail$.pipe( - map(notQuiteRight) + catchError(() => of(null)), + map(notQuiteRight), ) )) ) @@ -118,6 +129,7 @@ export class FeatureViewComponent { switchMap(() => concat( of(true), this.#featureDetail$.pipe( + catchError(() => of(null)), map(() => false) ) )) @@ -157,7 +169,8 @@ export class FeatureViewComponent { switchMap(() => concat( of([] as string[]), this.#featureDetail$.pipe( - map(val => (val.link || []).map(l => l.href)) + catchError(() => of(null as null)), + map(val => (val?.link || []).map(l => l.href)) ) )) ) @@ -292,6 +305,7 @@ export class FeatureViewComponent { ]).pipe( map(([ feature, busy, warnings, additionalLinks, downloadLink, desc ]) => { return { + featureId: feature.id, name: feature.name, links: feature.link, category: feature.category === 'Unknown category' diff --git a/src/features/guards.ts b/src/features/guards.ts index 426d94acfe6fb420636515f79bfc1621c8b725a0..6d13d451464a6b891f8a985adeaaa4f5f12f343f 100644 --- a/src/features/guards.ts +++ b/src/features/guards.ts @@ -3,7 +3,7 @@ import { VoiFeature } from "src/atlasComponents/sapi/sxplrTypes" export { VoiFeature } export function isVoiData(feature: unknown): feature is VoiFeature { - return !!feature['bbox'] + return !!(feature?.['bbox']) } export function notQuiteRight(_feature: unknown): string[] { diff --git a/src/features/module.ts b/src/features/module.ts index e23945532c2d0fb0588518a3ef850e3ea0271dbb..fc440770b1b41651b3c937fee95366cacfc39903 100644 --- a/src/features/module.ts +++ b/src/features/module.ts @@ -24,6 +24,7 @@ import { FEATURE_CONCEPT_TOKEN, FeatureConcept, TPRB } from "./util"; import { BehaviorSubject } from "rxjs"; import { TPBRViewCmp } from "./TPBRView/TPBRView.component"; import { DialogModule } from "src/ui/dialogInfo"; +import { CodeSnippet } from "src/atlasComponents/sapi/codeSnippets/codeSnippet.directive"; @NgModule({ imports: [ @@ -44,6 +45,7 @@ import { DialogModule } from "src/ui/dialogInfo"; PlotlyComponent, AtlasColorMapIntents, TPBRViewCmp, + CodeSnippet, ], declarations: [ EntryComponent, diff --git a/src/sharedModules/angularMaterial.exports.ts b/src/sharedModules/angularMaterial.exports.ts index 4c83edd6418f9fcbcb909b083664f654292e3a49..3e3b99556f4c57fdef9a5fffe88895877d357df9 100644 --- a/src/sharedModules/angularMaterial.exports.ts +++ b/src/sharedModules/angularMaterial.exports.ts @@ -1,8 +1,7 @@ export { MatTab, MatTabGroup } from "@angular/material/tabs"; export { ErrorStateMatcher } from "@angular/material/core"; -export { MatDialogConfig, MatDialog, MatDialogRef } from "@angular/material/dialog"; +export { MAT_DIALOG_DATA, MatDialogConfig, MatDialog, MatDialogRef } from "@angular/material/dialog"; export { MatSnackBar, MatSnackBarRef, SimpleSnackBar, MatSnackBarConfig } from "@angular/material/snack-bar"; -export { MAT_DIALOG_DATA } from "@angular/material/dialog"; export { MatBottomSheet, MatBottomSheetRef, MatBottomSheetConfig } from "@angular/material/bottom-sheet"; export { MatSlideToggle, MatSlideToggleChange } from "@angular/material/slide-toggle" export { MatTableDataSource } from "@angular/material/table" diff --git a/src/util/priority.ts b/src/util/priority.ts index 2dcd8a28b3e6fdbafeddf73bd9ec35c22d1830dd..03e99d3b518da0081dec7c57a7e3e7dd498bbd78 100644 --- a/src/util/priority.ts +++ b/src/util/priority.ts @@ -27,12 +27,26 @@ type Queue = { }) export class PriorityHttpInterceptor implements HttpInterceptor{ + static ErrorToString(err: HttpErrorResponse){ + if (err.status === 504) { + return "Gateway Timeout" + } + if (!!err.error.message) { + try { + const { detail } = JSON.parse(err.error.message) + return detail as string + } catch (e) { + return err.error.message as string + } + } + return err.statusText || err.status.toString() + } private retry = 0 private priorityQueue: Queue[] = [] private currentJob: Set<string> = new Set() - private archive: Map<string, (HttpErrorResponse|HttpResponse<unknown>|Error)> = new Map() + private archive: Map<string, (HttpResponse<unknown>|Error)> = new Map() private queue$: Subject<Queue> = new Subject() private result$: Subject<Result<unknown>> = new Subject() private error$: Subject<ErrorResult> = new Subject() @@ -95,11 +109,13 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ }) } if (val instanceof HttpErrorResponse) { - - this.archive.set(urlWithParams, val) + const error = new Error( + PriorityHttpInterceptor.ErrorToString(val) + ) + this.archive.set(urlWithParams, error) this.error$.next({ urlWithParams, - error: new Error(val.toString()), + error, status: val.status }) } @@ -136,10 +152,11 @@ export class PriorityHttpInterceptor implements HttpInterceptor{ const archive = this.archive.get(urlWithParams) if (archive) { if (archive instanceof Error) { - return throwError(archive) - } - if (archive instanceof HttpErrorResponse) { - return throwError(archive) + return throwError({ + urlWithParams, + error: archive, + status: 400 + }) } if (archive instanceof HttpResponse) { return of( archive.clone() )