From f05c475af6896dc9ce252056186ba41bf2055535 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Fri, 1 Apr 2022 11:51:05 +0200
Subject: [PATCH] Create containers for dataset images

---
 .../dataset/dataset-card.component.html       |  4 +
 .../dataset/dataset-form.component.html       | 18 -----
 .../dataset/dataset-form.component.ts         | 10 ---
 .../components/image/image-form.component.ts  |  7 +-
 .../image/image-table.component.html          | 60 +--------------
 .../components/image/image-table.component.ts | 38 +---------
 .../containers/edit-dataset.component.html    | 10 ---
 .../containers/edit-dataset.component.ts      | 36 +--------
 .../containers/edit-image.component.html      | 47 ++++++++++++
 .../containers/edit-image.component.ts        | 76 +++++++++++++++++++
 .../containers/image-list.component.html      | 38 ++++++++++
 .../containers/image-list.component.ts        | 51 +++++++++++++
 .../containers/new-image.component.html       | 46 +++++++++++
 .../dataset/containers/new-image.component.ts | 65 ++++++++++++++++
 .../dataset/dataset-routing.module.ts         | 11 ++-
 .../app/metamodel/effects/image.effects.ts    | 21 ++++-
 .../app/metamodel/selectors/image.selector.ts |  6 ++
 17 files changed, 375 insertions(+), 169 deletions(-)
 create mode 100644 client/src/app/admin/instance/dataset/containers/edit-image.component.html
 create mode 100644 client/src/app/admin/instance/dataset/containers/edit-image.component.ts
 create mode 100644 client/src/app/admin/instance/dataset/containers/image-list.component.html
 create mode 100644 client/src/app/admin/instance/dataset/containers/image-list.component.ts
 create mode 100644 client/src/app/admin/instance/dataset/containers/new-image.component.html
 create mode 100644 client/src/app/admin/instance/dataset/containers/new-image.component.ts

diff --git a/client/src/app/admin/instance/dataset/components/dataset/dataset-card.component.html b/client/src/app/admin/instance/dataset/components/dataset/dataset-card.component.html
index 7ea2e12a..0c757a81 100644
--- a/client/src/app/admin/instance/dataset/components/dataset/dataset-card.component.html
+++ b/client/src/app/admin/instance/dataset/components/dataset/dataset-card.component.html
@@ -17,6 +17,10 @@
             <span class="fas fa-cog"></span>
         </a>
         &nbsp;
+        <a routerLink="/admin/instance/configure-instance/{{ instance.name }}/dataset/image-list/{{dataset.name}}" class="btn btn-outline-primary" title="Configure images">
+            <span class="fas fa-image"></span>
+        </a>
+        &nbsp;
         <a title="Edit this dataset" routerLink="/admin/instance/configure-instance/{{ instance.name }}/dataset/edit-dataset/{{dataset.name}}" class="btn btn-outline-primary">
             <span class="fas fa-edit"></span>
         </a>
diff --git a/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.html b/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.html
index 0200ca1d..94c24e99 100644
--- a/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.html
+++ b/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.html
@@ -102,24 +102,6 @@
                 <input class="custom-control-input" type="checkbox" id="cone_search_plot_enabled" name="cone_search_plot_enabled" formControlName="cone_search_plot_enabled">
                 <label class="custom-control-label" for="cone_search_plot_enabled">Plot enabled</label>
             </div>
-            <app-image-table
-                [instanceDataPath]="instance.data_path"
-                [datasetDataPath]="form.controls.data_path.value"
-                [files]="files"
-                [filesIsLoading]="filesIsLoading"
-                [filesIsLoaded]="filesIsLoaded"
-                [imageList]="imageList"
-                [imageListIsLoading]="imageListIsLoading"
-                [imageListIsLoaded]="imageListIsLoaded"
-                [fitsImageLimits]="fitsImageLimits"
-                [fitsImageLimitsIsLoading]="fitsImageLimitsIsLoading"
-                [fitsImageLimitsIsLoaded]="fitsImageLimitsIsLoaded"
-                (loadRootDirectory)="loadRootDirectory.emit($event)"
-                (retrieveFitsImageLimits)="retrieveFitsImageLimits.emit($event)"
-                (addNewImage)="addNewImage.emit($event)"
-                (editImage)="editImage.emit($event)"
-                (deleteImage)="deleteImage.emit($event)">
-            </app-image-table>
         </accordion-group>
         <accordion-group heading="Download" [isOpen]="true">
             <div class="custom-control custom-switch">
