From 7232eb0b54d4fab60bd351b12deb6b5415073337 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Tue, 20 Jul 2021 11:39:06 +0200
Subject: [PATCH] Datatable => done

---
 client/package.json                           |  1 +
 .../result/datatable-tab.component.html       | 25 ++++++++
 .../result/datatable-tab.component.ts         | 46 +++++++++++++
 .../search/components/result/index.ts         |  2 +
 .../containers/abstract-search.component.ts   |  2 +-
 .../search/containers/result.component.html   | 12 ++--
 .../search/containers/result.component.ts     | 17 ++---
 .../src/app/instance/search/search.module.ts  |  4 +-
 .../datatable/datatable.component.html        | 18 +++---
 .../datatable/datatable.component.ts          | 64 ++++++++++++-------
 .../renderer/image-renderer.component.ts      |  2 +-
 .../shared-search/components/index.ts         |  4 +-
 .../shared-search/shared-search.module.ts     |  4 +-
 .../instance/store/reducers/search.reducer.ts | 27 +++++++-
 .../store/selectors/search.selector.ts        | 20 ++++--
 client/src/app/shared/shared.module.ts        |  5 +-
 client/src/styles.scss                        |  1 +
 client/tsconfig.json                          |  3 +-
 client/yarn.lock                              |  8 ++-
 19 files changed, 204 insertions(+), 61 deletions(-)
 create mode 100644 client/src/app/instance/search/components/result/datatable-tab.component.html
 create mode 100644 client/src/app/instance/search/components/result/datatable-tab.component.ts

diff --git a/client/package.json b/client/package.json
index 502facc1..7e7cad96 100644
--- a/client/package.json
+++ b/client/package.json
@@ -29,6 +29,7 @@
     "keycloak-angular": "^8.2.0",
     "keycloak-js": "^14.0.0",
     "ngx-bootstrap": "^7.0.0-rc.1",
