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

Merge branch '54-data-path-by-file-explorer' into 'develop'

Resolve "Data path by file explorer"

Closes #54

See merge request !37
parents 7949a074 fb9e52e1
Pipeline #4872 passed with stages
in 10 minutes and 30 seconds
......@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Added
- #53: Authentication is optional (environment: authenticationEnabled)
- #54: Administrator can select dataset data_path via a file explorer
### Changed
- #52: Update dependencies (Angular v11, ngrx v11, ...)
......
......@@ -35,7 +35,14 @@
</div>
<div class="form-group">
<label for="data_path">Data path</label>
<input id="data_path" type="text" class="form-control" name="data_path" [ngModel]="model.data_path">
<div class="input-group mb-3">
<div class="input-group-prepend">
<button (click)="openModal(template); $event.stopPropagation()" class="btn btn-outline-secondary" type="button">
<i class="fas fa-folder-open"></i>
</button>
</div>
<input id="data_path" type="text" class="form-control" name="data_path" [ngModel]="model.data_path">
</div>
</div>
<div class="form-group">
<div class="form-row">
......@@ -130,4 +137,51 @@
<div class="form-group mt-3">
<ng-content></ng-content>
</div>
</form>
\ No newline at end of file
</form>
<ng-template #template>
<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">
<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 (bytes)</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 !== '.'">
<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 }}</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]="fileExplorerPristine" (click)="selectDirectory()" class="btn btn-primary">Select this directory</button>
</div>
</ng-template>
\ No newline at end of file
import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core';
import { Component, Input, Output, EventEmitter, ViewChild, TemplateRef } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Dataset, Project, DatasetFamily } from '../../store/model';
import { BsModalService } from 'ngx-bootstrap/modal';
import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service';
import { Dataset, Project, DatasetFamily, FileInfo } from '../../store/model';
@Component({
selector: 'app-form-dataset',
......@@ -14,9 +17,45 @@ export class FormDatasetComponent {
@Input() tableList: string[];
@Input() datasetFamilyList: DatasetFamily[];
@Input() idDatasetFamilyQueryParam: number = 0;
@Input() directoryInfo: FileInfo[];
@Input() directoryInfoIsLoading: boolean;
@Input() directoryInfoIsLoaded: boolean;
@Output() changeProject: EventEmitter<number> = new EventEmitter();
@Output() loadDirectoryInfo: EventEmitter<string> = new EventEmitter();
@Output() submitted: EventEmitter<Dataset> = new EventEmitter();
modalRef: BsModalRef;
fileExplorerPristine = true;
fileExplorerPath = '';
constructor(private modalService: BsModalService) { }
openModal(template: TemplateRef<any>) {
this.fileExplorerPristine = true;
this.fileExplorerPath = this.ngForm.controls['data_path'].value;
this.modalRef = this.modalService.show(template);
this.loadDirectoryInfo.emit(this.fileExplorerPath);
}
changePath(fileInfo: FileInfo) {
if (fileInfo.name === '.' || fileInfo.type !== 'dir') {
return;
}
if (fileInfo.name === '..') {
this.fileExplorerPath = this.fileExplorerPath.substr(0, this.fileExplorerPath.lastIndexOf("/"));
} else {
this.fileExplorerPath += '/' + fileInfo.name;
}
this.fileExplorerPristine = false;
this.loadDirectoryInfo.emit(this.fileExplorerPath);
}
selectDirectory() {
this.ngForm.controls['data_path'].setValue(this.fileExplorerPath);
this.ngForm.controls['data_path'].markAsDirty();
this.modalRef.hide();
}
getDatasetFamilyByIdQueryParam(): DatasetFamily {
return this.datasetFamilyList.find(datasetFamily => datasetFamily.id === this.idDatasetFamilyQueryParam);
......
......@@ -24,6 +24,8 @@
<div class="row">
<div class="col">
<app-form-dataset [model]="dataset | async" [datasetFamilyList]="datasetFamilyList | async"
[directoryInfo]="directoryInfo | async" [directoryInfoIsLoading]="directoryInfoIsLoading | async"
[directoryInfoIsLoaded]="directoryInfoIsLoaded | async" (loadDirectoryInfo)="loadDirectoryInfo($event)"
(submitted)="editDataset($event)" #formDataset>
<button [disabled]="!formDataset.ngForm.form.valid || formDataset.ngForm.form.pristine"
type="submit" class="btn btn-primary">
......
......@@ -2,11 +2,13 @@ import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { DatasetFamily, Dataset } from '../../store/model';
import { DatasetFamily, Dataset, FileInfo } 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';
import * as datasetSelector from '../../store/selector/dataset.selector';
import * as fileExplorerAction from '../../store/action/file-explorer.action';
import * as fileExplorerSelector from '../../store/selector/file-explorer.selector';
import * as instanceSelector from '../../store/selector/instance.selector';
import * as metamodelReducer from '../../store/reducer';
......@@ -23,6 +25,9 @@ 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>;
constructor(private store: Store<metamodelReducer.State>) {
this.instanceSelected = store.select(instanceSelector.getInstanceSelected);
......@@ -33,6 +38,9 @@ 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);
}
ngOnInit() {
......@@ -40,6 +48,10 @@ export class EditDatasetComponent implements OnInit {
this.store.dispatch(new datasetActions.LoadDatasetListAction());
}
loadDirectoryInfo(path: string) {
this.store.dispatch(new fileExplorerAction.LoadDirectoryInfoAction(path));
}
editDataset(dataset: Dataset) {
this.store.dispatch(new datasetActions.EditDatasetAction(dataset));
}
......
import { Action } from '@ngrx/store';
import { FileInfo } 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 class LoadDirectoryInfoAction implements Action {
type = LOAD_DIRECTORY_INFO;
constructor(public payload: string) { }
}
export class LoadDirectoryInfoSuccessAction implements Action {
type = LOAD_DIRECTORY_INFO_SUCCESS;
constructor(public payload: FileInfo[]) { }
}
export class LoadDirectoryInfoFailAction implements Action {
type = LOAD_DIRECTORY_INFO_FAIL;
constructor(public payload: {} = null) { }
}
export type Actions
= LoadDirectoryInfoAction
| LoadDirectoryInfoSuccessAction
| LoadDirectoryInfoFailAction;
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 { FileInfo } from '../model';
import * as fileExplorerActions from '../action/file-explorer.action';
import { FileExplorerService } from '../service/file-explorer.service';
@Injectable()
export class FileExplorerEffects {
constructor(
private actions$: Actions,
private fileExplorerService: FileExplorerService,
private toastr: ToastrService
) { }
@Effect()
loadDirectoryInfoAction$ = this.actions$.pipe(
ofType(fileExplorerActions.LOAD_DIRECTORY_INFO),
switchMap((action: fileExplorerActions.LoadDirectoryInfoAction) => {
return this.fileExplorerService.retrieveDirectoryInfo(action.payload).pipe(
map((directoryInfo: FileInfo[]) =>
new fileExplorerActions.LoadDirectoryInfoSuccessAction(directoryInfo)),
catchError(() => of(new fileExplorerActions.LoadDirectoryInfoFailAction()))
)
})
);
@Effect({ dispatch: false })
loadDirectoryInfoFailedAction$ = this.actions$.pipe(
ofType(fileExplorerActions.LOAD_DIRECTORY_INFO_FAIL),
tap(_ => this.toastr.error('Loading Failed!', 'Directory info failed'))
);
}
......@@ -8,6 +8,7 @@ import { CriteriaFamilyEffects } from './criteria-family.effects';
import { OutputFamilyEffects } from './output-family.effects';
import { OutputCategoryEffects } from './output-category.effects';
import { GroupEffects } from './group.effects';
import { FileExplorerEffects } from './file-explorer.effects';
export const metamodelEffects = [
DatabaseEffects,
......@@ -19,5 +20,6 @@ export const metamodelEffects = [
CriteriaFamilyEffects,
OutputFamilyEffects,
OutputCategoryEffects,
GroupEffects
GroupEffects,
FileExplorerEffects
];
export interface FileInfo {
name: string;
size: number;
type: string;
mimetype: string;
}
\ No newline at end of file
......@@ -10,4 +10,5 @@ export * from './output-family.model';
export * from './output-category.model';
export * from './group.model';
export * from './displayable.model';
export * from './renderer';
\ No newline at end of file
export * from './renderer';
export * from './file-info.model';
\ No newline at end of file
import * as actions from '../action/file-explorer.action';
import { FileInfo } from '../model';
export interface State {
directoryInfoIsLoading: boolean;
directoryInfoIsLoaded: boolean;
directoryInfo: FileInfo[];
}
const initialState: State = {
directoryInfoIsLoading: false,
directoryInfoIsLoaded: false,
directoryInfo: []
};
export function reducer(state: State = initialState, action: actions.Actions): State {
switch (action.type) {
case actions.LOAD_DIRECTORY_INFO:
return {
...state,
directoryInfoIsLoading: true,
directoryInfoIsLoaded: false,
directoryInfo: []
};
case actions.LOAD_DIRECTORY_INFO_SUCCESS:
const directoryInfo = action.payload as FileInfo[];
return {
...state,
directoryInfo,
directoryInfoIsLoading: false,
directoryInfoIsLoaded: true
};
case actions.LOAD_DIRECTORY_INFO_FAIL:
return {
...state,
directoryInfoIsLoading: false
};
default:
return state;
}
}
export const getDirectoryInfoIsLoading = (state: State) => state.directoryInfoIsLoading;
export const getDirectoryInfoIsLoaded = (state: State) => state.directoryInfoIsLoaded;
export const getDirectoryInfo = (state: State) => state.directoryInfo;
......@@ -10,6 +10,7 @@ import * as criteriaFamily from './criteria-family.reducer';
import * as outputFamily from './output-family.reducer';
import * as outputCategory from './output-category.reducer';
import * as group from './group.reducer';
import * as fileExplorer from './file-explorer.reducer';
export interface State {
project: project.State;
......@@ -22,6 +23,7 @@ export interface State {
outputFamily: outputFamily.State;
outputCategory: outputCategory.State;
group: group.State;
fileExplorer: fileExplorer.State;
}
const reducers = {
......@@ -34,7 +36,8 @@ const reducers = {
criteriaFamily: criteriaFamily.reducer,
outputFamily: outputFamily.reducer,
outputCategory: outputCategory.reducer,
group: group.reducer
group: group.reducer,
fileExplorer: fileExplorer.reducer
};
const productionReducer = combineReducers(reducers);
......
import { createSelector } from '@ngrx/store';
import * as reducer from '../reducer';
import * as fileExplorer from '../reducer/file-explorer.reducer';
export const getFileExplorerState = createSelector(
reducer.getMetamodelState,
(state: reducer.State) => state.fileExplorer
);
export const getDirectoryInfoIsLoading = createSelector(
getFileExplorerState,
fileExplorer.getDirectoryInfoIsLoading
);
export const getDirectoryInfoIsLoaded = createSelector(
getFileExplorerState,
fileExplorer.getDirectoryInfoIsLoaded
);
export const getDirectoryInfo = createSelector(
getFileExplorerState,
fileExplorer.getDirectoryInfo
);
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { FileInfo } from '../model';
import { environment } from '../../../../environments/environment';
@Injectable()
export class FileExplorerService {
private API_PATH: string = environment.apiUrl + '/';
constructor(private http: HttpClient) { }
retrieveDirectoryInfo(path: string): Observable<FileInfo[]> {
return this.http.get<FileInfo[]>(this.API_PATH + 'file-explorer/' + path);
}
}
......@@ -8,6 +8,7 @@ import { CriteriaFamilyService } from './criteria-family.service';
import { OutputFamilyService } from './output-family.service';
import { OutputCategoryService } from './output-category.service';
import { GroupService } from './group.service';
import { FileExplorerService } from './file-explorer.service';
export const metamodelServices = [
DatabaseService,
......@@ -19,5 +20,6 @@ export const metamodelServices = [
CriteriaFamilyService,
OutputFamilyService,
OutputCategoryService,
GroupService
GroupService,
FileExplorerService
];
export const VERSIONS = {
anisServer: '3.5.0',
anisClient: '3.5.0',
anisAdmin: '3.5.0'
anisServer: '3.6.0',
anisClient: '3.6.0',
anisAdmin: '3.6.0'
};
......@@ -12,3 +12,7 @@
.theme-color {
color: #A8C96E;
}
.cursor-pointer {
cursor: pointer;
}
\ No newline at end of file
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