diff --git a/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.ts b/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.ts
index fd5949c5..ea13faa1 100644
--- a/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.ts
+++ b/client/src/app/admin/instance/dataset/components/dataset/dataset-form.component.ts
@@ -30,18 +30,8 @@ export class DatasetFormComponent implements OnInit, OnChanges {
     @Input() filesIsLoading: boolean;
     @Input() filesIsLoaded: boolean;
     @Input() attributeList: Attribute[];
-    @Input() imageList: Image[];
-    @Input() imageListIsLoading: boolean;
-    @Input() imageListIsLoaded: boolean;
-    @Input() fitsImageLimits: FitsImageLimits;
-    @Input() fitsImageLimitsIsLoading: boolean;
-    @Input() fitsImageLimitsIsLoaded: boolean;
     @Output() changeSurvey: EventEmitter<number> = new EventEmitter();
     @Output() loadRootDirectory: EventEmitter<string> = new EventEmitter();
-    @Output() retrieveFitsImageLimits: EventEmitter<string> = new EventEmitter();
-    @Output() addNewImage: EventEmitter<Image> = new EventEmitter();
-    @Output() editImage: EventEmitter<Image> = new EventEmitter();
-    @Output() deleteImage: EventEmitter<Image> = new EventEmitter();
     @Output() onSubmit: EventEmitter<Dataset> = new EventEmitter();
 
     public isNewDataset = true;
diff --git a/client/src/app/admin/instance/dataset/components/image/image-form.component.ts b/client/src/app/admin/instance/dataset/components/image/image-form.component.ts
index 955277af..1ad3258b 100644
--- a/client/src/app/admin/instance/dataset/components/image/image-form.component.ts
+++ b/client/src/app/admin/instance/dataset/components/image/image-form.component.ts
@@ -10,7 +10,7 @@
 import { Component, Input, Output, OnInit, OnChanges, EventEmitter, SimpleChanges } from '@angular/core';
 import { FormGroup, FormControl, Validators } from '@angular/forms';
 
-import { Image } from 'src/app/metamodel/models';
+import { Image, Dataset, Instance } from 'src/app/metamodel/models';
 import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
 
 @Component({
@@ -19,7 +19,8 @@ import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
 })
 export class ImageFormComponent implements OnInit, OnChanges {
     @Input() image: Image;
-    @Input() datasetDataPath: string;
+    @Input() instance: Instance;
+    @Input() dataset: Dataset;
     @Input() files: FileInfo[];
     @Input() filesIsLoading: boolean;
     @Input() filesIsLoaded: boolean;
@@ -55,7 +56,7 @@ export class ImageFormComponent implements OnInit, OnChanges {
     }
 
     onChangeFileSelect(path: string) {
-        this.loadRootDirectory.emit(`${this.datasetDataPath}${path}`);
+        this.loadRootDirectory.emit(`${this.instance.data_path}${this.dataset.data_path}${path}`);
     }
 
     onFileSelect(fileInfo: FileInfo) {
diff --git a/client/src/app/admin/instance/dataset/components/image/image-table.component.html b/client/src/app/admin/instance/dataset/components/image/image-table.component.html
index 3440ea61..a5256f23 100644
--- a/client/src/app/admin/instance/dataset/components/image/image-table.component.html
+++ b/client/src/app/admin/instance/dataset/components/image/image-table.component.html
@@ -1,6 +1,3 @@
-<button (click)="openModal(templateNewImage)" class="btn btn-outline-primary mt-3" type="button">
-    <i class="far fa-image"></i> Add new image
-</button>
 <div class="mt-2 table-responsive">
     <table class="table">
         <thead>
@@ -26,7 +23,9 @@
                 <td class="align-middle">{{ image.pmin }}</td>
                 <td class="align-middle">{{ image.pmax }}</td>
                 <td class="align-middle">
-                    <a title="Edit this image" (click)="openModal(templateEditImage, image)" class="btn btn-outline-primary">
+                    <a title="Edit this image"
+                        routerLink="/admin/instance/configure-instance/{{ instance.name }}/dataset/image-list/{{ dataset.name }}/edit-image/{{ image.id }}" 
+                        class="btn btn-outline-primary">
                         <span class="fas fa-edit"></span>
                     </a>
                 </td>
@@ -41,56 +40,3 @@
         </tbody>
     </table>
 </div>
-
-<ng-template #templateNewImage>
-    <div class="modal-header">
-        <h4 class="modal-title pull-left"><strong>Add a new image</strong></h4>
-    </div>
-    <div class="modal-body">
-        <app-image-form 
-            [datasetDataPath]="datasetDataPath"
-            [files]="files"
-            [filesIsLoading]="filesIsLoading"
-            [filesIsLoaded]="filesIsLoaded"
-            [fitsImageLimits]="fitsImageLimits"
-            [fitsImageLimitsIsLoading]="fitsImageLimitsIsLoading"
-            [fitsImageLimitsIsLoaded]="fitsImageLimitsIsLoaded"
-            (loadRootDirectory)="onChangeDataPath($event)"
-            (retrieveFitsImageLimits)="retrieveFitsImageLimits.emit($event)"
-            (onSubmit)="addNewImage.emit($event)" 
-            #formAddDatasetFamily>
-            <button [disabled]="!formAddDatasetFamily.form.valid || formAddDatasetFamily.form.pristine" (click)="modalRef.hide()" type="submit" class="btn btn-primary">
-                <span class="fa fa-database"></span> Add new image
-            </button>
-            &nbsp;
-            <button (click)="modalRef.hide()" type="button" class="btn btn-danger">Cancel</button>
-        </app-image-form>
-    </div>
-</ng-template>
-
-<ng-template #templateEditImage>
-    <div class="modal-header">
-        <h4 class="modal-title pull-left"><strong>Edit image</strong></h4>
-    </div>
-    <div class="modal-body">
-        <app-image-form 
-            [image]="imageForEdit"
-            [datasetDataPath]="datasetDataPath"
-            [files]="files"
-            [filesIsLoading]="filesIsLoading"
-            [filesIsLoaded]="filesIsLoaded"
-            [fitsImageLimits]="fitsImageLimits"
-            [fitsImageLimitsIsLoading]="fitsImageLimitsIsLoading"
-            [fitsImageLimitsIsLoaded]="fitsImageLimitsIsLoaded"
-            (loadRootDirectory)="onChangeDataPath($event)"
-            (retrieveFitsImageLimits)="retrieveFitsImageLimits.emit($event)"
-            (onSubmit)="editImage.emit($event)" 
-            #formAddDatasetFamily>
-            <button [disabled]="!formAddDatasetFamily.form.valid || formAddDatasetFamily.form.pristine" (click)="modalRef.hide()" type="submit" class="btn btn-primary">
-                <span class="fa fa-database"></span> Edit image
-            </button>
-            &nbsp;
-            <button (click)="modalRef.hide()" type="button" class="btn btn-danger">Cancel</button>
-        </app-image-form>
-    </div>
-</ng-template>
\ No newline at end of file
diff --git a/client/src/app/admin/instance/dataset/components/image/image-table.component.ts b/client/src/app/admin/instance/dataset/components/image/image-table.component.ts
index fd75a9d9..669b4306 100644
--- a/client/src/app/admin/instance/dataset/components/image/image-table.component.ts
+++ b/client/src/app/admin/instance/dataset/components/image/image-table.component.ts
@@ -7,13 +7,9 @@
  * file that was distributed with this source code.
  */
 
-import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, TemplateRef } from '@angular/core';
+import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core';
 
-import { BsModalService } from 'ngx-bootstrap/modal';
-import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
-
-import { Image } from 'src/app/metamodel/models';
-import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
+import { Dataset, Image, Instance } from 'src/app/metamodel/models';
 
 @Component({
     selector: 'app-image-table',
@@ -21,34 +17,8 @@ import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
     changeDetection: ChangeDetectionStrategy.OnPush
 })
 export class ImageTableComponent {
-    @Input() instanceDataPath: string;
-    @Input() datasetDataPath: string;
-    @Input() files: FileInfo[];
-    @Input() filesIsLoading: boolean;
-    @Input() filesIsLoaded: boolean;
+    @Input() instance: Instance;
+    @Input() dataset: Dataset;
     @Input() imageList: Image[];
-    @Input() imageListIsLoading: boolean;
-    @Input() imageListIsLoaded: boolean;
-    @Input() fitsImageLimits: FitsImageLimits;
-    @Input() fitsImageLimitsIsLoading: boolean;
-    @Input() fitsImageLimitsIsLoaded: boolean;
-    @Output() loadRootDirectory: EventEmitter<string> = new EventEmitter();
-    @Output() retrieveFitsImageLimits: EventEmitter<string> = new EventEmitter();
-    @Output() addNewImage: EventEmitter<Image> = new EventEmitter();
-    @Output() editImage: EventEmitter<Image> = new EventEmitter();
     @Output() deleteImage: EventEmitter<Image> = new EventEmitter();
-
-    modalRef: BsModalRef;
-    imageForEdit: Image;
-
-    constructor(private modalService: BsModalService) { }
-
-    openModal(template: TemplateRef<any>, image: Image = null) {
-        this.imageForEdit = image;
-        this.modalRef = this.modalService.show(template);
-    }
-
-    onChangeDataPath(path: string) {
-        this.loadRootDirectory.emit(`${this.instanceDataPath}${path}`);
-    }
 }
diff --git a/client/src/app/admin/instance/dataset/containers/edit-dataset.component.html b/client/src/app/admin/instance/dataset/containers/edit-dataset.component.html
index abdb60b5..a1ab6612 100644
--- a/client/src/app/admin/instance/dataset/containers/edit-dataset.component.html
+++ b/client/src/app/admin/instance/dataset/containers/edit-dataset.component.html
@@ -37,18 +37,8 @@
                 [filesIsLoading]="filesIsLoading | async"
                 [filesIsLoaded]="filesIsLoaded | async"
                 [attributeList]="attributeList | async"
-                [imageList]="imageList | async"
-                [imageListIsLoading]="imageListIsLoading | async"
-                [imageListIsLoaded]="imageListIsLoaded | async"
-                [fitsImageLimits]="fitsImageLimits | async"
-                [fitsImageLimitsIsLoading]="fitsImageLimitsIsLoading | async"
-                [fitsImageLimitsIsLoaded]="fitsImageLimitsIsLoaded | async"
                 (changeSurvey)="loadTableList($event)"
                 (loadRootDirectory)="loadRootDirectory($event)"
-                (retrieveFitsImageLimits)="retrieveFitsImageLimits($event)"
-                (addNewImage)="addNewImage($event)"
-                (editImage)="editImage($event)"
-                (deleteImage)="deleteImage($event)"
                 (onSubmit)="editDataset($event)"
                 #formDataset>
                 <button [disabled]="!formDataset.form.valid || formDataset.form.pristine" type="submit" class="btn btn-primary">
diff --git a/client/src/app/admin/instance/dataset/containers/edit-dataset.component.ts b/client/src/app/admin/instance/dataset/containers/edit-dataset.component.ts
index a323f77f..ef598be4 100644
--- a/client/src/app/admin/instance/dataset/containers/edit-dataset.component.ts
+++ b/client/src/app/admin/instance/dataset/containers/edit-dataset.component.ts
@@ -8,13 +8,12 @@
  */
 
 import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
 
 import { Observable } from 'rxjs';
 import { Store } from '@ngrx/store';
 
 import { Instance, Survey, DatasetFamily, Dataset, Attribute, Image } from 'src/app/metamodel/models';
-import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
+import { FileInfo } from 'src/app/admin/store/models';
 import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
 import * as datasetActions from 'src/app/metamodel/actions/dataset.actions';
 import * as attributeSelector from 'src/app/metamodel/selectors/attribute.selector';
@@ -27,9 +26,6 @@ import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector
 import * as adminFileExplorerActions from 'src/app/admin/store/actions/admin-file-explorer.actions';
 import * as adminFileExplorerSelector from 'src/app/admin/store/selectors/admin-file-explorer.selector';
 import * as imageActions from 'src/app/metamodel/actions/image.actions';
-import * as imageSelector from 'src/app/metamodel/selectors/image.selector';
-import * as fitsImageActions from 'src/app/admin/store/actions/fits-image.actions';
-import * as fitsImageSelector from 'src/app/admin/store/selectors/fits-image.selector';
 
 @Component({
     selector: 'app-edit-dataset',
@@ -56,14 +52,8 @@ export class EditDatasetComponent implements OnInit {
     public files: Observable<FileInfo[]>;
     public filesIsLoading: Observable<boolean>;
     public filesIsLoaded: Observable<boolean>;
-    public imageList: Observable<Image[]>;
-    public imageListIsLoading: Observable<boolean>;
-    public imageListIsLoaded: Observable<boolean>;
-    public fitsImageLimits: Observable<FitsImageLimits>;
-    public fitsImageLimitsIsLoading: Observable<boolean>;
-    public fitsImageLimitsIsLoaded: Observable<boolean>;
 
-    constructor(private store: Store<{ }>, private route: ActivatedRoute) {
+    constructor(private store: Store<{ }>) {
         this.instance = store.select(instanceSelector.selectInstanceByRouteName);
         this.datasetSelected = store.select(datasetSelector.selectDatasetNameByRoute);
         this.datasetList = store.select(datasetSelector.selectAllDatasets);
@@ -84,12 +74,6 @@ export class EditDatasetComponent implements OnInit {
         this.files = store.select(adminFileExplorerSelector.selectFiles);
         this.filesIsLoading = store.select(adminFileExplorerSelector.selectFilesIsLoading);
         this.filesIsLoaded = store.select(adminFileExplorerSelector.selectFilesIsLoaded);
-        this.imageList = store.select(imageSelector.selectAllImages);
-        this.imageListIsLoading = store.select(imageSelector.selectImageListIsLoading);
-        this.imageListIsLoaded = store.select(imageSelector.selectImageListIsLoaded);
-        this.fitsImageLimits = store.select(fitsImageSelector.selectFitsImageLimits);
-        this.fitsImageLimitsIsLoading = store.select(fitsImageSelector.selectFitsImageLimitsIsLoading);
-        this.fitsImageLimitsIsLoaded = store.select(fitsImageSelector.selectFitsImageLimitsIsLoaded);
     }
 
     ngOnInit() {
@@ -105,22 +89,6 @@ export class EditDatasetComponent implements OnInit {
         this.store.dispatch(adminFileExplorerActions.loadFiles({ path }));
     }
 
-    addNewImage(image: Image) {
-        this.store.dispatch(imageActions.addImage({ image }));
-    }
-
-    editImage(image: Image) {
-        this.store.dispatch(imageActions.editImage({ image }));
-    }
-
-    deleteImage(image: Image) {
-        this.store.dispatch(imageActions.deleteImage({ image }));
-    }
-
-    retrieveFitsImageLimits(filePath: string) {
-        this.store.dispatch(fitsImageActions.retrieveFitsImageLimits({ filePath }));
-    }
-
     editDataset(dataset: Dataset) {
         this.store.dispatch(datasetActions.editDataset({ dataset }));
     }
diff --git a/client/src/app/admin/instance/dataset/containers/edit-image.component.html b/client/src/app/admin/instance/dataset/containers/edit-image.component.html
new file mode 100644
index 00000000..177f0ddc
--- /dev/null
+++ b/client/src/app/admin/instance/dataset/containers/edit-image.component.html
@@ -0,0 +1,47 @@
+<app-spinner *ngIf="(datasetListIsLoading | async) || (imageListIsLoading | async)"></app-spinner>
+
+<div *ngIf="(datasetListIsLoaded | async) && (imageListIsLoaded | async)" class="container-fluid">
+    <nav aria-label="breadcrumb">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a routerLink="/admin/instance/instance-list">Instances</a></li>
+            <li class="breadcrumb-item">
+                <a routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/dataset-list">
+                    Configure instance {{ (instance | async).label }}
+                </a>
+            </li>
+            <li class="breadcrumb-item">
+                <a routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/image-list/{{ (dataset | async).name }}">
+                    {{ (dataset | async).label }} image list
+                </a>
+            </li>
+            <li class="breadcrumb-item active" aria-current="page">Edit image</li>
+        </ol>
+    </nav>
+</div>
+
+<div *ngIf="(datasetListIsLoaded | async) && (imageListIsLoaded | async)" class="container">
+    <div class="row">
+        <div class="col-12">
+            <app-image-form 
+                [image]="image | async"
+                [instance]="instance | async"
+                [dataset]="dataset | async"
+                [files]="files | async"
+                [filesIsLoading]="filesIsLoading | async"
+                [filesIsLoaded]="filesIsLoaded | async"
+                [fitsImageLimits]="fitsImageLimits | async"
+                [fitsImageLimitsIsLoading]="fitsImageLimitsIsLoading | async"
+                [fitsImageLimitsIsLoaded]="fitsImageLimitsIsLoaded | async"
+                (loadRootDirectory)="loadRootDirectory($event)"
+                (retrieveFitsImageLimits)="retrieveFitsImageLimits($event)"
+                (onSubmit)="editImage($event)" 
+                #formEditImage>
+                <button [disabled]="!formEditImage.form.valid || formEditImage.form.pristine" type="submit" class="btn btn-primary">
+                    <span class="fa fa-database"></span> Edit image
+                </button>
+                &nbsp;
+                <button routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/image-list/{{ (dataset | async).name }}" type="button" class="btn btn-danger">Cancel</button>
+            </app-image-form>
+        </div>
+    </div>
+</div>
diff --git a/client/src/app/admin/instance/dataset/containers/edit-image.component.ts b/client/src/app/admin/instance/dataset/containers/edit-image.component.ts
new file mode 100644
index 00000000..b9b2f033
--- /dev/null
+++ b/client/src/app/admin/instance/dataset/containers/edit-image.component.ts
@@ -0,0 +1,76 @@
+/**
+ * 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 { Component } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { Store } from '@ngrx/store';
+
+import { Instance, Dataset, Image } from 'src/app/metamodel/models';
+import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
+import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector';
+import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
+import * as adminFileExplorerActions from 'src/app/admin/store/actions/admin-file-explorer.actions';
+import * as adminFileExplorerSelector from 'src/app/admin/store/selectors/admin-file-explorer.selector';
+import * as imageActions from 'src/app/metamodel/actions/image.actions';
+import * as imageSelector from 'src/app/metamodel/selectors/image.selector';
+import * as fitsImageActions from 'src/app/admin/store/actions/fits-image.actions';
+import * as fitsImageSelector from 'src/app/admin/store/selectors/fits-image.selector';
+
+@Component({
+    selector: 'app-edit-image',
+    templateUrl: 'edit-image.component.html'
+})
+export class EditImageComponent {
+    public instance: Observable<Instance>;
+    public dataset: Observable<Dataset>;
+    public datasetListIsLoading: Observable<boolean>;
+    public datasetListIsLoaded: Observable<boolean>;
+    public files: Observable<FileInfo[]>;
+    public filesIsLoading: Observable<boolean>;
+    public filesIsLoaded: Observable<boolean>;
+    public fitsImageLimits: Observable<FitsImageLimits>;
+    public fitsImageLimitsIsLoading: Observable<boolean>;
+    public fitsImageLimitsIsLoaded: Observable<boolean>;
+    public image: Observable<Image>;
+    public imageListIsLoading: Observable<boolean>;
+    public imageListIsLoaded: Observable<boolean>;
+
+    constructor(private store: Store<{ }>) {
+        this.instance = store.select(instanceSelector.selectInstanceByRouteName);
+        this.dataset = store.select(datasetSelector.selectDatasetByRouteName);
+        this.datasetListIsLoading = store.select(datasetSelector.selectDatasetListIsLoading);
+        this.datasetListIsLoaded = store.select(datasetSelector.selectDatasetListIsLoaded);
+        this.files = store.select(adminFileExplorerSelector.selectFiles);
+        this.filesIsLoading = store.select(adminFileExplorerSelector.selectFilesIsLoading);
+        this.filesIsLoaded = store.select(adminFileExplorerSelector.selectFilesIsLoaded);
+        this.fitsImageLimits = store.select(fitsImageSelector.selectFitsImageLimits);
+        this.fitsImageLimitsIsLoading = store.select(fitsImageSelector.selectFitsImageLimitsIsLoading);
+        this.fitsImageLimitsIsLoaded = store.select(fitsImageSelector.selectFitsImageLimitsIsLoaded);
+        this.image = store.select(imageSelector.selectImageByRouteId);
+        this.imageListIsLoading = store.select(imageSelector.selectImageListIsLoading);
+        this.imageListIsLoaded = store.select(imageSelector.selectImageListIsLoaded);
+    }
+
+    ngOnInit() {
+        this.store.dispatch(imageActions.loadImageList());
+    }
+
+    loadRootDirectory(path: string) {
+        this.store.dispatch(adminFileExplorerActions.loadFiles({ path }));
+    }
+
+    retrieveFitsImageLimits(filePath: string) {
+        this.store.dispatch(fitsImageActions.retrieveFitsImageLimits({ filePath }));
+    }
+
+    editImage(image: Image) {
+        this.store.dispatch(imageActions.editImage({ image }));
+    }
+}
\ No newline at end of file
diff --git a/client/src/app/admin/instance/dataset/containers/image-list.component.html b/client/src/app/admin/instance/dataset/containers/image-list.component.html
new file mode 100644
index 00000000..8826de3f
--- /dev/null
+++ b/client/src/app/admin/instance/dataset/containers/image-list.component.html
@@ -0,0 +1,38 @@
+<div class="container-fluid">
+    <nav aria-label="breadcrumb">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a routerLink="/admin/instance/instance-list">Instances</a></li>
+            <li class="breadcrumb-item">
+                <a routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/dataset-list">
+                    Configure instance {{ (instance | async).label }}
+                </a>
+            </li>
+            <li class="breadcrumb-item active" aria-current="page">{{ (dataset | async).label }} image list</li>
+        </ol>
+    </nav>
+
+    <app-spinner *ngIf="imageListIsLoading | async"></app-spinner>
+
+    <ng-container *ngIf="imageListIsLoaded | async">
+        <div class="row">
+            <div class="col-12">
+                <button title="Add a new image" 
+                    class="btn btn-outline-success float-right"
+                    routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/image-list/{{ (dataset | async).name }}/new-image">
+                    <span class="fas fa-plus"></span> New image
+                </button>
+            </div>
+        </div>
+    
+        <div class="row mt-1">
+            <div class="col-12">
+                <app-image-table 
+                    [instance]="instance | async"
+                    [dataset]="dataset | async"
+                    [imageList]="imageList | async" 
+                    (deleteImage)="deleteImage($event)">
+                </app-image-table>
+            </div>
+        </div>
+    </ng-container>
+</div>
diff --git a/client/src/app/admin/instance/dataset/containers/image-list.component.ts b/client/src/app/admin/instance/dataset/containers/image-list.component.ts
new file mode 100644
index 00000000..e3b12da9
--- /dev/null
+++ b/client/src/app/admin/instance/dataset/containers/image-list.component.ts
@@ -0,0 +1,51 @@
+/**
+ * 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 { Component, OnInit } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { Store } from '@ngrx/store';
+
+import { Image, Instance, Dataset } from 'src/app/metamodel/models';
+import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector';
+import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
+import * as imageActions from 'src/app/metamodel/actions/image.actions';
+import * as imageSelector from 'src/app/metamodel/selectors/image.selector';
+
+@Component({
+    selector: 'app-image-list',
+    templateUrl: 'image-list.component.html'
+})
+export class ImageListComponent implements OnInit {
+    public instance: Observable<Instance>;
+    public dataset: Observable<Dataset>;
+    public datasetListIsLoading: Observable<boolean>;
+    public datasetListIsLoaded: Observable<boolean>;
+    public imageList: Observable<Image[]>;
+    public imageListIsLoading: Observable<boolean>;
+    public imageListIsLoaded: Observable<boolean>;
+
+    constructor(private store: Store<{ }>) {
+        this.instance = store.select(instanceSelector.selectInstanceByRouteName);
+        this.dataset = store.select(datasetSelector.selectDatasetByRouteName);
+        this.datasetListIsLoading = store.select(datasetSelector.selectDatasetListIsLoading);
+        this.datasetListIsLoaded = store.select(datasetSelector.selectDatasetListIsLoaded);
+        this.imageList = store.select(imageSelector.selectAllImages);
+        this.imageListIsLoading = store.select(imageSelector.selectImageListIsLoading);
+        this.imageListIsLoaded = store.select(imageSelector.selectImageListIsLoaded);
+    }
+
+    ngOnInit() {
+        this.store.dispatch(imageActions.loadImageList());
+    }
+
+    deleteImage(image: Image) {
+        this.store.dispatch(imageActions.deleteImage({ image }));
+    }
+}
\ No newline at end of file
diff --git a/client/src/app/admin/instance/dataset/containers/new-image.component.html b/client/src/app/admin/instance/dataset/containers/new-image.component.html
new file mode 100644
index 00000000..8560f4ab
--- /dev/null
+++ b/client/src/app/admin/instance/dataset/containers/new-image.component.html
@@ -0,0 +1,46 @@
+<app-spinner *ngIf="datasetListIsLoading | async"></app-spinner>
+
+<div *ngIf="datasetListIsLoaded | async" class="container-fluid">
+    <nav aria-label="breadcrumb">
+        <ol class="breadcrumb">
+            <li class="breadcrumb-item"><a routerLink="/admin/instance/instance-list">Instances</a></li>
+            <li class="breadcrumb-item">
+                <a routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/dataset-list">
+                    Configure instance {{ (instance | async).label }}
+                </a>
+            </li>
+            <li class="breadcrumb-item">
+                <a routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/image-list/{{ (dataset | async).name }}">
+                    {{ (dataset | async).label }} image list
+                </a>
+            </li>
+            <li class="breadcrumb-item active" aria-current="page">Add a new image</li>
+        </ol>
+    </nav>
+</div>
+
+<div *ngIf="datasetListIsLoaded | async" class="container">
+    <div class="row">
+        <div class="col-12">
+            <app-image-form 
+                [instance]="instance | async"
+                [dataset]="dataset | async"
+                [files]="files | async"
+                [filesIsLoading]="filesIsLoading | async"
+                [filesIsLoaded]="filesIsLoaded | async"
+                [fitsImageLimits]="fitsImageLimits | async"
+                [fitsImageLimitsIsLoading]="fitsImageLimitsIsLoading | async"
+                [fitsImageLimitsIsLoaded]="fitsImageLimitsIsLoaded | async"
+                (loadRootDirectory)="loadRootDirectory($event)"
+                (retrieveFitsImageLimits)="retrieveFitsImageLimits($event)"
+                (onSubmit)="addNewImage($event)" 
+                #formAddImage>
+                <button [disabled]="!formAddImage.form.valid || formAddImage.form.pristine" type="submit" class="btn btn-primary">
+                    <span class="fa fa-database"></span> Add new image
+                </button>
+                &nbsp;
+                <button routerLink="/admin/instance/configure-instance/{{ (instance | async).name }}/dataset/image-list/{{ (dataset | async).name }}" type="button" class="btn btn-danger">Cancel</button>
+            </app-image-form>
+        </div>
+    </div>
+</div>
diff --git a/client/src/app/admin/instance/dataset/containers/new-image.component.ts b/client/src/app/admin/instance/dataset/containers/new-image.component.ts
new file mode 100644
index 00000000..5dd243e6
--- /dev/null
+++ b/client/src/app/admin/instance/dataset/containers/new-image.component.ts
@@ -0,0 +1,65 @@
+/**
+ * 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 { Component } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { Store } from '@ngrx/store';
+
+import { Instance, Dataset, Image } from 'src/app/metamodel/models';
+import { FileInfo, FitsImageLimits } from 'src/app/admin/store/models';
+import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector';
+import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector';
+import * as adminFileExplorerActions from 'src/app/admin/store/actions/admin-file-explorer.actions';
+import * as adminFileExplorerSelector from 'src/app/admin/store/selectors/admin-file-explorer.selector';
+import * as imageActions from 'src/app/metamodel/actions/image.actions';
+import * as fitsImageActions from 'src/app/admin/store/actions/fits-image.actions';
+import * as fitsImageSelector from 'src/app/admin/store/selectors/fits-image.selector';
+
+@Component({
+    selector: 'app-new-image',
+    templateUrl: 'new-image.component.html'
+})
+export class NewImageComponent {
+    public instance: Observable<Instance>;
+    public dataset: Observable<Dataset>;
+    public datasetListIsLoading: Observable<boolean>;
+    public datasetListIsLoaded: Observable<boolean>;
+    public files: Observable<FileInfo[]>;
+    public filesIsLoading: Observable<boolean>;
+    public filesIsLoaded: Observable<boolean>;
+    public fitsImageLimits: Observable<FitsImageLimits>;
+    public fitsImageLimitsIsLoading: Observable<boolean>;
+    public fitsImageLimitsIsLoaded: Observable<boolean>;
+
+    constructor(private store: Store<{ }>) {
+        this.instance = store.select(instanceSelector.selectInstanceByRouteName);
+        this.dataset = store.select(datasetSelector.selectDatasetByRouteName);
+        this.datasetListIsLoading = store.select(datasetSelector.selectDatasetListIsLoading);
+        this.datasetListIsLoaded = store.select(datasetSelector.selectDatasetListIsLoaded);
+        this.files = store.select(adminFileExplorerSelector.selectFiles);
+        this.filesIsLoading = store.select(adminFileExplorerSelector.selectFilesIsLoading);
+        this.filesIsLoaded = store.select(adminFileExplorerSelector.selectFilesIsLoaded);
+        this.fitsImageLimits = store.select(fitsImageSelector.selectFitsImageLimits);
+        this.fitsImageLimitsIsLoading = store.select(fitsImageSelector.selectFitsImageLimitsIsLoading);
+        this.fitsImageLimitsIsLoaded = store.select(fitsImageSelector.selectFitsImageLimitsIsLoaded);
+    }
+
+    loadRootDirectory(path: string) {
+        this.store.dispatch(adminFileExplorerActions.loadFiles({ path }));
+    }
+
+    retrieveFitsImageLimits(filePath: string) {
+        this.store.dispatch(fitsImageActions.retrieveFitsImageLimits({ filePath }));
+    }
+
+    addNewImage(image: Image) {
+        this.store.dispatch(imageActions.addImage({ image }));
+    }
+}
\ No newline at end of file
diff --git a/client/src/app/admin/instance/dataset/dataset-routing.module.ts b/client/src/app/admin/instance/dataset/dataset-routing.module.ts
index 5779ddc7..31d8edf4 100644
--- a/client/src/app/admin/instance/dataset/dataset-routing.module.ts
+++ b/client/src/app/admin/instance/dataset/dataset-routing.module.ts
@@ -14,12 +14,18 @@ import { DatasetListComponent } from './containers/dataset-list.component';
 import { NewDatasetComponent } from './containers/new-dataset.component';
 import { EditDatasetComponent } from './containers/edit-dataset.component';
 import { AttributeListComponent } from './containers/attribute-list.component';
+import { ImageListComponent } from './containers/image-list.component';
+import { NewImageComponent } from './containers/new-image.component';
+import { EditImageComponent } from './containers/edit-image.component';
 
 const routes: Routes = [
     { path: 'dataset-list', component: DatasetListComponent },
     { path: 'new-dataset', component: NewDatasetComponent },
     { path: 'edit-dataset/:dname', component: EditDatasetComponent },
     { path: 'configure-dataset/:dname', component: AttributeListComponent },
+    { path: 'image-list/:dname', component: ImageListComponent },
+    { path: 'image-list/:dname/new-image', component: NewImageComponent },
+    { path: 'image-list/:dname/edit-image/:id', component: EditImageComponent }
 ];
 
 /**
@@ -36,5 +42,8 @@ export const routedComponents = [
     DatasetListComponent,
     NewDatasetComponent,
     EditDatasetComponent,
-    AttributeListComponent
+    AttributeListComponent,
+    ImageListComponent,
+    NewImageComponent,
+    EditImageComponent
 ];
diff --git a/client/src/app/metamodel/effects/image.effects.ts b/client/src/app/metamodel/effects/image.effects.ts
index 59dce9ad..dcfc1d5c 100644
--- a/client/src/app/metamodel/effects/image.effects.ts
+++ b/client/src/app/metamodel/effects/image.effects.ts
@@ -8,6 +8,7 @@
  */
 
 import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
 
 import { Actions, createEffect, ofType, concatLatestFrom } from '@ngrx/effects';
 import { Store } from '@ngrx/store';
@@ -18,6 +19,7 @@ import { ToastrService } from 'ngx-toastr';
 import * as imageActions from '../actions/image.actions';
 import { ImageService } from '../services/image.service';
 import * as datasetSelector from '../selectors/dataset.selector';
+import * as instanceSelector from '../selectors/instance.selector';
 
 /**
  * @class
@@ -64,7 +66,14 @@ export class ImageEffects {
     addImageSuccess$ = createEffect(() =>
         this.actions$.pipe(
             ofType(imageActions.addImageSuccess),
-            tap(() => this.toastr.success('Image successfully added', 'The new image was added into the database'))
+            concatLatestFrom(() => [
+                this.store.select(instanceSelector.selectInstanceNameByRoute),
+                this.store.select(datasetSelector.selectDatasetNameByRoute)
+            ]),
+            tap(([, instanceName, datasetName]) => {
+                this.router.navigateByUrl(`/admin/instance/configure-instance/${instanceName}/dataset/image-list/${datasetName}`);
+                this.toastr.success('Image successfully added', 'The new image was added into the database');
+            })
         ), { dispatch: false }
     );
 
@@ -99,7 +108,14 @@ export class ImageEffects {
     editImageSuccess$ = createEffect(() =>
         this.actions$.pipe(
             ofType(imageActions.editImageSuccess),
-            tap(() => this.toastr.success('Image successfully edited', 'The existing image has been edited into the database'))
+            concatLatestFrom(() => [
+                this.store.select(instanceSelector.selectInstanceNameByRoute),
+                this.store.select(datasetSelector.selectDatasetNameByRoute)
+            ]),
+            tap(([, instanceName, datasetName]) => {
+                this.router.navigateByUrl(`/admin/instance/configure-instance/${instanceName}/dataset/image-list/${datasetName}`);
+                this.toastr.success('Image successfully edited', 'The existing image has been edited into the database');
+            })
         ), { dispatch: false }
     );
 
@@ -151,6 +167,7 @@ export class ImageEffects {
     constructor(
         private actions$: Actions,
         private imageService: ImageService,
+        private router: Router,
         private toastr: ToastrService,
         private store: Store<{ }>
     ) {}
diff --git a/client/src/app/metamodel/selectors/image.selector.ts b/client/src/app/metamodel/selectors/image.selector.ts
index 5902eb3f..ce5e4842 100644
--- a/client/src/app/metamodel/selectors/image.selector.ts
+++ b/client/src/app/metamodel/selectors/image.selector.ts
@@ -46,3 +46,9 @@ export const selectImageListIsLoaded = createSelector(
     selectImageState,
     fromImage.selectImageListIsLoaded
 );
+
+export const selectImageByRouteId = createSelector(
+    selectImageEntities,
+    reducer.selectRouterState,
+    (entities, router) => entities[router.state.params.id]
+);
\ No newline at end of file
-- 
GitLab