+    "ngx-json-viewer": "^3.0.2",
     "ngx-toastr": "^14.0.0",
     "rxjs": "~6.6.0",
     "tslib": "^2.1.0",
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html
new file mode 100644
index 00000000..670acae1
--- /dev/null
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.html
@@ -0,0 +1,25 @@
+<accordion *ngIf="getDataset().config.datatable.datatable_enabled" [isAnimated]="true">
+    <accordion-group #ag [isOpen]="getDataset().config.datatable.datatable_opened" [panelClass]="'custom-accordion'" class="my-2">
+        <button class="btn btn-link btn-block clearfix" accordion-heading>
+            <span class="pull-left float-left">
+                Display result details
+                &nbsp;
+                <span *ngIf="ag.isOpen"><span class="fas fa-chevron-up"></span></span>
+                <span *ngIf="!ag.isOpen"><span class="fas fa-chevron-down"></span></span>
+            </span>
+        </button>
+        <app-datatable
+                [dataset]="getDataset()"
+                [attributeList]="attributeList"
+                [outputList]="outputList"
+                [dataLength]="dataLength"
+                [data]="data"
+                [dataIsLoading]="dataIsLoading"
+                [dataIsLoaded]="dataIsLoaded"
+                [selectedData]="selectedData"
+                (retrieveData)="retrieveData.emit($event)"
+                (addSelectedData)="addSelectedData.emit($event)"
+                (deleteSelectedData)="deleteSelectedData.emit($event)">
+        </app-datatable>
+    </accordion-group>
+</accordion>
diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts
new file mode 100644
index 00000000..0b7b2258
--- /dev/null
+++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts
@@ -0,0 +1,46 @@
+/**
+ * This file is part of Anis Client.
+ *
+ * @copyright Laboratoire d'Astrophysique de Marseille / CNRS
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+
+import { Attribute, Dataset } from 'src/app/metamodel/models';
+import { Pagination } from 'src/app/instance/store/models';
+
+@Component({
+    selector: 'app-datatable-tab',
+    templateUrl: 'datatable-tab.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+/**
+ * @class
+ * @classdesc Search result datatable tab component.
+ */
+export class DatatableTabComponent {
+    @Input() datasetSelected: string;
+    @Input() datasetList: Dataset[];
+    @Input() attributeList: Attribute[];
+    @Input() outputList: number[];
+    @Input() dataLength: number;
+    @Input() data: any[];
+    @Input() dataIsLoading: boolean;
+    @Input() dataIsLoaded: boolean;
+    @Input() selectedData: any[];
+    @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter();
+    @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
+    @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
+
+    /**
+     * Returns selected dataset for the search.
+     *
+     * @return Dataset
+     */
+    getDataset(): Dataset {
+        return this.datasetList.find(dataset => dataset.name === this.datasetSelected);
+    }
+}
diff --git a/client/src/app/instance/search/components/result/index.ts b/client/src/app/instance/search/components/result/index.ts
index 68174bc2..f710426a 100644
--- a/client/src/app/instance/search/components/result/index.ts
+++ b/client/src/app/instance/search/components/result/index.ts
@@ -1,9 +1,11 @@
+import { DatatableTabComponent } from './datatable-tab.component';
 import { DownloadComponent } from './download.component';
 import { ReminderComponent } from './reminder.component';
 import { SampComponent } from './samp.component';
 import { UrlDisplayComponent } from './url-display.component';
 
 export const resultComponents = [
+    DatatableTabComponent,
     DownloadComponent,
     ReminderComponent,
     SampComponent,
diff --git a/client/src/app/instance/search/containers/abstract-search.component.ts b/client/src/app/instance/search/containers/abstract-search.component.ts
index a1a3340b..a6ebdbbe 100644
--- a/client/src/app/instance/search/containers/abstract-search.component.ts
+++ b/client/src/app/instance/search/containers/abstract-search.component.ts
@@ -73,7 +73,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy {
         Promise.resolve(null).then(() => this.store.dispatch(searchActions.initSearch()));
         this.attributeListIsLoadedSubscription = this.attributeListIsLoaded.subscribe(attributeListIsLoaded => {
             if (attributeListIsLoaded) {
-                this.store.dispatch(searchActions.loadDefaultFormParameters());
+                Promise.resolve(null).then(() => this.store.dispatch(searchActions.loadDefaultFormParameters()));
             }
         });
     }
diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html
index 3bb295f6..9eacc2ff 100644
--- a/client/src/app/instance/search/containers/result.component.html
+++ b/client/src/app/instance/search/containers/result.component.html
@@ -60,19 +60,21 @@
                 [dataLength]="dataLength | async"
                 [isConeSearchAdded]="isConeSearchAdded | async"
                 [coneSearch]="coneSearch | async">
-            </app-cone-search-plot-tab>
+            </app-cone-search-plot-tab> -->
             <app-datatable-tab
-                [datasetName]="datasetName | async"
+                [datasetSelected]="datasetSelected | async"
                 [datasetList]="datasetList | async"
                 [attributeList]="attributeList | async"
                 [outputList]="outputList | async"
-                [searchData]="searchData | async"
                 [dataLength]="dataLength | async"
+                [data]="data | async"
+                [dataIsLoading]="dataIsLoading | async"
+                [dataIsLoaded]="dataIsLoaded | async"
                 [selectedData]="selectedData | async"
-                (getSearchData)="getSearchData($event)"
+                (retrieveData)="retrieveData($event)"
                 (addSelectedData)="addSearchData($event)"
                 (deleteSelectedData)="deleteSearchData($event)">
-            </app-datatable-tab> -->
+            </app-datatable-tab>
         </ng-container>
     </div>
 </div>
diff --git a/client/src/app/instance/search/containers/result.component.ts b/client/src/app/instance/search/containers/result.component.ts
index 85bd6dc4..1e1c3359 100644
--- a/client/src/app/instance/search/containers/result.component.ts
+++ b/client/src/app/instance/search/containers/result.component.ts
@@ -34,6 +34,10 @@ export class ResultComponent extends AbstractSearchComponent {
     public dataLength: Observable<number>;
     public dataLengthIsLoading: Observable<boolean>;
     public dataLengthIsLoaded: Observable<boolean>;
+    public data: Observable<any>;
+    public dataIsLoading: Observable<boolean>;
+    public dataIsLoaded: Observable<boolean>;
+    public selectedData: Observable<any>;
     public sampRegistered: Observable<boolean>;
 
     private pristineSubscription: Subscription;
@@ -43,6 +47,10 @@ export class ResultComponent extends AbstractSearchComponent {
         this.dataLength = this.store.select(searchSelector.selectDataLength);
         this.dataLengthIsLoading = this.store.select(searchSelector.selectDataLengthIsLoading);
         this.dataLengthIsLoaded = this.store.select(searchSelector.selectDataLengthIsLoaded);
+        this.data = this.store.select(searchSelector.selectData);
+        this.dataIsLoading = this.store.select(searchSelector.selectDataIsLoading);
+        this.dataIsLoaded = this.store.select(searchSelector.selectDataIsLoaded);
+        this.selectedData = this.store.select(searchSelector.selectSelectedData);
         this.sampRegistered = this.store.select(sampSelector.selectRegistered);
     }
 
@@ -71,19 +79,12 @@ export class ResultComponent extends AbstractSearchComponent {
         this.store.dispatch(sampActions.broadcastVotable({ url }));
     }
 
-    /**
-     * Dispatches action to retrieve result number.
-     */
-    getDataLength(): void {
-        // this.store.dispatch(searchActions.retrieveDataLength());
-    }
-
     /**
      * Dispatches action to retrieve data with the given pagination.
      *
      * @param {Pagination} pagination - The pagination parameters.
      */
-    getSearchData(pagination: Pagination): void {
+    retrieveData(pagination: Pagination): void {
         this.store.dispatch(searchActions.retrieveData({ pagination }));
     }
 
diff --git a/client/src/app/instance/search/search.module.ts b/client/src/app/instance/search/search.module.ts
index 8ae63a5d..63eef0ff 100644
--- a/client/src/app/instance/search/search.module.ts
+++ b/client/src/app/instance/search/search.module.ts
@@ -10,14 +10,14 @@
 import { NgModule } from '@angular/core';
 
 import { SharedModule } from 'src/app/shared/shared.module';
-// import { SharedSearchModule } from '../shared-search/shared-search.module';
+import { SharedSearchModule } from '../shared-search/shared-search.module';
 import { SearchRoutingModule, routedComponents } from './search-routing.module';
 import { dummiesComponents } from './components';
 
 @NgModule({
     imports: [
         SharedModule,
-        // SharedSearchModule,
+        SharedSearchModule,
         SearchRoutingModule
     ],
     declarations: [
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.html b/client/src/app/instance/shared-search/components/datatable/datatable.component.html
index fb1fdb31..8a373adc 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.html
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.html
@@ -1,8 +1,10 @@
-<div class="table-responsive">
+<app-spinner *ngIf="(dataIsLoading)"></app-spinner>
+
+<div *ngIf="(dataIsLoaded)" class="table-responsive">
     <table class="table table-bordered table-hover">
         <thead>
             <tr>
-                <th *ngIf="dataset.config.datatable.selectable_row">#</th>
+                <th *ngIf="dataset.config.datatable.datatable_selectable_rows">#</th>
                 <th *ngFor="let attribute of getOutputList()" scope="col" class="clickable" (click)="sort(attribute.id)">
                     {{ attribute.label }}
                     <span *ngIf="attribute.id === sortedCol" class="pl-2">
@@ -26,7 +28,7 @@
         </thead>
         <tbody>
             <tr *ngFor="let datum of data">
-                <td *ngIf="dataset.config.datatable.selectable_row" class="data-selected"
+                <td *ngIf="dataset.config.datatable.datatable_selectable_rows" class="data-selected"
                     (click)="toggleSelection(datum)">
                     <button class="btn btn-block text-left p-0 m-0">
                         <span *ngIf="!isSelected(datum)">
@@ -43,35 +45,35 @@
                             <app-detail-renderer
                                 [value]="datum[attribute.label]"
                                 [datasetName]="dataset.name"
-                                [config]="attribute.renderer_config">
+                                [config]="getRendererConfig(attribute)">
                             </app-detail-renderer>
                         </div>
                         <div *ngSwitchCase="'link'">
                             <app-link-renderer
                                 [value]="datum[attribute.label]"
                                 [datasetName]="dataset.name"
-                                [config]="attribute.renderer_config">
+                                [config]="getRendererConfig(attribute)">
                             </app-link-renderer>
                         </div>
                         <div *ngSwitchCase="'download'">
                             <app-download-renderer
                                 [value]="datum[attribute.label]"
                                 [datasetName]="dataset.name"
-                                [config]="attribute.renderer_config">
+                                [config]="getRendererConfig(attribute)">
                             </app-download-renderer>
                         </div>
                         <div *ngSwitchCase="'image'">
                             <app-image-renderer
                                 [value]="datum[attribute.label]"
                                 [datasetName]="dataset.name"
-                                [config]="attribute.renderer_config">
+                                [config]="getRendererConfig(attribute)">
                             </app-image-renderer>
                         </div>
                         <div *ngSwitchCase="'json'">
                             <app-json-renderer
                                 [value]="datum[attribute.label]"
                                 [attributeLabel]="attribute.label"
-                                [config]="attribute.renderer_config">
+                                [config]="getRendererConfig(attribute)">
                             </app-json-renderer>
                         </div>
                         <div *ngSwitchDefault>
diff --git a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
index 8cfef40a..56d115ee 100644
--- a/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/datatable.component.ts
@@ -9,7 +9,7 @@
 
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 
-import { Attribute, Dataset } from 'src/app/metamodel/models';
+import { Attribute, Dataset, DetailRendererConfig, DownloadRendererConfig, ImageRendererConfig, LinkRendererConfig, RendererConfig } from 'src/app/metamodel/models';
 import { Pagination, PaginationOrder } from 'src/app/instance/store/models';
 
 
@@ -25,36 +25,56 @@ import { Pagination, PaginationOrder } from 'src/app/instance/store/models';
  * @implements OnInit
  */
 export class DatatableComponent implements OnInit {
-    @Input() datasetSelected: string;
-    @Input() datasetList: Dataset[];
+    @Input() dataset: Dataset;
     @Input() attributeList: Attribute[];
     @Input() outputList: number[];
-    @Input() data: any[];
     @Input() dataLength: number;
+    @Input() data: any[];
+    @Input() dataIsLoading: boolean;
+    @Input() dataIsLoaded: boolean;
     @Input() selectedData: any[] = [];
-    @Output() getData: EventEmitter<Pagination> = new EventEmitter();
+    @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter();
     @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
     @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
-    nbItems = 10;
-    page = 1;
-    sortedCol: number = null;
-    sortedOrder: PaginationOrder = PaginationOrder.a;
+
+    public page = 1;
+    public nbItems = 10;
+    public sortedCol: number = null;
+    public sortedOrder: PaginationOrder = PaginationOrder.a;
     
     ngOnInit() {
         this.sortedCol = this.attributeList.find(a => a.order_by).id;
+        Promise.resolve(null).then(() => this.retrieveData.emit({
+            dname: this.dataset.name,
+            page: this.page,
+            nbItems: this.nbItems,
+            sortedCol: this.sortedCol,
+            order: this.sortedOrder
+        }));
     }
 
-    getDataset() {
-        return this.datasetList.find(dataset => dataset.name === this.datasetSelected);
-    }
-
-    /**
-     * Checks if there is no data selected.
-     *
-     * @return boolean
-     */
-    noSelectedData(): boolean {
-        return this.selectedData.length < 1;
+    getRendererConfig(attribute: Attribute) {
+        let config = null;
+        switch(attribute.renderer) {
+            case 'detail':
+                config = attribute.renderer_config as DetailRendererConfig;
+                break;
+            case 'link':
+                config = attribute.renderer_config as LinkRendererConfig;
+                break;
+            case 'download':
+                config = attribute.renderer_config as DownloadRendererConfig;
+                break;
+            case 'image':
+                config = attribute.renderer_config as ImageRendererConfig;
+                break;
+            case 'json':
+                config = attribute.renderer_config as RendererConfig;
+                break;
+            default:
+                config = null;
+        }
+        return config;
     }
 
     /**
@@ -111,13 +131,13 @@ export class DatatableComponent implements OnInit {
     changePage(nb: number): void {
         this.page = nb;
         const pagination: Pagination = {
-            dname: this.getDataset().name,
+            dname: this.dataset.name,
             page: this.page,
             nbItems: this.nbItems,
             sortedCol: this.sortedCol,
             order: this.sortedOrder
         };
-        this.getData.emit(pagination);
+        this.retrieveData.emit(pagination);
     }
 
     /**
diff --git a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
index dac78788..75521e7d 100644
--- a/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
+++ b/client/src/app/instance/shared-search/components/datatable/renderer/image-renderer.component.ts
@@ -12,7 +12,7 @@ import { Component, Input, ChangeDetectionStrategy, TemplateRef } from '@angular
 import { BsModalService } from 'ngx-bootstrap/modal';
 import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
 
-import { ImageRendererConfig } from 'src/app/metamodel/models/renderers/image-renderer-config.model';
+import { ImageRendererConfig } from 'src/app/metamodel/models/renderers';
 import { environment } from 'src/environments/environment';
 
 @Component({
diff --git a/client/src/app/instance/shared-search/components/index.ts b/client/src/app/instance/shared-search/components/index.ts
index 1bd2ebbd..f3ee95d0 100644
--- a/client/src/app/instance/shared-search/components/index.ts
+++ b/client/src/app/instance/shared-search/components/index.ts
@@ -1,7 +1,7 @@
-import { coneSearchComponents } from './cone-search';
+// import { coneSearchComponents } from './cone-search';
 import { datatableComponents } from './datatable';
 
 export const sharedComponents = [
-    coneSearchComponents,
+    // coneSearchComponents,
     datatableComponents
 ];
diff --git a/client/src/app/instance/shared-search/shared-search.module.ts b/client/src/app/instance/shared-search/shared-search.module.ts
index e43fcc5e..8d66fd66 100644
--- a/client/src/app/instance/shared-search/shared-search.module.ts
+++ b/client/src/app/instance/shared-search/shared-search.module.ts
@@ -8,6 +8,7 @@
  */
 
 import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
 
 import { SharedModule } from 'src/app/shared/shared.module';
 import { sharedComponents } from './components';
@@ -15,7 +16,8 @@ import { sharedPipes } from './pipes';
 
 @NgModule({
     imports: [
-        SharedModule
+        SharedModule,
+        RouterModule
     ],
     declarations: [
         sharedComponents,
diff --git a/client/src/app/instance/store/reducers/search.reducer.ts b/client/src/app/instance/store/reducers/search.reducer.ts
index 66221436..ee7d006a 100644
--- a/client/src/app/instance/store/reducers/search.reducer.ts
+++ b/client/src/app/instance/store/reducers/search.reducer.ts
@@ -22,10 +22,12 @@ export interface State {
     coneSearchAdded: boolean;
     criteriaList: Criterion[];
     outputList: number[];
-    searchData: any[];
     dataLengthIsLoading: boolean;
     dataLengthIsLoaded: boolean;
     dataLength: number;
+    data: any[],
+    dataIsLoading: boolean,
+    dataIsLoaded: boolean,
     selectedData: any[];
 }
 
@@ -39,10 +41,12 @@ export const initialState: State = {
     coneSearchAdded: false,
     criteriaList: [],
     outputList: [],
-    searchData: [],
     dataLengthIsLoading: false,
     dataLengthIsLoaded: false,
     dataLength: null,
+    data: [],
+    dataIsLoading: false,
+    dataIsLoaded: false,
     selectedData: []
 };
 
@@ -115,6 +119,21 @@ export const searchReducer = createReducer(
         ...state,
         dataLengthIsLoading: false
     })),
+    on(searchActions.retrieveData, state => ({
+        ...state,
+        dataIsLoading: true,
+        dataIsLoaded: false
+    })),
+    on(searchActions.retrieveDataSuccess, (state, { data }) => ({
+        ...state,
+        data,
+        dataIsLoading: false,
+        dataIsLoaded: true
+    })),
+    on(searchActions.retrieveDataFail, state => ({
+        ...state,
+        dataIsLoading: false
+    })),
     on(searchActions.destroyResults, state => ({
         ...state,
         searchData: [],
@@ -135,8 +154,10 @@ export const selectResultStepChecked = (state: State) => state.resultStepChecked
 export const selectIsConeSearchAdded = (state: State) => state.coneSearchAdded;
 export const selectCriteriaList = (state: State) => state.criteriaList;
 export const selectOutputList = (state: State) => state.outputList;
-export const selectSearchData = (state: State) => state.searchData;
 export const selectDataLengthIsLoading = (state: State) => state.dataLengthIsLoading;
 export const selectDataLengthIsLoaded = (state: State) => state.dataLengthIsLoaded;
 export const selectDataLength = (state: State) => state.dataLength;
+export const selectData = (state: State) => state.data;
+export const selectDataIsLoading = (state: State) => state.dataIsLoading;
+export const selectDataIsLoaded = (state: State) => state.dataIsLoaded;
 export const selectSelectedData = (state: State) => state.selectedData;
diff --git a/client/src/app/instance/store/selectors/search.selector.ts b/client/src/app/instance/store/selectors/search.selector.ts
index 57c9c1ba..ee70032c 100644
--- a/client/src/app/instance/store/selectors/search.selector.ts
+++ b/client/src/app/instance/store/selectors/search.selector.ts
@@ -63,11 +63,6 @@ export const selectOutputList = createSelector(
     fromSearch.selectOutputList
 );
 
-export const selectSearchData = createSelector(
-    selectInstanceState,
-    fromSearch.selectSearchData
-);
-
 export const selectDataLengthIsLoading = createSelector(
     selectInstanceState,
     fromSearch.selectDataLengthIsLoading
@@ -83,6 +78,21 @@ export const selectDataLength = createSelector(
     fromSearch.selectDataLength
 );
 
+export const selectDataIsLoading = createSelector(
+    selectInstanceState,
+    fromSearch.selectDataIsLoading
+);
+
+export const selectDataIsLoaded = createSelector(
+    selectInstanceState,
+    fromSearch.selectDataIsLoaded
+);
+
+export const selectData = createSelector(
+    selectInstanceState,
+    fromSearch.selectData
+);
+
 export const selectSelectedData = createSelector(
     selectInstanceState,
     fromSearch.selectSelectedData
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index b9c2adcc..b402511e 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -23,6 +23,7 @@ import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
 import { TabsModule } from 'ngx-bootstrap/tabs';
 import { PaginationModule } from 'ngx-bootstrap/pagination';
 import { NgSelectModule } from '@ng-select/ng-select';
+import { NgxJsonViewerModule } from 'ngx-json-viewer';
 
 import { sharedComponents } from './components';
 import { sharedPipes } from './pipes';
@@ -47,7 +48,8 @@ import { sharedPipes } from './pipes';
         BsDatepickerModule.forRoot(),
         TabsModule.forRoot(),
         PaginationModule.forRoot(),
-        NgSelectModule
+        NgSelectModule,
+        NgxJsonViewerModule
     ],
     exports: [
         CommonModule,
@@ -64,6 +66,7 @@ import { sharedPipes } from './pipes';
         TabsModule,
         PaginationModule,
         NgSelectModule,
+        NgxJsonViewerModule,
         sharedComponents,
         sharedPipes
     ]
diff --git a/client/src/styles.scss b/client/src/styles.scss
index 89e28473..93c9c212 100644
--- a/client/src/styles.scss
+++ b/client/src/styles.scss
@@ -27,6 +27,7 @@
 @import "~bootstrap/scss/popover";
 @import "~bootstrap/scss/tooltip";
 @import "~bootstrap/scss/progress";
+@import "~bootstrap/scss/pagination";
 @import "~bootstrap/scss/utilities";
 
 /* Import ngx-toastr bootstrap 4 alert styled design */
diff --git a/client/tsconfig.json b/client/tsconfig.json
index 12fb7bd5..824b803c 100644
--- a/client/tsconfig.json
+++ b/client/tsconfig.json
@@ -25,6 +25,7 @@
     "enableI18nLegacyMessageIdFormat": false,
     "strictInjectionParameters": true,
     "strictInputAccessModifiers": true,
-    "strictTemplates": true
+    "strictTemplates": true,
+    "strictDomEventTypes": false
   }
 }
diff --git a/client/yarn.lock b/client/yarn.lock
index 922a5191..2a005f93 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -4908,7 +4908,6 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2:
   resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a"
   integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ==
   dependencies:
-    encoding "^0.1.12"
     minipass "^3.1.0"
     minipass-sized "^1.0.3"
     minizlib "^2.0.0"
@@ -5070,6 +5069,13 @@ ngx-bootstrap@^7.0.0-rc.1:
   dependencies:
     tslib "^2.0.0"
 
+ngx-json-viewer@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/ngx-json-viewer/-/ngx-json-viewer-3.0.2.tgz#91e72fe41f80756181aa0d36b4bfaeac5df5b1b1"
+  integrity sha512-XBj0DgUDIBOeJuAczlFQIIMCaELJGoEbvjBWIXHIh2QebiB5lY6itslRkbE5TAgFn1bYK+2ToxqwspLgP4DDJg==
+  dependencies:
+    tslib "^2.0.0"
+
 ngx-toastr@^14.0.0:
   version "14.0.0"
   resolved "https://registry.yarnpkg.com/ngx-toastr/-/ngx-toastr-14.0.0.tgz#20e4737ef330b892a453768cd98b980558aeb286"
-- 
GitLab