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

WIP manage attributes by dataset

parent 90ea14bf
......@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #55: Add image renderer configuration
### Changed
- #57: GUI improvments
- #52: Update dependencies (Angular v11, ngrx v11, ...)
- #56: Cone-search configuration improvement
......
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Label</th>
<th>Save</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let attribute of attributeList">
<td>{{ attribute.id }}</td>
<td>{{ attribute.name }}</td>
<td>{{ attribute.label }}</td>
<td>
<button (click)="deleteAttribute.emit(attribute)" class="btn btn-outline-danger">
<i class="fas fa-trash-alt"></i> Delete attribute
</button>
</td>
</tr>
<tr *ngFor="let column of getColumnAvailable(); let index = index">
<td>{{ getMaxId() + index + 1 }}</td>
<td>{{ column.name }}</td>
<td>{{ column.name }}</td>
<td>
<button (click)="add(getMaxId() + index + 1, column)" class="btn btn-outline-primary">
<i class="fas fa-save"></i> Add attribute
</button>
</td>
</tr>
</tbody>
</table>
</div>
\ No newline at end of file
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core';
import { Attribute, Column } from '../../store/model';
@Component({
selector: 'app-form-manage-attribute',
templateUrl: 'form-manage-attribute.component.html',
styleUrls: [ 'form-manage-attribute.component.css' ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormManageAttributeComponent {
@Input() attributeList: Attribute[];
@Input() columnList: Column[];
@Output() addAttribute: EventEmitter<Attribute> = new EventEmitter();
@Output() deleteAttribute: EventEmitter<Attribute> = new EventEmitter();
getColumnAvailable(): Column[] {
return this.columnList.filter(c => !this.attributeList.map(a => a.name).includes(c.name));
}
getMaxId(): number {
return Math.max(...this.attributeList.map(a => a.id));
}
add(id: number, column: Column): void {
this.addAttribute.emit({
id,
name: column.name,
label: column.name,
form_label: column.name,
type: column.type,
criteria_display: id * 10,
output_display: id * 10,
display_detail: id * 10,
order_display: id * 10
})
}
}
import { FormManageAttributeComponent } from './form-manage-attribute.component';
import { FormAttributeListComponent } from './form-attribute-list.component';
import { TableDesignComponent } from './design/table-design.component';
import { TrDesignComponent } from './design/tr-design.component';
......@@ -24,6 +25,7 @@ import { OutputCategoryListComponent } from './families/output-category-list.co
import { FormOutputCategoryComponent } from './families/form-output-category.component';
export const attributeDummiesComponents = [
FormManageAttributeComponent,
FormAttributeListComponent,
TableDesignComponent,
TrDesignComponent,
......
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/instance-list">Instances</a></li>
<li class="breadcrumb-item"><a routerLink="/configure-instance/{{ instanceSelected | async }}">Configure
instance {{ instanceSelected | async }}</a></li>
<li class="breadcrumb-item active" aria-current="page">Manage attributes
{{ datasetSelected | async }}</li>
</ol>
</nav>
<div *ngIf="(attributeListIsLoading | async) || (columnListIsLoading | async)" 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>
<div *ngIf="(attributeListIsLoaded | async) && (columnListIsLoaded | async)">
<app-form-manage-attribute
[attributeList]="attributeList | async"
[columnList]="columnList | async"
(addAttribute)="addAttribute($event)"
(deleteAttribute)="deleteAttribute($event)">
</app-form-manage-attribute>
</div>
</div>
\ No newline at end of file
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { Attribute, Column} from '../../store/model';
import * as projectActions from '../../store/action/project.action';
import * as datasetActions from '../../store/action/dataset.action';
import * as attributeActions from '../../store/action/attribute.action';
import * as databaseActions from '../../store/action/database.action';
import * as attributeSelector from '../../store/selector/attribute.selector';
import * as datasetSelector from '../../store/selector/dataset.selector';
import * as instanceSelector from '../../store/selector/instance.selector';
import * as databaseSelector from '../../store/selector/database.selector';
import * as metamodelReducer from '../../store/reducer';
@Component({
selector: 'app-manage-attribute',
templateUrl: 'manage-attribute.component.html',
styleUrls: [ 'manage-attribute.component.css' ]
})
export class ManageAttributeComponent implements OnInit {
public instanceSelected: Observable<string>;
public datasetSelected: Observable<string>;
public attributeList: Observable<Attribute[]>;
public attributeListIsLoading: Observable<boolean>;
public attributeListIsLoaded: Observable<boolean>;
public columnList: Observable<Column[]>;
public columnListIsLoading: Observable<boolean>;
public columnListIsLoaded: Observable<boolean>;
constructor(private store: Store<metamodelReducer.State>) {
this.instanceSelected = store.select(instanceSelector.getInstanceSelected);
this.datasetSelected = store.select(datasetSelector.getDatasetSelected);
this.attributeList = store.select(attributeSelector.getAttributeList);
this.attributeListIsLoading = store.select(attributeSelector.getAttributeListIsLoading);
this.attributeListIsLoaded = store.select(attributeSelector.getAttributeListIsLoaded);
this.columnList = store.select(databaseSelector.getColumnList);
this.columnListIsLoading = store.select(databaseSelector.getColumnListIsLoading);
this.columnListIsLoaded = store.select(databaseSelector.getColumnListIsLoaded);
}
ngOnInit() {
this.store.dispatch(new projectActions.LoadProjectListAction());
this.store.dispatch(new datasetActions.LoadDatasetListAction());
this.store.dispatch(new attributeActions.LoadAttributeListAction());
this.store.dispatch(new databaseActions.LoadColumnListAction());
}
addAttribute(attribute: Attribute): void {
this.store.dispatch(new attributeActions.AddNewAttributeAction(attribute));
}
deleteAttribute(attribute: Attribute): void {
this.store.dispatch(new attributeActions.DeleteAttributeAction(attribute));
}
}
......@@ -13,6 +13,7 @@ import { NewInstanceComponent } from './containers/instance/new-instance.compone
import { EditInstanceComponent } from './containers/instance/edit-instance.component';
import { ConfigureInstanceComponent } from './containers/instance/configure-instance.component';
import { NewDatasetComponent } from './containers/dataset/new-dataset.component';
import { ManageAttributeComponent } from './containers/attribute/manage-attribute.component';
import { EditDatasetComponent } from './containers/dataset/edit-dataset.component';
import { AttributeComponent } from './containers/attribute/attribute.component';
import { GroupComponent } from './containers/group/group.component';
......@@ -30,6 +31,7 @@ const routes: Routes = [
{ path: 'configure-instance/:iname/new-group', component: NewGroupComponent },
{ path: 'configure-instance/:iname/edit-group/:id', component: EditGroupComponent },
{ path: 'configure-instance/:iname/new-dataset', component: NewDatasetComponent },
{ path: 'configure-instance/:iname/manage-attribute/:dname', component: ManageAttributeComponent },
{ path: 'configure-instance/:iname/edit-dataset/:dname', component: EditDatasetComponent },
{ path: 'configure-instance/:iname/configure-dataset/:dname', component: AttributeComponent },
{
......@@ -75,6 +77,7 @@ export const routedComponents = [
EditDatabaseComponent,
ConfigureInstanceComponent,
NewDatasetComponent,
ManageAttributeComponent,
EditDatasetComponent,
AttributeComponent,
GroupComponent,
......
......@@ -5,9 +5,15 @@ import { Attribute } from '../model';
export const LOAD_ATTRIBUTE_LIST = '[Attribute] Load Attribute List';
export const LOAD_ATTRIBUTE_LIST_SUCCESS = '[Attribute] Load Attribute List Sucess';
export const LOAD_ATTRIBUTE_LIST_FAIL = '[Attribute] Load Attribute List Fail';
export const ADD_NEW_ATTRIBUTE = '[Attribute] Add New Attribute';
export const ADD_NEW_ATTRIBUTE_SUCCESS = '[Attribute] Add New Attribute Success';
export const ADD_NEW_ATTRIBUTE_FAIL = '[Attribute] Add New Attribute Fail';
export const EDIT_ATTRIBUTE = '[Attribute] Edit Attribute';
export const EDIT_ATTRIBUTE_SUCCESS = '[Attribute] Edit Attribute Success';
export const EDIT_ATTRIBUTE_FAIL = '[Attribute] Edit Attribute Fail';
export const DELETE_ATTRIBUTE = '[Attribute] Delete Attribute';
export const DELETE_ATTRIBUTE_SUCCESS = '[Attribute] Delete Attribute Success';
export const DELETE_ATTRIBUTE_FAIL = '[Attribute] Delete Attribute Fail';
export const GENERATE_OPTION_LIST = '[Attribute] Generate Option List';
export const GENERATE_OPTION_LIST_SUCCESS = '[Attribute] Generate Option List Success';
export const GENERATE_OPTION_LIST_FAIL = '[Attribute] Generate Option List Fail';
......@@ -30,6 +36,24 @@ export class LoadAttributeListFailAction implements Action {
constructor(public payload: {} = null) { }
}
export class AddNewAttributeAction implements Action {
type = ADD_NEW_ATTRIBUTE;
constructor(public payload: Attribute) { }
}
export class AddNewAttributeSuccessAction implements Action {
type = ADD_NEW_ATTRIBUTE_SUCCESS;
constructor(public payload: Attribute) { }
}
export class AddNewAttributeFailAction implements Action {
type = ADD_NEW_ATTRIBUTE_FAIL;
constructor(public payload: {} = null) { }
}
export class EditAttributeAction implements Action {
type = EDIT_ATTRIBUTE;
......@@ -48,6 +72,24 @@ export class EditAttributeFailAction implements Action {
constructor(public payload: {} = null) { }
}
export class DeleteAttributeAction implements Action {
type = DELETE_ATTRIBUTE;
constructor(public payload: Attribute) { }
}
export class DeleteAttributeSuccessAction implements Action {
type = DELETE_ATTRIBUTE_SUCCESS;
constructor(public payload: Attribute) { }
}
export class DeleteAttributeFailAction implements Action {
type = DELETE_ATTRIBUTE_FAIL;
constructor(public payload: {} = null) { }
}
export class GenerateOptionListAction implements Action {
type = GENERATE_OPTION_LIST;
......@@ -70,9 +112,15 @@ export type Actions
= LoadAttributeListAction
| LoadAttributeListSuccessAction
| LoadAttributeListFailAction
| AddNewAttributeAction
| AddNewAttributeSuccessAction
| AddNewAttributeFailAction
| EditAttributeAction
| EditAttributeSuccessAction
| EditAttributeFailAction
| DeleteAttributeAction
| DeleteAttributeSuccessAction
| DeleteAttributeFailAction
| GenerateOptionListAction
| GenerateOptionListSuccessAction
| GenerateOptionListFailAction;
import { Action } from '@ngrx/store';
import { Database } from '../model';
import { Database, Column } from '../model';
export const LOAD_DATABASE_LIST = '[Database] Load Database List';
export const LOAD_DATABASE_LIST_WIP = '[Database] Load Database List WIP';
export const LOAD_DATABASE_LIST_SUCCESS = '[Database] Load Database List Success';
export const LOAD_DATABASE_LIST_FAIL = '[Database] Load Database List Fail';
export const LOAD_TABLE_LIST = 'Load Table List';
export const LOAD_TABLE_LIST_SUCCESS = 'Load Table List Success';
export const LOAD_TABLE_LIST_FAIL = 'Load Table List Fail';
export const LOAD_TABLE_LIST = '[Database] Load Table List';
export const LOAD_TABLE_LIST_SUCCESS = '[Database] Load Table List Success';
export const LOAD_TABLE_LIST_FAIL = '[Database] Load Table List Fail';
export const LOAD_COLUMN_LIST = '[Database] Load Column List';
export const LOAD_COLUMN_LIST_SUCCESS = '[Database] Load Column List Success';
export const LOAD_COLUMN_LIST_FAIL = '[Database] Load Column List Fail';
export const ADD_NEW_DATABASE = '[Database] Add New Database';
export const ADD_NEW_DATABASE_SUCCESS = '[Database] Add New Database Success';
export const ADD_NEW_DATABASE_FAIL = '[Database] Add New Database Fail';
......@@ -61,6 +64,24 @@ export class LoadTableListFailAction implements Action {
constructor(public payload: {} = null) { }
}
export class LoadColumnListAction implements Action {
type = LOAD_COLUMN_LIST;
constructor(public payload: {} = null) { }
}
export class LoadColumnListSuccessAction implements Action {
type = LOAD_COLUMN_LIST_SUCCESS;
constructor(public payload: Column[]) { }
}
export class LoadColumnListFailAction implements Action {
type = LOAD_COLUMN_LIST_FAIL;
constructor(public payload: {} = null) { }
}
export class AddNewDatabaseAction implements Action {
type = ADD_NEW_DATABASE;
......@@ -123,6 +144,9 @@ export type Actions
| LoadTableListAction
| LoadTableListSuccessAction
| LoadTableListFailAction
| LoadColumnListAction
| LoadColumnListSuccessAction
| LoadColumnListFailAction
| AddNewDatabaseAction
| AddNewDatabaseSuccessAction
| AddNewDatabaseFailAction
......
......@@ -61,6 +61,34 @@ export class AttributeEffects {
map(_ => this.toastr.error('Loading Failed!', 'Generate option list failed'))
);
@Effect()
addNewAttributeAction$ = this.actions$.pipe(
ofType(attributeActions.ADD_NEW_ATTRIBUTE),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const datasetName = state.router.state.params.dname;
const addNewAttributeAction = action as attributeActions.AddNewAttributeAction;
return this.attributeService.addAttribute(datasetName, addNewAttributeAction.payload).pipe(
map((attribute: Attribute) => new attributeActions.AddNewAttributeSuccessAction(attribute)),
catchError(() => of(new attributeActions.AddNewAttributeFailAction()))
)
})
);
@Effect({dispatch: false})
addNewAttributeSuccessAction$ = this.actions$.pipe(
ofType(attributeActions.ADD_NEW_ATTRIBUTE_SUCCESS),
map(action => {
this.toastr.success('Add attribute success!', 'The new attribute has been created!');
})
);
@Effect({dispatch: false})
addNewAttributeFailedAction$ = this.actions$.pipe(
ofType(attributeActions.ADD_NEW_ATTRIBUTE_FAIL),
map(_ => this.toastr.error('Add attribute failed!', 'The new attribute could not be created into the database'))
);
@Effect()
editAttributeAction$ = this.actions$.pipe(
ofType(attributeActions.EDIT_ATTRIBUTE),
......@@ -88,4 +116,32 @@ export class AttributeEffects {
ofType(attributeActions.EDIT_ATTRIBUTE_FAIL),
map(_ => this.toastr.error('Edit attribute failed!', 'The existing entities could not be edited into the database'))
);
@Effect()
deleteAttributeAction$ = this.actions$.pipe(
ofType(attributeActions.DELETE_ATTRIBUTE),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const datasetName = state.router.state.params.dname;
const deleteAttributeAction = action as attributeActions.DeleteAttributeAction;
return this.attributeService.deleteAttribute(datasetName, deleteAttributeAction.payload).pipe(
map(_ => new attributeActions.DeleteAttributeSuccessAction(deleteAttributeAction.payload)),
catchError(() => of(new attributeActions.DeleteAttributeFailAction()))
)
})
);
@Effect({dispatch: false})
deleteDatabaseSuccessAction$ = this.actions$.pipe(
ofType(attributeActions.DELETE_ATTRIBUTE_SUCCESS),
map(_ => {
this.toastr.success('Delete attribute success!', 'The attribute has been deleted!');
})
);
@Effect({dispatch: false})
deleteAttributeFailedAction$ = this.actions$.pipe(
ofType(attributeActions.DELETE_ATTRIBUTE_FAIL),
map(_ => this.toastr.error('Delete attribute failed!', 'The attribute could not be deleted into the database'))
);
}
......@@ -8,7 +8,8 @@ import { of } from 'rxjs';
import { withLatestFrom, switchMap, map, catchError, tap } from 'rxjs/operators';
import * as fromMetamodel from '../reducer';
import { Database } from '../model';
import * as fromRouter from '../../../shared/utils';
import { Database, Column } from '../model';
import * as databaseActions from '../action/database.action';
import { DatabaseService } from '../service/database.service';
......@@ -16,7 +17,7 @@ import { DatabaseService } from '../service/database.service';
export class DatabaseEffects {
constructor(
private actions$: Actions,
private store$: Store<{metamodel: fromMetamodel.State}>,
private store$: Store<{router: fromRouter.RouterReducerState, metamodel: fromMetamodel.State}>,
private databaseService: DatabaseService,
private router: Router,
private toastr: ToastrService
......@@ -70,6 +71,25 @@ export class DatabaseEffects {
map(_ => this.toastr.error('Loading Failed!', 'Table list loading failed'))
);
@Effect()
loadColumnListAction$ = this.actions$.pipe(
ofType(databaseActions.LOAD_COLUMN_LIST),
withLatestFrom(this.store$),
switchMap(([action, state]) => {
const datasetName = state.router.state.params.dname;
return this.databaseService.retrieveColumns(datasetName).pipe(
map((columnList: Column[]) => new databaseActions.LoadColumnListSuccessAction(columnList)),
catchError(() => of(new databaseActions.LoadColumnListFailAction()))
)
})
);
@Effect()
loadColumnListFailAction$ = this.actions$.pipe(
ofType(databaseActions.LOAD_COLUMN_LIST_FAIL),
map(_ => this.toastr.error('Loading Failed!', 'Column list loading failed'))
);
@Effect()
addNewDatabaseAction$ = this.actions$.pipe(
ofType(databaseActions.ADD_NEW_DATABASE),
......
......@@ -62,8 +62,9 @@ export class DatasetEffects {
ofType(datasetActions.ADD_NEW_DATASET_SUCCESS),
withLatestFrom(this.store$),
map(([action, state]) => {
const addNewDatasetSuccessAction = action as datasetActions.AddNewDatasetSuccessAction;
const instanceName = state.router.state.params.iname;
this.router.navigate(['/configure-instance/' + instanceName]);
this.router.navigate(['/configure-instance/' + instanceName + '/manage-attribute/' + addNewDatasetSuccessAction.payload.name]);
this.toastr.success('Add dataset success!', 'The new dataset has been created!');
})
);
......
......@@ -4,35 +4,34 @@ import { RendererConfig } from './renderer/renderer-config.model';
export interface Attribute {
id: number;
name: string;
table_name: string;
label: string;
form_label: string;
description: string;
description?: string;
output_display: number;
criteria_display: number;
search_flag: string;
search_type: string;
operator: string;
search_flag?: string;
search_type?: string;
operator?: string;
type: string;
min: string;
max: string;
placeholder_min: string;
placeholder_max: string;
renderer: string;
renderer_config: RendererConfig;
min?: string;
max?: string;
placeholder_min?: string;
placeholder_max?: string;
renderer?: string;
renderer_config?: RendererConfig;
display_detail: number;
selected: boolean;
order_by: boolean;
order_display: number;
detail: boolean;
renderer_detail: string;
options: Option[];
vo_utype: string;
vo_ucd: string;
vo_unit: string;
vo_description: string;
vo_datatype: string;
vo_size: number;
id_criteria_family: number;
id_output_category: number;
selected?: boolean;
order_by?: boolean;
order_display?: number;
detail?: boolean;
renderer_detail?: string;
options?: Option[];
vo_utype?: string;
vo_ucd?: string;
vo_unit?: string;
vo_description?: string;
vo_datatype?: string;
vo_size?: number;
id_criteria_family?: number;
id_output_category?: number;
}
export interface Column {
name: string;
type: string;
};
......@@ -11,4 +11,5 @@ export * from './output-category.model';
export * from './group.model';
export * from './displayable.model';
export * from './renderer';
export * from './file-info.model';
\ No newline at end of file
export * from './file-info.model';
export * from './column.model';
......@@ -71,6 +71,17 @@ export function reducer(state: State = initialState, action: actions.Actions): S
optionListGeneratedIsLoading: false
}
case actions.ADD_NEW_ATTRIBUTE_SUCCESS:
const addedAttribute = action.payload as Attribute;
return {
...state,
attributeList: [
...state.attributeList,
addedAttribute
]
};
case actions.EDIT_ATTRIBUTE_SUCCESS:
const editedAttribute = action.payload as Attribute;
......@@ -82,6 +93,14 @@ export function reducer(state: State = initialState, action: actions.Actions): S
].sort(sortAttributeListByOutputDisplay)
};
case actions.DELETE_ATTRIBUTE_SUCCESS:
const deletedAttribute = action.payload as Attribute;
return {
...state,
attributeList: state.attributeList.filter(attribute => attribute.id !== deletedAttribute.id)
};
default:
return state;
}
......
import * as actions from '../action/database.action';