From 4aa3a8250fdb076b64c1f8cfdb962e6610681d74 Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Thu, 18 Apr 2024 14:39:18 +0200
Subject: [PATCH] feat: added code snippet

---
 .helm/adhoc/certificate-sxplr-ebrains.yml     |  2 +-
 .helm/adhoc/ingress-main.yml                  | 29 +++++-
 docs/releases/v2.14.5.md                      |  1 +
 .../sapi/codeSnippets/codeSnippet.dialog.ts   | 31 +++++++
 .../codeSnippets/codeSnippet.directive.ts     | 88 +++++++++++++++++++
 .../sapi/codeSnippets/codeSnippet.style.scss  | 20 +++++
 .../codeSnippets/codeSnippet.template.html    | 31 +++++++
 src/atlasComponents/sapi/sxplrTypes.ts        |  1 +
 src/atlasComponents/sapi/translateV3.ts       |  1 +
 .../sapiViews/core/region/module.ts           |  2 +
 .../region/rich/region.rich.template.html     | 26 ++++++
 .../userAnnotations/tools/module.ts           |  5 +-
 .../textareaCopyExport.component.ts           | 17 +++-
 .../textareaCopyExport.style.css              |  0
 .../textareaCopyExport.template.html          |  2 +
 .../feature-view/feature-view.component.html  | 24 +++++
 .../feature-view/feature-view.component.ts    | 28 ++++--
 src/features/guards.ts                        |  2 +-
 src/features/module.ts                        |  2 +
 src/sharedModules/angularMaterial.exports.ts  |  3 +-
 src/util/priority.ts                          | 33 +++++--
 21 files changed, 324 insertions(+), 24 deletions(-)
 create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.dialog.ts
 create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.directive.ts
 create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.style.scss
 create mode 100644 src/atlasComponents/sapi/codeSnippets/codeSnippet.template.html
 rename src/{atlasComponents/userAnnotations/tools => components}/textareaCopyExport/textareaCopyExport.component.ts (71%)
 rename src/{atlasComponents/userAnnotations/tools => components}/textareaCopyExport/textareaCopyExport.style.css (100%)
 rename src/{atlasComponents/userAnnotations/tools => components}/textareaCopyExport/textareaCopyExport.template.html (94%)

diff --git a/.helm/adhoc/certificate-sxplr-ebrains.yml b/.helm/adhoc/certificate-sxplr-ebrains.yml
index 19a96659e..6e73e4c5e 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 3c69b3b6a..230fcf5aa 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 ec6e17c76..243ce4cf3 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 000000000..63c4e5c2f
--- /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 000000000..1967c3cdc
--- /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 000000000..2a159cdfc
--- /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 000000000..0dbf045b3
--- /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 11d685807..331767f01 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 a2abb1375..03659664a 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 e72f1b154..8bb8f8bf7 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 25a7643b6..33e467679 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 31c675db2..8085478cc 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 f749e39b4..b9482ce1f 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 65fe0d112..7e023d9af 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 05bffb881..eea4f1017 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 f95b096cd..bbcbfb28b 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 426d94acf..6d13d4514 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 e23945532..fc440770b 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 4c83edd64..3e3b99556 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 2dcd8a28b..03e99d3b5 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() )
-- 
GitLab