Commit 57dd402e authored by François Agneray's avatar François Agneray
Browse files

Merge branch '59-dataset-configuration-add-images-section' into 'develop'

Resolve "Dataset configuration: add images section"

Closes #59

See merge request !39
parents 8802ff4a db65df2d
Pipeline #5554 passed with stages
in 16 minutes and 13 seconds
......@@ -13,10 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #55: Add image renderer configuration
### Changed
- #58: Manage attributes by dataset (add or delete attribute)
- #57: GUI improvments
- #52: Update dependencies (Angular v11, ngrx v11, ...)
- #56: Cone-search configuration improvement
### Fixed
- #40: Bug cascade deletion
## [3.5.0]
### Added
- #51: Add datasets rights depending on user group
......
......@@ -39,7 +39,7 @@
<label for="data_path">Data path</label>
<div class="input-group mb-3">
<div class="input-group-prepend">
<button (click)="openModal(template); $event.stopPropagation()" class="btn btn-outline-secondary" type="button">
<button (click)="dataPathOpenModal(templateDataPath); $event.stopPropagation()" class="btn btn-outline-secondary" type="button">
<i class="fas fa-folder-open"></i>
</button>
</div>
......@@ -55,6 +55,49 @@
<label class="custom-control-label" for="public">Public</label>
</div>
</accordion-group>
<accordion-group [isDisabled]="!model.data_path" heading="Images configuration">
<button (click)="newImageOpenModal(templateAddImage); $event.stopPropagation()" class="btn btn-outline-primary" type="button">
<i class="far fa-image"></i> Add new image
</button>
<div *ngIf="datasetImages.length > 0" class="mt-2 table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Path</th>
<th scope="col">Size</th>
<th scope="col">RA min/max</th>
<th scope="col">DEC min/max</th>
<th scope="col">Stretch</th>
<th scope="col">Pmin</th>
<th scope="col">Pmax</th>
<th scope="col">Edit</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let image of datasetImages">
<td class="align-middle">{{ image.name }}</td>
<td class="align-middle">{{ image.size | formatFileSize: false }}</td>
<td class="align-middle">[{{ image.ra_min }} ; {{ image.ra_max }}]</td>
<td class="align-middle">[{{ image.dec_min }} ; {{ image.dec_max }}]</td>
<td class="align-middle">{{ image.stretch }}</td>
<td class="align-middle">{{ image.pmin }}</td>
<td class="align-middle">{{ image.pmax }}</td>
<td class="align-middle">
<button (click)="editImageOpenModal(templateEditImage, image)" title="Edit this image" class="btn btn-outline-primary" type="button">
<span class="fas fa-edit"></span>
</button>
</td>
<td class="align-middle">
<button title="Delete this image" (click)="deleteImage(image)" class="btn btn-outline-danger">
<span class="fas fa-trash-alt"></span>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</accordion-group>
<accordion-group heading="Cone-search configuration" [isDisabled]="isConeSearchDisabled()">
<div class="custom-control custom-switch">
<input class="custom-control-input" type="checkbox" id="cone_search_enabled" name="cone_search_enabled" [ngModel]="model.config.cone_search.enabled">
......@@ -84,6 +127,43 @@
<input class="custom-control-input" type="checkbox" id="plot_enabled" name="plot_enabled" [ngModel]="model.config.cone_search.plot_enabled" [disabled]="!ngForm.form.value.cone_search_enabled">
<label class="custom-control-label" for="plot_enabled">Activate plot</label>
</div>
<div *ngIf="ngForm.form.value.plot_enabled" class="mt-2 table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Enabled</th>
<th scope="col">Display</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-middle">SDSS (DR16)</td>
<td class="align-middle">
<div class="custom-control custom-switch">
<input class="custom-control-input" type="checkbox" id="sdss_enabled" name="sdss_enabled" [ngModel]="model.config.cone_search.sdss_enabled">
<label class="custom-control-label" for="sdss_enabled"></label>
</div>
</td>
<td class="align-middle">
<input id="sdss_display" type="number" class="form-control" name="sdss_display" value="10" [ngModel]="model.config.cone_search.sdss_display">
</td>
</tr>
<tr *ngFor="let image of datasetImages">
<td class="align-middle">{{ getImageFilename(image.name) }}</td>
<td class="align-middle">
<div class="custom-control custom-switch">
<input class="custom-control-input" type="checkbox" id="{{ image.id }}_enabled" name="{{ image.id }}_enabled" [ngModel]="getBackgroundEnabled(image)">
<label class="custom-control-label" for="{{ image.id }}_enabled"></label>
</div>
</td>
<td class="align-middle">
<input id="{{ image.id }}_display" type="number" class="form-control" name="{{ image.id }}_display" [ngModel]="getBackgroundDisplay(image)">
</td>
</tr>
</tbody>
</table>
</div>
</accordion-group>
<accordion-group heading="Download configuration">
<div class="custom-control custom-switch">
......@@ -161,12 +241,12 @@
</div>
</form>
<ng-template #template>
<ng-template #templateDataPath>
<div class="modal-header">
<h4 class="modal-title pull-left">ANIS file explorer</h4>
</div>
<div>
<div *ngIf="directoryInfoIsLoading" class="row justify-content-center mt-5">
<div *ngIf="rootDirectoryInfoIsLoading" class="row justify-content-center mt-5">
<span class="fas fa-circle-notch fa-spin fa-3x"></span>
<span class="sr-only">Loading...</span>
</div>
......@@ -175,18 +255,18 @@
<i class="far fa-folder"></i>
{{ fileExplorerPath }}
</p>
<div *ngIf="directoryInfoIsLoaded" class="table-responsive">
<div *ngIf="rootDirectoryInfoIsLoaded" class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th scope="col">Name</th>
<th scope="col">Size (bytes)</th>
<th scope="col">Size</th>
<th scope="col">MimeType</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let fileInfo of directoryInfo" (click)="changePath(fileInfo)" [class.cursor-pointer]="fileInfo.type === 'dir' && fileInfo.name !== '.'">
<tr *ngFor="let fileInfo of rootDirectoryInfo" (click)="dataPathAction(fileInfo)" [class.cursor-pointer]="fileInfo.type === 'dir' && fileInfo.name !== '.'">
<td>
<span *ngIf="fileInfo.type === 'dir'"><i class="far fa-folder"></i></span>
<span *ngIf="fileInfo.type === 'file'"><i class="far fa-file"></i></span>
......@@ -194,7 +274,7 @@
<td class="align-middle">
{{ fileInfo.name }}
</td>
<td class="align-middle">{{ fileInfo.size }}</td>
<td class="align-middle">{{ fileInfo.size | formatFileSize: false }}</td>
<td class="align-middle">{{ fileInfo.mimetype }}</td>
</tr>
</tbody>
......@@ -204,6 +284,119 @@
<div class="modal-footer">
<button (click)="modalRef.hide()" class="btn btn-danger">Cancel</button>
&nbsp;
<button [disabled]="fileExplorerPristine" (click)="selectDirectory()" class="btn btn-primary">Select this directory</button>
<button [disabled]="dataPathFileExplorerPristine" (click)="selectDirectory()" class="btn btn-primary">Select this directory</button>
</div>
</ng-template>
<ng-template #templateAddImage>
<div class="modal-header">
<h4 class="modal-title pull-left">Add an image</h4>
</div>
<div>
<div *ngIf="datasetDirectoryInfoIsLoading" class="row justify-content-center mt-5">
<span class="fas fa-circle-notch fa-spin fa-3x"></span>
<span class="sr-only">Loading...</span>
</div>
<p class="ml-3 mt-3">
<i class="far fa-folder"></i>
{{ fileExplorerPath }}
</p>
<div *ngIf="datasetDirectoryInfoIsLoaded" class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th scope="col">Name</th>
<th scope="col">Size</th>
<th scope="col">MimeType</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let fileInfo of datasetDirectoryInfo"
(click)="newImageAction(fileInfo)"
[class.table-active]="fileInfo === imageSelected && imageLimitIsLoaded"
[class.cursor-pointer]="fileInfo.name !== '.'">
<td>
<span *ngIf="fileInfo.type === 'dir'"><i class="far fa-folder"></i></span>
<span *ngIf="fileInfo.type === 'file'"><i class="far fa-file"></i></span>
</td>
<td class="align-middle">
{{ fileInfo.name }}
</td>
<td class="align-middle">{{ fileInfo.size | formatFileSize: false }}</td>
<td class="align-middle">{{ fileInfo.mimetype }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button (click)="modalRef.hide()" class="btn btn-danger">Cancel</button>
&nbsp;
<button [disabled]="!imageSelected || !imageLimitIsLoaded" (click)="selectImage()" class="btn btn-primary">
Select this image
<span *ngIf="imageLimitIsLoading">
<span class="fas fa-circle-notch fa-spin fa-1x"></span>
</span>
</button>
</div>
</ng-template>
<ng-template #templateEditImage>
<div class="modal-header">
<h4 class="modal-title pull-left">Edit an image</h4>
</div>
<div class="modal-body">
{{ imageSelected | json }}
<form name="formImage" #fi="ngForm" novalidate>
<div class="form-group">
<label for="name">Name</label>
<input id="name" type="text" class="form-control" name="name" [ngModel]="imageSelected.name" [disabled]="imageSelected.name" required>
</div>
<div class="form-group">
<label for="name">Size</label>
<input id="name" type="text" class="form-control" name="size" [ngModel]="imageSelected.size" [disabled]="imageSelected.size" required>
</div>
<div class="form-group">
<label for="name">Ra min</label>
<input id="name" type="text" class="form-control" name="ra_min" [ngModel]="imageSelected.ra_min" [disabled]="imageSelected.ra_min" required>
</div>
<div class="form-group">
<label for="name">Ra max</label>
<input id="name" type="text" class="form-control" name="ra_max" [ngModel]="imageSelected.ra_max" [disabled]="imageSelected.ra_max" required>
</div>
<div class="form-group">
<label for="name">Dec min</label>
<input id="name" type="text" class="form-control" name="dec_min" [ngModel]="imageSelected.dec_min" [disabled]="imageSelected.dec_min" required>
</div>
<div class="form-group">
<label for="name">Dec max</label>
<input id="name" type="text" class="form-control" name="dec_max" [ngModel]="imageSelected.dec_max" [disabled]="imageSelected.dec_max" required>
</div>
<div class="form-group">
<label for="stretch">Stretch</label>
<select id="stretch" class="form-control" name="stretch" [ngModel]="imageSelected.stretch" required>
<option value="linear">Linear</option>
<option value="sqrt">Sqrt</option>
</select>
</div>
<div class="form-group">
<label for="name">Pmin</label>
<input id="name" type="number" step="0.01" min="0" max="100" class="form-control" name="pmin" [ngModel]="imageSelected.pmin" required>
</div>
<div class="form-group">
<label for="name">Pmax</label>
<input id="name" type="number" step="0.01" min="0" max="100" class="form-control" name="pmax" [ngModel]="imageSelected.pmax" required>
</div>
</form>
</div>
<div class="modal-footer">
<button (click)="modalRef.hide()" class="btn btn-danger">Cancel</button>
&nbsp;
<button [disabled]="!fi.form.valid || fi.form.pristine" (click)="editImage(fi.form.value)"
type="submit" class="btn btn-primary">
<i class="fa fa-database"></i> Update image information
</button>
</div>
</ng-template>
\ No newline at end of file
import { Component, Input, Output, EventEmitter, ViewChild, TemplateRef } from '@angular/core';
import { Component, Input, Output, EventEmitter, ViewChild, TemplateRef, OnChanges, SimpleChanges } from '@angular/core';
import { NgForm } from '@angular/forms';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
import { Dataset, Survey, Attribute, DatasetFamily, FileInfo } from '../../store/model';
import { Dataset, Survey, Attribute, DatasetFamily, FileInfo, ImageLimit } from '../../store/model';
@Component({
selector: 'app-form-dataset',
templateUrl: 'form-dataset.component.html',
styleUrls: ['form-dataset.component.css']
})
export class FormDatasetComponent {
export class FormDatasetComponent implements OnChanges {
@ViewChild(NgForm, {static: true}) ngForm: NgForm;
@Input() model: Dataset = new Dataset();
@Input() surveyList: Survey[];
......@@ -19,34 +19,46 @@ export class FormDatasetComponent {
@Input() tableList: string[];
@Input() datasetFamilyList: DatasetFamily[];
@Input() idDatasetFamilyQueryParam: number = 0;
@Input() directoryInfo: FileInfo[];
@Input() directoryInfoIsLoading: boolean;
@Input() directoryInfoIsLoaded: boolean;
@Input() rootDirectoryInfo: FileInfo[];
@Input() rootDirectoryInfoIsLoading: boolean;
@Input() rootDirectoryInfoIsLoaded: boolean;
@Input() datasetDirectoryInfo: FileInfo[];
@Input() datasetDirectoryInfoIsLoading: boolean;
@Input() datasetDirectoryInfoIsLoaded: boolean;
@Input() imageLimitIsLoading: boolean;
@Input() imageLimitIsLoaded: boolean;
@Input() imageLimit: ImageLimit;
@Output() changeSurvey: EventEmitter<number> = new EventEmitter();
@Output() loadDirectoryInfo: EventEmitter<string> = new EventEmitter();
@Output() loadRootDirectoryInfo: EventEmitter<string> = new EventEmitter();
@Output() loadDatasetDirectoryInfo: EventEmitter<string> = new EventEmitter();
@Output() loadFitsImageLimits: EventEmitter<string> = new EventEmitter();
@Output() submitted: EventEmitter<Dataset> = new EventEmitter();
modalRef: BsModalRef;
fileExplorerPristine = true;
dataPathFileExplorerPristine = true;
fileExplorerPath = '';
datasetFileExplorerPath = '';
imageSelected = null;
datasetImages = [];
constructor(private modalService: BsModalService) { }
ngOnInit() {
console.log(this.model);
ngOnChanges(changes: SimpleChanges) {
this.datasetImages = changes.model.currentValue.config.images.map(i => ({...i}));
}
openModal(template: TemplateRef<any>) {
this.fileExplorerPristine = true;
dataPathOpenModal(template: TemplateRef<any>) {
this.dataPathFileExplorerPristine = true;
this.fileExplorerPath = this.ngForm.controls['data_path'].value;
if (!this.fileExplorerPath) {
this.fileExplorerPath = '';
}
this.modalRef = this.modalService.show(template);
this.loadDirectoryInfo.emit(this.fileExplorerPath);
this.loadRootDirectoryInfo.emit(this.fileExplorerPath);
}
changePath(fileInfo: FileInfo) {
dataPathAction(fileInfo: FileInfo): void {
if (fileInfo.name === '.' || fileInfo.type !== 'dir') {
return;
}
......@@ -56,17 +68,83 @@ export class FormDatasetComponent {
} else {
this.fileExplorerPath += '/' + fileInfo.name;
}
this.fileExplorerPristine = false;
this.loadDirectoryInfo.emit(this.fileExplorerPath);
this.dataPathFileExplorerPristine = false;
this.loadRootDirectoryInfo.emit(this.fileExplorerPath);
}
selectDirectory() {
this.ngForm.controls['data_path'].setValue(this.fileExplorerPath);
this.ngForm.controls['data_path'].markAsDirty();
this.modalRef.hide();
}
isConeSearchDisabled() {
newImageOpenModal(template: TemplateRef<any>): void {
this.imageSelected = null;
this.modalRef = this.modalService.show(template);
this.loadDatasetDirectoryInfo.emit(this.datasetFileExplorerPath);
}
newImageAction(fileInfo: FileInfo): void {
if (fileInfo.name === '.') {
return;
}
if (fileInfo.type === 'file' && fileInfo.mimetype === 'image/fits') {
this.loadFitsImageLimits.emit(this.datasetFileExplorerPath + '/' + fileInfo.name);
this.imageSelected = fileInfo;
} else {
if (fileInfo.name === '..') {
this.datasetFileExplorerPath = this.datasetFileExplorerPath.substr(0, this.datasetFileExplorerPath.lastIndexOf("/"));
} else {
this.datasetFileExplorerPath += '/' + fileInfo.name;
}
this.imageSelected = null;
this.loadDatasetDirectoryInfo.emit(this.datasetFileExplorerPath);
}
}
selectImage(): void {
const name = this.datasetFileExplorerPath + '/' + this.imageSelected.name;
let id = 1;
if (this.datasetImages.length > 0) {
id = Math.max(...this.datasetImages.map(i => i.id)) + 1;
}
this.datasetImages.push({
id,
name,
size: this.imageSelected.size,
...this.imageLimit,
stretch: 'linear',
pmin: 0.25,
pmax: 99.75
});
this.modalRef.hide();
this.ngForm.controls['data_path'].markAsDirty();
}
editImageOpenModal(template: TemplateRef<any>, image) {
this.imageSelected = image;
this.modalRef = this.modalService.show(template);
}
editImage(image) {
this.datasetImages = [
...this.datasetImages.filter(i => i.id !== this.imageSelected.id),
{
...this.imageSelected,
...image
}
];
this.modalRef.hide();
this.ngForm.controls['data_path'].markAsDirty();
}
deleteImage(image): void {
this.datasetImages = this.datasetImages.filter(i => i.id !== image.id);
this.ngForm.controls['data_path'].markAsDirty();
}
isConeSearchDisabled(): boolean {
return (!this.model.name) ? true : false;
}
......@@ -74,6 +152,28 @@ export class FormDatasetComponent {
return this.datasetFamilyList.find(datasetFamily => datasetFamily.id === this.idDatasetFamilyQueryParam);
}
getBackgroundEnabled(image) {
const m = this.model.config.cone_search.background.find(i => i.id === image.id);
if (m) {
return m.enabled;
} else {
return false;
}
}
getBackgroundDisplay(image) {
const m = this.model.config.cone_search.background.find(i => i.id === image.id);
if (m) {
return m.display;
} else {
return (image.id + 1) * 10;
}
}
getImageFilename(path: string) {
return path.replace(/^.*[\\\/]/, '');
}
onChange(surveyName: string): void {
this.changeSurvey.emit(this.surveyList.find(survey => survey.name === surveyName).id_database);
}
......@@ -91,12 +191,20 @@ export class FormDatasetComponent {
datasetEmitted.id_dataset_family = this.idDatasetFamilyQueryParam;
}
datasetEmitted.config = {
images: this.datasetImages,
cone_search: {
enabled: this.ngForm.form.value.cone_search_enabled,
opened: this.ngForm.form.value.cone_search_opened,
column_ra: +this.ngForm.form.value.column_ra,
column_dec: +this.ngForm.form.value.column_dec,
plot_enabled: this.ngForm.form.value.plot_enabled
plot_enabled: this.ngForm.form.value.plot_enabled,
sdss_enabled: this.ngForm.form.value.sdss_enabled,
sdss_display: this.ngForm.form.value.sdss_display,
background: this.datasetImages.map((v, i) => {
const e = `${v.id}_enabled`;
const d = `${v.id}_display`;
return {id: v.id, enabled: this.ngForm.form.value[e], display: this.ngForm.form.value[d]}
})
},
download: {
enabled: this.ngForm.form.value.download_enabled,
......
......@@ -23,10 +23,23 @@
<div *ngIf="(datasetFamilyListIsLoaded | async) && (datasetListIsLoaded | async) && (attributeListIsLoaded | async)">
<div class="row">
<div class="col">
<app-form-dataset [model]="dataset | async" [datasetFamilyList]="datasetFamilyList | async"
[directoryInfo]="directoryInfo | async" [directoryInfoIsLoading]="directoryInfoIsLoading | async"
[directoryInfoIsLoaded]="directoryInfoIsLoaded | async" [attributeList]="attributeList | async"
(loadDirectoryInfo)="loadDirectoryInfo($event)" (submitted)="editDataset($event)" #formDataset>
<app-form-dataset [model]="dataset | async"
[datasetFamilyList]="datasetFamilyList | async"
[rootDirectoryInfo]="rootDirectoryInfo | async"
[rootDirectoryInfoIsLoading]="rootDirectoryInfoIsLoading | async"
[rootDirectoryInfoIsLoaded]="rootDirectoryInfoIsLoaded | async"
[datasetDirectoryInfo]="datasetDirectoryInfo | async"
[datasetDirectoryInfoIsLoading]="datasetDirectoryInfoIsLoading | async"
[datasetDirectoryInfoIsLoaded]="datasetDirectoryInfoIsLoaded | async"
[attributeList]="attributeList | async"
[imageLimit]="imageLimit | async"
[imageLimitIsLoading]="imageLimitIsLoading | async"
[imageLimitIsLoaded]="imageLimitIsLoaded | async"
(loadRootDirectoryInfo)="loadRootDirectoryInfo($event)"
(loadDatasetDirectoryInfo)="loadDatasetDirectoryInfo($event)"
(loadFitsImageLimits)="loadFitsImageLimits($event)"
(submitted)="editDataset($event)"
#formDataset>
<button [disabled]="!formDataset.ngForm.form.valid || formDataset.ngForm.form.pristine"
type="submit" class="btn btn-primary">
<i class="fa fa-database"></i> Update dataset information
......
......@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { DatasetFamily, Dataset, Attribute, FileInfo } from '../../store/model';
import { DatasetFamily, Dataset, Attribute, FileInfo, ImageLimit } from '../../store/model';
import * as datasetFamilyActions from '../../store/action/dataset-family.action';
import * as datasetFamilySelector from '../../store/selector/dataset-family.selector';
import * as datasetActions from '../../store/action/dataset.action';
......@@ -27,12 +27,18 @@ export class EditDatasetComponent implements OnInit {
public datasetListIsLoading: Observable<boolean>;
public datasetListIsLoaded: Observable<boolean>;
public dataset: Observable<Dataset>;
public directoryInfo: Observable<FileInfo[]>;
public directoryInfoIsLoading: Observable<boolean>;
public directoryInfoIsLoaded: Observable<boolean>;
public rootDirectoryInfo: Observable<FileInfo[]>;
public rootDirectoryInfoIsLoading: Observable<boolean>;
public rootDirectoryInfoIsLoaded: Observable<boolean>;
public datasetDirectoryInfo: Observable<FileInfo[]>;
public datasetDirectoryInfoIsLoading: Observable<boolean>;
public datasetDirectoryInfoIsLoaded: Observable<boolean>;
public attributeList: Observable<Attribute[]>;
public attributeListIsLoading: Observable<boolean>;
public attributeListIsLoaded: Observable<boolean>;
public imageLimitIsLoading: Observable<boolean>;
public imageLimitIsLoaded: Observable<boolean>;
public imageLimit: Observable<ImageLimit>;
constructor(private store: Store<metamodelReducer.State>) {
this.instanceSelected = store.select(instanceSelector.getInstanceSelected);
......@@ -43,12 +49,18 @@ export class EditDatasetComponent implements OnInit {
this.datasetListIsLoading = store.select(datasetSelector.getDatasetListIsLoading);
this.datasetListIsLoaded = store.select(datasetSelector.getDatasetListIsLoaded);
this.dataset = store.select(datasetSelector.getDatasetByRouteName);
this.directoryInfo = store.select(fileExplorerSelector.getDirectoryInfo);
this.directoryInfoIsLoading = store.select(fileExplorerSelector.getDirectoryInfoIsLoading);
this.directoryInfoIsLoaded = store.select(fileExplorerSelector.getDirectoryInfoIsLoaded);
this.rootDirectoryInfo = store.select(fileExplorerSelector.getRootDirectoryInfo);
this.rootDirectoryInfoIsLoading = store.select(fileExplorerSelector.getRootDirectoryInfoIsLoading);