"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": [],
## 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
"@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"
--- /dev/null
+{
+ "/api/": {
+ "target": "http://localhost:8080",
+ "secure": false,
+ "logLevel": "debug"
+ }
+}
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 { }
-<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>
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();
}));
-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',
})
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();
+ }
+
}
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: [
],
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 { }
--- /dev/null
+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 { }
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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(['']);
+ });
+ }
+}
--- /dev/null
+<a title="Sign Out"
+ (click)="logout()">
+ <i class="fa fa-sign-out"></i> Logout
+</a>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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']);
+ });
+ }
+}
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)],
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: []
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]
<!-- /.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>
-.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;
- }
-}
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;
beforeEach(async(() => {
TestBed.configureTestingModule({
- declarations: [ NavigationComponent ]
+ imports: [
+ SharedModule,
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [
+ NavigationComponent,
+ LogoutComponent
+ ]
})
.compileComponents();
}));
--- /dev/null
+<!-- This component should be deleted by the developer that implements the first route / page -->
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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() {
+ }
+
+}
--- /dev/null
+export class Credentials {
+ username: string;
+ password: string;
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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']);
+ }
+ }
+ });
+ }
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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();
+ });
+ }
+}
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 { }
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;