]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard_v2: Add frontend login/logout
authorRicardo Marques <rimarques@suse.com>
Thu, 25 Jan 2018 16:10:29 +0000 (16:10 +0000)
committerRicardo Dias <rdias@suse.com>
Mon, 5 Mar 2018 13:07:01 +0000 (13:07 +0000)
Signed-off-by: Ricardo Marques <rimarques@suse.com>
37 files changed:
src/pybind/mgr/.gitignore
src/pybind/mgr/dashboard_v2/frontend/.angular-cli.json
src/pybind/mgr/dashboard_v2/frontend/README.md
src/pybind/mgr/dashboard_v2/frontend/package.json
src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.html
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.spec.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/app.component.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/app.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/core/core-routing.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/core/core.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.scss
src/pybind/mgr/dashboard_v2/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/app/shared/shared.module.ts
src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png [new file with mode: 0644]
src/pybind/mgr/dashboard_v2/frontend/src/openattic-theme.scss

index 485dee64bcfb48793379b200a1afd14e85a8aaf4..59aab7cfe1b28bc3685d25dfaef3b563f51bfac2 100644 (file)
@@ -1 +1,2 @@
 .idea
+proxy.conf.json
index 9d39081a536f6c2d665355c2e1a51ce4fdf554c1..d95ac3d6e5fb5f6f810a7c24495ad1489058bc6d 100644 (file)
@@ -20,6 +20,8 @@
       "prefix": "oa",
       "styles": [
         "../node_modules/bootstrap/dist/css/bootstrap.css",
+        "../node_modules/ng2-toastr/bundles/ng2-toastr.min.css",
+        "../node_modules/font-awesome/css/font-awesome.css",
         "styles.scss"
       ],
       "scripts": [],
