diff --git a/client/src/app/admin/admin-routing.module.ts b/client/src/app/admin/admin-routing.module.ts index 25defcf4d391f5c082adf10d0a76c2fe141b3b00..2d9bc73d5f6732ccee5fd960fc9b11d2e5b164b5 100644 --- a/client/src/app/admin/admin-routing.module.ts +++ b/client/src/app/admin/admin-routing.module.ts @@ -2,13 +2,23 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AdminComponent } from './containers/admin.component'; -import { InstanceListComponent } from './containers/instance-list.component'; +import { InstanceListComponent } from './containers/instance/instance-list.component'; +import { SurveyComponent } from './containers/survey/survey.component'; +import { SurveyListComponent } from './containers/survey/survey-list.component'; +import { DatabaseListComponent } from './containers/database/database-list.component'; const routes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', redirectTo: 'instance-list', pathMatch: 'full' }, - { path: 'instance-list', component: InstanceListComponent } + { path: 'instance-list', component: InstanceListComponent }, + { + path: 'survey', component: SurveyComponent, children: [ + { path: '', redirectTo: 'survey-list', pathMatch: 'full' }, + { path: 'survey-list', component: SurveyListComponent }, + { path: 'database-list', component: DatabaseListComponent } + ] + } ] } ]; @@ -21,5 +31,8 @@ export class AdminRoutingModule { } export const routedComponents = [ AdminComponent, - InstanceListComponent + InstanceListComponent, + SurveyComponent, + SurveyListComponent, + DatabaseListComponent ]; diff --git a/client/src/app/admin/components/admin-nav.component.scss b/client/src/app/admin/components/admin-nav.component.scss deleted file mode 100644 index 1cc4ce8a2b777fb65944b30cbb3363caf453b52f..0000000000000000000000000000000000000000 --- a/client/src/app/admin/components/admin-nav.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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. - */ - -.dropdown-up { - top: 80% !important; - right: 5px !important; -} - -img { - height: 60px; -} diff --git a/client/src/app/admin/components/database/database-table.component.html b/client/src/app/admin/components/database/database-table.component.html new file mode 100644 index 0000000000000000000000000000000000000000..51d8a27b1d23547ceb684dcc39232d59f1a23989 --- /dev/null +++ b/client/src/app/admin/components/database/database-table.component.html @@ -0,0 +1,45 @@ +<div class="table-responsive"> + <table class="table table-striped"> + <thead> + <tr> + <th scope="col">ID</th> + <th scope="col">Label</th> + <th scope="col">Name</th> + <th scope="col">Type</th> + <th scope="col">Host</th> + <th scope="col">Port</th> + <th scope="col">Login</th> + <th scope="col">Password</th> + <th scope="col">Nb surveys</th> + <th scope="col">Edit</th> + <th scope="col">Delete</th> + </tr> + </thead> + <tbody> + <tr *ngFor="let database of databaseList"> + <td class="align-middle">{{ database.id }}</td> + <td class="align-middle">{{ database.label }}</td> + <td class="align-middle">{{ database.dbname }}</td> + <td class="align-middle">{{ database.dbtype }}</td> + <td class="align-middle">{{ database.dbhost }}</td> + <td class="align-middle">{{ database.dbport }}</td> + <td class="align-middle">{{ database.dblogin }}</td> + <td class="align-middle">*******</td> + <td class="align-middle">{{ getNbSurveyByDatabase(database.id) }}</td> + <td class="align-middle"> + <a title="Edit this database" routerLink="/edit-database/{{database.id}}" class="btn btn-outline-primary"> + <span class="fas fa-edit"></span> + </a> + </td> + <td class="align-middle"> + <app-delete-btn + [disabled]="!isNoSurveyAttachedToDatabase(database.id)" + [type]="'database'" + [label]="database.label" + (confirm)="deleteDatabase.emit(database)"> + </app-delete-btn> + </td> + </tr> + </tbody> + </table> +</div> diff --git a/client/src/app/admin/components/database/database-table.component.ts b/client/src/app/admin/components/database/database-table.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5c2114042489c9e27b01a9f9a383c65fb384bcf --- /dev/null +++ b/client/src/app/admin/components/database/database-table.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, TemplateRef } from '@angular/core'; + +import { Database, Survey } from 'src/app/metamodel/store/models'; + +@Component({ + selector: 'app-database-table', + templateUrl: 'database-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DatabaseTableComponent { + @Input() databaseList: Database[]; + @Input() surveyList: Survey[]; + @Output() deleteDatabase: EventEmitter<Database> = new EventEmitter(); + + isNoSurveyAttachedToDatabase(idDatabase: number): boolean { + return this.getNbSurveyByDatabase(idDatabase) === 0; + } + + getNbSurveyByDatabase(idDatabase: number): number { + return this.surveyList.filter(p => p.id_database === idDatabase).length + } +} diff --git a/client/src/app/admin/components/index.ts b/client/src/app/admin/components/index.ts index 66ba1f12f72cb872b69efceb3ce95d48c8666a23..db4028f0248207b68ef9d0722367098429eb5802 100644 --- a/client/src/app/admin/components/index.ts +++ b/client/src/app/admin/components/index.ts @@ -1,7 +1,11 @@ -import { AdminNavComponent } from "./admin-nav.component"; -import { InstanceCardComponent } from "./instance-card.component"; +import { InstanceCardComponent } from "./instance/instance-card.component"; +import { DeleteBtnComponent } from "./shared/delete-btn.component"; +import { SurveyTableComponent } from "./survey/survey-table.component"; +import { DatabaseTableComponent } from "./database/database-table.component"; export const dummiesComponents = [ - AdminNavComponent, - InstanceCardComponent + InstanceCardComponent, + DeleteBtnComponent, + SurveyTableComponent, + DatabaseTableComponent ]; diff --git a/client/src/app/admin/components/instance-card.component.ts b/client/src/app/admin/components/instance-card.component.ts deleted file mode 100644 index 8de81228fcd83c1f9ed5cd5389736798b8fb3d01..0000000000000000000000000000000000000000 --- a/client/src/app/admin/components/instance-card.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, TemplateRef } from '@angular/core'; - -import { BsModalService } from 'ngx-bootstrap/modal'; -import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; - -import { Instance } from 'src/app/metamodel/store/models'; - -@Component({ - selector: 'app-instance-card', - templateUrl: 'instance-card.component.html', - styleUrls: [ 'instance-card.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class InstanceCardComponent { - @Input() instance: Instance; - @Output() deleteInstance: EventEmitter<Instance> = new EventEmitter(); - - modalRef: BsModalRef; - instanceForDel: Instance; - - constructor(private modalService: BsModalService) { } - - openModal(template: TemplateRef<any>, instance: Instance) { - this.instanceForDel = instance; - this.modalRef = this.modalService.show(template); - } - - confirmDel() { - this.deleteInstance.emit(this.instanceForDel); - this.modalRef.hide(); - } -} diff --git a/client/src/app/admin/components/instance-card.component.html b/client/src/app/admin/components/instance/instance-card.component.html similarity index 57% rename from client/src/app/admin/components/instance-card.component.html rename to client/src/app/admin/components/instance/instance-card.component.html index 93012d5acba875ae50c36b2dae63ac6bfdc2e654..6a97f30e7e72c851fbcc0a13519a4e7e89864f3d 100644 --- a/client/src/app/admin/components/instance-card.component.html +++ b/client/src/app/admin/components/instance/instance-card.component.html @@ -16,22 +16,10 @@ <span class="fas fa-edit"></span> </a> - <button title="Delete this instance" (click)="openModal(template, instance); $event.stopPropagation()" class="btn btn-outline-danger"> - <span class="fas fa-trash-alt"></span> - </button> + <app-delete-btn + [type]="'instance'" + [label]="instance.label" + (confirm)="deleteInstance.emit(instance)"> + </app-delete-btn> </div> </div> - -<ng-template #template> - <div class="modal-header"> - <h4 class="modal-title pull-left">Confirm</h4> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete this instance : <strong>{{ instanceForDel.label }}</strong> ?</p> - <p> - <button (click)="modalRef.hide()" class="btn btn-outline-primary">No</button> - - <button (click)="confirmDel()" class="btn btn-outline-danger">Yes</button> - </p> - </div> -</ng-template> diff --git a/client/src/app/admin/components/instance-card.component.scss b/client/src/app/admin/components/instance/instance-card.component.scss similarity index 100% rename from client/src/app/admin/components/instance-card.component.scss rename to client/src/app/admin/components/instance/instance-card.component.scss diff --git a/client/src/app/admin/components/instance/instance-card.component.ts b/client/src/app/admin/components/instance/instance-card.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffa29bdb30256df496fbb5c243fa6f17cb9a6571 --- /dev/null +++ b/client/src/app/admin/components/instance/instance-card.component.ts @@ -0,0 +1,14 @@ +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; + +import { Instance } from 'src/app/metamodel/store/models'; + +@Component({ + selector: 'app-instance-card', + templateUrl: 'instance-card.component.html', + styleUrls: [ 'instance-card.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class InstanceCardComponent { + @Input() instance: Instance; + @Output() deleteInstance: EventEmitter<Instance> = new EventEmitter(); +} diff --git a/client/src/app/admin/components/shared/delete-btn.component.html b/client/src/app/admin/components/shared/delete-btn.component.html new file mode 100644 index 0000000000000000000000000000000000000000..c0e8bf406f4998f44d63110f0976cc99d6edbe24 --- /dev/null +++ b/client/src/app/admin/components/shared/delete-btn.component.html @@ -0,0 +1,17 @@ +<button [disabled]="disabled" title="Delete this {{ type }}" (click)="openModal(template); $event.stopPropagation()" class="btn btn-outline-danger"> + <span class="fas fa-trash-alt"></span> +</button> + +<ng-template #template> + <div class="modal-header"> + <h4 class="modal-title pull-left">Confirm</h4> + </div> + <div class="modal-body"> + <p>Are you sure you want to delete this {{ type }} : <strong>{{ label }}</strong> ?</p> + <p> + <button (click)="this.abort.emit(); this.modalRef.hide()" class="btn btn-outline-primary">No</button> + + <button (click)="this.confirm.emit(); this.modalRef.hide()" class="btn btn-outline-danger">Yes</button> + </p> + </div> +</ng-template> diff --git a/client/src/app/admin/components/shared/delete-btn.component.ts b/client/src/app/admin/components/shared/delete-btn.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..084d3370b651f8a5e94f61dba24620d60b05e8e1 --- /dev/null +++ b/client/src/app/admin/components/shared/delete-btn.component.ts @@ -0,0 +1,25 @@ +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, TemplateRef } from '@angular/core'; + +import { BsModalService } from 'ngx-bootstrap/modal'; +import { BsModalRef } from 'ngx-bootstrap/modal/bs-modal-ref.service'; + +@Component({ + selector: 'app-delete-btn', + templateUrl: 'delete-btn.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DeleteBtnComponent { + @Input() type: string; + @Input() label: string; + @Input() disabled: boolean = false; + @Output() confirm: EventEmitter<{}> = new EventEmitter(); + @Output() abort: EventEmitter<{}> = new EventEmitter(); + + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { } + + openModal(template: TemplateRef<any>) { + this.modalRef = this.modalService.show(template); + } +} diff --git a/client/src/app/admin/components/survey/survey-table.component.html b/client/src/app/admin/components/survey/survey-table.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a30dc3701f796870de2585d3b844eccd06c2c7ae --- /dev/null +++ b/client/src/app/admin/components/survey/survey-table.component.html @@ -0,0 +1,40 @@ +<div class="table-responsive"> + <table class="table table-striped"> + <thead> + <tr> + <th scope="col">Name</th> + <th scope="col">Label</th> + <th scope="col">Description</th> + <th scope="col">Link</th> + <th scope="col">Manager</th> + <th scope="col">Database</th> + <th scope="col">Nb datasets</th> + <th scope="col">Edit</th> + <th scope="col">Delete</th> + </tr> + </thead> + <tbody> + <tr *ngFor="let survey of surveyList"> + <td class="align-middle">{{ survey.name }}</td> + <td class="align-middle">{{ survey.label }}</td> + <td class="align-middle">{{ survey.description }}</td> + <td class="align-middle"><a [href]="survey.link" target="_blank">{{ survey.link }}</a></td> + <td class="align-middle">{{ survey.manager }}</td> + <td class="align-middle">{{ getDatabaseById(survey.id_database).label }}</td> + <td class="align-middle">{{ survey.nb_datasets }}</td> + <td class="align-middle"> + <a title="Edit this survey" routerLink="/edit-survey/{{survey.name}}" class="btn btn-outline-primary"> + <span class="fas fa-edit"></span> + </a> + </td> + <td class="align-middle"> + <app-delete-btn + [type]="'survey'" + [label]="survey.label" + (confirm)="deleteSurvey.emit(survey)"> + </app-delete-btn> + </td> + </tr> + </tbody> + </table> +</div> diff --git a/client/src/app/admin/components/survey/survey-table.component.ts b/client/src/app/admin/components/survey/survey-table.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a2979653f165d03556c5b37735d3aabe4ab7eec --- /dev/null +++ b/client/src/app/admin/components/survey/survey-table.component.ts @@ -0,0 +1,18 @@ +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; + +import { Survey, Database } from 'src/app/metamodel/store/models'; + +@Component({ + selector: 'app-survey-table', + templateUrl: 'survey-table.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SurveyTableComponent { + @Input() surveyList: Survey[]; + @Input() databaseList: Database[]; + @Output() deleteSurvey: EventEmitter<Survey> = new EventEmitter(); + + getDatabaseById(idDatabase: number): Database { + return this.databaseList.find(database => database.id === idDatabase); + } +} diff --git a/client/src/app/admin/containers/admin.component.html b/client/src/app/admin/containers/admin.component.html index a7f62f6a14edb08429eda0f8d0efd4dbb7866d17..979df50e47db62138793be21dff9adf7df5e4cf1 100644 --- a/client/src/app/admin/containers/admin.component.html +++ b/client/src/app/admin/containers/admin.component.html @@ -1,11 +1,12 @@ <header> - <app-admin-nav + <app-navbar + [links]="links" [isAuthenticated]="isAuthenticated | async" [userProfile]="userProfile | async" (login)="login()" (logout)="logout()" (openEditProfile)="openEditProfile()"> - </app-admin-nav> + </app-navbar> </header> <main role="main" class="container-fluid pb-4"> <router-outlet></router-outlet> diff --git a/client/src/app/admin/containers/admin.component.ts b/client/src/app/admin/containers/admin.component.ts index ec295f32e9d64ac6733a699491a994c46d77c5f3..79ad61f2d8d7f0c4fe67891f39af0f3469a4aa14 100644 --- a/client/src/app/admin/containers/admin.component.ts +++ b/client/src/app/admin/containers/admin.component.ts @@ -26,6 +26,12 @@ import * as authSelector from 'src/app/auth/auth.selector'; * @implements OnInit */ export class AdminComponent { + public links = [ + { label: 'Portal', icon: 'fas fa-level-up-alt', routerLink: '/portal-home' }, + { label: 'Instances', icon: 'fas fa-tools', routerLink: 'instance-list' }, + { label: 'Surveys', icon: 'fas fa-database', routerLink: 'survey'}, + { label: 'Settings', icon: 'fas fa-wrench', routerLink: 'settings'} + ]; public isAuthenticated: Observable<boolean>; public userProfile: Observable<UserProfile>; public userRoles: Observable<string[]>; diff --git a/client/src/app/admin/containers/database/database-list.component.html b/client/src/app/admin/containers/database/database-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3059a3ac691592651bb27e1fe6786e439c5cf58d --- /dev/null +++ b/client/src/app/admin/containers/database/database-list.component.html @@ -0,0 +1,21 @@ +<app-spinner *ngIf="(surveyListIsLoading | async) || (databaseListIsLoading | async)"></app-spinner> + +<div *ngIf="(surveyListIsLoaded | async) && (databaseListIsLoaded | async)"> + <div class="row"> + <div class="col-12"> + <button title="Add a new database" class="btn btn-outline-success float-right" routerLink="/new-database"> + <span class="fas fa-plus"></span> New database + </button> + </div> + </div> + + <div class="row mt-1"> + <div class="col-12"> + <app-database-table + [databaseList]="databaseList | async" + [surveyList]="surveyList | async" + (deleteDatabase)="deleteDatabase($event)"> + </app-database-table> + </div> + </div> +</div> diff --git a/client/src/app/admin/containers/database/database-list.component.ts b/client/src/app/admin/containers/database/database-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d892e1d3ff90676dc861515ecacd1b21eb559882 --- /dev/null +++ b/client/src/app/admin/containers/database/database-list.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { Database, Survey } from 'src/app/metamodel/store/models'; +import * as databaseActions from 'src/app/metamodel/store/actions/database.actions'; +import * as surveyActions from 'src/app/metamodel/store/actions/survey.actions'; +import * as databaseSelector from 'src/app/metamodel/store/selectors/database.selector'; +import * as surveySelector from 'src/app/metamodel/store/selectors/survey.selector'; + +@Component({ + selector: 'app-database-list', + templateUrl: 'database-list.component.html' +}) +export class DatabaseListComponent implements OnInit { + public databaseListIsLoading: Observable<boolean>; + public databaseListIsLoaded: Observable<boolean>; + public databaseList: Observable<Database[]>; + public surveyListIsLoading: Observable<boolean>; + public surveyListIsLoaded: Observable<boolean>; + public surveyList: Observable<Survey[]>; + + constructor(private store: Store<{ }>) { + this.databaseListIsLoading = store.select(databaseSelector.selectDatabaseListIsLoading); + this.databaseListIsLoaded = store.select(databaseSelector.selectDatabaseListIsLoaded); + this.databaseList = store.select(databaseSelector.selectAllDatabases); + this.surveyListIsLoading = store.select(surveySelector.selectSurveyListIsLoading); + this.surveyListIsLoaded = store.select(surveySelector.selectSurveyListIsLoaded); + this.surveyList = store.select(surveySelector.selectAllSurveys); + } + + ngOnInit() { + this.store.dispatch(databaseActions.loadDatabaseList()); + this.store.dispatch(surveyActions.loadSurveyList()); + } + + deleteDatabase(database: Database) { + // this.store.dispatch(new databaseActions.DeleteDatabaseAction(database)); + } +} diff --git a/client/src/app/admin/containers/instance-list.component.html b/client/src/app/admin/containers/instance/instance-list.component.html similarity index 77% rename from client/src/app/admin/containers/instance-list.component.html rename to client/src/app/admin/containers/instance/instance-list.component.html index 7a97b4a025dcd3c29f7b1a13c41acf3b2e41ced7..eaee20019538fafc0b80687a6693601453fa1154 100644 --- a/client/src/app/admin/containers/instance-list.component.html +++ b/client/src/app/admin/containers/instance/instance-list.component.html @@ -7,15 +7,13 @@ </div> <div class="container"> - <div *ngIf="instanceListIsLoading | 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> + <app-spinner *ngIf="instanceListIsLoading | async"></app-spinner> <div *ngIf="instanceListIsLoaded | async" class="row row-cols-1 row-cols-sm-2 row-cols-md-3"> <app-instance-card *ngFor="let instance of (instanceList | async)" - [instance]="instance"> + [instance]="instance" + (deleteInstance)="deleteInstance($event)"> </app-instance-card> <div class="col mb-3 h-100 d-table"> <div routerLink="/new-instance" class="card card-add d-table-cell align-middle pointer" title="Add a new instance"> diff --git a/client/src/app/admin/containers/instance-list.component.scss b/client/src/app/admin/containers/instance/instance-list.component.scss similarity index 100% rename from client/src/app/admin/containers/instance-list.component.scss rename to client/src/app/admin/containers/instance/instance-list.component.scss diff --git a/client/src/app/admin/containers/instance-list.component.ts b/client/src/app/admin/containers/instance/instance-list.component.ts similarity index 89% rename from client/src/app/admin/containers/instance-list.component.ts rename to client/src/app/admin/containers/instance/instance-list.component.ts index 037bdb03890d1aab457732c3d04d967824140b6b..96b49894e3c07c855009970fdba42de8c60b5901 100644 --- a/client/src/app/admin/containers/instance-list.component.ts +++ b/client/src/app/admin/containers/instance/instance-list.component.ts @@ -26,7 +26,7 @@ export class InstanceListComponent implements OnInit { this.store.dispatch(instanceActions.loadInstanceList()); } - /* deleteInstance(instance: Instance) { - this.store.dispatch(new instanceActions.DeleteInstanceAction(instance)); - } */ + deleteInstance(instance: Instance) { + console.log(`Instance ${instance.name} deleted`); + } } diff --git a/client/src/app/admin/containers/survey/survey-list.component.html b/client/src/app/admin/containers/survey/survey-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..55f3cb566ed3c2f1862e35e0f31a127326c94a31 --- /dev/null +++ b/client/src/app/admin/containers/survey/survey-list.component.html @@ -0,0 +1,21 @@ +<app-spinner *ngIf="(surveyListIsLoading | async) || (databaseListIsLoading | async)"></app-spinner> + +<div *ngIf="(surveyListIsLoaded | async) && (databaseListIsLoaded | async)"> + <div class="row"> + <div class="col-12"> + <button title="Add a new survey" class="btn btn-outline-success float-right" routerLink="/new-survey"> + <span class="fas fa-plus"></span> New survey + </button> + </div> + </div> + + <div class="row mt-1"> + <div class="col-12"> + <app-survey-table + [surveyList]="surveyList | async" + [databaseList]="databaseList | async" + (deleteSurvey)="deleteSurvey($event)"> + </app-survey-table> + </div> + </div> +</div> diff --git a/client/src/app/admin/containers/survey/survey-list.component.ts b/client/src/app/admin/containers/survey/survey-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..276c6619d613a6d885014d608e0378e9575f2f32 --- /dev/null +++ b/client/src/app/admin/containers/survey/survey-list.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { Survey, Database } from 'src/app/metamodel/store/models'; +import * as surveyActions from 'src/app/metamodel/store/actions/survey.actions'; +import * as databaseActions from 'src/app/metamodel/store/actions/database.actions'; +import * as surveySelector from 'src/app/metamodel/store/selectors/survey.selector'; +import * as databaseSelector from 'src/app/metamodel/store/selectors/database.selector'; + +@Component({ + selector: 'app-survey-list', + templateUrl: 'survey-list.component.html' +}) +export class SurveyListComponent implements OnInit { + public surveyListIsLoading: Observable<boolean>; + public surveyListIsLoaded: Observable<boolean>; + public surveyList: Observable<Survey[]>; + public databaseListIsLoading: Observable<boolean>; + public databaseListIsLoaded: Observable<boolean>; + public databaseList: Observable<Database[]>; + + constructor(private store: Store<{ }>) { + this.surveyListIsLoading = store.select(surveySelector.selectSurveyListIsLoading); + this.surveyListIsLoaded = store.select(surveySelector.selectSurveyListIsLoaded); + this.surveyList = store.select(surveySelector.selectAllSurveys); + this.databaseListIsLoading = store.select(databaseSelector.selectDatabaseListIsLoading); + this.databaseListIsLoaded = store.select(databaseSelector.selectDatabaseListIsLoaded); + this.databaseList = store.select(databaseSelector.selectAllDatabases); + } + + ngOnInit() { + this.store.dispatch(surveyActions.loadSurveyList()); + this.store.dispatch(databaseActions.loadDatabaseList()); + } + + deleteSurvey(survey: Survey) { + // this.store.dispatch(new surveyActions.DeleteSurveyAction(survey)); + } +} diff --git a/client/src/app/admin/containers/survey/survey.component.html b/client/src/app/admin/containers/survey/survey.component.html new file mode 100644 index 0000000000000000000000000000000000000000..009c950909d454002676459519fe8f101da25bf2 --- /dev/null +++ b/client/src/app/admin/containers/survey/survey.component.html @@ -0,0 +1,26 @@ +<div class="container-fluid"> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li *ngIf="isSurveyList()" class="breadcrumb-item active" aria-current="page">Surveys</li> + <li *ngIf="isDatabaseList()" class="breadcrumb-item active" aria-current="page">Databases</li> + </ol> + </nav> +</div> + +<div class="container-fluid"> + <div class="card text-center"> + <div class="card-header"> + <ul class="nav nav-tabs card-header-tabs"> + <li class="nav-item"> + <a class="nav-link" routerLink="survey-list" routerLinkActive="active">Survey list</a> + </li> + <li class="nav-item"> + <a class="nav-link" routerLink="database-list" routerLinkActive="active">Database list</a> + </li> + </ul> + </div> + <div class="card-body"> + <router-outlet></router-outlet> + </div> + </div> +</div> diff --git a/client/src/app/admin/containers/survey/survey.component.ts b/client/src/app/admin/containers/survey/survey.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5d4d1c9cf77e428d7b0a18da5da4e7572e5836f --- /dev/null +++ b/client/src/app/admin/containers/survey/survey.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-survey', + templateUrl: 'survey.component.html' +}) +export class SurveyComponent { + constructor(private router: Router) {} + + isSurveyList() { + return this.router.url.includes('/survey/survey-list'); + } + + isDatabaseList() { + return this.router.url.includes('/survey/database-list'); + } +} diff --git a/client/src/app/auth/auth.effects.ts b/client/src/app/auth/auth.effects.ts index f00de8c0fc69335ac6bb6bb0c4c4f8eafa737095..4352427f0abc59dd9e681501abcb07e5e6227e2a 100644 --- a/client/src/app/auth/auth.effects.ts +++ b/client/src/app/auth/auth.effects.ts @@ -56,7 +56,7 @@ export class AuthEffects { openEditProfile$ = createEffect(() => this.actions$.pipe( - ofType(authActions.authSuccess), + ofType(authActions.openEditProfile), tap(_ => window.open(environment.ssoAuthUrl + '/realms/' + environment.ssoRealm + '/account', '_blank')) ), { dispatch: false } diff --git a/client/src/app/metamodel/store/reducers/database.reducer.ts b/client/src/app/metamodel/store/reducers/database.reducer.ts index b7b2e39c75d91dda2d4e1099b8b13f56dd13b430..1651db99e6dbc6cea057d63e6170ec82e7d707aa 100644 --- a/client/src/app/metamodel/store/reducers/database.reducer.ts +++ b/client/src/app/metamodel/store/reducers/database.reducer.ts @@ -5,8 +5,6 @@ import { Database } from '../models'; import * as databaseActions from '../actions/database.actions'; export interface State extends EntityState<Database> { - // additional entities state properties - selectedDatabaseId: number | null; databaseListIsLoading: boolean; databaseListIsLoaded: boolean; } @@ -14,8 +12,6 @@ export interface State extends EntityState<Database> { export const adapter: EntityAdapter<Database> = createEntityAdapter<Database>(); export const initialState: State = adapter.getInitialState({ - // additional entity state properties - selectedDatabaseId: null, databaseListIsLoading: false, databaseListIsLoaded: false }); @@ -52,9 +48,6 @@ export const databaseReducer = createReducer( }) ); -export const getSelectedDatabaseId = (state: State) => state.selectedDatabaseId; - -// get the selectors const { selectIds, selectEntities, @@ -62,14 +55,10 @@ const { selectTotal, } = adapter.getSelectors(); -// select the array of database ids export const selectDatabaseIds = selectIds; - -// select the dictionary of database entities export const selectDatabaseEntities = selectEntities; - -// select the array of databases export const selectAllDatabases = selectAll; - -// select the total database count export const selectDatabaseTotal = selectTotal; + +export const selectDatabaseListIsLoading = (state: State) => state.databaseListIsLoading; +export const selectDatabaseListIsLoaded = (state: State) => state.databaseListIsLoaded; diff --git a/client/src/app/metamodel/store/reducers/survey.reducer.ts b/client/src/app/metamodel/store/reducers/survey.reducer.ts index 09fc82b19bf19100e12799738edccde8b558f26d..d4854e5145cbe8ca9259204cbaa6a13a8cbab247 100644 --- a/client/src/app/metamodel/store/reducers/survey.reducer.ts +++ b/client/src/app/metamodel/store/reducers/survey.reducer.ts @@ -53,3 +53,6 @@ export const selectSurveyIds = selectIds; export const selectSurveyEntities = selectEntities; export const selectAllSurveys = selectAll; export const selectSurveyTotal = selectTotal; + +export const selectSurveyListIsLoading = (state: State) => state.surveyListIsLoading; +export const selectSurveyListIsLoaded = (state: State) => state.surveyListIsLoaded; diff --git a/client/src/app/metamodel/store/selectors/database.selectors.ts b/client/src/app/metamodel/store/selectors/database.selector.ts similarity index 76% rename from client/src/app/metamodel/store/selectors/database.selectors.ts rename to client/src/app/metamodel/store/selectors/database.selector.ts index 023a7914aa45b171d97497be06fff8c2d93c89b4..0f1acfb76371fc378298099b27cd8201e0b8b492 100644 --- a/client/src/app/metamodel/store/selectors/database.selectors.ts +++ b/client/src/app/metamodel/store/selectors/database.selector.ts @@ -28,7 +28,12 @@ export const selectDatabaseTotal = createSelector( fromDatabase.selectDatabaseTotal ); -export const selectCurrentDatabaseId = createSelector( +export const selectDatabaseListIsLoading = createSelector( selectDatabaseState, - fromDatabase.getSelectedDatabaseId + fromDatabase.selectDatabaseListIsLoading +); + +export const selectDatabaseListIsLoaded = createSelector( + selectDatabaseState, + fromDatabase.selectDatabaseListIsLoaded ); diff --git a/client/src/app/metamodel/store/selectors/survey.selector.ts b/client/src/app/metamodel/store/selectors/survey.selector.ts index 1d8adabf31e690a2a01400de4b11df4741ae52f9..c6b87928815cf9ed8fd3fd28a79a33843ee91fce 100644 --- a/client/src/app/metamodel/store/selectors/survey.selector.ts +++ b/client/src/app/metamodel/store/selectors/survey.selector.ts @@ -27,3 +27,13 @@ export const selectSurveyTotal = createSelector( selectSurveyState, fromSurvey.selectSurveyTotal ); + +export const selectSurveyListIsLoading = createSelector( + selectSurveyState, + fromSurvey.selectSurveyListIsLoading +); + +export const selectSurveyListIsLoaded = createSelector( + selectSurveyState, + fromSurvey.selectSurveyListIsLoaded +); diff --git a/client/src/app/portal/components/index.ts b/client/src/app/portal/components/index.ts index 91c70bc24c291da4ff601bebde618dea972c7cdf..40453624e735334f47730b61c15608a3ae934c39 100644 --- a/client/src/app/portal/components/index.ts +++ b/client/src/app/portal/components/index.ts @@ -1,7 +1,5 @@ -import { PortalNavComponent } from "./portal-nav.component"; import { InstanceCardComponent } from './instance-card.component'; export const dummiesComponents = [ - PortalNavComponent, InstanceCardComponent ]; diff --git a/client/src/app/portal/components/portal-nav.component.html b/client/src/app/portal/components/portal-nav.component.html deleted file mode 100644 index b8775b7ad7e9d1233a13e5658b5c5f39919c7d50..0000000000000000000000000000000000000000 --- a/client/src/app/portal/components/portal-nav.component.html +++ /dev/null @@ -1,97 +0,0 @@ -<nav class="navbar navbar-light bg-light navbar-expand-md fixed-top border-bottom"> - <!-- Logo --> - <a href="{{ baseHref }}" class="navbar-brand"> - <img src="assets/cesam_anis40.png" alt="CeSAM logo" /> - </a> - - <!-- Right Navigation --> - <div class="collapse navbar-collapse" id="navbarCollapse"> - <ul class="navbar-nav mr-auto"> - <li class="nav-item pr-3"> - <a class="nav-link" routerLink="/portal-home" routerLinkActive="active"> - <span class="fas fa-home"></span> Home - </a> - </li> - <li class="nav-item pr-3"> - <a class="nav-link" routerLink="/admin" routerLinkActive="active"> - <span class="fas fa-tools"></span> Admin - </a> - </li> - </ul> - <button *ngIf="authenticationEnabled && !isAuthenticated" - class="btn btn-outline-success my-2 my-sm-0" - id="button-sign-in" - (click)="login.emit()"> - Sign In / Register - </button> - <span *ngIf="isAuthenticated" id="dropdown-menu" dropdown> - <button id="button-basic" dropdownToggle type="button" class="btn btn-light" aria-controls="dropdown-basic"> - <span class="fa-stack theme-color"> - <span class="fas fa-circle fa-2x"></span> - <span class="fas fa-user fa-stack-1x fa-inverse"></span> - </span> - - <span class="fas fa-chevron-down text-secondary"></span> - </button> - <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" - aria-labelledby="basic-link"> - <li id="li-email" role="menuitem"> - <span class="dropdown-item font-italic">{{ userProfile.email }}</span> - </li> - <li class="divider dropdown-divider"></li> - <li role="menuitem"> - <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> - <span class="fas fa-id-card"></span> Edit profile - </a> - </li> - <li class="divider dropdown-divider"></li> - <li role="menuitem"> - <a class="dropdown-item text-danger pointer" (click)="logout.emit()"> - <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out - </a> - </li> - </ul> - </span> - </div> - - <!-- Dropdown appearing on mobile only --> - <span dropdown> - <button id="button-basic" dropdownToggle type="button" class="navbar-toggler" aria-controls="dropdown-basic"> - <span class="fas fa-bars"></span> - </button> - <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" - aria-labelledby="basic-link"> - <li *ngIf="isAuthenticated" role="menuitem"> - <span class="dropdown-item font-italic">{{ userProfile.email }}</span> - </li> - <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> - <li role="menuitem"> - <a class="dropdown-item" routerLink="/portal-home"> - <span class="fas fa-home fa-fw"></span> Home - </a> - </li> - <li role="menuitem"> - <a class="dropdown-item" routerLink="/admin"> - <span class="fas fa-tools fa-fw"></span> Admin - </a> - </li> - <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> - <li *ngIf="isAuthenticated" role="menuitem"> - <a class="dropdown-item pointer" (click)="openEditProfile.emit()"> - <span class="fas fa-id-card"></span> Edit profile - </a> - </li> - <li *ngIf="authenticationEnabled" class="divider dropdown-divider"></li> - <li *ngIf="authenticationEnabled && !isAuthenticated" role="menuitem"> - <a class="dropdown-item pointer text-success" (click)="login.emit()"> - <span class="fas fa-sign-in-alt fa-fw"></span> Sign In / Register - </a> - </li> - <li *ngIf="isAuthenticated" role="menuitem"> - <a class="dropdown-item pointer text-danger" (click)="logout.emit()"> - <span class="fas fa-sign-out-alt fa-fw"></span> Sign Out - </a> - </li> - </ul> - </span> -</nav> diff --git a/client/src/app/portal/components/portal-nav.component.scss b/client/src/app/portal/components/portal-nav.component.scss deleted file mode 100644 index 1cc4ce8a2b777fb65944b30cbb3363caf453b52f..0000000000000000000000000000000000000000 --- a/client/src/app/portal/components/portal-nav.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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. - */ - -.dropdown-up { - top: 80% !important; - right: 5px !important; -} - -img { - height: 60px; -} diff --git a/client/src/app/portal/components/portal-nav.component.ts b/client/src/app/portal/components/portal-nav.component.ts deleted file mode 100644 index 3feeb0caf45fb522e95da2d3231386323d2dab3f..0000000000000000000000000000000000000000 --- a/client/src/app/portal/components/portal-nav.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; - -import { UserProfile } from 'src/app/auth/user-profile.model'; -import { environment } from 'src/environments/environment' - -@Component({ - selector: 'app-portal-nav', - templateUrl: 'portal-nav.component.html', - styleUrls: [ 'portal-nav.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class PortalNavComponent { - @Input() isAuthenticated: boolean; - @Input() userProfile: UserProfile = null; - @Output() login: EventEmitter<any> = new EventEmitter(); - @Output() logout: EventEmitter<any> = new EventEmitter(); - @Output() openEditProfile: EventEmitter<any> = new EventEmitter(); - - baseHref: string = environment.baseHref; - authenticationEnabled: boolean = environment.authenticationEnabled; -} diff --git a/client/src/app/portal/containers/portal-home.component.html b/client/src/app/portal/containers/portal-home.component.html index 5c3a44e8f442ff36e1685c4e8eb2bfd0cdca07fc..16f93da003ef45253778b21864f45a82588fee1a 100644 --- a/client/src/app/portal/containers/portal-home.component.html +++ b/client/src/app/portal/containers/portal-home.component.html @@ -1,11 +1,12 @@ <header> - <app-portal-nav + <app-navbar + [links]="links" [isAuthenticated]="isAuthenticated | async" [userProfile]="userProfile | async" (login)="login()" (logout)="logout()" (openEditProfile)="openEditProfile()"> - </app-portal-nav> + </app-navbar> </header> <main role="main" class="container-fluid pb-4"> <div class="container"> diff --git a/client/src/app/portal/containers/portal-home.component.ts b/client/src/app/portal/containers/portal-home.component.ts index 7cb4d9e6541144169a183a4889cb0f7de302e130..99cae9ad8f1cad5d0c879395b58c4a5dcc57d724 100644 --- a/client/src/app/portal/containers/portal-home.component.ts +++ b/client/src/app/portal/containers/portal-home.component.ts @@ -29,6 +29,10 @@ import * as instanceSelector from 'src/app/metamodel/store/selectors/instance.se * @implements OnInit */ export class PortalHomeComponent implements OnInit { + public links = [ + { label: 'Home', icon: 'fas fa-home', routerLink: '/portal-home' }, + { label: 'Admin', icon: 'fas fa-tools', routerLink: '/admin' } + ]; public isAuthenticated: Observable<boolean>; public userProfile: Observable<UserProfile>; public userRoles: Observable<string[]>; diff --git a/client/src/app/shared/components/index.ts b/client/src/app/shared/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..29c944c70370f2905b6be7084b012b87b0beca8f --- /dev/null +++ b/client/src/app/shared/components/index.ts @@ -0,0 +1,7 @@ +import { SpinnerComponent } from "./spinner.component"; +import { NavbarComponent } from './navbar.component'; + +export const sharedComponents = [ + SpinnerComponent, + NavbarComponent +]; diff --git a/client/src/app/admin/components/admin-nav.component.html b/client/src/app/shared/components/navbar.component.html similarity index 87% rename from client/src/app/admin/components/admin-nav.component.html rename to client/src/app/shared/components/navbar.component.html index 312e8462af22e43f5ba06f694458f4fa6f8c8ae6..2c8295162061fc29f04bce23a4c0541146b2ee41 100644 --- a/client/src/app/admin/components/admin-nav.component.html +++ b/client/src/app/shared/components/navbar.component.html @@ -7,9 +7,9 @@ <!-- Right Navigation --> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> - <li class="nav-item pr-3"> - <a class="nav-link" routerLink="" routerLinkActive="active"> - <span class="fas fa-home"></span> Portal + <li *ngFor="let link of links" class="nav-item pr-3"> + <a class="nav-link" [routerLink]="link.routerLink" routerLinkActive="active"> + <span [ngClass]="link.icon"></span> {{ link.label }} </a> </li> </ul> @@ -28,8 +28,7 @@ <span class="fas fa-chevron-down text-secondary"></span> </button> - <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" - aria-labelledby="basic-link"> + <ul id="basic-link-dropdown" *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown-up" role="menu" aria-labelledby="basic-link"> <li id="li-email" role="menuitem"> <span class="dropdown-item font-italic">{{ userProfile.email }}</span> </li> @@ -60,9 +59,9 @@ <span class="dropdown-item font-italic">{{ userProfile.email }}</span> </li> <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> - <li role="menuitem"> - <a class="dropdown-item" routerLink=""> - <span class="fas fa-home fa-fw"></span> Portal + <li *ngFor="let link of links" role="menuitem"> + <a class="dropdown-item" routerLink="link.routerLink"> + <span [ngClass]="link.icon" class="fa-fw"></span> {{ link.label }} </a> </li> <li *ngIf="isAuthenticated" class="divider dropdown-divider"></li> diff --git a/client/src/app/shared/components/navbar.component.scss b/client/src/app/shared/components/navbar.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..89e681e65e5c5e5047326ea9eba97de2c2a97fab --- /dev/null +++ b/client/src/app/shared/components/navbar.component.scss @@ -0,0 +1,8 @@ +.dropdown-up { + top: 80% !important; + right: 5px !important; +} + +img { + height: 60px; +} diff --git a/client/src/app/admin/components/admin-nav.component.ts b/client/src/app/shared/components/navbar.component.ts similarity index 76% rename from client/src/app/admin/components/admin-nav.component.ts rename to client/src/app/shared/components/navbar.component.ts index 9198c4c27761c82f7676afd27d3e73d36a5991d4..b08be00b7876136f776d87647be7b6b8b48939fe 100644 --- a/client/src/app/admin/components/admin-nav.component.ts +++ b/client/src/app/shared/components/navbar.component.ts @@ -4,12 +4,13 @@ import { UserProfile } from 'src/app/auth/user-profile.model'; import { environment } from 'src/environments/environment' @Component({ - selector: 'app-admin-nav', - templateUrl: 'admin-nav.component.html', - styleUrls: [ 'admin-nav.component.scss' ], + selector: 'app-navbar', + templateUrl: 'navbar.component.html', + styleUrls: [ 'navbar.component.scss' ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AdminNavComponent { +export class NavbarComponent { + @Input() links: {label: string, icon: string, routerLink: string}[]; @Input() isAuthenticated: boolean; @Input() userProfile: UserProfile = null; @Output() login: EventEmitter<any> = new EventEmitter(); diff --git a/client/src/app/shared/components/spinner.component.html b/client/src/app/shared/components/spinner.component.html new file mode 100644 index 0000000000000000000000000000000000000000..96aaf1d49471a6237678101fd811247964393c9f --- /dev/null +++ b/client/src/app/shared/components/spinner.component.html @@ -0,0 +1,4 @@ +<div 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> diff --git a/client/src/app/shared/components/spinner.component.ts b/client/src/app/shared/components/spinner.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbd6f1b9cfd90614c05682e9d85556bbc0633f08 --- /dev/null +++ b/client/src/app/shared/components/spinner.component.ts @@ -0,0 +1,8 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-spinner', + templateUrl: 'spinner.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SpinnerComponent { } \ No newline at end of file diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 31735204744e62a643a10cde7b2351600895c1b8..e68d42e0663870201e0a5cda890393671be8f649 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -1,13 +1,20 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; import { ModalModule } from 'ngx-bootstrap/modal'; +import { sharedComponents } from './components'; + @NgModule({ + declarations: [ + sharedComponents + ], imports: [ CommonModule, + RouterModule, CollapseModule.forRoot(), BsDropdownModule.forRoot(), ModalModule.forRoot() @@ -16,7 +23,8 @@ import { ModalModule } from 'ngx-bootstrap/modal'; CommonModule, CollapseModule, BsDropdownModule, - ModalModule + ModalModule, + sharedComponents ] }) export class SharedModule { } \ No newline at end of file diff --git a/client/src/styles.scss b/client/src/styles.scss index 79ca148d7838fea513cdf4f2c8362e932bade1d3..5e2753dccf479fe017d9694d2d2487a957431a49 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -16,6 +16,8 @@ @import "~bootstrap/scss/buttons"; @import "~bootstrap/scss/transitions"; @import "~bootstrap/scss/dropdown"; +@import "~bootstrap/scss/modal"; +@import "~bootstrap/scss/tables"; @import "~bootstrap/scss/utilities"; /* Global styles */