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

Images configuration => WIP

parent 8802ff4a
......@@ -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,31 @@
<label class="custom-control-label" for="public">Public</label>
</div>
</accordion-group>
<accordion-group 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 class="mt-2 table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Size</th>
<th scope="col">RA min/max</th>
<th scope="col">DEC min/max</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let image of imagesSelected">
<td class="align-middle">{{ image.name }}</td>
<td class="align-middle">{{ image.size | formatFileSize: false }}</td>
<td></td>
<td></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">
......@@ -161,7 +186,7 @@
</div>
</form>
<ng-template #template>
<ng-template #templateDataPath>
<div class="modal-header">
<h4 class="modal-title pull-left">ANIS file explorer</h4>
</div>
......@@ -181,12 +206,12 @@
<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 directoryInfo" (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 +219,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 +229,51 @@
<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="directoryInfoIsLoading" 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="directoryInfoIsLoaded" 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 directoryInfo" (click)="newImageAction(fileInfo)" [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>
</div>
</ng-template>
\ No newline at end of file
......@@ -27,8 +27,9 @@ export class FormDatasetComponent {
@Output() submitted: EventEmitter<Dataset> = new EventEmitter();
modalRef: BsModalRef;
fileExplorerPristine = true;
dataPathFileExplorerPristine = true;
fileExplorerPath = '';
imagesSelected = [];
constructor(private modalService: BsModalService) { }
......@@ -36,8 +37,8 @@ export class FormDatasetComponent {
console.log(this.model);
}
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 = '';
......@@ -46,7 +47,7 @@ export class FormDatasetComponent {
this.loadDirectoryInfo.emit(this.fileExplorerPath);
}
changePath(fileInfo: FileInfo) {
dataPathAction(fileInfo: FileInfo): void {
if (fileInfo.name === '.' || fileInfo.type !== 'dir') {
return;
}
......@@ -56,7 +57,31 @@ export class FormDatasetComponent {
} else {
this.fileExplorerPath += '/' + fileInfo.name;
}
this.fileExplorerPristine = false;
this.dataPathFileExplorerPristine = false;
this.loadDirectoryInfo.emit(this.fileExplorerPath);
}
newImageOpenModal(template: TemplateRef<any>): void {
this.fileExplorerPath = this.ngForm.controls['data_path'].value;
this.modalRef = this.modalService.show(template);
this.loadDirectoryInfo.emit(this.fileExplorerPath);
}
newImageAction(fileInfo: FileInfo): void {
if (fileInfo.name === '.') {
return;
}
if (fileInfo.type === 'file') {
this.imagesSelected.push(fileInfo);
this.modalRef.hide();
} else {
if (fileInfo.name === '..') {
this.fileExplorerPath = this.fileExplorerPath.substr(0, this.fileExplorerPath.lastIndexOf("/"));
} else {
this.fileExplorerPath += '/' + fileInfo.name;
}
}
this.loadDirectoryInfo.emit(this.fileExplorerPath);
}
......
import { Action } from '@ngrx/store';
import { FileInfo } from '../model';
import { FileInfo, ImageLimit } from '../model';
export const LOAD_DIRECTORY_INFO = '[FileExplorer] Load Directory Info';
export const LOAD_DIRECTORY_INFO_SUCCESS = '[FileExplorer] Load Directory Info Success';
export const LOAD_DIRECTORY_INFO_FAIL = '[FileExplorer] Load Directory Info Fail';
export const LOAD_IMAGE_LIMIT = '[FileExplorer] Load Image Limit';
export const LOAD_IMAGE_LIMIT_SUCCESS = '[FileExplorer] Load Image Limit Success';
export const LOAD_IMAGE_LIMIT_FAIL = '[FileExplorer] Load Image Limit Fail';
export class LoadDirectoryInfoAction implements Action {
type = LOAD_DIRECTORY_INFO;
readonly type = LOAD_DIRECTORY_INFO;
constructor(public payload: string) { }
}
export class LoadDirectoryInfoSuccessAction implements Action {
type = LOAD_DIRECTORY_INFO_SUCCESS;
readonly type = LOAD_DIRECTORY_INFO_SUCCESS;
constructor(public payload: FileInfo[]) { }
}
export class LoadDirectoryInfoFailAction implements Action {
type = LOAD_DIRECTORY_INFO_FAIL;
readonly type = LOAD_DIRECTORY_INFO_FAIL;
constructor(public payload: {} = null) { }
}
export class LoadImageLimit implements Action {
readonly type = LOAD_IMAGE_LIMIT;
constructor(public payload: string) { }
}
export class LoadImageLimitSuccess implements Action {
readonly type = LOAD_IMAGE_LIMIT_SUCCESS;
constructor(public payload: ImageLimit) { }
}
export class LoadImageLimitFail implements Action {
readonly type = LOAD_IMAGE_LIMIT_FAIL;
constructor(public payload: {} = null) { }
}
......@@ -27,4 +48,7 @@ export class LoadDirectoryInfoFailAction implements Action {
export type Actions
= LoadDirectoryInfoAction
| LoadDirectoryInfoSuccessAction
| LoadDirectoryInfoFailAction;
| LoadDirectoryInfoFailAction
| LoadImageLimit
| LoadImageLimitSuccess
| LoadImageLimitFail;
......@@ -2,10 +2,12 @@ import { Injectable } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { switchMap, map, catchError, tap } from 'rxjs/operators';
import { switchMap, map, catchError, tap, withLatestFrom } from 'rxjs/operators';
import { FileInfo } from '../model';
import * as fromRouter from '../../../shared/utils';
import { FileInfo, ImageLimit } from '../model';
import * as fileExplorerActions from '../action/file-explorer.action';
import { FileExplorerService } from '../service/file-explorer.service';
......@@ -13,6 +15,7 @@ import { FileExplorerService } from '../service/file-explorer.service';
export class FileExplorerEffects {
constructor(
private actions$: Actions,
private store$: Store<{router: fromRouter.RouterReducerState}>,
private fileExplorerService: FileExplorerService,
private toastr: ToastrService
) { }
......@@ -34,4 +37,24 @@ export class FileExplorerEffects {
ofType(fileExplorerActions.LOAD_DIRECTORY_INFO_FAIL),
tap(_ => this.toastr.error('Loading Failed!', 'Directory info failed'))
);
loadImageLimitAction$ = this.actions$.pipe(
ofType(fileExplorerActions.LOAD_IMAGE_LIMIT),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const loadImageLimitAction = action as fileExplorerActions.LoadImageLimit;
const dname = state.router.state.params.dname;
return this.fileExplorerService.retrieveFitsImageLimits(dname, loadImageLimitAction.payload).pipe(
map((imageLimit: ImageLimit) =>
new fileExplorerActions.LoadImageLimitSuccess(imageLimit)),
catchError(() => of(new fileExplorerActions.LoadImageLimitFail()))
)
})
)
@Effect({ dispatch: false })
loadImageLimitFailedAction$ = this.actions$.pipe(
ofType(fileExplorerActions.LOAD_IMAGE_LIMIT_FAIL),
tap(_ => this.toastr.error('Loading Failed!', 'Image limit info failed'))
);
}
export interface ImageLimit {
ra_min: number;
ra_max: number;
dec_min: number;
dec_max: number;
}
......@@ -13,3 +13,4 @@ export * from './displayable.model';
export * from './renderer';
export * from './file-info.model';
export * from './column.model';
export * from './image-limit.model';
import * as actions from '../action/file-explorer.action';
import { FileInfo } from '../model';
import { FileInfo, ImageLimit } from '../model';
export interface State {
directoryInfoIsLoading: boolean;
directoryInfoIsLoaded: boolean;
directoryInfo: FileInfo[];
imageLimitIsLoading: boolean;
imageLimitIsLoaded: boolean;
imageLimit: ImageLimit;
}
const initialState: State = {
directoryInfoIsLoading: false,
directoryInfoIsLoaded: false,
directoryInfo: []
directoryInfo: [],
imageLimitIsLoading: false,
imageLimitIsLoaded: false,
imageLimit: null
};
export function reducer(state: State = initialState, action: actions.Actions): State {
......@@ -25,11 +31,9 @@ export function reducer(state: State = initialState, action: actions.Actions): S
};
case actions.LOAD_DIRECTORY_INFO_SUCCESS:
const directoryInfo = action.payload as FileInfo[];
return {
...state,
directoryInfo,
directoryInfo: action.payload,
directoryInfoIsLoading: false,
directoryInfoIsLoaded: true
};
......@@ -40,6 +44,28 @@ export function reducer(state: State = initialState, action: actions.Actions): S
directoryInfoIsLoading: false
};
case actions.LOAD_IMAGE_LIMIT:
return {
...state,
imageLimitIsLoading: true,
imageLimitIsLoaded: false,
imageLimit: null
};
case actions.LOAD_IMAGE_LIMIT_SUCCESS:
return {
...state,
imageLimitIsLoading: false,
imageLimitIsLoaded: true,
imageLimit: action.payload
};
case actions.LOAD_IMAGE_LIMIT_FAIL:
return {
...state,
imageLimitIsLoading: false
};
default:
return state;
}
......@@ -48,3 +74,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S
export const getDirectoryInfoIsLoading = (state: State) => state.directoryInfoIsLoading;
export const getDirectoryInfoIsLoaded = (state: State) => state.directoryInfoIsLoaded;
export const getDirectoryInfo = (state: State) => state.directoryInfo;
export const getImageLimitIsLoading = (state: State) => state.imageLimitIsLoading;
export const getImageLimitIsLoaded = (state: State) => state.imageLimitIsLoaded;
export const getImageLimit = (state: State) => state.imageLimit;
......@@ -22,3 +22,18 @@ export const getDirectoryInfo = createSelector(
getFileExplorerState,
fileExplorer.getDirectoryInfo
);
export const getImageLimitIsLoading = createSelector(
getFileExplorerState,
fileExplorer.getImageLimitIsLoading
);
export const getImageLimitIsLoaded = createSelector(
getFileExplorerState,
fileExplorer.getImageLimitIsLoaded
);
export const getImageLimit= createSelector(
getFileExplorerState,
fileExplorer.getImageLimit
);
......@@ -3,16 +3,25 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { FileInfo } from '../model';
import { FileInfo, ImageLimit } from '../model';
import { environment } from '../../../../environments/environment';
@Injectable()
export class FileExplorerService {
private API_PATH: string = environment.apiUrl + '/';
private SERVICES_PATH: string = environment.servicesUrl + '/';
constructor(private http: HttpClient) { }
retrieveDirectoryInfo(path: string): Observable<FileInfo[]> {
return this.http.get<FileInfo[]>(this.API_PATH + 'file-explorer/' + path);
}
retrieveDatasetDirectoryInfo(datasetName: string, path: string): Observable<FileInfo[]> {
return this.http.get<FileInfo[]>(this.API_PATH + '/dataset-file-explorer/' + datasetName + '/' + path);
}
retrieveFitsImageLimits(datasetName: string, filename: string) {
return this.http.get<ImageLimit>(this.SERVICES_PATH + 'get-fits-image-limits/' + datasetName + '?filename=' + filename);
}
}
import { Pipe, PipeTransform } from '@angular/core';
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const FILE_SIZE_UNITS_LONG = ['Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', 'Pettabytes', 'Exabytes', 'Zettabytes', 'Yottabytes'];
@Pipe({
name: 'formatFileSize'
})
export class FormatFileSizePipe implements PipeTransform {
transform(sizeInBytes: number, longForm: boolean): string {
const units = longForm ? FILE_SIZE_UNITS_LONG : FILE_SIZE_UNITS;
let power = Math.round(Math.log(sizeInBytes) / Math.log(1024));
power = Math.min(power, units.length - 1);
const size = sizeInBytes / Math.pow(1024, power); // size in new units
const formattedSize = Math.round(size * 100) / 100; // keep up to 2 decimals
const unit = units[power];
return `${formattedSize} ${unit}`;
}
}
......@@ -8,6 +8,8 @@ import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { ModalModule } from 'ngx-bootstrap/modal';
import { AccordionModule } from 'ngx-bootstrap/accordion';
import { FormatFileSizePipe } from './format-file-size.pipe';
@NgModule({
imports: [
CommonModule,
......@@ -19,6 +21,9 @@ import { AccordionModule } from 'ngx-bootstrap/accordion';
AccordionModule.forRoot(),
RouterModule
],
declarations: [
FormatFileSizePipe
],
exports: [
CommonModule,
FormsModule,
......@@ -26,7 +31,8 @@ import { AccordionModule } from 'ngx-bootstrap/accordion';
ToastrModule,
BsDropdownModule,
AccordionModule,
ModalModule
ModalModule,
FormatFileSizePipe
]
})
export class SharedModule { }
export const environment = {
production: true,
apiUrl: '/server',
servicesUrl: '/services',
baseHref: '/admin',
authenticationEnabled: true,
ssoAuthUrl: 'https://keycloak.lam.fr/auth/',
......
......@@ -5,6 +5,7 @@
export const environment = {
production: false,
apiUrl: 'http://localhost:8080',
servicesUrl: 'http://localhost:5000',
baseHref: '/',
authenticationEnabled: false,
ssoAuthUrl: 'http://localhost:8180/auth',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment