From e2ad7333daeea9e9e10441d6135521aab9299422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Wed, 23 Jun 2021 17:07:44 +0200 Subject: [PATCH] Add admin module --- client/src/app/admin/admin-routing.module.ts | 25 ++++++ client/src/app/admin/admin.module.ts | 13 ++- .../admin/components/admin-nav.component.html | 87 +++++++++++++++++++ .../admin/components/admin-nav.component.scss | 17 ++++ .../admin/components/admin-nav.component.ts | 21 +++++ client/src/app/admin/components/index.ts | 7 ++ .../components/instance-card.component.html | 37 ++++++++ .../components/instance-card.component.scss | 3 + .../components/instance-card.component.ts | 32 +++++++ .../app/admin/containers/admin.component.html | 12 +++ .../app/admin/containers/admin.component.ts | 50 +++++++++++ .../containers/instance-list.component.html | 28 ++++++ .../containers/instance-list.component.scss | 13 +++ .../containers/instance-list.component.ts | 32 +++++++ client/src/app/app.module.ts | 2 + .../metamodel/store/models/instance.model.ts | 2 + client/src/app/shared/shared.module.ts | 7 +- client/src/styles.scss | 1 + 18 files changed, 383 insertions(+), 6 deletions(-) create mode 100644 client/src/app/admin/admin-routing.module.ts create mode 100644 client/src/app/admin/components/admin-nav.component.html create mode 100644 client/src/app/admin/components/admin-nav.component.scss create mode 100644 client/src/app/admin/components/admin-nav.component.ts create mode 100644 client/src/app/admin/components/index.ts create mode 100644 client/src/app/admin/components/instance-card.component.html create mode 100644 client/src/app/admin/components/instance-card.component.scss create mode 100644 client/src/app/admin/components/instance-card.component.ts create mode 100644 client/src/app/admin/containers/admin.component.html create mode 100644 client/src/app/admin/containers/admin.component.ts create mode 100644 client/src/app/admin/containers/instance-list.component.html create mode 100644 client/src/app/admin/containers/instance-list.component.scss create mode 100644 client/src/app/admin/containers/instance-list.component.ts diff --git a/client/src/app/admin/admin-routing.module.ts b/client/src/app/admin/admin-routing.module.ts new file mode 100644 index 00000000..25defcf4 --- /dev/null +++ b/client/src/app/admin/admin-routing.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { AdminComponent } from './containers/admin.component'; +import { InstanceListComponent } from './containers/instance-list.component'; + +const routes: Routes = [ + { + path: 'admin', component: AdminComponent, children: [ + { path: '', redirectTo: 'instance-list', pathMatch: 'full' }, + { path: 'instance-list', component: InstanceListComponent } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AdminRoutingModule { } + +export const routedComponents = [ + AdminComponent, + InstanceListComponent +]; diff --git a/client/src/app/admin/admin.module.ts b/client/src/app/admin/admin.module.ts index 6ef69a9c..bc12f969 100644 --- a/client/src/app/admin/admin.module.ts +++ b/client/src/app/admin/admin.module.ts @@ -1,12 +1,17 @@ import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; +import { SharedModule } from 'src/app/shared/shared.module'; +import { AdminRoutingModule, routedComponents } from './admin-routing.module'; +import { dummiesComponents } from './components'; @NgModule({ - declarations: [], imports: [ - SharedModule + SharedModule, + AdminRoutingModule ], - providers: [] + declarations: [ + routedComponents, + dummiesComponents + ] }) export class AdminModule { } diff --git a/client/src/app/admin/components/admin-nav.component.html b/client/src/app/admin/components/admin-nav.component.html new file mode 100644 index 00000000..312e8462 --- /dev/null +++ b/client/src/app/admin/components/admin-nav.component.html @@ -0,0 +1,87 @@ +<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="" routerLinkActive="active"> + <span class="fas fa-home"></span> Portal + </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=""> + <span class="fas fa-home fa-fw"></span> Portal + </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/admin/components/admin-nav.component.scss b/client/src/app/admin/components/admin-nav.component.scss new file mode 100644 index 00000000..1cc4ce8a --- /dev/null +++ b/client/src/app/admin/components/admin-nav.component.scss @@ -0,0 +1,17 @@ +/** + * 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/admin-nav.component.ts b/client/src/app/admin/components/admin-nav.component.ts new file mode 100644 index 00000000..9198c4c2 --- /dev/null +++ b/client/src/app/admin/components/admin-nav.component.ts @@ -0,0 +1,21 @@ +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-admin-nav', + templateUrl: 'admin-nav.component.html', + styleUrls: [ 'admin-nav.component.scss' ], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminNavComponent { + @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/admin/components/index.ts b/client/src/app/admin/components/index.ts new file mode 100644 index 00000000..66ba1f12 --- /dev/null +++ b/client/src/app/admin/components/index.ts @@ -0,0 +1,7 @@ +import { AdminNavComponent } from "./admin-nav.component"; +import { InstanceCardComponent } from "./instance-card.component"; + +export const dummiesComponents = [ + AdminNavComponent, + InstanceCardComponent +]; diff --git a/client/src/app/admin/components/instance-card.component.html b/client/src/app/admin/components/instance-card.component.html new file mode 100644 index 00000000..93012d5a --- /dev/null +++ b/client/src/app/admin/components/instance-card.component.html @@ -0,0 +1,37 @@ +<div class="col mb-3 card"> + <div class="card-body"> + <h5 class="card-title font-weight-bold">{{ instance.label }}</h5> + <ul class="card-text list-unstyled pl-4"> + <li>Dataset families: <span class="badge badge-secondary">{{ instance.nb_dataset_families }}</span></li> + <li>Datasets: <span class="badge badge-secondary">{{ instance.nb_datasets }}</span></li> + <li><a target="blank" href="{{instance.client_url}}">{{instance.client_url}}</a></li> + </ul> + </div> + <div class="card-footer bg-transparent text-right"> + <a routerLink="/configure-instance/{{instance.name}}" class="btn btn-outline-primary" title="Configure this instance"> + <span class="fas fa-cog"></span> + </a> + + <a title="Edit this instance" routerLink="/edit-instance/{{instance.name}}" class="btn btn-outline-primary"> + <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> + </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-card.component.scss new file mode 100644 index 00000000..1f2ed672 --- /dev/null +++ b/client/src/app/admin/components/instance-card.component.scss @@ -0,0 +1,3 @@ +.card { + height: 200px; +} \ No newline at end of file diff --git a/client/src/app/admin/components/instance-card.component.ts b/client/src/app/admin/components/instance-card.component.ts new file mode 100644 index 00000000..8de81228 --- /dev/null +++ b/client/src/app/admin/components/instance-card.component.ts @@ -0,0 +1,32 @@ +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/containers/admin.component.html b/client/src/app/admin/containers/admin.component.html new file mode 100644 index 00000000..a7f62f6a --- /dev/null +++ b/client/src/app/admin/containers/admin.component.html @@ -0,0 +1,12 @@ +<header> + <app-admin-nav + [isAuthenticated]="isAuthenticated | async" + [userProfile]="userProfile | async" + (login)="login()" + (logout)="logout()" + (openEditProfile)="openEditProfile()"> + </app-admin-nav> +</header> +<main role="main" class="container-fluid pb-4"> + <router-outlet></router-outlet> +</main> diff --git a/client/src/app/admin/containers/admin.component.ts b/client/src/app/admin/containers/admin.component.ts new file mode 100644 index 00000000..ec295f32 --- /dev/null +++ b/client/src/app/admin/containers/admin.component.ts @@ -0,0 +1,50 @@ +/** + * 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. + */ + +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { UserProfile } from 'src/app/auth/user-profile.model'; +import * as authActions from 'src/app/auth/auth.action'; +import * as authSelector from 'src/app/auth/auth.selector'; + +@Component({ + selector: 'app-admin', + templateUrl: 'admin.component.html' +}) +/** + * @class + * @classdesc Portal home container. + * + * @implements OnInit + */ +export class AdminComponent { + public isAuthenticated: Observable<boolean>; + public userProfile: Observable<UserProfile>; + public userRoles: Observable<string[]>; + + constructor(private store: Store<{ }>) { + this.isAuthenticated = store.select(authSelector.selectIsAuthenticated); + this.userProfile = store.select(authSelector.selectUserProfile); + this.userRoles = store.select(authSelector.selectUserRoles); + } + + login(): void { + this.store.dispatch(authActions.login()); + } + + logout(): void { + this.store.dispatch(authActions.logout()); + } + + openEditProfile(): void { + this.store.dispatch(authActions.openEditProfile()); + } +} diff --git a/client/src/app/admin/containers/instance-list.component.html b/client/src/app/admin/containers/instance-list.component.html new file mode 100644 index 00000000..7a97b4a0 --- /dev/null +++ b/client/src/app/admin/containers/instance-list.component.html @@ -0,0 +1,28 @@ +<div class="container-fluid"> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item active" aria-current="page">Instances</li> + </ol> + </nav> +</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> + + <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"> + </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"> + <div class="card-body text-center"> + <span class="fas fa-plus fa-4x text-light"></span> + </div> + </div> + </div> + </div> +</div> diff --git a/client/src/app/admin/containers/instance-list.component.scss b/client/src/app/admin/containers/instance-list.component.scss new file mode 100644 index 00000000..4382b0ae --- /dev/null +++ b/client/src/app/admin/containers/instance-list.component.scss @@ -0,0 +1,13 @@ +.card { + height: 200px; +} + +.card-add { + background-color: #A8C96E; + transition: font-size 0.3s; +} + +.card-add:hover { + background-color: #9dc25b; + font-size: 20px; +} diff --git a/client/src/app/admin/containers/instance-list.component.ts b/client/src/app/admin/containers/instance-list.component.ts new file mode 100644 index 00000000..037bdb03 --- /dev/null +++ b/client/src/app/admin/containers/instance-list.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { Instance } from 'src/app/metamodel/store/models'; +import * as instanceActions from 'src/app/metamodel/store/actions/instance.actions'; +import * as instanceSelector from 'src/app/metamodel/store/selectors/instance.selector'; + +@Component({ + selector: 'app-instance-list', + templateUrl: 'instance-list.component.html', + styleUrls: [ 'instance-list.component.scss' ] +}) +export class InstanceListComponent implements OnInit { + public instanceListIsLoading: Observable<boolean>; + public instanceListIsLoaded: Observable<boolean>; + public instanceList: Observable<Instance[]>; + + constructor(private store: Store<{ }>) { + this.instanceListIsLoading = store.select(instanceSelector.selectInstanceListIsLoading); + this.instanceListIsLoaded = store.select(instanceSelector.selectInstanceListIsLoaded); + this.instanceList = store.select(instanceSelector.selectAllInstances); + } + + ngOnInit() { + this.store.dispatch(instanceActions.loadInstanceList()); + } + + /* deleteInstance(instance: Instance) { + this.store.dispatch(new instanceActions.DeleteInstanceAction(instance)); + } */ +} diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index e55a0ae8..5c2935b8 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -16,6 +16,7 @@ import { CoreModule } from './core/core.module'; import { AuthModule } from './auth/auth.module'; import { MetamodelModule } from './metamodel/metamodel.module'; import { PortalModule } from './portal/portal.module'; +import { AdminModule } from './admin/admin.module'; import { AppComponent } from './core/containers/app.component'; @NgModule({ @@ -27,6 +28,7 @@ import { AppComponent } from './core/containers/app.component'; AuthModule, MetamodelModule, PortalModule, + AdminModule, AppRoutingModule, StoreModule.forRoot(reducers, { metaReducers, diff --git a/client/src/app/metamodel/store/models/instance.model.ts b/client/src/app/metamodel/store/models/instance.model.ts index 8b0db4c7..da259bcf 100644 --- a/client/src/app/metamodel/store/models/instance.model.ts +++ b/client/src/app/metamodel/store/models/instance.model.ts @@ -17,4 +17,6 @@ export interface Instance { allowed: boolean; }; }; + nb_dataset_families: number; + nb_datasets: number; } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index b65e35b4..31735204 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -3,17 +3,20 @@ import { CommonModule } from '@angular/common'; import { CollapseModule } from 'ngx-bootstrap/collapse'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { ModalModule } from 'ngx-bootstrap/modal'; @NgModule({ imports: [ CommonModule, CollapseModule.forRoot(), - BsDropdownModule.forRoot() + BsDropdownModule.forRoot(), + ModalModule.forRoot() ], exports: [ CommonModule, CollapseModule, - BsDropdownModule + BsDropdownModule, + ModalModule ] }) export class SharedModule { } \ No newline at end of file diff --git a/client/src/styles.scss b/client/src/styles.scss index b446e581..79ca148d 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -12,6 +12,7 @@ @import "~bootstrap/scss/nav"; @import "~bootstrap/scss/navbar"; @import "~bootstrap/scss/card"; +@import "~bootstrap/scss/breadcrumb"; @import "~bootstrap/scss/buttons"; @import "~bootstrap/scss/transitions"; @import "~bootstrap/scss/dropdown"; -- GitLab