diff --git a/src/app/metamodel/action/attribute.action.ts b/src/app/metamodel/action/attribute.action.ts
index 4b1d08a0739faaa321933c4985ec069784138bff..199d3326144b3e44a4dcac872b3ecf729aac9dfe 100644
--- a/src/app/metamodel/action/attribute.action.ts
+++ b/src/app/metamodel/action/attribute.action.ts
@@ -4,6 +4,7 @@ import { Attribute } from '../model';
 
 export const LOAD_ATTRIBUTE_SEARCH_META = '[Attribute] Load Attribute Search Meta';
 export const LOAD_ATTRIBUTE_SEARCH_META_SUCCESS = '[Attribute] Load Attribute Search Meta Success';
+export const LOAD_ATTRIBUTE_SEARCH_MULTIPLE_META_SUCCESS = '[Attribute] Load Attribute Search Multiple Meta Success';
 export const LOAD_ATTRIBUTE_SEARCH_META_FAIL = '[Attribute] Load Attribute Search Meta Fail';
 
 export class LoadAttributeSearchMetaAction implements Action {
@@ -18,6 +19,12 @@ export class LoadAttributeSearchMetaSuccessAction implements Action {
     constructor(public payload: Attribute[]) { }
 }
 
+export class LoadAttributeSearchMultipleMetaSuccessAction implements Action {
+    readonly type = LOAD_ATTRIBUTE_SEARCH_MULTIPLE_META_SUCCESS;
+
+    constructor(public payload: Attribute[]) { }
+}
+
 export class LoadAttributeSearchMetaFailAction implements Action {
     readonly type = LOAD_ATTRIBUTE_SEARCH_META_FAIL;
 
@@ -27,4 +34,5 @@ export class LoadAttributeSearchMetaFailAction implements Action {
 export type Actions
     = LoadAttributeSearchMetaAction
     | LoadAttributeSearchMetaSuccessAction
+    | LoadAttributeSearchMultipleMetaSuccessAction
     | LoadAttributeSearchMetaFailAction;
diff --git a/src/app/metamodel/effects/attribute.effects.ts b/src/app/metamodel/effects/attribute.effects.ts
index fd22724f57bc88ddb66ada5938dcdccee357b92b..8b9d239dd47536680a4aeaaa44d3d6e50648da6a 100644
--- a/src/app/metamodel/effects/attribute.effects.ts
+++ b/src/app/metamodel/effects/attribute.effects.ts
@@ -3,30 +3,50 @@ import { Injectable } from '@angular/core';
 import { ToastrService } from 'ngx-toastr';
 import { Effect, Actions, ofType } from '@ngrx/effects';
 import { of } from 'rxjs';
-import { switchMap, map, catchError, tap } from 'rxjs/operators';
+import { switchMap, map, catchError, tap, withLatestFrom } from 'rxjs/operators';
 
 import { Attribute } from '../model';
 import * as attributeActions from '../action/attribute.action';
 import { AttributeService } from '../services/attribute.service';
+import { Store } from "@ngrx/store";
+import * as fromRouter from "@ngrx/router-store";
+import * as utils from "../../shared/utils";
+import * as fromSearch from "../../search/store/search.reducer";
+import * as fromMetamodel from "../reducers";
+import * as fromConeSearch from "../../shared/cone-search/store/cone-search.reducer";
 
 @Injectable()
 export class AttributeEffects {
     constructor(
         private actions$: Actions,
         private attributeService: AttributeService,
-        private toastr: ToastrService
+        private toastr: ToastrService,
+        private store$: Store<{
+            router: fromRouter.RouterReducerState<utils.RouterStateUrl>,
+            search: fromSearch.State,
+            metamodel: fromMetamodel.State,
+            coneSearch: fromConeSearch.State
+        }>
     ) { }
 
     @Effect()
     loadAttributeSearchMetaAction$ = this.actions$.pipe(
         ofType(attributeActions.LOAD_ATTRIBUTE_SEARCH_META),
-        switchMap((action: attributeActions.LoadAttributeSearchMetaAction) =>
-            this.attributeService.retrieveAttributeSearchMeta(action.payload).pipe(
-                map((attributeList: Attribute[]) =>
-                    new attributeActions.LoadAttributeSearchMetaSuccessAction(attributeList)),
+        withLatestFrom(this.store$),
+        switchMap(([action, state]) => {
+            const loadAttributeSearchMetaAction = action as attributeActions.LoadAttributeSearchMetaAction;
+            return this.attributeService.retrieveAttributeSearchMeta(loadAttributeSearchMetaAction.payload).pipe(
+                map((attributeList: Attribute[]) => {
+                    const module: string = state.router.state.url.split('/')[1];
+                    if (module === 'search') {
+                        new attributeActions.LoadAttributeSearchMetaSuccessAction(attributeList)
+                    } else {
+                        new attributeActions.LoadAttributeSearchMultipleMetaSuccessAction(attributeList)
+                    }
+                    }),
                 catchError(() => of(new attributeActions.LoadAttributeSearchMetaFailAction()))
             )
-        )
+        })
     );
 
     @Effect({ dispatch: false })
diff --git a/src/app/search-multiple/components/index.ts b/src/app/search-multiple/components/index.ts
index e9759b2e82a6247e29246651c3eea281e8d71cef..3ae2d9e2a671005cd1036bbd00537e54d4449778 100644
--- a/src/app/search-multiple/components/index.ts
+++ b/src/app/search-multiple/components/index.ts
@@ -4,6 +4,8 @@ import { DatasetListComponent } from './datasets/dataset-list.component';
 import { DatasetsByProjetComponent } from './datasets/datasets-by-projet.component';
 import { OverviewComponent } from './result/overview.component';
 import { DatasetsResultComponent } from './result/datasets-result.component';
+import { DatatableComponent } from "../../shared/datatable/datatable.component";
+import { RendererComponents } from "../../shared/datatable/renderer";
 
 export const dummiesComponents = [
     ProgressBarMultipleComponent,
@@ -11,5 +13,7 @@ export const dummiesComponents = [
     DatasetListComponent,
     DatasetsByProjetComponent,
     OverviewComponent,
-    DatasetsResultComponent
+    DatasetsResultComponent,
+    DatatableComponent,
+    RendererComponents
 ];
\ No newline at end of file
diff --git a/src/app/search-multiple/components/result/datasets-result.component.html b/src/app/search-multiple/components/result/datasets-result.component.html
index f39830447585f11da10b4a58b5f72b69815c8649..95c9678d71345352d68c7aa7f4f7fc2454836e47 100644
--- a/src/app/search-multiple/components/result/datasets-result.component.html
+++ b/src/app/search-multiple/components/result/datasets-result.component.html
@@ -1,7 +1,7 @@
 <div *ngIf="datasetsCountIsLoaded">
     <accordion [isAnimated]="true">
         <ng-container *ngFor="let dataset of getOrderedDatasetWithResults()">
-            <accordion-group (isOpenChange)="retrieveData.emit(dataset.name)" #ag [panelClass]="'custom-accordion'" [isOpen]="false" class="my-2">
+            <accordion-group (isOpenChange)="retrieveMeta.emit(dataset.name)" #ag [panelClass]="'custom-accordion'" [isOpen]="false" class="my-2">
                 <button class="btn btn-link btn-block clearfix" accordion-heading>
                     <div class="pull-left float-left">
                         {{ dataset.label }} <span class="badge badge-pill badge-primary">{{ getCount(dataset.name) }}</span>
@@ -14,27 +14,18 @@
                     </span>
                     </div>
                 </button>
-                toto
+<!--                <app-datatable-->
+<!--                    [dataset]="dataset"-->
+<!--                    [datasetAttributeList]="getDatasetAttributeList(dataset.name)"-->
+<!--                    [outputList]="outputList"-->
+<!--                    [data]="data"-->
+<!--                    [dataLength]="getCount(dataset.name)"-->
+<!--                    (getData)="getData(dataset.name, $event)"-->
+<!--                    (addSelectedData)="addSelectedData($event)"-->
+<!--                    (deleteSelectedData)="deleteSelectedData($event)"-->
+<!--                    (executeProcess)="executeProcess($event)">-->
+<!--                </app-datatable>-->
             </accordion-group>
         </ng-container>
     </accordion>
-</div>
-
-
-<!--        <app-result-datatable-->
-<!--            [datasetName]="datasetName | async"-->
-<!--            [datasetList]="datasetList | async"-->
-<!--            [queryParams]="queryParams | async"-->
-<!--            [datasetAttributeList]="datasetAttributeList | async"-->
-<!--            [outputList]="outputList | async"-->
-<!--            [searchData]="searchData | async"-->
-<!--            [dataLength]="dataLength | async"-->
-<!--            [selectedData]="selectedData | async"-->
-<!--            (getSearchData)="getSearchData($event)"-->
-<!--            (addSelectedData)="addSearchData($event)"-->
-<!--            (deleteSelectedData)="deleteSearchData($event)"-->
-<!--            [processWip]="processWip | async"-->
-<!--            [processDone]="processDone | async"-->
-<!--            [processId]="processId | async"-->
-<!--            (executeProcess)="executeProcess($event)">-->
-<!--        </app-result-datatable>-->
\ No newline at end of file
+</div>
\ No newline at end of file
diff --git a/src/app/search-multiple/components/result/datasets-result.component.ts b/src/app/search-multiple/components/result/datasets-result.component.ts
index 386ff6148a481bae9a9260866f1dd10bf066e404..c892de98b81b339d3e130d1f1b8b1265187fe6c6 100644
--- a/src/app/search-multiple/components/result/datasets-result.component.ts
+++ b/src/app/search-multiple/components/result/datasets-result.component.ts
@@ -1,8 +1,8 @@
-import {Component, Input, Output, ChangeDetectionStrategy, EventEmitter} from '@angular/core';
+import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core';
 
-import { Dataset, Project } from "../../../metamodel/model";
+import { Attribute, Dataset, Project } from "../../../metamodel/model";
 import { DatasetCount } from "../../store/model";
-import {sortByDisplay} from "../../../shared/utils";
+import { sortByDisplay } from "../../../shared/utils";
 
 @Component({
     selector: 'app-datasets-result',
@@ -14,7 +14,10 @@ export class DatasetsResultComponent {
     @Input() projectList: Project[];
     @Input() datasetList: Dataset[];
     @Input() selectedDatasets: string[];
+    // TODO: change any type
+    @Input() datasetsAttributeList: any;
     @Input() datasetsCount: DatasetCount[];
+    @Output() retrieveMeta: EventEmitter<string> = new EventEmitter();
     @Output() retrieveData: EventEmitter<string> = new EventEmitter();
 
     getOrderedDatasetWithResults(): Dataset[] {
@@ -37,4 +40,24 @@ export class DatasetsResultComponent {
     getCount(dname: string): number {
         return this.datasetsCount.find(c => c.dname === dname).count;
     }
+
+    // getDatasetAttributeList(dname: string): Attribute[] {
+    //
+    // }
+
+    getData(dname: string, pagination: any): void {
+        console.log(dname, pagination);
+    }
+
+    addSelectedData(d: number | string): void {
+        console.log('addSelectedData: ' + d);
+    }
+
+    deleteSelectedData(d: number | string): void {
+        console.log('deleteSelectedData: ' + d);
+    }
+
+    executeProcess(typeProcess: string): void {
+        console.log('executeProcess: ' + typeProcess);
+    }
 }
\ No newline at end of file
diff --git a/src/app/search-multiple/containers/result-multiple.component.html b/src/app/search-multiple/containers/result-multiple.component.html
index d066d5d2ccdc24938bb4b046c60acf83f9f16705..f39ce987348fb8aee84bcfa8b161a36a21eca0c6 100644
--- a/src/app/search-multiple/containers/result-multiple.component.html
+++ b/src/app/search-multiple/containers/result-multiple.component.html
@@ -17,7 +17,7 @@
             [datasetList]="datasetList | async"
             [selectedDatasets]="selectedDatasets | async"
             [datasetsCount]="datasetsCount | async"
-            (retrieveData)="retrieveData($event)">
+            (retrieveMeta)="retrieveMeta($event)">
         </app-datasets-result>
     </div>
 </div>
diff --git a/src/app/search-multiple/containers/result-multiple.component.ts b/src/app/search-multiple/containers/result-multiple.component.ts
index 43f43a134344e1c5eba39b0412c4099535c44b24..b6a2cd186e80213ee0a96e4dd0cfb6f8209b2896 100644
--- a/src/app/search-multiple/containers/result-multiple.component.ts
+++ b/src/app/search-multiple/containers/result-multiple.component.ts
@@ -8,6 +8,7 @@ import { Attribute, Dataset, Project } from '../../metamodel/model';
 import { ConeSearch } from "../../shared/cone-search/store/model";
 import * as searchMultipleActions from '../store/search-multiple.action';
 import * as datasetActions from '../../metamodel/action/dataset.action';
+import * as attributeActions from '../../metamodel/action/attribute.action';
 import * as fromSearchMultiple from '../store/search-multiple.reducer';
 import * as fromMetamodel from '../../metamodel/reducers';
 import * as searchMultipleSelector from '../store/search-multiple.selector';
@@ -77,8 +78,8 @@ export class ResultMultipleComponent implements OnInit, OnDestroy {
         this.store.dispatch(new searchMultipleActions.RetrieveDatasetsCountAction());
     }
 
-    retrieveData(dname: string): void {
-        console.log(dname);
+    retrieveMeta(dname: string): void {
+        this.store.dispatch(new attributeActions.LoadAttributeSearchMetaAction(dname));
     }
 
     // getSearchData(pagination: [number, number, number, string]): void {
diff --git a/src/app/search-multiple/store/search-multiple.effects.ts b/src/app/search-multiple/store/search-multiple.effects.ts
index 06b14bef8ff4b46eef86c08bccb9df393cc75ba2..21a48f039b3f183f203bcb7abd2f52cd2f0b7a05 100644
--- a/src/app/search-multiple/store/search-multiple.effects.ts
+++ b/src/app/search-multiple/store/search-multiple.effects.ts
@@ -20,6 +20,7 @@ import * as datasetActions from "../../metamodel/action/dataset.action";
 import {Attribute, Dataset, Family, Project} from "../../metamodel/model";
 import * as searchActions from "../../search/store/search.action";
 import * as documentationActions from "../../documentation/store/documentation.action";
+import * as attributeActions from "../../metamodel/action/attribute.action";
 
 @Injectable()
 export class SearchMultipleEffects {
@@ -122,6 +123,117 @@ export class SearchMultipleEffects {
         tap(_ => this.toastr.error('Loading Failed!', 'The data count of datasets loading failed'))
     );
 
+    @Effect()
+    loadDatasetAttributeListSuccessAction$ = this.actions$.pipe(
+        ofType(attributeActions.LOAD_ATTRIBUTE_SEARCH_MULTIPLE_META_SUCCESS),
+        withLatestFrom(this.store$),
+        switchMap(([action, state]) => {
+            const loadAttributeSearchMultipleMetaSuccessAction = action as attributeActions.LoadAttributeSearchMultipleMetaSuccessAction;
+            console.log('MODULE MULTIPLE');
+
+            return of();
+
+            // const actions: Action[] = [];
+            //
+            // let defaultOutputList = loadAttributeSearchMetaSuccessAction.payload
+            //     .filter(attribute => attribute.selected && attribute.id_output_category)
+            //     .sort((a, b) => a.output_display - b.output_display)
+            //     .map(attribute => attribute.id);
+            //
+            // if (state.router.state.queryParams.a) {
+            //     defaultOutputList = state.router.state.queryParams.a.split(';').map((o: string) => parseInt(o, 10));
+            // }
+            //
+            // actions.push(new searchActions.UpdateOutputListAction(defaultOutputList));
+            //
+            // let defaultCriteriaList = loadAttributeSearchMetaSuccessAction.payload
+            //     .filter(attribute => attribute.id_criteria_family && attribute.search_type && attribute.min)
+            //     .map(attribute => {
+            //         switch (attribute.search_type) {
+            //             case 'field':
+            //             case 'select':
+            //             case 'datalist':
+            //             case 'radio':
+            //             case 'date':
+            //             case 'date-time':
+            //             case 'time':
+            //                 return { id: attribute.id, type: 'field', value: attribute.min.toString(), operator: attribute.operator };
+            //             case 'list':
+            //                 return { id: attribute.id, type: 'list', values: attribute.min.toString().split('|') };
+            //             case 'between':
+            //             case 'between-date':
+            //                 return { id: attribute.id, type: 'between', min: attribute.min.toString(), max: attribute.max.toString() };
+            //             case 'select-multiple':
+            //             case 'checkbox':
+            //                 const msValues = attribute.min.toString().split('|');
+            //                 const options = attribute.options.filter(option => msValues.includes(option.value));
+            //                 return { id: attribute.id, type: 'multiple', options };
+            //             case 'json':
+            //                 const [path, operator, value] = attribute.min.toString().split('|');
+            //                 return { id: attribute.id, type: 'json', path, operator, value };
+            //
+            //             default:
+            //                 return null;
+            //         }
+            //     });
+            //
+            // if (state.router.state.queryParams.c) {
+            //     defaultCriteriaList = state.router.state.queryParams.c.split(';').map((c: string) => {
+            //         const params = c.split('::');
+            //         const attribute = loadAttributeSearchMetaSuccessAction.payload.find(a => a.id === parseInt(params[0], 10));
+            //         switch (attribute.search_type) {
+            //             case 'field':
+            //             case 'select':
+            //             case 'datalist':
+            //             case 'radio':
+            //             case 'date':
+            //             case 'date-time':
+            //             case 'time':
+            //                 return { id: parseInt(params[0], 10), type: 'field', operator: params[1], value: params[2] };
+            //             case 'list':
+            //                 return { id: parseInt(params[0], 10), type: 'list', values: params[2].split('|') };
+            //             case 'between':
+            //             case 'between-date':
+            //                 if (params[1] === 'bw') {
+            //                     const bwValues = params[2].split('|');
+            //                     return { id: parseInt(params[0], 10), type: 'between', min: bwValues[0], max: bwValues[1] };
+            //                 } else if (params[1] === 'gte') {
+            //                     return { id: parseInt(params[0], 10), type: 'between', min: params[2], max: null };
+            //                 } else {
+            //                     return { id: parseInt(params[0], 10), type: 'between', min: null, max: params[2] };
+            //                 }
+            //             case 'select-multiple':
+            //             case 'checkbox':
+            //                 const msValues = params[2].split('|');
+            //                 const options = attribute.options.filter(option => msValues.includes(option.value));
+            //                 return { id: parseInt(params[0], 10), type: 'multiple', options };
+            //             case 'json':
+            //                 const [path, operator, value] = params[2].split('|');
+            //                 return { id: parseInt(params[0], 10), type: 'json', path, operator, value };
+            //
+            //             default:
+            //                 return null;
+            //         }
+            //     });
+            // }
+            //
+            // actions.push(new searchActions.UpdateCriteriaListAction(defaultCriteriaList));
+            //
+            // if (state.router.state.queryParams.cs) {
+            //     const params = state.router.state.queryParams.cs.split(':');
+            //     const coneSearch: ConeSearch = {
+            //         ra: parseFloat(params[0]),
+            //         dec: parseFloat(params[1]),
+            //         radius: parseInt(params[2], 10)
+            //     };
+            //     actions.push(new searchActions.IsConeSearchAddedAction(true));
+            //     actions.push(new coneSearchActions.AddConeSearchAction(coneSearch));
+            // }
+            //
+            // return actions;
+        })
+    );
+
     // @Effect()
     // retrieveDataAction$ = this.actions$.pipe(
     //     ofType(searchMultipleActions.RETRIEVE_DATA),
diff --git a/src/app/shared/datatable/datatable.component.css b/src/app/shared/datatable/datatable.component.css
new file mode 100644
index 0000000000000000000000000000000000000000..19bd4e74589df063181a8bf5fd71302d12bca16f
--- /dev/null
+++ b/src/app/shared/datatable/datatable.component.css
@@ -0,0 +1,32 @@
+.data-selected {
+    cursor: pointer;
+}
+
+.data-selected button:focus {
+    box-shadow: none;
+}
+
+ul {
+    margin-bottom: 0;
+}
+
+.custom-select {
+    width: fit-content;
+}
+
+.clickable:hover {
+    cursor: pointer;
+    background-color: #F7F7F7;
+}
+
+.unsorted {
+    color: #c5c5c5;
+}
+
+.clickable:hover .unsorted, .on-hover, .inactive, .clickable:hover .active {
+    display: none;
+}
+
+.clickable:hover .on-hover, .clickable:hover .inactive {
+    display: inline;
+}
diff --git a/src/app/shared/datatable/datatable.component.html b/src/app/shared/datatable/datatable.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..24e9cc554c037b381ce9e2ea586ccd2dca6da59e
--- /dev/null
+++ b/src/app/shared/datatable/datatable.component.html
@@ -0,0 +1,127 @@
+<div *ngIf="!dataLength || !outputList || !datasetAttributeList || !data" class="text-center">
+    <span class="fas fa-circle-notch fa-spin fa-3x"></span>
+    <span class="sr-only">Loading...</span>
+</div>
+<div *ngIf="dataLength && outputList && datasetAttributeList && data">
+    <div *ngIf="dataset.selectable_row" class="mb-2">
+        <button [disabled]="noSelectedData() || processWip" (click)="emitProcess('csv')"
+            class="btn btn-sm btn-outline-primary">
+            To CSV
+        </button>
+        <span *ngIf="processWip" class="float-right mr-2">
+            <span class="fas fa-circle-notch fa-spin fa-2x"></span>
+        </span>
+        <a *ngIf="processDone" href="http://0.0.0.0:8085/{{ processId }}.csv"
+            class="btn btn-sm btn-outline-secondary float-right">
+            Download your CSV
+        </a>
+    </div>
+    <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+            <thead>
+                <tr>
+                    <th *ngIf="dataset.selectable_row"></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">
+                            <span [ngClass]="{'active': sortedOrder === 'a', 'inactive': sortedOrder === 'd'}">
+                                <span class="fas fa-sort-amount-down-alt"></span>
+                            </span>
+                            <span [ngClass]="{'active': sortedOrder === 'd', 'inactive': sortedOrder === 'a'}">
+                                <span class="fas fa-sort-amount-up"></span>
+                            </span>
+                        </span>
+                        <span *ngIf="attribute.id !== sortedCol" class="pl-2">
+                            <span class="unsorted">
+                                <span class="fas fa-arrows-alt-v"></span>
+                            </span>
+                            <span class="on-hover">
+                                <span class="fas fa-sort-amount-down-alt"></span>
+                            </span>
+                        </span>
+                    </th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr *ngFor="let datum of data">
+                    <td *ngIf="dataset.selectable_row" class="data-selected"
+                        (click)="toggleSelection(datum)">
+                        <button class="btn btn-block text-left p-0 m-0">
+                            <div *ngIf="!isSelected(datum)">
+                                <span class="far fa-square fa-lg text-secondary"></span>
+                            </div>
+                            <div *ngIf="isSelected(datum)">
+                                <span class="fas fa-check-square fa-lg theme-color"></span>
+                            </div>
+                        </button>
+                    </td>
+                    <td *ngFor="let attribute of getOutputList()" class="align-middle">
+                        <div *ngIf="datum[attribute.label]" [ngSwitch]="attribute.renderer">
+                            <div *ngSwitchCase="'detail'">
+                                <app-detail
+                                    [value]="datum[attribute.label]"
+                                    [datasetName]="dataset.name"
+                                    [config]="attribute.renderer_config">
+                                </app-detail>
+                            </div>
+                            <div *ngSwitchCase="'link'">
+                                <app-link
+                                    [value]="datum[attribute.label]"
+                                    [datasetName]="dataset.name"
+                                    [config]="attribute.renderer_config">
+                                </app-link>
+                            </div>
+                            <div *ngSwitchCase="'download'">
+                                <app-download
+                                    [value]="datum[attribute.label]"
+                                    [datasetName]="dataset.name"
+                                    [config]="attribute.renderer_config">
+                                </app-download>
+                            </div>
+                            <div *ngSwitchCase="'image'">
+                                <app-image
+                                    [value]="datum[attribute.label]"
+                                    [datasetName]="dataset.name"
+                                    [config]="attribute.renderer_config">
+                                </app-image>
+                            </div>
+                            <div *ngSwitchCase="'json'">
+                                <app-json
+                                    [value]="datum[attribute.label]"
+                                    [attributeLabel]="attribute.label"
+                                    [config]="attribute.renderer_config">
+                                </app-json>
+                            </div>
+                            <div *ngSwitchDefault>
+                                {{ datum[attribute.label] }}
+                            </div>
+                        </div>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+    <div class="row mt-3">
+        <div class="col">
+            Showing
+            <select class="custom-select" (change)="changeNbItems($event.target.value)">
+                <option value="10" selected="true">10</option>
+                <option value="20">20</option>
+                <option value="50">50</option>
+                <option value="100">100</option>
+            </select>
+            of {{ dataLength }} items
+        </div>
+        <div class="col-auto">
+            <pagination
+            [totalItems]="dataLength"
+            [boundaryLinks]="true"
+            [rotate]="true"
+            [maxSize]="5"
+            [itemsPerPage]="nbItems"
+            previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;" lastText="&raquo;"
+            (pageChanged)="changePage($event.page)">
+        </pagination>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/app/shared/datatable/datatable.component.spec.ts b/src/app/shared/datatable/datatable.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e7cdcadd7c4268a47a65eda14a6d2fa7db108856
--- /dev/null
+++ b/src/app/shared/datatable/datatable.component.spec.ts
@@ -0,0 +1,193 @@
+// import { ComponentFixture, TestBed } from '@angular/core/testing';
+// import { Component, Input } from '@angular/core';
+// import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+//
+// import { AccordionModule } from 'ngx-bootstrap/accordion';
+// import { DatatableSectionComponent } from './datatable.component';
+// import { Dataset } from '../../../metamodel/model';
+// import { ATTRIBUTE_LIST, DATASET_LIST } from '../../../../settings/test-data';
+//
+// describe('[Search][Result] Component: DatatableComponent', () => {
+//     @Component({ selector: 'app-img', template: '' })
+//     class ImgStubComponent {
+//         @Input() src: string;
+//     }
+//
+//     @Component({ selector: 'app-thumbnail', template: '' })
+//     class ThumbnailStubComponent {
+//         @Input() src: string;
+//         @Input() attributeName: string;
+//     }
+//
+//     @Component({ selector: 'app-link', template: '' })
+//     class LinkStubComponent {
+//         @Input() href: string;
+//     }
+//
+//     @Component({ selector: 'app-btn', template: '' })
+//     class BtnStubComponent {
+//         @Input() href: string;
+//     }
+//
+//     @Component({ selector: 'app-detail', template: '' })
+//     class DetailStubComponent {
+//         @Input() style: string;
+//         @Input() datasetName: string;
+//         @Input() data: string | number;
+//     }
+//
+//     @Component({ selector: 'app-download', template: '' })
+//     class DownloadStubComponent {
+//         @Input() href: string;
+//     }
+//
+//     @Component({ selector: 'app-json-renderer', template: '' })
+//     class JsonStubComponent {
+//         @Input() attributeName: string;
+//         @Input() json: string;
+//     }
+//
+//     @Component({ selector: 'pagination', template: '' })
+//     class PaginationStubComponent {
+//         @Input() totalItems: number;
+//         @Input() boundaryLinks: boolean;
+//         @Input() rotate: boolean;
+//         @Input() maxSize: number;
+//     }
+//
+//     let component: DatatableSectionComponent;
+//     let fixture: ComponentFixture<DatatableSectionComponent>;
+//
+//     beforeEach(() => {
+//         TestBed.configureTestingModule({
+//             declarations: [
+//                 DatatableSectionComponent,
+//                 ImgStubComponent,
+//                 ThumbnailStubComponent,
+//                 LinkStubComponent,
+//                 BtnStubComponent,
+//                 DetailStubComponent,
+//                 DownloadStubComponent,
+//                 JsonStubComponent,
+//                 PaginationStubComponent
+//             ],
+//             imports: [AccordionModule.forRoot(), BrowserAnimationsModule]
+//         });
+//         fixture = TestBed.createComponent(DatatableSectionComponent);
+//         component = fixture.componentInstance;
+//     });
+//
+//     it('should create the component', () => {
+//         expect(component).toBeTruthy();
+//     });
+//
+//     it('#isDatatableOpened() should return if datatable has to be opened or not', () => {
+//         component.datasetList = DATASET_LIST;
+//         component.datasetName = 'cat_1';
+//         expect(component.isDatatableOpened()).toBeTruthy();
+//         component.datasetName = 'cat_2';
+//         expect(component.isDatatableOpened()).toBeFalsy();
+//     });
+//
+//     it('#getOutputList() should return filtered output list', () => {
+//         component.outputList = [2]
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         expect(component.getOutputList().length).toBe(1);
+//     });
+//
+//     it('#getDataset() should return dataset object', () => {
+//         component.datasetList = DATASET_LIST;
+//         component.datasetName = 'cat_1';
+//         const dataset: Dataset = component.getDataset();
+//         expect(dataset.name).toBe('cat_1');
+//         expect(dataset.label).toBe('Cat 1');
+//     });
+//
+//     it('#getAttributeId(attributeName) should return id of attributeName', () => {
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         expect(component.getAttributeId('name_one')).toBe(1);
+//     });
+//
+//     it('#toggleSelection(datum) should return add datum to selectedData', () => {
+//         const datum = { label_one: 123456 };
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         component.selectedData = [];
+//         component.addSelectedData.subscribe((event: any) => expect(event).toBe(123456));
+//         component.toggleSelection(datum);
+//     });
+//
+//     it('#toggleSelection(datum) should return remove datum to selectedData', () => {
+//         const datum = { label_one: 123456 };
+//         component.selectedData = [123456];
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         component.deleteSelectedData.subscribe((event: any) => expect(event).toBe(123456));
+//         component.toggleSelection(datum);
+//     });
+//
+//     it('#isSelected(datum) should return true datum is selected', () => {
+//         const datum = { label_one: 123456 };
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         component.selectedData = [123456];
+//         expect(component.isSelected(datum)).toBeTruthy();
+//     });
+//
+//     it('#isSelected(datum) should return false datum is not selected', () => {
+//         const datum = { label_one: 123456 };
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         component.selectedData = [];
+//         expect(component.isSelected(datum)).toBeFalsy();
+//     });
+//
+//     it('#noSelectedData() should return true if no selectedData', () => {
+//         component.selectedData = [];
+//         expect(component.noSelectedData()).toBeTruthy();
+//     });
+//
+//     it('#noSelectedData() should return false if there are selectedData', () => {
+//         component.selectedData = [123456];
+//         expect(component.noSelectedData()).toBeFalsy();
+//     });
+//
+//     it('#emitProcess() should raise executeProcess event and transmit process type', () => {
+//         component.executeProcess.subscribe((event: string) => expect(event).toBe('test'));
+//         component.emitProcess('test');
+//     });
+//
+//     it('#changePage() should change page value and raise getSearchData event', () => {
+//         component.sortedCol = 1;
+//         component.sortedOrder = 'a';
+//         component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([2, 10, 1, 'a']));
+//         component.changePage(2);
+//     });
+//
+//     it('#changeNbItems() should change nbItems value and raise getSearchData event', () => {
+//         component.sortedCol = 1;
+//         component.sortedOrder = 'a';
+//         component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 20, 1, 'a']));
+//         component.changeNbItems(20);
+//     });
+//
+//     it('#ngOnInit() should init sortedCol and sortedOrder values', () => {
+//         component.datasetAttributeList = ATTRIBUTE_LIST;
+//         component.ngOnInit();
+//         expect(component.sortedCol).toEqual(1);
+//         expect(component.sortedOrder).toBe('a');
+//     });
+//
+//     it('#sort() should raise getSearchData event with correct parameters', () => {
+//         component.sortedOrder = 'a';
+//         let subscribtion = component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 10, 1, 'a']));
+//         component.sort(1);
+//         subscribtion.unsubscribe();
+//         component.sortedCol = 1;
+//         component.sortedOrder = 'a';
+//         subscribtion = component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 10, 1, 'd']));
+//         component.sort(1);
+//         subscribtion.unsubscribe();
+//         component.sortedCol = 1;
+//         component.sortedOrder = 'd';
+//         subscribtion = component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 10, 1, 'a']));
+//         component.sort(1);
+//     });
+// });
+//
diff --git a/src/app/shared/datatable/datatable.component.ts b/src/app/shared/datatable/datatable.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02d0823af89086358cfcc36b501d5a5981a29bc0
--- /dev/null
+++ b/src/app/shared/datatable/datatable.component.ts
@@ -0,0 +1,97 @@
+import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter, ViewEncapsulation, OnInit } from '@angular/core';
+
+// import { SearchQueryParams } from '../../store/model';
+import { Attribute, Dataset } from 'src/app/metamodel/model';
+
+
+@Component({
+    selector: 'app-datatable',
+    templateUrl: 'datatable.component.html',
+    styleUrls: ['datatable.component.css'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+    encapsulation: ViewEncapsulation.None
+})
+export class DatatableComponent implements OnInit {
+    @Input() dataset: Dataset;
+    // @Input() queryParams: SearchQueryParams;
+    @Input() datasetAttributeList: Attribute[];
+    @Input() outputList: number[];
+    @Input() data: any[];
+    @Input() dataLength: number;
+    @Input() selectedData: any[] = [];
+    @Input() processWip: boolean = false;
+    @Input() processDone: boolean = false;
+    @Input() processId: string = null;
+    @Output() getData: EventEmitter<[number, number, number, string]> = new EventEmitter();
+    @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter();
+    @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter();
+    @Output() executeProcess: EventEmitter<string> = new EventEmitter();
+    nbItems = 10;
+    page = 1;
+    sortedCol: number = null;
+    sortedOrder: string = null;
+
+    ngOnInit() {
+        this.sortedCol = this.datasetAttributeList.find(a => a.search_flag === 'ID').id; 
+        this.sortedOrder = 'a';
+        this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]);
+    }
+
+    getOutputList(): Attribute[] {
+        return this.datasetAttributeList
+            .filter(a => this.outputList.includes(a.id))
+            .sort((a, b) => a.output_display - b.output_display);
+    }
+
+    getAttributeId(attributeName: string): number {
+        const attribute = this.datasetAttributeList.find(a => a.name === attributeName);
+        return attribute.id;
+    }
+
+    toggleSelection(datum: any): void {
+        const attribute = this.datasetAttributeList.find(a => a.search_flag === 'ID');
+
+        const index = this.selectedData.indexOf(datum[attribute.label]);
+        if (index > -1) {
+            this.deleteSelectedData.emit(datum[attribute.label]);
+        } else {
+            this.addSelectedData.emit(datum[attribute.label]);
+        }
+    }
+
+    isSelected(datum: any): boolean {
+        const attribute = this.datasetAttributeList.find(a => a.search_flag === 'ID');
+
+        if (this.selectedData.indexOf(datum[attribute.label]) > -1) {
+            return true;
+        }
+        return false;
+    }
+
+    noSelectedData(): boolean {
+        return this.selectedData.length < 1;
+    }
+
+    emitProcess(typeProcess: string): void {
+        this.executeProcess.emit(typeProcess);
+    }
+
+    changePage(nb: number): void {
+        this.page = nb;
+        this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]);
+    } 
+    changeNbItems(nb: number): void {
+        this.nbItems = nb;
+        this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]);
+    }
+
+    sort(id: number): void {
+        if (id === this.sortedCol) {
+            this.sortedOrder = this.sortedOrder === 'a' ? 'd' : 'a';
+        } else {
+            this.sortedCol = id;
+            this.sortedOrder = 'a';
+        }
+        this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]);
+    }
+}
diff --git a/src/app/shared/datatable/renderer/detail.component.html b/src/app/shared/datatable/renderer/detail.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..0b6336b1e1acb9cf348dc857b43bd2fedc62ddc2
--- /dev/null
+++ b/src/app/shared/datatable/renderer/detail.component.html
@@ -0,0 +1,4 @@
+<a routerLink="/detail/{{ datasetName }}/{{ value }}" [target]="config.blank == true ? '_blank' : '_self'"
+    [ngClass]="{'btn btn-outline-primary btn-sm' : config.display == 'text-button'}">
+    {{ value }}
+</a>
diff --git a/src/app/shared/datatable/renderer/detail.component.spec.ts b/src/app/shared/datatable/renderer/detail.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eb79818742ce02ac9ed386dbdad2dc9c6f98008a
--- /dev/null
+++ b/src/app/shared/datatable/renderer/detail.component.spec.ts
@@ -0,0 +1,22 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { DetailComponent } from './detail.component';
+
+describe('[Search][Result][Renderer] Component: DetailComponent', () => {
+    let component: DetailComponent;
+    let fixture: ComponentFixture<DetailComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [DetailComponent],
+            imports: [RouterTestingModule]
+        });
+        fixture = TestBed.createComponent(DetailComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/src/app/shared/datatable/renderer/detail.component.ts b/src/app/shared/datatable/renderer/detail.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..900ed6c9cc0ed894cdb9c89e4ed00e75d140aa41
--- /dev/null
+++ b/src/app/shared/datatable/renderer/detail.component.ts
@@ -0,0 +1,19 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { RendererConfig } from "../../../metamodel/model";
+
+interface DetailConfig extends RendererConfig {
+    display: string;
+    blank: boolean;
+}
+
+@Component({
+    selector: 'app-detail',
+    templateUrl: 'detail.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class DetailComponent {
+    @Input() value: string | number;
+    @Input() datasetName: string;
+    @Input() config: DetailConfig;
+}
diff --git a/src/app/shared/datatable/renderer/download.component.html b/src/app/shared/datatable/renderer/download.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..430f6d477c5e259505a4abf00839fd14a9df8856
--- /dev/null
+++ b/src/app/shared/datatable/renderer/download.component.html
@@ -0,0 +1,4 @@
+<a [href]="getHref()" [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button')}">
+    <span *ngIf="config.display !== 'icon-button'">{{ config.text }}</span>
+    <span *ngIf="config.display === 'icon-button'" class="{{config.icon}}"></span>
+</a>
\ No newline at end of file
diff --git a/src/app/shared/datatable/renderer/download.component.spec.ts b/src/app/shared/datatable/renderer/download.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..47a8674d2aefe3cba0dfa41187ada36822a2661c
--- /dev/null
+++ b/src/app/shared/datatable/renderer/download.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DownloadComponent } from './download.component';
+import { environment } from "../../../../../environments/environment";
+
+describe('[Search][Result][Renderer] Component: DownloadComponent', () => {
+    let component: DownloadComponent;
+    let fixture: ComponentFixture<DownloadComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [DownloadComponent]
+        });
+        fixture = TestBed.createComponent(DownloadComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+    
+    it('#getHref() should return file url', () => {
+        component.datasetName = 'dname';
+        component.value = 'val';
+        expect(component.getHref()).toBe(environment.apiUrl + '/download-file/dname/val');
+    });
+});
diff --git a/src/app/shared/datatable/renderer/download.component.ts b/src/app/shared/datatable/renderer/download.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bbd64eeff295c5bb5ecdba5ee4f32358709fc3fe
--- /dev/null
+++ b/src/app/shared/datatable/renderer/download.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { RendererConfig } from '../../../metamodel/model';
+import { getHost } from "../../utils";
+
+interface LinkConfig extends RendererConfig {
+    display: string;
+    text: string;
+    icon: string;
+}
+
+@Component({
+    selector: 'app-download',
+    templateUrl: 'download.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class DownloadComponent {
+    @Input() value: string;
+    @Input() datasetName: string;
+    @Input() config: LinkConfig;
+
+    getHref(): string {
+        return getHost() + '/download-file/' + this.datasetName + '/' + this.value;
+    }
+}
diff --git a/src/app/shared/datatable/renderer/image.component.html b/src/app/shared/datatable/renderer/image.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..471be7f6a950997b0bf1519feebc7affe4c3b94d
--- /dev/null
+++ b/src/app/shared/datatable/renderer/image.component.html
@@ -0,0 +1 @@
+<img [src]="getValue()" [alt]="getValue()">
\ No newline at end of file
diff --git a/src/app/shared/datatable/renderer/image.component.spec.ts b/src/app/shared/datatable/renderer/image.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c89f860dbe86aff3f9bc65f2b174d7f4e72e4ca7
--- /dev/null
+++ b/src/app/shared/datatable/renderer/image.component.spec.ts
@@ -0,0 +1,27 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ImageComponent } from './image.component';
+import { environment } from "../../../../../environments/environment";
+
+describe('[Search][Result][Renderer] Component: ImageComponent', () => {
+    let component: ImageComponent;
+    let fixture: ComponentFixture<ImageComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [ImageComponent]
+        });
+        fixture = TestBed.createComponent(ImageComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+    
+    it('#getValue() should return image url', () => {
+        component.datasetName = 'dname';
+        component.value = 'val';
+        expect(component.getValue()).toEqual(environment.apiUrl + '/download-file/dname/val');
+    });
+});
diff --git a/src/app/shared/datatable/renderer/image.component.ts b/src/app/shared/datatable/renderer/image.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29477adb3ea4c6ec60d84debdebbacc782916963
--- /dev/null
+++ b/src/app/shared/datatable/renderer/image.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { RendererConfig } from '../../../metamodel/model';
+import { getHost } from "../../utils";
+
+interface ImageConfig extends RendererConfig {
+    display: string;
+    dataset_file: boolean;
+    blank: boolean;
+}
+
+@Component({
+    selector: 'app-image',
+    templateUrl: 'image.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ImageComponent {
+    @Input() value: string | number;
+    @Input() datasetName: string;
+    @Input() config: ImageConfig;
+
+    getValue(): string {
+        return getHost() + '/download-file/' + this.datasetName + '/' + this.value;
+    }
+}
diff --git a/src/app/shared/datatable/renderer/index.ts b/src/app/shared/datatable/renderer/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce40ca2d3d5111d1125744c23658002281bd0451
--- /dev/null
+++ b/src/app/shared/datatable/renderer/index.ts
@@ -0,0 +1,13 @@
+import { DetailComponent } from './detail.component';
+import { ImageComponent } from './image.component';
+import { JsonComponent } from './json.component';
+import { LinkComponent } from './link.component';
+import { DownloadComponent } from './download.component';
+
+export const RendererComponents = [
+    DetailComponent,
+    ImageComponent,
+    JsonComponent,
+    LinkComponent,
+    DownloadComponent
+];
diff --git a/src/app/shared/datatable/renderer/json.component.html b/src/app/shared/datatable/renderer/json.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..95f6bf5cbac116ba299049a5cbbf7c9be31549c9
--- /dev/null
+++ b/src/app/shared/datatable/renderer/json.component.html
@@ -0,0 +1,15 @@
+<button class="btn btn-outline-primary btn-sm" (click)="openModal(modal)">
+    JSON
+</button>
+
+<ng-template #modal>
+    <div class="modal-header">
+        <h4 class="modal-title pull-left">{{ attributeLabel }}</h4>
+        <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
+            <span aria-hidden="true">&times;</span>
+        </button>
+    </div>
+    <div class="modal-body">
+        <ngx-json-viewer [json]="value"></ngx-json-viewer>
+    </div>
+</ng-template>
\ No newline at end of file
diff --git a/src/app/shared/datatable/renderer/json.component.spec.ts b/src/app/shared/datatable/renderer/json.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e8b0896a8b169139c0df831be80ef22df7832e47
--- /dev/null
+++ b/src/app/shared/datatable/renderer/json.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NgxJsonViewerModule } from 'ngx-json-viewer';
+import { ModalModule } from 'ngx-bootstrap/modal';
+import { BsModalService } from 'ngx-bootstrap/modal';
+
+import { JsonComponent } from './json.component';
+
+describe('[Search][Result][Renderer] Component: JsonComponent', () => {
+    let component: JsonComponent;
+    let fixture: ComponentFixture<JsonComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [JsonComponent],
+            imports: [NgxJsonViewerModule, ModalModule.forRoot()],
+            providers: [BsModalService]
+        });
+        fixture = TestBed.createComponent(JsonComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+});
diff --git a/src/app/shared/datatable/renderer/json.component.ts b/src/app/shared/datatable/renderer/json.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9963350fde91d1ea691faa3e1a0986eb08e5abab
--- /dev/null
+++ b/src/app/shared/datatable/renderer/json.component.ts
@@ -0,0 +1,29 @@
+import { Component, ChangeDetectionStrategy, Input, TemplateRef } from '@angular/core';
+import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
+
+import { RendererConfig } from '../../../metamodel/model';
+
+interface JsonConfig extends RendererConfig {
+}
+
+@Component({
+    selector: 'app-json',
+    templateUrl: 'json.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class JsonComponent {
+    @Input() value: string | number;
+    @Input() attributeLabel: string;
+    @Input() config: JsonConfig;
+
+    modalRef: BsModalRef;
+
+    constructor(private modalService: BsModalService) { }
+
+    openModal(template: TemplateRef<any>) {
+        this.modalRef = this.modalService.show(
+            template,
+            Object.assign({}, { class: 'modal-fit-content' })
+        );
+    }
+}
diff --git a/src/app/shared/datatable/renderer/link.component.html b/src/app/shared/datatable/renderer/link.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..2ec236b52990cc59eb07abbfdb083c94ff56f00d
--- /dev/null
+++ b/src/app/shared/datatable/renderer/link.component.html
@@ -0,0 +1,5 @@
+<a [href]="getValue()" target="{{(config.blank) ? '_blank' : '_self'}}"
+    [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button')}">
+    <span *ngIf="config.display !== 'icon-button'">{{ getText() }}</span>
+    <span *ngIf="config.display === 'icon-button'" class="{{config.icon}}"></span>
+</a>
\ No newline at end of file
diff --git a/src/app/shared/datatable/renderer/link.component.spec.ts b/src/app/shared/datatable/renderer/link.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..74887d573b2ab805bc59e21c398df712c4717672
--- /dev/null
+++ b/src/app/shared/datatable/renderer/link.component.spec.ts
@@ -0,0 +1,44 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LinkComponent } from './link.component';
+
+describe('[Search][Result][Renderer] Component: LinkComponent', () => {
+    let component: LinkComponent;
+    let fixture: ComponentFixture<LinkComponent>;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            declarations: [LinkComponent]
+        });
+        fixture = TestBed.createComponent(LinkComponent);
+        component = fixture.componentInstance;
+    });
+
+    it('should create the component', () => {
+        expect(component).toBeTruthy();
+    });
+
+    it('#getValue() should return link url', () => {
+        component.config = {
+            href: 'url',
+            display: 'display',
+            text: 'text',
+            icon: 'icon',
+            blank: true
+        };
+        component.value = 'val';
+        expect(component.getValue()).toEqual('url');
+    });
+    
+    it('#getText() should return link text', () => {
+        component.config = {
+            href: 'url',
+            display: 'display',
+            text: 'text',
+            icon: 'icon',
+            blank: true
+        };
+        component.value = 'val';
+        expect(component.getText()).toEqual('text');
+    });
+});
diff --git a/src/app/shared/datatable/renderer/link.component.ts b/src/app/shared/datatable/renderer/link.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f22acc8ea1457600005e2d983db43b56b07d19b
--- /dev/null
+++ b/src/app/shared/datatable/renderer/link.component.ts
@@ -0,0 +1,30 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { RendererConfig } from '../../../metamodel/model';
+
+interface LinkConfig extends RendererConfig {
+    href: string;
+    display: string;
+    text: string;
+    icon: string;
+    blank: boolean;
+}
+
+@Component({
+    selector: 'app-link',
+    templateUrl: 'link.component.html',
+    changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class LinkComponent {
+    @Input() value: string | number;
+    @Input() datasetName: string;
+    @Input() config: LinkConfig;
+
+    getValue() {
+        return this.config.href.replace('$value', this.value.toString());
+    }
+
+    getText() {
+        return this.config.text.replace('$value', this.value.toString());
+    }
+}