From 906019dd1f2ab958e0455256a14efdaae7a7dd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Thu, 24 Jun 2021 16:59:04 +0200 Subject: [PATCH] Admin => add surveys and databases routes --- client/src/app/admin/admin-routing.module.ts | 19 +++- .../admin/components/admin-nav.component.scss | 17 ---- .../database/database-table.component.html | 45 +++++++++ .../database/database-table.component.ts | 22 +++++ client/src/app/admin/components/index.ts | 12 ++- .../components/instance-card.component.ts | 32 ------ .../instance-card.component.html | 22 +---- .../instance-card.component.scss | 0 .../instance/instance-card.component.ts | 14 +++ .../shared/delete-btn.component.html | 17 ++++ .../components/shared/delete-btn.component.ts | 25 +++++ .../survey/survey-table.component.html | 40 ++++++++ .../survey/survey-table.component.ts | 18 ++++ .../app/admin/containers/admin.component.html | 5 +- .../app/admin/containers/admin.component.ts | 6 ++ .../database/database-list.component.html | 21 ++++ .../database/database-list.component.ts | 40 ++++++++ .../instance-list.component.html | 8 +- .../instance-list.component.scss | 0 .../{ => instance}/instance-list.component.ts | 6 +- .../survey/survey-list.component.html | 21 ++++ .../survey/survey-list.component.ts | 40 ++++++++ .../containers/survey/survey.component.html | 26 +++++ .../containers/survey/survey.component.ts | 18 ++++ client/src/app/auth/auth.effects.ts | 2 +- .../store/reducers/database.reducer.ts | 17 +--- .../store/reducers/survey.reducer.ts | 3 + ...base.selectors.ts => database.selector.ts} | 9 +- .../store/selectors/survey.selector.ts | 10 ++ client/src/app/portal/components/index.ts | 2 - .../components/portal-nav.component.html | 97 ------------------- .../components/portal-nav.component.scss | 17 ---- .../portal/components/portal-nav.component.ts | 21 ---- .../containers/portal-home.component.html | 5 +- .../containers/portal-home.component.ts | 4 + client/src/app/shared/components/index.ts | 7 ++ .../components/navbar.component.html} | 15 ++- .../shared/components/navbar.component.scss | 8 ++ .../components/navbar.component.ts} | 9 +- .../shared/components/spinner.component.html | 4 + .../shared/components/spinner.component.ts | 8 ++ client/src/app/shared/shared.module.ts | 10 +- client/src/styles.scss | 2 + 43 files changed, 472 insertions(+), 252 deletions(-) delete mode 100644 client/src/app/admin/components/admin-nav.component.scss create mode 100644 client/src/app/admin/components/database/database-table.component.html create mode 100644 client/src/app/admin/components/database/database-table.component.ts delete mode 100644 client/src/app/admin/components/instance-card.component.ts rename client/src/app/admin/components/{ => instance}/instance-card.component.html (57%) rename client/src/app/admin/components/{ => instance}/instance-card.component.scss (100%) create mode 100644 client/src/app/admin/components/instance/instance-card.component.ts create mode 100644 client/src/app/admin/components/shared/delete-btn.component.html create mode 100644 client/src/app/admin/components/shared/delete-btn.component.ts create mode 100644 client/src/app/admin/components/survey/survey-table.component.html create mode 100644 client/src/app/admin/components/survey/survey-table.component.ts create mode 100644 client/src/app/admin/containers/database/database-list.component.html create mode 100644 client/src/app/admin/containers/database/database-list.component.ts rename client/src/app/admin/containers/{ => instance}/instance-list.component.html (77%) rename client/src/app/admin/containers/{ => instance}/instance-list.component.scss (100%) rename client/src/app/admin/containers/{ => instance}/instance-list.component.ts (89%) create mode 100644 client/src/app/admin/containers/survey/survey-list.component.html create mode 100644 client/src/app/admin/containers/survey/survey-list.component.ts create mode 100644 client/src/app/admin/containers/survey/survey.component.html create mode 100644 client/src/app/admin/containers/survey/survey.component.ts rename client/src/app/metamodel/store/selectors/{database.selectors.ts => database.selector.ts} (76%) delete mode 100644 client/src/app/portal/components/portal-nav.component.html delete mode 100644 client/src/app/portal/components/portal-nav.component.scss delete mode 100644 client/src/app/portal/components/portal-nav.component.ts create mode 100644 client/src/app/shared/components/index.ts rename client/src/app/{admin/components/admin-nav.component.html => shared/components/navbar.component.html} (87%) create mode 100644 client/src/app/shared/components/navbar.component.scss rename client/src/app/{admin/components/admin-nav.component.ts => shared/components/navbar.component.ts} (76%) create mode 100644 client/src/app/shared/components/spinner.component.html create mode 100644 client/src/app/shared/components/spinner.component.ts diff --git a/client/src/app/admin/admin-routing.module.ts b/client/src/app/admin/admin-routing.module.ts index 25defcf4..2d9bc73d 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 1cc4ce8a..00000000 --- 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 00000000..51d8a27b --- /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 00000000..d5c21140 --- /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 66ba1f12..db4028f0 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 8de81228..00000000 --- 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 93012d5a..6a97f30e 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 00000000..ffa29bdb --- /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 00000000..c0e8bf40 --- /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 00000000..084d3370 --- /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 00000000..a30dc370 --- /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 00000000..0a297965 --- /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 a7f62f6a..979df50e 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 ec295f32..79ad61f2 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 00000000..3059a3ac --- /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 00000000..d892e1d3 --- /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 7a97b4a0..eaee2001 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 037bdb03..96b49894 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 00000000..55f3cb56 --- /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 00000000..276c6619 --- /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 00000000..009c9509 --- /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 00000000..d5d4d1c9 --- /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 f00de8c0..4352427f 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 b7b2e39c..1651db99 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 09fc82b1..d4854e51 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 023a7914..0f1acfb7 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 1d8adabf..c6b87928 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 91c70bc2..40453624 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 b8775b7a..00000000 --- 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 1cc4ce8a..00000000 --- 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 3feeb0ca..00000000 --- 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 5c3a44e8..16f93da0 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 7cb4d9e6..99cae9ad 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 00000000..29c944c7 --- /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 312e8462..2c829516 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 00000000..89e681e6 --- /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 9198c4c2..b08be00b 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 00000000..96aaf1d4 --- /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 00000000..bbd6f1b9 --- /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 31735204..e68d42e0 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 79ca148d..5e2753dc 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 */ -- GitLab