index 518ea551bb5f6bd011307a5f3767704956c7ee8e..5e39db51953b29c701e2190a9bcb16a3a4e6908f 100644 (file)
@@ -12,7 +12,11 @@ If you do not have installed [Angular CLI](https://github.com/angular/angular-cl
 
 ## Development server
 
-Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
+Create the `proxy.conf.json` file based on `proxy.conf.json.sample`.
+
+Run `npm start -- --proxy-config proxy.conf.json` for a dev server. 
+Navigate to `http://localhost:4200/`. 
+The app will automatically reload if you change any of the source files.
 
 ## Code scaffolding
 
index e7eb975aaf20ac4fd43a16405a9e61f4242c6dd7..8a6ce84fd359efafd2948afd1252fa9cbcb77f3a 100644 (file)
@@ -23,6 +23,8 @@
     "@angular/router": "^5.0.0",
     "bootstrap": "^3.3.7",
     "core-js": "^2.4.1",
+    "font-awesome": "4.7.0",
+    "ng2-toastr": "4.1.2",
     "ngx-bootstrap": "^2.0.1",
     "rxjs": "^5.5.2",
     "zone.js": "^0.8.14"
diff --git a/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample b/src/pybind/mgr/dashboard_v2/frontend/proxy.conf.json.sample
new file mode 100644 (file)
index 0000000..e654419
--- /dev/null
@@ -0,0 +1,7 @@
+{
+  "/api/": {
+    "target": "http://localhost:8080",
+    "secure": false,
+    "logLevel": "debug"
+  }
+}
index d425c6f56b578db1e8d7f8fa2d6c90693ba5d4fb..6f320da0067b9753bd2c3eafa5caab57fdd6dcdc 100644 (file)
@@ -1,10 +1,15 @@
 import { NgModule } from '@angular/core';
 import { Routes, RouterModule } from '@angular/router';
+import { AuthGuardService } from './shared/services/auth-guard.service';
+import { EmptyComponent } from './shared/empty/empty.component';
 
-const routes: Routes = [];
+const routes: Routes = [
+  // TODO configure an appropriate default route (maybe on ceph module?)
+  { path: '', canActivate: [AuthGuardService], component: EmptyComponent },
+];
 
 @NgModule({
-  imports: [RouterModule.forRoot(routes)],
+  imports: [RouterModule.forRoot(routes, {useHash: true})],
   exports: [RouterModule]
 })
 export class AppRoutingModule { }
index 41d2dd69a621b25841343c568f02c5dbd14df922..22e31e98d535c3270fe19a576edf02803327c19c 100644 (file)
@@ -1,5 +1,5 @@
-<oa-navigation></oa-navigation>
-
-<div class="container-fluid">
+<oa-navigation *ngIf="!isLogginActive()"></oa-navigation>
+<div class="container-fluid"
+     [ngClass]="{'full-height':isLogginActive()}">
   <router-outlet></router-outlet>
 </div>
index f99d866c4982d06429e166201f6aa7432d673c8c..37723daaf0f0f9d2ecee307e3b1234dd51bac08c 100644 (file)
@@ -1,17 +1,21 @@
 import { TestBed, async } from '@angular/core/testing';
 import { RouterTestingModule } from '@angular/router/testing';
 import { AppComponent } from './app.component';
-import { NavigationComponent } from './core/navigation/navigation/navigation.component';
+import { CoreModule } from './core/core.module';
+import { SharedModule } from './shared/shared.module';
+import { ToastModule } from 'ng2-toastr';
 
 describe('AppComponent', () => {
   beforeEach(async(() => {
     TestBed.configureTestingModule({
       imports: [
-        RouterTestingModule
+        RouterTestingModule,
+        CoreModule,
+        SharedModule,
+        ToastModule.forRoot()
       ],
       declarations: [
-        AppComponent,
-        NavigationComponent
+        AppComponent
       ],
     }).compileComponents();
   }));
index 3d2afc2725ea214ec08ac14cdfb0429fcb1ab475..d3c94b399ca73badd8a1877b48491002f3d53717 100644 (file)
@@ -1,4 +1,7 @@
-import { Component } from '@angular/core';
+import { Component, ViewContainerRef } from '@angular/core';
+import { AuthStorageService } from './shared/services/auth-storage.service';
+import { ToastsManager } from 'ng2-toastr';
+import { Router } from '@angular/router';
 
 @Component({
   selector: 'oa-root',
@@ -7,4 +10,16 @@ import { Component } from '@angular/core';
 })
 export class AppComponent {
   title = 'oa';
+
+  constructor(private authStorageService: AuthStorageService,
+              private router: Router,
+              public toastr: ToastsManager,
+              private vcr: ViewContainerRef) {
+    this.toastr.setRootViewContainerRef(vcr);
+  }
+
+  isLogginActive() {
+    return this.router.url === '/login' || !this.authStorageService.isLoggedIn();
+  }
+
 }
index 4375175cc36af25154badb19c4f55dfe345885a8..6bd89ae34b149865ecca187e403740d788564667 100644 (file)
@@ -3,10 +3,22 @@ import { NgModule } from '@angular/core';
 
 import { AppRoutingModule } from './app-routing.module';
 
+import { ToastModule, ToastOptions } from 'ng2-toastr/ng2-toastr';
+
 import { AppComponent } from './app.component';
 import { CoreModule } from './core/core.module';
 import { SharedModule } from './shared/shared.module';
 import { CephModule } from './ceph/ceph.module';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
+import { AuthInterceptorService } from './shared/services/auth-interceptor.service';
+
+export class CustomOption extends ToastOptions {
+  animate = 'flyRight';
+  newestOnTop = true;
+  showCloseButton = true;
+  enableHTML = true;
+}
 
 @NgModule({
   declarations: [
@@ -14,13 +26,26 @@ import { CephModule } from './ceph/ceph.module';
   ],
   imports: [
     BrowserModule,
+    BrowserAnimationsModule,
+    ToastModule.forRoot(),
     AppRoutingModule,
+    HttpClientModule,
     CoreModule,
     SharedModule,
     CephModule
   ],
   exports: [SharedModule],
-  providers: [],
+  providers: [
+    {
+      provide: HTTP_INTERCEPTORS,
+      useClass: AuthInterceptorService,
+      multi: true
+    },
+    {
+      provide: ToastOptions,
+      useClass: CustomOption
+    },
+  ],
   bootstrap: [AppComponent]
 })
 export class AppModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/auth.module.ts
new file mode 100644 (file)
index 0000000..51254ba
--- /dev/null
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { LoginComponent } from './login/login.component';
+import { LogoutComponent } from './logout/logout.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule
+  ],
+  declarations: [LoginComponent, LogoutComponent],
+  exports: [LogoutComponent]
+})
+export class AuthModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.html
new file mode 100644 (file)
index 0000000..09241b8
--- /dev/null
@@ -0,0 +1,52 @@
+<div class="login">
+  <div class="row full-height vertical-align">
+    <div class="col-sm-6 hidden-xs">
+      <img src="assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png"
+           alt="Ceph"
+           class="pull-right">
+    </div>
+    <div class="col-xs-10 col-sm-4 col-lg-3 col-xs-offset-1 col-sm-offset-0 col-md-offset-0 col-lg-offset-0">
+      <h1 translate>Welcome to ceph!</h1>
+      <form name="loginForm"
+            (ngSubmit)="login()"
+            #loginForm="ngForm"
+            novalidate>
+
+        <!-- Username -->
+        <div class="form-group has-feedback"
+             [ngClass]="{'has-error': (loginForm.submitted || username.dirty) && username.invalid}">
+          <input name="username"
+                 [(ngModel)]="model.username"
+                 #username="ngModel"
+                 type="text"
+                 placeholder="Enter your username..."
+                 class="form-control"
+                 required
+                 autofocus>
+          <div class="help-block"
+               *ngIf="(loginForm.submitted || username.dirty) && username.invalid">Username is required</div>
+        </div>
+
+        <!-- Password -->
+        <div class="form-group has-feedback"
+             [ngClass]="{'has-error': (loginForm.submitted || password.dirty) && password.invalid}">
+          <input id="password"
+                 name="password"
+                 [(ngModel)]="model.password"
+                 #password="ngModel"
+                 type="password"
+                 placeholder="Enter your password..."
+                 class="form-control"
+                 required>
+          <div class="help-block"
+               *ngIf="(loginForm.submitted || password.dirty) && password.invalid">Password is required</div>
+        </div>
+
+        <input type="submit"
+               class="btn btn-openattic btn-block"
+               [disabled]="loginForm.invalid"
+               value="Login">
+      </form>
+    </div>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.spec.ts
new file mode 100644 (file)
index 0000000..5ba4db1
--- /dev/null
@@ -0,0 +1,39 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+import { FormsModule } from '@angular/forms';
+import { SharedModule } from '../../../shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastModule } from 'ng2-toastr';
+
+describe('LoginComponent', () => {
+  let component: LoginComponent;
+  let fixture: ComponentFixture<LoginComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        FormsModule,
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule,
+        ToastModule.forRoot()
+      ],
+      declarations: [
+        LoginComponent
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/login/login.component.ts
new file mode 100644 (file)
index 0000000..60f4605
--- /dev/null
@@ -0,0 +1,36 @@
+import { Component, OnInit, ViewContainerRef } from '@angular/core';
+import { Credentials } from '../../../shared/models/credentials.model';
+import { AuthService } from '../../../shared/services/auth.service';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { Router } from '@angular/router';
+import { ToastsManager } from 'ng2-toastr';
+
+@Component({
+  selector: 'oa-login',
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.scss']
+})
+export class LoginComponent implements OnInit {
+
+  model = new Credentials();
+
+  constructor(private authService: AuthService,
+              private authStorageService: AuthStorageService,
+              private router: Router,
+              public toastr: ToastsManager,
+              private vcr: ViewContainerRef) {
+    this.toastr.setRootViewContainerRef(vcr);
+  }
+
+  ngOnInit() {
+    if (this.authStorageService.isLoggedIn()) {
+      this.router.navigate(['']);
+    }
+  }
+
+  login() {
+    this.authService.login(this.model).then(() => {
+      this.router.navigate(['']);
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.html
new file mode 100644 (file)
index 0000000..21b9881
--- /dev/null
@@ -0,0 +1,4 @@
+<a title="Sign Out"
+   (click)="logout()">
+  <i class="fa fa-sign-out"></i> Logout
+</a>
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.spec.ts
new file mode 100644 (file)
index 0000000..960f0ef
--- /dev/null
@@ -0,0 +1,35 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LogoutComponent } from './logout.component';
+import { SharedModule } from '../../../shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+describe('LogoutComponent', () => {
+  let component: LogoutComponent;
+  let fixture: ComponentFixture<LogoutComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [
+        LogoutComponent
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LogoutComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/core/auth/logout/logout.component.ts
new file mode 100644 (file)
index 0000000..9c3d437
--- /dev/null
@@ -0,0 +1,23 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { AuthService } from '../../../shared/services/auth.service';
+
+@Component({
+  selector: 'oa-logout',
+  templateUrl: './logout.component.html',
+  styleUrls: ['./logout.component.scss']
+})
+export class LogoutComponent implements OnInit {
+
+  constructor(private authService: AuthService,
+              private router: Router) { }
+
+  ngOnInit() {
+  }
+
+  logout() {
+    this.authService.logout().then(() => {
+      this.router.navigate(['/login']);
+    });
+  }
+}
index 405e5a0ff7d2004d0b71f22bf631d19c858df8d0..fbd98d27d12c258ec7d8736e903a0cb5bc24c354 100644 (file)
@@ -1,7 +1,10 @@
 import { NgModule } from '@angular/core';
 import { Routes, RouterModule } from '@angular/router';
+import { LoginComponent } from './auth/login/login.component';
 
-const routes: Routes = [];
+const routes: Routes = [
+  { path: 'login', component: LoginComponent }
+];
 
 @NgModule({
   imports: [RouterModule.forChild(routes)],
index a738ccbe780ea8204312a6c18de422d2bf6cf686..4f4a618ef40a2934b190920e9b70766c69669b3e 100644 (file)
@@ -2,12 +2,14 @@ import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { CoreRoutingModule } from './core-routing.module';
 import { NavigationModule } from './navigation/navigation.module';
+import { AuthModule } from './auth/auth.module';
 
 @NgModule({
   imports: [
     CommonModule,
     CoreRoutingModule,
-    NavigationModule
+    NavigationModule,
+    AuthModule
   ],
   exports: [NavigationModule],
   declarations: []
index bbac8779babe30eb1a5597b98f2a88f5bda3d665..42a0d5a6eed6adb9667bf696b2d0d155196d55a8 100644 (file)
@@ -1,10 +1,12 @@
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { NavigationComponent } from './navigation/navigation.component';
+import { AuthModule } from '../auth/auth.module';
 
 @NgModule({
   imports: [
-    CommonModule
+    CommonModule,
+    AuthModule
   ],
   declarations: [NavigationComponent],
   exports: [NavigationComponent]
index a097b052672b4e6843440e9189dbd1bf6578f581..d01067dfd374e2ac96d45ef87a62dd8ba19885d5 100644 (file)
@@ -85,6 +85,9 @@
     <!-- /.navbar-primary -->
 
     <ul class="nav navbar-nav navbar-utility">
+      <li class="tc_logout">
+        <oa-logout class="oa-navbar"></oa-logout>
+      </li>
     </ul>
     <!-- /.navbar-utility -->
   </div>
index 0f5acfd9d32c06acd0c50628b8ab6eabbdeac91f..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -1,143 +0,0 @@
-.navbar-openattic {
-  margin-bottom: 0;
-  background: #474544;
-  border: 0;
-  border-radius: 0;
-  border-top: 4px solid #288cea;
-  font-size: 1.2em;
-}
-.navbar-openattic .navbar-header {
-  display: flex;
-  float: none;
-}
-.navbar-openattic .navbar-toggle {
-  margin-left: auto;
-  border: 0;
-}
-.navbar-openattic .navbar-toggle:focus,
-.navbar-openattic .navbar-toggle:hover {
-  background-color: transparent;
-  outline: 0;
-}
-.navbar-openattic .navbar-toggle .icon-bar {
-  background-color: #ececec;
-}
-.navbar-openattic .navbar-toggle:focus .icon-bar,
-.navbar-openattic .navbar-toggle:hover .icon-bar {
-  -webkit-box-shadow: 0 0 3px #fff;
-  box-shadow: 0 0 3px #fff;
-}
-.navbar-openattic .navbar-collapse {
-  padding: 0;
-}
-.navbar-openattic .navbar-nav > li > a,
-.navbar-openattic .navbar-nav > li > .oa-navbar > a {
-  color: #ececec;
-  line-height: 1;
-  padding: 10px 20px;
-  position: relative;
-  display: block;
-  text-decoration: none;
-}
-.navbar-openattic .navbar-nav > li > a:focus,
-.navbar-openattic .navbar-nav > li > a:hover,
-.navbar-openattic .navbar-nav > li > .oa-navbar > a:focus,
-.navbar-openattic .navbar-nav > li > .oa-navbar > a:hover {
-  color: #ececec;
-}
-.navbar-openattic .navbar-nav > li > a:hover,
-.navbar-openattic .navbar-nav > li > .oa-navbar > a:hover {
-  background-color: #505050;
-}
-.navbar-openattic .navbar-nav > .open > a,
-.navbar-openattic .navbar-nav > .open > a:hover,
-.navbar-openattic .navbar-nav > .open > a:focus,
-.navbar-openattic .navbar-nav > .open > .oa-navbar > a,
-.navbar-openattic .navbar-nav > .open > .oa-navbar > a:hover,
-.navbar-openattic .navbar-nav > .open > .oa-navbar > a:focus {
-  color: #ececec;
-  border-color: transparent;
-  background-color: transparent;
-}
-.navbar-openattic .navbar-primary > li > a {
-  border: 0;
-}
-.navbar-openattic .navbar-primary > .active > a,
-.navbar-openattic .navbar-primary > .active > a:hover,
-.navbar-openattic .navbar-primary > .active > a:focus {
-  color: #ececec;
-  background-color: #288cea;
-  border: 0;
-}
-.navbar-openattic .navbar-utility a,
-.navbar-openattic .navbar-utility .fa {
-  font-size: 1em;
-}
-.navbar-openattic .navbar-utility > .active > a {
-  color: #ececec;
-  background-color: #505050;
-}
-.navbar-openattic .navbar-utility > li > .open > a,
-.navbar-openattic .navbar-utility > li > .open > a:hover,
-.navbar-openattic .navbar-utility > li > .open > a:focus {
-  color: #ececec;
-  border-color: transparent;
-  background-color: transparent;
-}
-@media (min-width: 768px) {
-  .navbar-openattic .navbar-primary > li > a {
-    border-bottom: 4px solid transparent;
-  }
-  .navbar-openattic .navbar-primary > .active > a,
-  .navbar-openattic .navbar-primary > .active > a:hover,
-  .navbar-openattic .navbar-primary > .active > a:focus {
-    background-color: transparent;
-    border-bottom: 4px solid #288cea;
-  }
-  .navbar-openattic .navbar-utility {
-    border-bottom: 0;
-    font-size: 11px;
-    position: absolute;
-    right: 0;
-    top: 0;
-  }
-}
-@media (max-width: 767px) {
-  .navbar-openattic .navbar-nav {
-    margin: 0;
-  }
-  .navbar-openattic .navbar-collapse,
-  .navbar-openattic .navbar-form {
-    border-color: #ececec;
-  }
-  .navbar-openattic .navbar-collapse {
-    padding: 0;
-  }
-  .navbar-nav .open .dropdown-menu {
-    padding-top: 0;
-    padding-bottom: 0;
-    background-color: #505050;
-  }
-  .navbar-nav .open .dropdown-menu .dropdown-header,
-  .navbar-nav .open .dropdown-menu > li > a {
-    padding: 5px 15px 5px 35px;
-  }
-  .navbar-openattic .navbar-nav .open .dropdown-menu > li > a {
-    color: #ececec;
-  }
-  .navbar-openattic .navbar-nav .open .dropdown-menu > .active > a {
-    color: #ececec;
-    background-color: #288cea;
-  }
-  .navbar-openattic .navbar-nav > li > a:hover {
-    background-color: #288cea;
-  }
-  .navbar-openattic .navbar-utility {
-    border-top: 1px solid #ececec;
-  }
-  .navbar-openattic .navbar-primary > .active > a,
-  .navbar-openattic .navbar-primary > .active > a:hover,
-  .navbar-openattic .navbar-primary > .active > a:focus {
-    background-color: #288cea;
-  }
-}
index 385771816024488fd1b5d578085cbf1e35bded61..aca8268568b433f8a2905bdfbc2a4632d7fffcac 100644 (file)
@@ -1,6 +1,10 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { NavigationComponent } from './navigation.component';
+import { LogoutComponent } from '../../auth/logout/logout.component';
+import { RouterTestingModule } from '@angular/router/testing';
+import { SharedModule } from '../../../shared/shared.module';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
 
 describe('NavigationComponent', () => {
   let component: NavigationComponent;
@@ -8,7 +12,15 @@ describe('NavigationComponent', () => {
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      declarations: [ NavigationComponent ]
+      imports: [
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule
+      ],
+      declarations: [
+        NavigationComponent,
+        LogoutComponent
+      ]
     })
     .compileComponents();
   }));
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.html
new file mode 100644 (file)
index 0000000..15798c6
--- /dev/null
@@ -0,0 +1 @@
+<!-- This component should be deleted by the developer that implements the first route / page -->
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.scss b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.spec.ts
new file mode 100644 (file)
index 0000000..2bbad56
--- /dev/null
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { EmptyComponent } from './empty.component';
+
+describe('EmptyComponent', () => {
+  let component: EmptyComponent;
+  let fixture: ComponentFixture<EmptyComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ EmptyComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(EmptyComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/empty/empty.component.ts
new file mode 100644 (file)
index 0000000..3c35d4b
--- /dev/null
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'oa-empty',
+  templateUrl: './empty.component.html',
+  styleUrls: ['./empty.component.scss']
+})
+export class EmptyComponent implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/models/credentials.model.ts
new file mode 100644 (file)
index 0000000..2c2b7d7
--- /dev/null
@@ -0,0 +1,4 @@
+export class Credentials {
+  username: string;
+  password: string;
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-guard.service.ts
new file mode 100644 (file)
index 0000000..3d2cffb
--- /dev/null
@@ -0,0 +1,18 @@
+import { Injectable } from '@angular/core';
+import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthGuardService implements CanActivate {
+
+  constructor(private router: Router, private authStorageService: AuthStorageService) {
+  }
+
+  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    if (this.authStorageService.isLoggedIn()) {
+      return true;
+    }
+    this.router.navigate(['/login']);
+    return false;
+  }
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-interceptor.service.ts
new file mode 100644 (file)
index 0000000..a0640ac
--- /dev/null
@@ -0,0 +1,35 @@
+import { Injectable } from '@angular/core';
+import { AuthStorageService } from './auth-storage.service';
+import {
+  HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest,
+  HttpResponse
+} from '@angular/common/http';
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/operator/do';
+import { ToastsManager } from 'ng2-toastr';
+import { Router } from '@angular/router';
+
+@Injectable()
+export class AuthInterceptorService implements HttpInterceptor {
+
+  constructor(private router: Router,
+              private authStorageService: AuthStorageService,
+              public toastr: ToastsManager) {
+  }
+
+  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+    return next.handle(request).do((event: HttpEvent<any>) => {
+      if (event instanceof HttpResponse) {
+        // do nothing
+      }
+    }, (err: any) => {
+      if (err instanceof HttpErrorResponse) {
+        this.toastr.error(err.error.detail || '', `${err.status} - ${err.statusText}`);
+        if (err.status === 401) {
+          this.authStorageService.remove();
+          this.router.navigate(['/login']);
+        }
+      }
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth-storage.service.ts
new file mode 100644 (file)
index 0000000..cd6dbbe
--- /dev/null
@@ -0,0 +1,21 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class AuthStorageService {
+
+  constructor() {
+  }
+
+  set(username: string) {
+    localStorage.setItem('dashboard_username', username);
+  }
+
+  remove() {
+    localStorage.removeItem('dashboard_username');
+  }
+
+  isLoggedIn() {
+    return localStorage.getItem('dashboard_username') !== null;
+  }
+
+}
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts b/src/pybind/mgr/dashboard_v2/frontend/src/app/shared/services/auth.service.ts
new file mode 100644 (file)
index 0000000..30706ab
--- /dev/null
@@ -0,0 +1,24 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Credentials } from '../models/credentials.model';
+import { AuthStorageService } from './auth-storage.service';
+
+@Injectable()
+export class AuthService {
+
+  constructor(private authStorageService: AuthStorageService,
+              private http: HttpClient) {
+  }
+
+  login(credentials: Credentials) {
+    return this.http.post('/api/auth', credentials).toPromise().then((resp: Credentials) => {
+      this.authStorageService.set(resp.username);
+    });
+  }
+
+  logout() {
+    return this.http.delete('/api/auth').toPromise().then(() => {
+      this.authStorageService.remove();
+    });
+  }
+}
index fffbe5b239e85b2003c5854121015c267bad3983..ef3e58a57dbfe1b8c4d2566538aac9fb71c66d7b 100644 (file)
@@ -1,10 +1,19 @@
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
+import { AuthService } from './services/auth.service';
+import { AuthStorageService } from './services/auth-storage.service';
+import { AuthGuardService } from './services/auth-guard.service';
+import { EmptyComponent } from './empty/empty.component';
 
 @NgModule({
   imports: [
     CommonModule
   ],
-  declarations: []
+  declarations: [EmptyComponent],
+  providers: [
+    AuthService,
+    AuthStorageService,
+    AuthGuardService
+  ]
 })
 export class SharedModule { }
diff --git a/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png
new file mode 100644 (file)
index 0000000..26d602b
Binary files /dev/null and b/src/pybind/mgr/dashboard_v2/frontend/src/assets/Ceph_Logo_Stacked_RGB_White_120411_fa_256x256.png differ
index 6810369409cc629bda17b6424da8330e91c587d9..d34ba15a3816366b1fc3713ad7602402fdc11809 100755 (executable)
@@ -319,6 +319,151 @@ ul.task-queue-pagination {
   padding-top: 10px;
 }
 
+/* Navbar */
+.navbar-openattic {
+  margin-bottom: 0;
+  background: #474544;
+  border: 0;
+  border-radius: 0;
+  border-top: 4px solid #288cea;
+  font-size: 1.2em;
+}
+.navbar-openattic .navbar-header {
+  display: flex;
+  float: none;
+}
+.navbar-openattic .navbar-toggle {
+  margin-left: auto;
+  border: 0;
+}
+.navbar-openattic .navbar-toggle:focus,
+.navbar-openattic .navbar-toggle:hover {
+  background-color: transparent;
+  outline: 0;
+}
+.navbar-openattic .navbar-toggle .icon-bar {
+  background-color: #ececec;
+}
+.navbar-openattic .navbar-toggle:focus .icon-bar,
+.navbar-openattic .navbar-toggle:hover .icon-bar {
+  -webkit-box-shadow: 0 0 3px #fff;
+  box-shadow: 0 0 3px #fff;
+}
+.navbar-openattic .navbar-collapse {
+  padding: 0;
+}
+.navbar-openattic .navbar-nav>li>a,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a {
+  color: #ececec;
+  line-height: 1;
+  padding: 10px 20px;
+  position: relative;
+  display: block;
+  text-decoration: none;
+}
+.navbar-openattic .navbar-nav>li>a:focus,
+.navbar-openattic .navbar-nav>li>a:hover,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:focus,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
+  color: #ececec;
+}
+.navbar-openattic .navbar-nav>li>a:hover,
+.navbar-openattic .navbar-nav>li>.oa-navbar>a:hover {
+  background-color: #505050;
+}
+.navbar-openattic .navbar-nav>.open>a,
+.navbar-openattic .navbar-nav>.open>a:hover,
+.navbar-openattic .navbar-nav>.open>a:focus,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a:hover,
+.navbar-openattic .navbar-nav>.open>.oa-navbar>a:focus {
+  color: #ececec;
+  border-color: transparent;
+  background-color: transparent;
+}
+.navbar-openattic .navbar-primary>li>a {
+  border: 0;
+}
+.navbar-openattic .navbar-primary>.active>a,
+.navbar-openattic .navbar-primary>.active>a:hover,
+.navbar-openattic .navbar-primary>.active>a:focus {
+  color: #ececec;
+  background-color: #288cea;
+  border: 0;
+}
+.navbar-openattic .navbar-utility a,
+.navbar-openattic .navbar-utility .fa{
+  font-size: 1.0em;
+}
+.navbar-openattic .navbar-utility>.active>a {
+  color: #ececec;
+  background-color: #505050;
+}
+.navbar-openattic .navbar-utility>li>.open>a,
+.navbar-openattic .navbar-utility>li>.open>a:hover,
+.navbar-openattic .navbar-utility>li>.open>a:focus {
+  color: #ececec;
+  border-color: transparent;
+  background-color: transparent;
+}
+@media (min-width: 768px) {
+  .navbar-openattic .navbar-primary>li>a {
+    border-bottom: 4px solid transparent;
+  }
+  .navbar-openattic .navbar-primary>.active>a,
+  .navbar-openattic .navbar-primary>.active>a:hover,
+  .navbar-openattic .navbar-primary>.active>a:focus {
+    background-color: transparent;
+    border-bottom: 4px solid #288cea;
+  }
+  .navbar-openattic .navbar-utility {
+    border-bottom: 0;
+    font-size: 11px;
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+}
+@media (max-width: 767px) {
+  .navbar-openattic .navbar-nav {
+    margin: 0;
+  }
+  .navbar-openattic .navbar-collapse,
+  .navbar-openattic .navbar-form {
+    border-color: #ececec;
+  }
+  .navbar-openattic .navbar-collapse {
+    padding: 0;
+  }
+  .navbar-nav .open .dropdown-menu {
+    padding-top: 0;
+    padding-bottom: 0;
+    background-color: #505050;
+  }
+  .navbar-nav .open .dropdown-menu .dropdown-header,
+  .navbar-nav .open .dropdown-menu>li>a {
+    padding: 5px 15px 5px 35px;
+  }
+  .navbar-openattic .navbar-nav .open .dropdown-menu>li>a {
+    color: #ececec;
+  }
+  .navbar-openattic .navbar-nav .open .dropdown-menu>.active>a {
+    color: #ececec;
+    background-color: #288cea;
+  }
+  .navbar-openattic .navbar-nav>li>a:hover {
+    background-color: #288cea;
+  }
+  .navbar-openattic .navbar-utility {
+    border-top: 1px solid #ececec;
+  }
+  .navbar-openattic .navbar-primary>.active>a,
+  .navbar-openattic .navbar-primary>.active>a:hover,
+  .navbar-openattic .navbar-primary>.active>a:focus {
+    background-color: #288cea;
+  }
+}
+
 /* Navs */
 .nav-tabs {
   margin-bottom: 15px;