Per-Arne
5 years ago
163 changed files with 1599 additions and 3476 deletions
@ -0,0 +1,39 @@ |
|||||
|
import json |
||||
|
import multiprocessing |
||||
|
import os |
||||
|
|
||||
|
workers_per_core_str = os.getenv("WORKERS_PER_CORE", "1") |
||||
|
web_concurrency_str = os.getenv("WEB_CONCURRENCY", None) |
||||
|
host = os.getenv("HOST", "unix:/tmp/gunicorn.sock") |
||||
|
port = os.getenv("PORT", "80") |
||||
|
use_loglevel = os.getenv("LOG_LEVEL", "info") |
||||
|
use_bind = host if "unix" in host else f"{host}:{port}" |
||||
|
|
||||
|
cores = multiprocessing.cpu_count() |
||||
|
workers_per_core = float(workers_per_core_str) |
||||
|
default_web_concurrency = workers_per_core * cores |
||||
|
if web_concurrency_str: |
||||
|
web_concurrency = int(web_concurrency_str) |
||||
|
assert web_concurrency > 0 |
||||
|
else: |
||||
|
web_concurrency = max(int(default_web_concurrency), 2) |
||||
|
|
||||
|
# Gunicorn config variables |
||||
|
loglevel = use_loglevel |
||||
|
workers = web_concurrency |
||||
|
bind = use_bind |
||||
|
keepalive = 120 |
||||
|
errorlog = "-" |
||||
|
|
||||
|
# For debugging and testing |
||||
|
log_data = { |
||||
|
"loglevel": loglevel, |
||||
|
"workers": workers, |
||||
|
"bind": bind, |
||||
|
"worker-tmp-dir": "/dev/shm", |
||||
|
# Additional, non-gunicorn variables |
||||
|
"workers_per_core": workers_per_core, |
||||
|
"host": host, |
||||
|
"port": port, |
||||
|
} |
||||
|
print(json.dumps(log_data)) |
@ -0,0 +1,32 @@ |
|||||
|
import os |
||||
|
from os.path import isdir |
||||
|
DEFAULT_MODULE_LOCATIONS = [("app.main", "/app/app/main.py"), ("main", "/app/main.py")] |
||||
|
DEFAULT_GUNICORN_CONF = [(None, "/app/gunicorn_config.py"), (None, "/app/startup/gunicorn_config.py")] |
||||
|
|
||||
|
|
||||
|
def get_location(pot): |
||||
|
for i in pot: |
||||
|
if not isdir(i[1]): |
||||
|
continue |
||||
|
# Last record will be "defauilt" |
||||
|
return i[0] if i[0] else i[1] |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
MODULE_NAME = os.getenv("MODULE_LOCATION", get_location(DEFAULT_MODULE_LOCATIONS)) |
||||
|
VARIABLE_NAME = os.getenv("VARIABLE_NAME", "app") |
||||
|
APP_MODULE = os.getenv("APP_MODULE", f"{MODULE_NAME}:{VARIABLE_NAME}") |
||||
|
GUNICORN_CONF = os.getenv("GUNICORN_CONF", get_location(DEFAULT_GUNICORN_CONF)) |
||||
|
OPTIONS = [ |
||||
|
"-k", |
||||
|
"uvicorn.workers.UvicornWorker", |
||||
|
"-c", |
||||
|
f"{GUNICORN_CONF} {APP_MODULE}" |
||||
|
] |
||||
|
|
||||
|
# Set envs |
||||
|
os.putenv("VARIABLE_NAME", VARIABLE_NAME) |
||||
|
os.putenv("APP_MODULE", APP_MODULE) |
||||
|
os.putenv("GUNICORN_CONF", GUNICORN_CONF) |
||||
|
|
||||
|
os.system(f"gunicorn -k uvicorn.workers.UvicornWorker -c {GUNICORN_CONF} {APP_MODULE}") |
@ -0,0 +1,31 @@ |
|||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
|
||||
|
import { LayoutComponent } from './layout/layout.component'; |
||||
|
import {MatSidenavModule} from "@angular/material/sidenav"; |
||||
|
import {MatToolbarModule} from "@angular/material/toolbar"; |
||||
|
import {MatListModule} from "@angular/material/list"; |
||||
|
import {MatIconModule} from "@angular/material/icon"; |
||||
|
import {MatButtonModule} from "@angular/material/button"; |
||||
|
import {FlexLayoutModule} from "@angular/flex-layout"; |
||||
|
import {RouterModule} from "@angular/router"; |
||||
|
|
||||
|
|
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [LayoutComponent], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
MatSidenavModule, |
||||
|
MatToolbarModule, |
||||
|
MatListModule, |
||||
|
MatIconModule, |
||||
|
MatButtonModule, |
||||
|
FlexLayoutModule, |
||||
|
RouterModule |
||||
|
], |
||||
|
exports: [ |
||||
|
|
||||
|
] |
||||
|
}) |
||||
|
export class LayoutModule { } |
@ -0,0 +1,56 @@ |
|||||
|
<div style="height: 100vh;"> |
||||
|
|
||||
|
|
||||
|
<mat-toolbar color="primary"> |
||||
|
<mat-toolbar-row> |
||||
|
<button mat-icon-button (click)="sidenav.toggle()" fxShow="true" fxHide.gt-sm> |
||||
|
<mat-icon>menu</mat-icon> |
||||
|
</button> |
||||
|
<span>{{config.applicationName}}</span> |
||||
|
<span class="example-spacer"></span> |
||||
|
<div fxShow="true" fxHide.lt-md="true"> |
||||
|
|
||||
|
<!-- The following menu items will be hidden on both SM and XS screen sizes --> |
||||
|
|
||||
|
<a *ngFor="let item of menu" [routerLink]="item.link" mat-button> |
||||
|
<mat-icon>{{item.icon}}</mat-icon> |
||||
|
{{item.text}} |
||||
|
</a> |
||||
|
|
||||
|
</div> |
||||
|
</mat-toolbar-row> |
||||
|
|
||||
|
</mat-toolbar> |
||||
|
|
||||
|
<mat-sidenav-container fxFlexFill> |
||||
|
<mat-sidenav #sidenav> |
||||
|
<mat-nav-list> |
||||
|
<a href="#" mat-list-item> |
||||
|
<mat-icon>notifications</mat-icon> |
||||
|
Notifications |
||||
|
</a> |
||||
|
<a href="#" mat-list-item> |
||||
|
<mat-icon>message</mat-icon> |
||||
|
Messages</a> |
||||
|
<a href="#" mat-list-item> |
||||
|
<mat-icon>account_box</mat-icon> |
||||
|
My Account |
||||
|
</a> |
||||
|
<a href="#" mat-list-item> |
||||
|
<mat-icon>lock</mat-icon> |
||||
|
My Account |
||||
|
</a> |
||||
|
<a (click)="sidenav.toggle()" mat-list-item> |
||||
|
<mat-icon>close</mat-icon> Close |
||||
|
</a> |
||||
|
</mat-nav-list> |
||||
|
</mat-sidenav> |
||||
|
<mat-sidenav-content fxFlexFill> |
||||
|
<router-outlet></router-outlet> |
||||
|
</mat-sidenav-content> |
||||
|
</mat-sidenav-container> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
</div> |
@ -0,0 +1,19 @@ |
|||||
|
.sidenav-container { |
||||
|
height: 100%; |
||||
|
} |
||||
|
|
||||
|
.sidenav { |
||||
|
width: 200px; |
||||
|
} |
||||
|
|
||||
|
.sidenav .mat-toolbar { |
||||
|
background: inherit; |
||||
|
} |
||||
|
|
||||
|
.mat-toolbar.mat-primary { |
||||
|
position: sticky; |
||||
|
top: 0; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
|
@ -0,0 +1,25 @@ |
|||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
||||
|
|
||||
|
import { LayoutComponent } from './layout.component'; |
||||
|
|
||||
|
describe('LayoutComponent', () => { |
||||
|
let component: LayoutComponent; |
||||
|
let fixture: ComponentFixture<LayoutComponent>; |
||||
|
|
||||
|
beforeEach(async(() => { |
||||
|
TestBed.configureTestingModule({ |
||||
|
declarations: [ LayoutComponent ] |
||||
|
}) |
||||
|
.compileComponents(); |
||||
|
})); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
fixture = TestBed.createComponent(LayoutComponent); |
||||
|
component = fixture.componentInstance; |
||||
|
fixture.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
it('should create', () => { |
||||
|
expect(component).toBeTruthy(); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,29 @@ |
|||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import {Observable} from "rxjs"; |
||||
|
import {BreakpointObserver, Breakpoints} from "@angular/cdk/layout"; |
||||
|
import {map, shareReplay} from "rxjs/operators"; |
||||
|
import {ConfigService} from "../../services/config.service"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-layout', |
||||
|
templateUrl: './layout.component.html', |
||||
|
styleUrls: ['./layout.component.scss'] |
||||
|
}) |
||||
|
export class LayoutComponent implements OnInit { |
||||
|
|
||||
|
isHandset$: Observable<boolean> = this.breakpointObserver.observe(Breakpoints.Handset) |
||||
|
.pipe( |
||||
|
map(result => result.matches), |
||||
|
shareReplay() |
||||
|
); |
||||
|
|
||||
|
menu: Array<{link: Array<string>, icon: string, text: string}> = [ |
||||
|
{ link: ["/page/dashboard"], icon: "home", text: "Dashboard"} |
||||
|
]; |
||||
|
|
||||
|
constructor(private breakpointObserver: BreakpointObserver, public config: ConfigService) {} |
||||
|
ngOnInit(): void { |
||||
|
console.log("Layout") |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,5 +0,0 @@ |
|||||
<div class="mdl-layout mdl-js-layout"> |
|
||||
<main class="mdl-layout__content"> |
|
||||
<router-outlet></router-outlet> |
|
||||
</main> |
|
||||
</div> |
|
@ -1,11 +0,0 @@ |
|||||
@import '~theme/helpers'; |
|
||||
|
|
||||
app-blank-layout .mdl-layout .mdl-layout__content { |
|
||||
padding: 16px; |
|
||||
display: flex; |
|
||||
} |
|
||||
|
|
||||
// FIXME: responsibility leak |
|
||||
.not-found .mdl-layout__content { |
|
||||
background-image: url('#{$image-path}/404.svg'); |
|
||||
} |
|
@ -1,18 +0,0 @@ |
|||||
import { Component, HostBinding } from '@angular/core'; |
|
||||
import { Router } from '@angular/router'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'app-blank-layout', |
|
||||
styleUrls: ['./blank-layout.component.scss'], |
|
||||
templateUrl: './blank-layout.component.html', |
|
||||
}) |
|
||||
export class BlankLayoutComponent { |
|
||||
// FIXME: responsibility leak
|
|
||||
@HostBinding('class.not-found') private get notFound() { |
|
||||
return this.router.url === '/pages/404'; |
|
||||
} |
|
||||
|
|
||||
constructor( |
|
||||
private router: Router, |
|
||||
) { } |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export { BlankLayoutComponent } from './blank-layout.component'; |
|
@ -1,89 +0,0 @@ |
|||||
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-drawer mdl-layout--fixed-header has-drawer"> |
|
||||
<div class="mdl-layout__header"> |
|
||||
<base-page-top> |
|
||||
|
|
||||
<span class="mdl-layout__title">{{title}}</span> |
|
||||
<nav class="mdl-navigation"> |
|
||||
<base-menu-item *ngFor="let item of menu" [data]="item"></base-menu-item> |
|
||||
|
|
||||
<div class="mdl-layout-spacer"></div> |
|
||||
</nav> |
|
||||
|
|
||||
<div class="mdl-layout-spacer"></div> |
|
||||
|
|
||||
|
|
||||
<!-- |
|
||||
<div class="mdl-textfield mdl-js-textfield mdl-textfield--expandable search"> |
|
||||
<label class="mdl-button mdl-js-button mdl-button--icon" for="search"> |
|
||||
<i class="material-icons">search</i> |
|
||||
</label> |
|
||||
|
|
||||
<div class="mdl-textfield__expandable-holder"> |
|
||||
<input class="mdl-textfield__input" type="text" id="search"/> |
|
||||
<label class="mdl-textfield__label" for="search">Enter your query...</label> |
|
||||
</div> |
|
||||
</div>--> |
|
||||
|
|
||||
|
|
||||
|
|
||||
<div class="avatar-dropdown" id="icon" *ngIf="auth.user"> |
|
||||
<span>Logged in as {{auth.user?.username}}</span> |
|
||||
<!--<img src="assets/images/Icon_header.png">--> |
|
||||
</div> |
|
||||
<ul |
|
||||
class="mdl-menu mdl-list mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect mdl-shadow--2dp account-dropdown" |
|
||||
for="icon"> |
|
||||
<li class="mdl-list__item mdl-list__item--two-line"> |
|
||||
<span class="mdl-list__item-primary-content"> |
|
||||
<span class="material-icons mdl-list__item-avatar"></span> |
|
||||
<span>{{ auth.user?.username }}</span> |
|
||||
<span class="mdl-list__item-sub-title">{{ auth.user?.email }}</span> |
|
||||
</span> |
|
||||
</li> |
|
||||
<li class="list__item--border-top"></li> |
|
||||
<li class="mdl-menu__item mdl-list__item"> |
|
||||
<span class="mdl-list__item-primary-content" (click)="router.navigate(['/user/edit'])"> |
|
||||
<i class="material-icons mdl-list__item-icon">account_circle</i> |
|
||||
My account |
|
||||
</span> |
|
||||
</li> |
|
||||
<!--<li class="mdl-menu__item mdl-list__item"> |
|
||||
<span class="mdl-list__item-primary-content"> |
|
||||
<i class="material-icons mdl-list__item-icon">check_box</i> |
|
||||
My tasks |
|
||||
</span> |
|
||||
<span class="mdl-list__item-secondary-content"> |
|
||||
<span class="label background-color--primary">3 new</span> |
|
||||
</span> |
|
||||
</li> |
|
||||
<li class="mdl-menu__item mdl-list__item"> |
|
||||
<span class="mdl-list__item-primary-content"> |
|
||||
<i class="material-icons mdl-list__item-icon">perm_contact_calendar</i> |
|
||||
My events |
|
||||
</span> |
|
||||
</li>--> |
|
||||
<!--<li class="list__item--border-top"></li> |
|
||||
<li class="mdl-menu__item mdl-list__item"> |
|
||||
<span class="mdl-list__item-primary-content"> |
|
||||
<i class="material-icons mdl-list__item-icon">settings</i> |
|
||||
Settings |
|
||||
</span> |
|
||||
</li>--> |
|
||||
<li class="mdl-menu__item mdl-list__item"> |
|
||||
<span class="mdl-list__item-primary-content" (click)="logout()"> |
|
||||
<i class="material-icons mdl-list__item-icon text-color--secondary">exit_to_app</i> |
|
||||
Log out |
|
||||
</span> |
|
||||
</li> |
|
||||
</ul> |
|
||||
|
|
||||
</base-page-top> |
|
||||
</div> |
|
||||
|
|
||||
<!--<div class="mdl-layout__drawer"> |
|
||||
<app-sidebar></app-sidebar> |
|
||||
</div>--> |
|
||||
<main class="mdl-layout__content"> |
|
||||
<router-outlet></router-outlet> |
|
||||
</main> |
|
||||
</div> |
|
@ -1,28 +0,0 @@ |
|||||
import { Component, OnInit } from '@angular/core'; |
|
||||
import { Router } from '@angular/router'; |
|
||||
|
|
||||
import { AuthService } from '@services/*'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'app-common-layout', |
|
||||
templateUrl: './common-layout.component.html', |
|
||||
}) |
|
||||
export class CommonLayoutComponent implements OnInit { |
|
||||
public title = 'Wireguard Manager'; |
|
||||
public menu = [ |
|
||||
{ name: 'Dashboard', link: '/app/dashboard', icon: 'dashboard' }, |
|
||||
]; |
|
||||
|
|
||||
|
|
||||
constructor(public auth: AuthService, |
|
||||
public router: Router) {} |
|
||||
|
|
||||
public ngOnInit() { |
|
||||
|
|
||||
} |
|
||||
|
|
||||
public logout() { |
|
||||
this.auth.logout() |
|
||||
.subscribe(res => this.router.navigate(['/user/login'])); |
|
||||
} |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export { CommonLayoutComponent } from './common-layout.component'; |
|
@ -1 +0,0 @@ |
|||||
export { LayoutsModule } from './layouts.module'; |
|
@ -1,32 +0,0 @@ |
|||||
import { CommonModule } from '@angular/common'; |
|
||||
import { NgModule } from '@angular/core'; |
|
||||
import { RouterModule } from '@angular/router'; |
|
||||
|
|
||||
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card'; |
|
||||
import { MessageMenuComponent } from 'app/components/message-menu'; |
|
||||
import { NotificationMenuComponent } from 'app/components/notification-menu'; |
|
||||
import { SidebarComponent } from 'app/components/sidebar'; |
|
||||
import { ThemeModule } from 'theme'; |
|
||||
import { BlankLayoutComponent } from './blank-layout'; |
|
||||
import { CommonLayoutComponent } from './common-layout'; |
|
||||
|
|
||||
@NgModule({ |
|
||||
imports: [ |
|
||||
CommonModule, |
|
||||
ThemeModule, |
|
||||
RouterModule, |
|
||||
], |
|
||||
declarations: [ |
|
||||
CommonLayoutComponent, |
|
||||
BlankLayoutComponent, |
|
||||
BlankLayoutCardComponent, |
|
||||
SidebarComponent, |
|
||||
MessageMenuComponent, |
|
||||
NotificationMenuComponent, |
|
||||
], |
|
||||
exports: [ |
|
||||
CommonLayoutComponent, |
|
||||
BlankLayoutComponent, |
|
||||
], |
|
||||
}) |
|
||||
export class LayoutsModule { } |
|
@ -1,13 +1,12 @@ |
|||||
import { Component, HostBinding } from '@angular/core'; |
import { Component, HostBinding } from '@angular/core'; |
||||
|
|
||||
import { UpgradableComponent } from 'theme/components/upgradable'; |
|
||||
|
|
||||
@Component({ |
@Component({ |
||||
selector: 'app-components', |
selector: 'app-components', |
||||
templateUrl: './components.component.html', |
templateUrl: './components.component.html', |
||||
styleUrls: ['./components.component.scss'], |
styleUrls: ['./components.component.scss'], |
||||
}) |
}) |
||||
export class ComponentsComponent extends UpgradableComponent { |
export class ComponentsComponent { |
||||
@HostBinding('class.mdl-grid') private readonly mdlGrid = true; |
@HostBinding('class.mdl-grid') private readonly mdlGrid = true; |
||||
@HostBinding('class.ui-components') private readonly uiComponents = true; |
@HostBinding('class.ui-components') private readonly uiComponents = true; |
||||
|
|
@ -0,0 +1,104 @@ |
|||||
|
<mat-card class="dashboard-card"> |
||||
|
<mat-card-content class="dashboard-card-content"> |
||||
|
|
||||
|
<form [formGroup]="serverForm" (ngSubmit)="serverForm.valid && add(serverForm.value)" class="add-server-form"> |
||||
|
|
||||
|
<p>Essentials</p> |
||||
|
<table class="add-server-full-width" cellspacing="0"><tr> |
||||
|
<td> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Interface</mat-label> |
||||
|
<input formControlName="interface" matInput placeholder="wg0"> |
||||
|
</mat-form-field> |
||||
|
</td> |
||||
|
<td> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Address Scope</mat-label> |
||||
|
<input formControlName="address" matInput placeholder="10.0.200.1/24"> |
||||
|
</mat-form-field> |
||||
|
</td> |
||||
|
</tr></table> |
||||
|
|
||||
|
<table class="add-server-full-width" cellspacing="0"><tr> |
||||
|
<td> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Endpoint</mat-label> |
||||
|
<input formControlName="endpoint" matInput placeholder="my-address.com"> |
||||
|
</mat-form-field> |
||||
|
</td> |
||||
|
<td> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Port</mat-label> |
||||
|
<input formControlName="listen_port" matInput placeholder="51820"> |
||||
|
</mat-form-field> |
||||
|
</td> |
||||
|
</tr></table> |
||||
|
|
||||
|
<p>Keys</p> |
||||
|
<p> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Private-Key</mat-label> |
||||
|
<input formControlName="private_key" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Public-Key</mat-label> |
||||
|
<input formControlName="public_key" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Shared-Key</mat-label> |
||||
|
<input formControlName="shared_key" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<div class="add-server-button-group"> |
||||
|
<button type="button" [hidden]="!isEdit" (click)="getKeyPair()" mat-raised-button color="primary" disabled> |
||||
|
<i class="material-icons">vpn_key</i> |
||||
|
Generate KeyPair |
||||
|
</button> |
||||
|
<button type="button" [hidden]="!isEdit" (click)="getPSK()" mat-raised-button color="primary"> |
||||
|
<i class="material-icons">share</i> |
||||
|
Generate PSK |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<p>Scripts</p> |
||||
|
<p> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Post-Up</mat-label> |
||||
|
<input formControlName="post_up" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="add-server-full-width"> |
||||
|
<mat-label>Post-Down</mat-label> |
||||
|
<input formControlName="post_down" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
|
||||
|
<div class="add-server-button-group"> |
||||
|
<button mat-raised-button color="primary" [disabled]="!serverForm.valid" type="submit"> |
||||
|
<ng-container *ngIf="!isEdit">Add Server</ng-container> |
||||
|
<ng-container *ngIf="isEdit">Edit Server</ng-container> |
||||
|
</button> |
||||
|
|
||||
|
<button mat-raised-button color="warn" (click)="isEdit = false; serverForm.reset()"> |
||||
|
Reset |
||||
|
</button> |
||||
|
|
||||
|
</div> |
||||
|
</form> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
</mat-card-content> |
||||
|
</mat-card> |
@ -0,0 +1,18 @@ |
|||||
|
.add-server-form { |
||||
|
min-width: 150px; |
||||
|
//max-width: 500px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.add-server-full-width { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
td { |
||||
|
padding-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.add-server-button-group{ |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
@ -0,0 +1,85 @@ |
|||||
|
import {Component, Input, OnInit, ViewEncapsulation} from '@angular/core'; |
||||
|
import {FormControl, FormGroup, Validators} from "@angular/forms"; |
||||
|
import {IPValidator} from "../../../validators/ip-address.validator"; |
||||
|
import {NumberValidator} from "../../../validators/number.validator"; |
||||
|
import {Server} from "../../../interfaces/server"; |
||||
|
import {ServerService} from "../../../services/server.service"; |
||||
|
import {DataService} from "../../../services/data.service"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-add-server', |
||||
|
templateUrl: './add-server.component.html', |
||||
|
encapsulation: ViewEncapsulation.None, |
||||
|
styleUrls: ['./add-server.component.scss', '../dashboard2.component.css'] |
||||
|
}) |
||||
|
export class AddServerComponent implements OnInit { |
||||
|
|
||||
|
@Input() servers: Array<Server>; |
||||
|
|
||||
|
serverForm = new FormGroup({ |
||||
|
address: new FormControl('', [IPValidator.isIPAddress]), |
||||
|
interface: new FormControl('', [Validators.required, Validators.minLength(3)]), |
||||
|
listen_port: new FormControl('', [Validators.required, NumberValidator.stringIsNumber]), |
||||
|
endpoint: new FormControl('', Validators.required), |
||||
|
private_key: new FormControl('', [Validators.minLength(44), Validators.maxLength(44)]), |
||||
|
public_key: new FormControl('', [Validators.minLength(44), Validators.maxLength(44)]), |
||||
|
shared_key: new FormControl('', [Validators.minLength(44), Validators.maxLength(44)]), |
||||
|
post_up: new FormControl(''), |
||||
|
post_down: new FormControl(''), |
||||
|
|
||||
|
// Unused on backend
|
||||
|
is_running: new FormControl(false), |
||||
|
peers: new FormControl([]), |
||||
|
}); |
||||
|
isEdit: boolean = false; |
||||
|
editServer: Server = null; |
||||
|
|
||||
|
constructor(private serverAPI: ServerService, private comm: DataService) { } |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
|
||||
|
this.comm.on("server-edit").subscribe( (data: Server) => { |
||||
|
this.isEdit = true; |
||||
|
this.serverForm.setValue(data); |
||||
|
this.editServer = data; |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
add(form: Server) { |
||||
|
|
||||
|
if(this.isEdit){ |
||||
|
const idx = this.servers.indexOf(this.editServer); |
||||
|
this.serverAPI.editServer(this.editServer, form).subscribe((server: Server) => { |
||||
|
this.servers[idx] = server; |
||||
|
}); |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
this.serverAPI.addServer(form).subscribe((server: Server) => { |
||||
|
this.servers.push(server); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
this.isEdit = false; |
||||
|
this.serverForm.reset(); |
||||
|
this.editServer = null; |
||||
|
} |
||||
|
|
||||
|
getKeyPair() { |
||||
|
this.serverAPI.getKeyPair().subscribe((kp: any) => { |
||||
|
this.serverForm.patchValue({ |
||||
|
private_key: kp.private_key, |
||||
|
public_key: kp.public_key |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
getPSK() { |
||||
|
this.serverAPI.getPSK().subscribe((psk: any) => { |
||||
|
this.serverForm.patchValue({ |
||||
|
shared_key: psk.psk |
||||
|
}) |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
.grid-container { |
||||
|
margin: 20px; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card { |
||||
|
position: absolute; |
||||
|
top: 15px; |
||||
|
left: 15px; |
||||
|
right: 15px; |
||||
|
bottom: 15px; |
||||
|
} |
||||
|
|
||||
|
.more-button { |
||||
|
position: absolute; |
||||
|
top: 5px; |
||||
|
right: 10px; |
||||
|
} |
||||
|
|
@ -0,0 +1,11 @@ |
|||||
|
<div flex fxFill fxLayout="row" fxLayoutAlign="space-between" > |
||||
|
<div fxFlex="65"> |
||||
|
<app-server [(server)]="servers[idx]" [(servers)]="servers" *ngFor="let server of servers; let idx = index"></app-server> |
||||
|
</div> |
||||
|
|
||||
|
<div fxFlex="34"> |
||||
|
<app-add-server [(servers)]="servers"></app-add-server> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
</div> |
@ -0,0 +1,40 @@ |
|||||
|
import { LayoutModule } from '@angular/cdk/layout'; |
||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; |
||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
||||
|
import { MatButtonModule } from '@angular/material/button'; |
||||
|
import { MatCardModule } from '@angular/material/card'; |
||||
|
import { MatGridListModule } from '@angular/material/grid-list'; |
||||
|
import { MatIconModule } from '@angular/material/icon'; |
||||
|
import { MatMenuModule } from '@angular/material/menu'; |
||||
|
|
||||
|
import { Dashboard2Component } from './dashboard2.component'; |
||||
|
|
||||
|
describe('Dashboard2Component', () => { |
||||
|
let component: Dashboard2Component; |
||||
|
let fixture: ComponentFixture<Dashboard2Component>; |
||||
|
|
||||
|
beforeEach(async(() => { |
||||
|
TestBed.configureTestingModule({ |
||||
|
declarations: [Dashboard2Component], |
||||
|
imports: [ |
||||
|
NoopAnimationsModule, |
||||
|
LayoutModule, |
||||
|
MatButtonModule, |
||||
|
MatCardModule, |
||||
|
MatGridListModule, |
||||
|
MatIconModule, |
||||
|
MatMenuModule, |
||||
|
] |
||||
|
}).compileComponents(); |
||||
|
})); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
fixture = TestBed.createComponent(Dashboard2Component); |
||||
|
component = fixture.componentInstance; |
||||
|
fixture.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
it('should compile', () => { |
||||
|
expect(component).toBeTruthy(); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,44 @@ |
|||||
|
import {Component, OnInit} from '@angular/core'; |
||||
|
import { map } from 'rxjs/operators'; |
||||
|
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; |
||||
|
import {Server} from "../../interfaces/server"; |
||||
|
import {ServerService} from "../../services/server.service"; |
||||
|
import {Peer} from "../../interfaces/peer"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'dashboard2', |
||||
|
templateUrl: './dashboard2.component.html', |
||||
|
styleUrls: ['./dashboard2.component.css'] |
||||
|
}) |
||||
|
export class Dashboard2Component implements OnInit |
||||
|
{ |
||||
|
servers: Array<Server> = []; |
||||
|
|
||||
|
constructor(private breakpointObserver: BreakpointObserver, private serverAPI: ServerService) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.serverAPI.getServers() |
||||
|
.subscribe( (servers: Array<Server>) => { |
||||
|
this.servers.push(...servers); |
||||
|
servers.forEach((server) => { |
||||
|
|
||||
|
this.serverAPI.serverStats(server).subscribe((stats: Peer[]) => { |
||||
|
stats.forEach( item => { |
||||
|
const peer = server.peers.find(x => x.public_key == item.public_key); |
||||
|
peer._stats = item |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,49 @@ |
|||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import {Dashboard2Component} from "./dashboard2.component"; |
||||
|
import {MatGridListModule} from "@angular/material/grid-list"; |
||||
|
import {MatCardModule} from "@angular/material/card"; |
||||
|
import {MatMenuModule} from "@angular/material/menu"; |
||||
|
import {MatIconModule} from "@angular/material/icon"; |
||||
|
import {MatButtonModule} from "@angular/material/button"; |
||||
|
import {ServerComponent} from "./server/server.component"; |
||||
|
import {MatExpansionModule} from "@angular/material/expansion"; |
||||
|
import {AddServerComponent} from "./add-server/add-server.component"; |
||||
|
import {MatFormFieldModule} from "@angular/material/form-field"; |
||||
|
import {MatInputModule} from "@angular/material/input"; |
||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; |
||||
|
import {ComponentsModule} from "../components"; |
||||
|
import {FlexModule} from "@angular/flex-layout"; |
||||
|
import {MatTableModule} from "@angular/material/table"; |
||||
|
import {PeerComponent} from "./peer/peer.component"; |
||||
|
import {QRCodeModule} from "angularx-qrcode"; |
||||
|
|
||||
|
|
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [ |
||||
|
Dashboard2Component, |
||||
|
ServerComponent, |
||||
|
AddServerComponent, |
||||
|
PeerComponent |
||||
|
], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
MatGridListModule, |
||||
|
MatCardModule, |
||||
|
MatMenuModule, |
||||
|
MatIconModule, |
||||
|
MatButtonModule, |
||||
|
MatExpansionModule, |
||||
|
MatFormFieldModule, |
||||
|
MatInputModule, |
||||
|
ReactiveFormsModule, |
||||
|
ComponentsModule, |
||||
|
FlexModule, |
||||
|
MatTableModule, |
||||
|
FormsModule, |
||||
|
QRCodeModule, |
||||
|
|
||||
|
] |
||||
|
}) |
||||
|
export class Dashboard2Module { } |
@ -0,0 +1,76 @@ |
|||||
|
<div flex fxLayout="row" fxLayoutAlign="space-between"> |
||||
|
<div fxFlex="50"> |
||||
|
|
||||
|
|
||||
|
<form #peerForm="ngForm" class="peer-edit-form" (ngSubmit)="peerForm.valid && edit()" > |
||||
|
|
||||
|
<b>Essentials</b> |
||||
|
<table class="full-width" cellspacing="0"><tr> |
||||
|
<td> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Name</mat-label> |
||||
|
<input [disabled]="!peer._edit" name="name" matInput [(ngModel)]="peer.name"> |
||||
|
</mat-form-field> |
||||
|
</td> |
||||
|
<td> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Address</mat-label> |
||||
|
<input [disabled]="!peer._edit" name="address" matInput [(ngModel)]="peer.address"> |
||||
|
</mat-form-field> |
||||
|
</td> |
||||
|
</tr></table> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>DNS</mat-label> |
||||
|
<input [disabled]="!peer._edit" name="dns" [(ngModel)]="peer.dns" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Allowed IPs</mat-label> |
||||
|
<input [disabled]="!peer._edit" name="allowed_ips" [(ngModel)]="peer.allowed_ips" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
|
||||
|
<p>Keys</p> |
||||
|
<p> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Private-Key</mat-label> |
||||
|
<input [disabled]="!peer._edit" name="private_key" [(ngModel)]="peer.private_key" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Public-Key</mat-label> |
||||
|
<input [disabled]="!peer._edit" name="public_key" [(ngModel)]="peer.public_key" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<button |
||||
|
[hidden]="!peer._edit" |
||||
|
[disabled]="!peerForm.valid" |
||||
|
type="submit" |
||||
|
mat-raised-button color="primary"> |
||||
|
Submit changes |
||||
|
</button> |
||||
|
|
||||
|
</form> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<div fxFlex="33"> |
||||
|
<textarea readonly class="mdl-textfield--full-width" style="min-height: 250px; height: 100%; background-color: #202020; color: #00bcd4;">{{config}}</textarea> |
||||
|
|
||||
|
</div> |
||||
|
<div fxFlex="33"> |
||||
|
<qrcode [qrdata]="config" [width]="256" [errorCorrectionLevel]="'M'"></qrcode> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
@ -0,0 +1,17 @@ |
|||||
|
.peer-edit-form { |
||||
|
min-width: 150px; |
||||
|
//max-width: 500px; |
||||
|
width: 100%; |
||||
|
text-align: left; |
||||
|
} |
||||
|
|
||||
|
.full-width { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
td { |
||||
|
padding-left: 0 !important; |
||||
|
padding-right: 8px; |
||||
|
text-align: left !important; |
||||
|
} |
@ -0,0 +1,84 @@ |
|||||
|
import {Component, EventEmitter, Input, OnInit, ViewEncapsulation} from '@angular/core'; |
||||
|
import {ServerService} from "../../../services/server.service"; |
||||
|
import {Peer} from "../../../interfaces/peer"; |
||||
|
import {Server} from "../../../interfaces/server"; |
||||
|
import {FormControl, FormGroup} from "@angular/forms"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-peer', |
||||
|
templateUrl: './peer.component.html', |
||||
|
encapsulation: ViewEncapsulation.None, |
||||
|
styleUrls: ['./peer.component.scss'], |
||||
|
}) |
||||
|
export class PeerComponent implements OnInit { |
||||
|
|
||||
|
@Input("peer") peer: Peer; |
||||
|
@Input("server") server: Server; |
||||
|
@Input("selectedPeer") selectedPeer: Peer; |
||||
|
@Input("onEvent") editPeerEmitter: EventEmitter<any> = new EventEmitter<any>(); |
||||
|
|
||||
|
|
||||
|
|
||||
|
config: string = "Loading..."; |
||||
|
|
||||
|
|
||||
|
constructor(public serverAPI: ServerService) { } |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
|
||||
|
this.editPeerEmitter.subscribe( (msg) => { |
||||
|
if(msg.peer !== this.peer){ |
||||
|
return; |
||||
|
} |
||||
|
if(msg.type === "edit"){ |
||||
|
this.edit(); |
||||
|
|
||||
|
}else if(msg.type == "delete"){ |
||||
|
this.delete(); |
||||
|
}else if(msg.type == "open"){ |
||||
|
this.fetchConfig(); |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
edit(){ |
||||
|
if(this.peer._edit) { |
||||
|
|
||||
|
// Submit the edit (True -> False)
|
||||
|
const idx = this.server.peers.indexOf(this.peer); |
||||
|
this.serverAPI.editPeer(this.peer).subscribe((newPeer) => { |
||||
|
Object.keys(newPeer).forEach(k => { |
||||
|
this.server.peers[idx][k] = newPeer[k]; |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
} else if(!this.peer._edit) { |
||||
|
this.peer._expand = true; |
||||
|
|
||||
|
// Open for edit. aka do nothing (False -> True
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
this.peer._edit = !this.peer._edit; |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
delete(){ |
||||
|
const idx = this.server.peers.indexOf(this.peer); |
||||
|
this.serverAPI.deletePeer(this.peer).subscribe((apiServer) => { |
||||
|
this.server.peers.splice(idx, 1); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
fetchConfig() { |
||||
|
this.serverAPI.peerConfig(this.peer).subscribe((config: any) => { |
||||
|
this.config = config.config |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
pInt(string: string) { |
||||
|
return parseInt(string) |
||||
|
} |
||||
|
} |
@ -0,0 +1,248 @@ |
|||||
|
|
||||
|
<mat-card class="dashboard-card"> |
||||
|
<mat-card-header class="server-card-header"> |
||||
|
<mat-card-title> |
||||
|
<span>{{server.interface}}</span> |
||||
|
|
||||
|
<!-- This fills the remaining space of the current row --> |
||||
|
<span class="fill-remaining-space"></span> |
||||
|
|
||||
|
<i class="material-icons" [ngClass]="{'text-success': server.is_running, 'text-danger': !server.is_running}">check_circle</i> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[qrCode]="true" |
||||
|
[noConfirm]="false" |
||||
|
area="true" |
||||
|
icon="settings" |
||||
|
title="Configuration" |
||||
|
[text]="serverConfig" |
||||
|
hover="Show config for {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<span> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="addPeer()" |
||||
|
icon="person_add" |
||||
|
hover="Add peer to {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
*ngIf="!server.is_running" |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="start()" |
||||
|
icon="play_arrow" |
||||
|
hover="Start {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
*ngIf="server.is_running" |
||||
|
[noConfirm]="false" |
||||
|
(onConfirm)="stop()" |
||||
|
title="Stop server {{server.interface}}?" |
||||
|
text="Are you sure you want to stop this server? This may cause you or your clients to lose connection to the server." |
||||
|
icon="stop" |
||||
|
hover="Stop {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="false" |
||||
|
(onConfirm)="restart()" |
||||
|
title="Restart server {{server.interface}}?" |
||||
|
text="Are you sure you want to restart this server? This may cause you or your clients to lose connection to the server." |
||||
|
icon="autorenew" |
||||
|
hover="Restart {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="edit()" |
||||
|
icon="edit" |
||||
|
hover="Edit {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
(onConfirm)="delete()" |
||||
|
title="Delete {{server.interface}}" |
||||
|
text="Are you sure you want to delete {{server.interface}}" |
||||
|
icon="delete" |
||||
|
hover="Delete {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
</span> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</mat-card-title> |
||||
|
<mat-card-subtitle>{{server.address}} @ {{server.endpoint}}</mat-card-subtitle> |
||||
|
</mat-card-header> |
||||
|
|
||||
|
<mat-card-content class="dashboard-card-content"> |
||||
|
|
||||
|
|
||||
|
<table class="table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>Name</th> |
||||
|
<th>Address</th> |
||||
|
<th>Public-Key</th> |
||||
|
<th>Total tx/rx</th> |
||||
|
<th>Handshake</th> |
||||
|
<th>Manage</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
|
||||
|
|
||||
|
<tbody> |
||||
|
<ng-container *ngFor="let peer of server.peers; let idx = index;" (click)="selectedPeer = (selectedPeer != peer)? peer : null"> |
||||
|
|
||||
|
<tr (click)="openPeer(peer)"> |
||||
|
<td>{{peer.name}}</td> |
||||
|
<td>{{peer.address}}</td> |
||||
|
<td>{{peer.public_key}}</td> |
||||
|
<td>{{peer._stats?.tx || '0'}}/{{peer._stats?.rx || '0'}}</td> |
||||
|
<td>{{peer._stats?.handshake || 'N/A'}}</td> |
||||
|
<td> |
||||
|
|
||||
|
<!-- Edit buttons --> |
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="this.editPeerEmitter.emit({type: 'edit', peer: peer})" |
||||
|
icon="edit" |
||||
|
hover="Edit {{peer.name}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="false" |
||||
|
(onConfirm)="this.editPeerEmitter.emit({type: 'delete', peer: peer});" |
||||
|
text="Are you sure you want to delete {{peer.name}} ({{peer.public_key}})?" |
||||
|
title="Delete {{peer.name}}" |
||||
|
icon="delete" |
||||
|
hover="Delete {{peer.name}} ({{peer.public_key}})"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</td> |
||||
|
</tr> |
||||
|
<tr [hidden]="peer !== selectedPeer"> |
||||
|
<td colspan="6"> |
||||
|
<app-peer [onEvent]="this.editPeerEmitter" [(peer)]="server.peers[idx]" [(server)]="server"></app-peer> |
||||
|
</td> |
||||
|
</tr> |
||||
|
|
||||
|
|
||||
|
</ng-container> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
</mat-card-content> |
||||
|
<mat-card-actions> |
||||
|
</mat-card-actions> |
||||
|
</mat-card> |
||||
|
|
||||
|
|
||||
|
|
||||
|
<!-- |
||||
|
<mat-card class="dashboard-card"> |
||||
|
<mat-card-content class="dashboard-card-content"> |
||||
|
*Server* |
||||
|
<ng-container > |
||||
|
|
||||
|
|
||||
|
</ng-container> |
||||
|
</mat-card-content> |
||||
|
</mat-card> |
||||
|
|
||||
|
|
||||
|
--> |
||||
|
|
||||
|
<!-- |
||||
|
<div class=" mdl-card mdl-shadow--2dp"> |
||||
|
<div class="mdl-card__title mdl-card--border"> |
||||
|
<h2 class="mdl-card__title-text">{{server.interface}}</h2> |
||||
|
<span style="width:20px;"></span> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<div class="mdl-card__actions"> |
||||
|
|
||||
|
<div class="mdl-grid peer-item-header"> |
||||
|
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Name</div> |
||||
|
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Address</div> |
||||
|
<div class="mdl-cell--3-col mdl-cell--12-col-phone">Public-Key</div> |
||||
|
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Total tx/rx</div> |
||||
|
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Handshake</div> |
||||
|
<div class="mdl-cell--2-col mdl-cell--12-col-phone">Manage</div> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<div style="cursor: pointer;" *ngFor="let peer of server.peers; let idx = index;" > |
||||
|
<app-peer [(peer)]="server.peers[idx]" [(server)]="server"></app-peer> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<div class="mdl-card__supporting-text"> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
<div class="mdl-card__menu"> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="addPeer()" |
||||
|
icon="person_add" |
||||
|
hover="Add peer to {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
*ngIf="!server.is_running" |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="start()" |
||||
|
icon="play_arrow" |
||||
|
hover="Start {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
*ngIf="server.is_running" |
||||
|
[noConfirm]="false" |
||||
|
(onConfirm)="stop()" |
||||
|
title="Stop server {{server.interface}}?" |
||||
|
text="Are you sure you want to stop this server? This may cause you or your clients to lose connection to the server." |
||||
|
icon="stop" |
||||
|
hover="Stop {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="false" |
||||
|
(onConfirm)="restart()" |
||||
|
title="Restart server {{server.interface}}?" |
||||
|
text="Are you sure you want to restart this server? This may cause you or your clients to lose connection to the server." |
||||
|
icon="autorenew" |
||||
|
hover="Restart {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
[noConfirm]="true" |
||||
|
(onConfirm)="edit()" |
||||
|
icon="edit" |
||||
|
hover="Edit {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
|
||||
|
<app-modal-confirm |
||||
|
(onConfirm)="delete()" |
||||
|
title="Delete {{server.interface}}" |
||||
|
text="Are you sure you want to delete {{server.interface}}" |
||||
|
icon="delete" |
||||
|
hover="Delete {{server.interface}}"> |
||||
|
</app-modal-confirm> |
||||
|
</div> |
||||
|
</div>--> |
@ -0,0 +1,28 @@ |
|||||
|
|
||||
|
table { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
tr.example-detail-row { |
||||
|
height: 0 !important; |
||||
|
} |
||||
|
|
||||
|
tr.example-element-row:not(.example-expanded-row):hover { |
||||
|
background: whitesmoke; |
||||
|
} |
||||
|
|
||||
|
tr.example-element-row:not(.example-expanded-row):active { |
||||
|
background: #efefef; |
||||
|
} |
||||
|
|
||||
|
.example-element-row td { |
||||
|
border-bottom-width: 0; |
||||
|
} |
||||
|
|
||||
|
.example-element-detail { |
||||
|
overflow: hidden; |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
@ -0,0 +1,77 @@ |
|||||
|
import {Component, EventEmitter, Input, OnInit, ViewEncapsulation} from '@angular/core'; |
||||
|
import {Server} from "../../../interfaces/server"; |
||||
|
import {ServerService} from "../../../services/server.service"; |
||||
|
import {DataService} from "../../../services/data.service"; |
||||
|
import {Peer} from "../../../interfaces/peer"; |
||||
|
|
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-server', |
||||
|
templateUrl: './server.component.html', |
||||
|
encapsulation: ViewEncapsulation.None, |
||||
|
styleUrls: ['./server.component.scss', '../dashboard2.component.css'], |
||||
|
}) |
||||
|
export class ServerComponent implements OnInit { |
||||
|
@Input() server: Server; |
||||
|
@Input() servers: Array<Server>; |
||||
|
public editPeerEmitter: EventEmitter<any> = new EventEmitter<any>(); |
||||
|
|
||||
|
serverConfig: string; |
||||
|
|
||||
|
selectedPeer: Peer | null; |
||||
|
|
||||
|
constructor(private serverAPI: ServerService, private comm: DataService) { } |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
console.log("Server"); |
||||
|
|
||||
|
this.serverAPI.serverConfig(this.server).subscribe((x: any) => this.serverConfig = x.config) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
edit(){ |
||||
|
|
||||
|
this.comm.emit('server-edit', this.server); |
||||
|
} |
||||
|
|
||||
|
stop() { |
||||
|
this.serverAPI.stopServer(this.server).subscribe((apiServer) => { |
||||
|
this.server.is_running = apiServer.is_running |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
start() { |
||||
|
this.serverAPI.startServer(this.server).subscribe((apiServer) => { |
||||
|
this.server.is_running = apiServer.is_running |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
addPeer() { |
||||
|
this.serverAPI.addPeer(this.server).subscribe((peer) => { |
||||
|
this.server.peers.push(peer) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
restart() { |
||||
|
this.serverAPI.restartServer(this.server).subscribe((apiServer) => { |
||||
|
this.server.is_running = apiServer.is_running |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
delete() { |
||||
|
const index = this.servers.indexOf(this.server); |
||||
|
this.serverAPI.deleteServer(this.server).subscribe((apiServer) => { |
||||
|
this.servers.splice(index, 1); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
openPeer(peer: Peer) { |
||||
|
if(this.selectedPeer == peer){ |
||||
|
this.selectedPeer = null; |
||||
|
return |
||||
|
} |
||||
|
this.selectedPeer = peer; |
||||
|
this.editPeerEmitter.emit({type: 'open', peer: peer}); |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
*{-webkit-box-sizing:border-box;box-sizing:border-box}body{padding:0;margin:0}#notfound{position:relative;height:100vh}#notfound .notfound{position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.notfound{max-width:520px;width:100%;line-height:1.4;text-align:center}.notfound .notfound-404{position:relative;height:240px}.notfound .notfound-404 h1{font-family:montserrat,sans-serif;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);font-size:252px;font-weight:900;margin:0;color:#262626;text-transform:uppercase;letter-spacing:-40px;margin-left:-20px}.notfound .notfound-404 h1>span{text-shadow:-8px 0 0 #fff}.notfound .notfound-404 h3{font-family:cabin,sans-serif;position:relative;font-size:16px;font-weight:700;text-transform:uppercase;color:#262626;margin:0;letter-spacing:3px;padding-left:6px}.notfound h2{font-family:cabin,sans-serif;font-size:20px;font-weight:400;text-transform:uppercase;color:#000;margin-top:0;margin-bottom:25px}@media only screen and (max-width:767px){.notfound .notfound-404{height:200px}.notfound .notfound-404 h1{font-size:200px}}@media only screen and (max-width:480px){.notfound .notfound-404{height:162px}.notfound .notfound-404 h1{font-size:162px;height:150px;line-height:162px}.notfound h2{font-size:16px}} |
@ -0,0 +1,9 @@ |
|||||
|
<div id="notfound"> |
||||
|
<div class="notfound"> |
||||
|
<div class="notfound-404"> |
||||
|
<h3>Oops! Page not found</h3> |
||||
|
<h1><span>4</span><span>0</span><span>4</span></h1> |
||||
|
</div> |
||||
|
<h2>we are sorry, but the page you requested was not found</h2> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,8 @@ |
|||||
|
import { Component, HostBinding } from '@angular/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-error', |
||||
|
styleUrls: ['error.component.css'], |
||||
|
templateUrl: './error.component.html', |
||||
|
}) |
||||
|
export class ErrorComponent { } |
@ -0,0 +1,32 @@ |
|||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { Routes, RouterModule } from '@angular/router'; |
||||
|
import {Dashboard2Component} from "./dashboard2/dashboard2.component"; |
||||
|
import {LayoutComponent} from "../layout/layout/layout.component"; |
||||
|
import {ErrorComponent} from "./error"; |
||||
|
import {LoginComponent} from "./user/login/login.component"; |
||||
|
import {AuthGuard} from "@services/*"; |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ path: '', component: LayoutComponent, children: |
||||
|
[ |
||||
|
//{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
||||
|
{ path: 'dashboard', component: Dashboard2Component, pathMatch: 'full', canActivate: [AuthGuard]}, |
||||
|
{ path: '404', component: ErrorComponent, pathMatch: 'full' }, |
||||
|
] |
||||
|
}, |
||||
|
{ path: 'user', component: LayoutComponent, children: |
||||
|
[ |
||||
|
//{ path: 'dashboard', component: DashboardComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
||||
|
{ path: 'login', component: LoginComponent, pathMatch: 'full'}, |
||||
|
] |
||||
|
}, |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forChild(routes)], |
||||
|
exports: [RouterModule] |
||||
|
}) |
||||
|
export class PageRoutingModule { } |
@ -0,0 +1,24 @@ |
|||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import {PageRoutingModule} from "./page-routing.module"; |
||||
|
import {Dashboard2Module} from "./dashboard2/dashboard2.module"; |
||||
|
import {LoginComponent} from "./user/login/login.component"; |
||||
|
import {MatCardModule} from "@angular/material/card"; |
||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; |
||||
|
import {MatInputModule} from "@angular/material/input"; |
||||
|
|
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [LoginComponent], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
PageRoutingModule, |
||||
|
FormsModule, |
||||
|
Dashboard2Module, |
||||
|
MatCardModule, |
||||
|
ReactiveFormsModule, |
||||
|
MatInputModule, |
||||
|
], |
||||
|
|
||||
|
}) |
||||
|
export class PageModule { } |
@ -0,0 +1,48 @@ |
|||||
|
<div class="container"> |
||||
|
|
||||
|
<base-card> |
||||
|
<base-card-title> |
||||
|
<h2 class="mdl-card__title-text">Edit User</h2> |
||||
|
</base-card-title> |
||||
|
<base-card-body> |
||||
|
|
||||
|
<form [formGroup]="editForm" (ngSubmit)="editForm.valid && edit()" class="form"> |
||||
|
|
||||
|
<div class="mdl-grid"> |
||||
|
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> |
||||
|
<input formControlName="full_name" class="mdl-textfield__input" type="text" id="full_name" value=""/> |
||||
|
<label class="mdl-textfield__label" for="full_name">Full Name</label> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> |
||||
|
<input formControlName="username" class="mdl-textfield__input" type="text" id="username" /> |
||||
|
<label class="mdl-textfield__label" for="username">Username</label> |
||||
|
</div> |
||||
|
|
||||
|
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> |
||||
|
<input formControlName="password" class="mdl-textfield__input" type="text" id="password"/> |
||||
|
<label class="mdl-textfield__label" for="password">Password</label> |
||||
|
</div> |
||||
|
|
||||
|
<div class="mdl-cell mdl-cell--6-col mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> |
||||
|
<input formControlName="email" class="mdl-textfield__input" type="text" id="email"/> |
||||
|
<label class="mdl-textfield__label" for="email">Email</label> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<button [disabled]="!editForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue"> |
||||
|
Edit User |
||||
|
</button> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</form> |
||||
|
|
||||
|
|
||||
|
</base-card-body> |
||||
|
</base-card> |
||||
|
|
||||
|
</div> |
@ -0,0 +1,51 @@ |
|||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import {FormControl, FormGroup, Validators} from "@angular/forms"; |
||||
|
import {AuthService} from "@services/*"; |
||||
|
import {Router} from "@angular/router"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-edit', |
||||
|
templateUrl: './edit.component.html', |
||||
|
styleUrls: ['./edit.component.scss'] |
||||
|
}) |
||||
|
export class EditComponent implements OnInit { |
||||
|
|
||||
|
public editForm: FormGroup = new FormGroup({ |
||||
|
full_name: new FormControl(''), |
||||
|
password: new FormControl('', Validators.required), |
||||
|
email: new FormControl('', [ |
||||
|
Validators.required, |
||||
|
Validators.pattern('^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$'), |
||||
|
Validators.maxLength(20), |
||||
|
]), |
||||
|
username: new FormControl('', [Validators.required, Validators.maxLength(20)]), |
||||
|
}); |
||||
|
public user: any; |
||||
|
public error: string; |
||||
|
|
||||
|
constructor(private authService: AuthService, |
||||
|
private router: Router) { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.user = this.authService.user; |
||||
|
|
||||
|
|
||||
|
this.editForm.setValue({ |
||||
|
full_name: this.user.full_name, |
||||
|
password: "", |
||||
|
email: this.user.email, |
||||
|
username: this.user.username |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
public edit() { |
||||
|
if (this.editForm.valid) { |
||||
|
this.authService.edit(this.editForm.getRawValue()) |
||||
|
.subscribe(res => this.router.navigate(['/app/dashboard']), |
||||
|
error => this.error = error.message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,40 @@ |
|||||
|
|
||||
|
|
||||
|
<mat-card> |
||||
|
<mat-card-title> |
||||
|
Authenticate to Wireguard Management |
||||
|
</mat-card-title> |
||||
|
|
||||
|
<mat-card-content> |
||||
|
<form [formGroup]="loginForm" (ngSubmit)="loginForm.valid && login()" class="form"> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Username</mat-label> |
||||
|
<input type="text" id="username" formControlName="username" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
<p> |
||||
|
<mat-form-field class="full-width"> |
||||
|
<mat-label>Password</mat-label> |
||||
|
<input type="text" id="password" formControlName="password" matInput> |
||||
|
</mat-form-field> |
||||
|
</p> |
||||
|
|
||||
|
|
||||
|
<button [disabled]="!loginForm.valid" type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect button--colored-light-blue"> |
||||
|
SIGN IN |
||||
|
</button> |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
</form> |
||||
|
</mat-card-content> |
||||
|
|
||||
|
|
||||
|
</mat-card> |
||||
|
|
||||
|
|
@ -0,0 +1,25 @@ |
|||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; |
||||
|
|
||||
|
import { LoginComponent } from './login.component'; |
||||
|
|
||||
|
describe('LoginComponent', () => { |
||||
|
let component: LoginComponent; |
||||
|
let fixture: ComponentFixture<LoginComponent>; |
||||
|
|
||||
|
beforeEach(async(() => { |
||||
|
TestBed.configureTestingModule({ |
||||
|
declarations: [ LoginComponent ] |
||||
|
}) |
||||
|
.compileComponents(); |
||||
|
})); |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
fixture = TestBed.createComponent(LoginComponent); |
||||
|
component = fixture.componentInstance; |
||||
|
fixture.detectChanges(); |
||||
|
}); |
||||
|
|
||||
|
it('should create', () => { |
||||
|
expect(component).toBeTruthy(); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,53 @@ |
|||||
|
import { Component, OnInit } from '@angular/core'; |
||||
|
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; |
||||
|
import {AuthService} from "@services/*"; |
||||
|
import {Router} from "@angular/router"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-login', |
||||
|
templateUrl: './login.component.html', |
||||
|
styleUrls: ['./login.component.scss'] |
||||
|
}) |
||||
|
export class LoginComponent implements OnInit { |
||||
|
|
||||
|
public loginForm: FormGroup; |
||||
|
public username; |
||||
|
public password; |
||||
|
public error: string; |
||||
|
|
||||
|
constructor(private authService: AuthService, |
||||
|
private fb: FormBuilder, |
||||
|
private router: Router) { |
||||
|
|
||||
|
|
||||
|
this.loginForm = this.fb.group({ |
||||
|
password: new FormControl('', Validators.required), |
||||
|
username: new FormControl('', [ |
||||
|
Validators.required, |
||||
|
]), |
||||
|
}); |
||||
|
this.username = this.loginForm.get('username'); |
||||
|
this.password = this.loginForm.get('password'); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.authService.logout(); |
||||
|
this.loginForm.valueChanges.subscribe(() => { |
||||
|
this.error = null; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public login() { |
||||
|
this.error = null; |
||||
|
if (this.loginForm.valid) { |
||||
|
this.authService.login(this.loginForm.getRawValue()) |
||||
|
.subscribe(res => this.router.navigate(['/page/dashboard']), |
||||
|
error => this.error = error.message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onInputChange(event) { |
||||
|
event.target.required = true; |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,2 +0,0 @@ |
|||||
|
|
||||
|
|
@ -1,20 +0,0 @@ |
|||||
<div class="mdl-card mdl-card__blank-layout-card mdl-shadow--2dp"> |
|
||||
<div class="mdl-card__supporting-text color--dark-gray"> |
|
||||
<div class="mdl-grid"> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="mdl-card__title-text text-color--smooth-gray">DARKBOARD</span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="text--huge color-text--light-blue">404</span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="text--sorry text-color--white">Sorry, but there's nothing here</span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<a routerLink="/" class="mdl-button mdl-js-button color-text--light-blue pull-right"> |
|
||||
Go Back |
|
||||
</a> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
@ -1,10 +0,0 @@ |
|||||
import { Component, HostBinding } from '@angular/core'; |
|
||||
|
|
||||
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'app-error', |
|
||||
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'], |
|
||||
templateUrl: './error.component.html', |
|
||||
}) |
|
||||
export class ErrorComponent extends BlankLayoutCardComponent { } |
|
@ -1,27 +0,0 @@ |
|||||
<div class="mdl-card mdl-card__blank-layout-card mdl-shadow--2dp"> |
|
||||
<div class="mdl-card__supporting-text color--dark-gray"> |
|
||||
<div class="mdl-grid"> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="mdl-card__title-text text-color--smooth-gray">DARKBOARD</span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="blank-layout-card-name text-color--white">Forgot password?</span> |
|
||||
<span class="blank-layout-card-secondary-text text-color--smoke">Enter your email below to recieve your password</span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size"> |
|
||||
<input class="mdl-textfield__input" type="text" id="e-mail"> |
|
||||
<label class="mdl-textfield__label" for="e-mail">Email</label> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone submit-cell"> |
|
||||
<div class="mdl-layout-spacer"></div> |
|
||||
<a routerLink="/app/dashboard"> |
|
||||
<button class="mdl-button mdl-js-button mdl-button--raised color--light-blue"> |
|
||||
SEND PASSWORD |
|
||||
</button> |
|
||||
</a> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
@ -1,10 +0,0 @@ |
|||||
import { Component, HostBinding } from '@angular/core'; |
|
||||
|
|
||||
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'app-forgot-password', |
|
||||
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'], |
|
||||
templateUrl: './forgot-password.component.html', |
|
||||
}) |
|
||||
export class ForgotPasswordComponent extends BlankLayoutCardComponent { } |
|
@ -1 +0,0 @@ |
|||||
export { ForgotPasswordComponent } from './forgot-password.component'; |
|
@ -1 +0,0 @@ |
|||||
export { PagesModule } from './pages.module'; |
|
@ -1,30 +0,0 @@ |
|||||
import { ModuleWithProviders, NgModule } from '@angular/core'; |
|
||||
import { RouterModule, Routes } from '@angular/router'; |
|
||||
|
|
||||
import { LayoutsModule } from 'app/layouts'; |
|
||||
import { BlankLayoutComponent } from 'app/layouts/blank-layout'; |
|
||||
import { ErrorComponent } from './error'; |
|
||||
import { ForgotPasswordComponent } from './forgot-password'; |
|
||||
import { LoginComponent } from './login'; |
|
||||
import { SignUpComponent } from './sign-up'; |
|
||||
|
|
||||
@NgModule({ |
|
||||
imports: [ |
|
||||
RouterModule.forChild([ |
|
||||
{ |
|
||||
path: '', |
|
||||
component: BlankLayoutComponent, |
|
||||
children: [ |
|
||||
{ path: '404', component: ErrorComponent, pathMatch: 'full' }, |
|
||||
{ path: 'login', component: LoginComponent, pathMatch: 'full' }, |
|
||||
{ path: 'sign-up', component: SignUpComponent, pathMatch: 'full' }, |
|
||||
{ path: 'forgot-password', component: ForgotPasswordComponent, pathMatch: 'full' }, |
|
||||
{ path: '**', redirectTo: '404' }, |
|
||||
], |
|
||||
}, |
|
||||
]), |
|
||||
LayoutsModule, |
|
||||
], |
|
||||
exports: [RouterModule], |
|
||||
}) |
|
||||
export class PagesRoutingModule { } |
|
@ -1,30 +0,0 @@ |
|||||
import { CommonModule } from '@angular/common'; |
|
||||
import { NgModule } from '@angular/core'; |
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; |
|
||||
|
|
||||
import { ThemeModule } from 'theme'; |
|
||||
|
|
||||
import { TooltipModule } from '../../../theme/directives/tooltip/tooltip.module'; |
|
||||
import { ErrorComponent } from './error'; |
|
||||
import { ForgotPasswordComponent } from './forgot-password'; |
|
||||
|
|
||||
import { PagesRoutingModule } from './pages-routing.module'; |
|
||||
import { SignUpComponent } from './sign-up'; |
|
||||
|
|
||||
|
|
||||
@NgModule({ |
|
||||
imports: [ |
|
||||
CommonModule, |
|
||||
ThemeModule, |
|
||||
PagesRoutingModule, |
|
||||
FormsModule, |
|
||||
ReactiveFormsModule, |
|
||||
TooltipModule, |
|
||||
], |
|
||||
declarations: [ |
|
||||
ErrorComponent, |
|
||||
SignUpComponent, |
|
||||
ForgotPasswordComponent, |
|
||||
], |
|
||||
}) |
|
||||
export class PagesModule { } |
|
@ -1 +0,0 @@ |
|||||
export { SignUpComponent } from './sign-up.component'; |
|
@ -1,73 +0,0 @@ |
|||||
<div class="mdl-card mdl-card__blank-layout-card mdl-shadow--2dp"> |
|
||||
<div class="mdl-card__supporting-text color--dark-gray"> |
|
||||
<div class="mdl-grid"> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="mdl-card__title-text text-color--smooth-gray">DARKBOARD</span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<span class="blank-layout-card-name text-color--white">Sign up</span> |
|
||||
</div> |
|
||||
<form class="login-form" [formGroup]="signupForm" (submit)="login()" novalidate autocomplete="off"> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone"> |
|
||||
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size" |
|
||||
[class.is-invalid]="username.invalid && (username.dirty || username.touched)" |
|
||||
[class.is-valid]="username.valid && (username.dirty || username.touched)"> |
|
||||
<input formControlName="username" |
|
||||
(change)="onInputChange($event)" |
|
||||
class="mdl-textfield__input" type="text" id="username"> |
|
||||
<label class="mdl-textfield__label" for="username">Name</label> |
|
||||
|
|
||||
<div *ngIf="username.invalid && (username.dirty || username.touched)"> |
|
||||
<span *ngIf="username.errors.required" class="mdl-textfield__error"> |
|
||||
Name is required. <span class="color-text--orange"> Please, write any name.</span> |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size" |
|
||||
[class.is-invalid]="password.invalid && (password.dirty || password.touched)" |
|
||||
[class.is-valid]="password.valid && (password.dirty || password.touched)" id="forRass"> |
|
||||
<input formControlName="password" |
|
||||
(change)="onInputChange($event)" |
|
||||
class="mdl-textfield__input" type="password" id="password"> |
|
||||
<label class="mdl-textfield__label" for="password">Password</label> |
|
||||
<div *ngIf="password.invalid && (password.dirty || password.touched)"> |
|
||||
<span *ngIf="password.errors.required" class="mdl-textfield__error"> |
|
||||
Password is required. <span class="color-text--orange"> Please, write any password.</span> |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label full-size" |
|
||||
[class.is-invalid]="email.invalid && (email.dirty || email.touched)" |
|
||||
[class.is-valid]="email.valid && (email.dirty || email.touched)"> |
|
||||
<input formControlName="email" |
|
||||
pattern="{{emailPattern}}" |
|
||||
(change)="onInputChange($event)" |
|
||||
class="mdl-textfield__input" type="text" id="email"> |
|
||||
<label class="mdl-textfield__label" for="email">Email</label> |
|
||||
|
|
||||
<div *ngIf="email.invalid && (email.dirty || email.touched)"> |
|
||||
<span *ngIf="email.errors.required" class="mdl-textfield__error"> |
|
||||
Email is required. <span class="color-text--orange"> Please, write any valid email.</span> |
|
||||
</span> |
|
||||
<span *ngIf="email.errors.pattern" class="mdl-textfield__error"> |
|
||||
Email is invalid. |
|
||||
</span> |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<label baseCheckbox color="light-blue" class="checkbox--inline" inline></label> |
|
||||
<span class="blank-layout-card-link">I agree all statements in <a href="#" |
|
||||
class="underlined">terms of service</a></span> |
|
||||
</div> |
|
||||
<div class="mdl-cell mdl-cell--12-col mdl-cell--4-col-phone submit-cell"> |
|
||||
<a routerLink="/pages/login" class="blank-layout-card-link">I have already signed up</a> |
|
||||
<div class="mdl-layout-spacer"></div> |
|
||||
<button class="mdl-button mdl-js-button mdl-button--raised color--light-blue" |
|
||||
type="submit" [disabled]="signupForm.invalid"> |
|
||||
SIGN UP |
|
||||
</button> |
|
||||
</div> |
|
||||
</form> |
|
||||
</div> |
|
||||
</div> |
|
||||
</div> |
|
@ -1,61 +0,0 @@ |
|||||
import { Component, OnInit } from '@angular/core'; |
|
||||
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; |
|
||||
import { Router } from '@angular/router'; |
|
||||
|
|
||||
import { AuthService } from '@services/*'; |
|
||||
|
|
||||
import { BlankLayoutCardComponent } from 'app/components/blank-layout-card'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'app-sign-up', |
|
||||
styleUrls: ['../../../components/blank-layout-card/blank-layout-card.component.scss'], |
|
||||
templateUrl: './sign-up.component.html', |
|
||||
}) |
|
||||
export class SignUpComponent extends BlankLayoutCardComponent implements OnInit { |
|
||||
|
|
||||
public signupForm: FormGroup; |
|
||||
public email; |
|
||||
public password; |
|
||||
public username; |
|
||||
public emailPattern = '^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,5})$'; |
|
||||
public error: string; |
|
||||
|
|
||||
constructor(private authService: AuthService, |
|
||||
private fb: FormBuilder, |
|
||||
private router: Router) { |
|
||||
super(); |
|
||||
|
|
||||
this.signupForm = this.fb.group({ |
|
||||
password: new FormControl('', Validators.required), |
|
||||
email: new FormControl('', [ |
|
||||
Validators.required, |
|
||||
Validators.pattern(this.emailPattern), |
|
||||
Validators.maxLength(20), |
|
||||
]), |
|
||||
username: new FormControl('', [Validators.required, Validators.maxLength(10)]), |
|
||||
}); |
|
||||
this.email = this.signupForm.get('email'); |
|
||||
this.password = this.signupForm.get('password'); |
|
||||
this.username = this.signupForm.get('username'); |
|
||||
} |
|
||||
|
|
||||
public ngOnInit() { |
|
||||
this.authService.logout(); |
|
||||
this.signupForm.valueChanges.subscribe(() => { |
|
||||
this.error = null; |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public login() { |
|
||||
this.error = null; |
|
||||
if (this.signupForm.valid) { |
|
||||
this.authService.signup(this.signupForm.getRawValue()) |
|
||||
.subscribe(res => this.router.navigate(['/app/dashboard']), |
|
||||
error => this.error = error.message); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public onInputChange(event) { |
|
||||
event.target.required = true; |
|
||||
} |
|
||||
} |
|
@ -1,6 +0,0 @@ |
|||||
@import '~material-design-lite/src/functions'; |
|
||||
@import '~material-design-lite/src/variables'; |
|
||||
@import '~material-design-lite/src/mixins'; |
|
||||
@import '~material-design-lite/src/color-definitions'; |
|
||||
@import './scss/variables'; |
|
||||
@import './scss/mixins'; |
|
@ -1,14 +0,0 @@ |
|||||
import { Component, HostBinding, Input, ViewChild, ViewContainerRef } from '@angular/core'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'base-card base-card-actions', |
|
||||
styleUrls: ['./card.component.scss'], |
|
||||
template: `<ng-content></ng-content>`, |
|
||||
}) |
|
||||
export class CardActionsComponent { |
|
||||
@HostBinding('class.mdl-card__actions') private readonly mdlCardActions = true; |
|
||||
|
|
||||
constructor( |
|
||||
private viewContainerRef: ViewContainerRef, |
|
||||
) { } |
|
||||
} |
|
@ -1,18 +0,0 @@ |
|||||
import { Component, HostBinding, Input, ViewChild } from '@angular/core'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'base-card base-card-body', |
|
||||
styleUrls: ['./card.component.scss'], |
|
||||
template: `<ng-content></ng-content>`, |
|
||||
}) |
|
||||
export class CardBodyComponent { |
|
||||
@HostBinding('class.mdl-card__supporting-text') private readonly mdlCardSupportingText = true; |
|
||||
|
|
||||
@HostBinding('class.mdl-card--expand') private isExpanded = false; |
|
||||
|
|
||||
@Input() set expanded(value) { |
|
||||
if (value || value === '') { |
|
||||
this.isExpanded = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,10 +0,0 @@ |
|||||
import { Component, HostBinding, Input, ViewChild, ViewContainerRef } from '@angular/core'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'base-card base-card-menu', |
|
||||
styleUrls: ['./card.component.scss'], |
|
||||
template: `<ng-content></ng-content>`, |
|
||||
}) |
|
||||
export class CardMenuComponent { |
|
||||
@HostBinding('class.mdl-card__menu') private readonly mdlCardMenu = true; |
|
||||
} |
|
@ -1,21 +0,0 @@ |
|||||
import { Component, HostBinding, Input, ViewChild } from '@angular/core'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'base-card base-card-title', |
|
||||
styleUrls: ['./card.component.scss'], |
|
||||
template: `<ng-content></ng-content>`, |
|
||||
}) |
|
||||
export class CardTitleComponent { |
|
||||
@HostBinding('class.mdl-card__title') private readonly mdlCardTitle = true; |
|
||||
@HostBinding('class.mdl-card--border') private readonly mdlCardBorder = true; |
|
||||
|
|
||||
|
|
||||
|
|
||||
@HostBinding('class.mdl-card--expand') private isExpanded = false; |
|
||||
|
|
||||
@Input() set expanded(value) { |
|
||||
if (value || value === '') { |
|
||||
this.isExpanded = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,20 +0,0 @@ |
|||||
@import '~theme/helpers'; |
|
||||
|
|
||||
.mdl-card__title { |
|
||||
background-color: $card-title-background-color; |
|
||||
} |
|
||||
|
|
||||
.mdl-card__supporting-text { |
|
||||
line-height: 22px; |
|
||||
width: calc(100% - #{$card-horizontal-padding*2}); |
|
||||
overflow: visible; |
|
||||
} |
|
||||
|
|
||||
.mdl-card__actions { |
|
||||
padding: 8px 16px; |
|
||||
} |
|
||||
|
|
||||
.mdl-card { |
|
||||
height: 100%; |
|
||||
overflow: visible; |
|
||||
} |
|
@ -1,15 +0,0 @@ |
|||||
import { Component, HostBinding, Input, ViewChild, ViewContainerRef } from '@angular/core'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'base-card', |
|
||||
styleUrls: ['./card.component.scss'], |
|
||||
template: `<ng-content></ng-content>`, |
|
||||
}) |
|
||||
export class CardComponent { |
|
||||
@HostBinding('class.mdl-card') private readonly mdlCard = true; |
|
||||
@HostBinding('class.mdl-shadow--2dp') private readonly mdlShadow2DP = true; |
|
||||
|
|
||||
constructor( |
|
||||
private viewContainerRef: ViewContainerRef, |
|
||||
) { } |
|
||||
} |
|
@ -1,29 +0,0 @@ |
|||||
import { CommonModule } from '@angular/common'; |
|
||||
import { NgModule } from '@angular/core'; |
|
||||
|
|
||||
import { CardActionsComponent } from './card-actions.component'; |
|
||||
import { CardBodyComponent } from './card-body.component'; |
|
||||
import { CardMenuComponent } from './card-menu.component'; |
|
||||
import { CardTitleComponent } from './card-title.component'; |
|
||||
import { CardComponent } from './card.component'; |
|
||||
|
|
||||
@NgModule({ |
|
||||
imports: [ |
|
||||
CommonModule, |
|
||||
], |
|
||||
declarations: [ |
|
||||
CardComponent, |
|
||||
CardTitleComponent, |
|
||||
CardMenuComponent, |
|
||||
CardBodyComponent, |
|
||||
CardActionsComponent, |
|
||||
], |
|
||||
exports: [ |
|
||||
CardComponent, |
|
||||
CardTitleComponent, |
|
||||
CardMenuComponent, |
|
||||
CardBodyComponent, |
|
||||
CardActionsComponent, |
|
||||
], |
|
||||
}) |
|
||||
export class CardModule { } |
|
@ -1,6 +0,0 @@ |
|||||
export { CardActionsComponent } from './card-actions.component'; |
|
||||
export { CardBodyComponent } from './card-body.component'; |
|
||||
export { CardMenuComponent } from './card-menu.component'; |
|
||||
export { CardTitleComponent } from './card-title.component'; |
|
||||
export { CardComponent } from './card.component'; |
|
||||
export { CardModule } from './card.module'; |
|
@ -1,23 +0,0 @@ |
|||||
import { Component, HostBinding, Input } from '@angular/core'; |
|
||||
|
|
||||
import { ToggleComponent } from 'theme/components/toggle/toggle.component'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'label[baseCheckbox]', |
|
||||
styleUrls: ['../toggle/toggle.component.scss'], |
|
||||
template: ` |
|
||||
<input type="checkbox" [id]="innerID" class="mdl-checkbox__input" [checked]="isChecked" (change)="isChecked = !isChecked"> |
|
||||
<span class="mdl-checkbox__label"><ng-content></ng-content></span> |
|
||||
`,
|
|
||||
}) |
|
||||
export class CheckboxComponent extends ToggleComponent { |
|
||||
private isInline = false; |
|
||||
@Input() private set inline(value) { |
|
||||
if (value || value === '') { |
|
||||
this.isInline = true; |
|
||||
} |
|
||||
} |
|
||||
@HostBinding('class') private get className() { |
|
||||
return `mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect checkbox--colored-${this.color} ${this.isInline && 'checkbox--inline'}`; |
|
||||
} |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export { CheckboxComponent } from './checkbox.component'; |
|
@ -1,17 +0,0 @@ |
|||||
import { Component, HostBinding, Input } from '@angular/core'; |
|
||||
|
|
||||
import { ToggleComponent } from 'theme/components/toggle/toggle.component'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'label[baseIconToggle]', |
|
||||
styleUrls: ['../toggle/toggle.component.scss'], |
|
||||
template: ` |
|
||||
<input type="checkbox" [id]="innerID" class="mdl-icon-toggle__input" [checked]="isChecked" (change)="isChecked = !isChecked"> |
|
||||
<i class="mdl-icon-toggle__label material-icons"><ng-content></ng-content></i> |
|
||||
`,
|
|
||||
}) |
|
||||
export class IconToggleComponent extends ToggleComponent { |
|
||||
@HostBinding('class') private get className() { |
|
||||
return `mdl-icon-toggle mdl-js-icon-toggle mdl-js-ripple-effect icon-toggle--colored-${this.color}`; |
|
||||
} |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export { IconToggleComponent } from './icon-toggle.component'; |
|
@ -1 +0,0 @@ |
|||||
export { PageTopComponent } from './page-top.component'; |
|
@ -1,62 +0,0 @@ |
|||||
@import '~theme/helpers'; |
|
||||
|
|
||||
@media screen and (max-width: $layout-screen-size-threshold) { |
|
||||
.mdl-layout__header { |
|
||||
display: flex !important; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.account-dropdown { |
|
||||
&.mdl-menu { |
|
||||
width: 310px; |
|
||||
} |
|
||||
|
|
||||
.mdl-list__item { |
|
||||
font-size: 1rem; |
|
||||
|
|
||||
&:first-child { |
|
||||
font-size: 16px; |
|
||||
padding-top: $list-min-padding/2; |
|
||||
padding-bottom: $list-min-padding/2; |
|
||||
height: $account-dropdown-avatar-size + $list-min-padding; |
|
||||
|
|
||||
.mdl-list__item-primary-content { |
|
||||
height: $account-dropdown-avatar-size; |
|
||||
line-height: 28px; |
|
||||
|
|
||||
.mdl-list__item-avatar { |
|
||||
height: $account-dropdown-avatar-size; |
|
||||
width: $account-dropdown-avatar-size; |
|
||||
background: url("#{$image-path}/Icon.png"); |
|
||||
background-size: cover; |
|
||||
} |
|
||||
|
|
||||
.mdl-list__item-sub-title { |
|
||||
font-weight: 300; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
&:hover .mdl-list__item-icon { |
|
||||
color: $list-icon-hover-color; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.list__item--border-top { |
|
||||
margin-top: 8px; |
|
||||
padding-top: 8px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.settings-dropdown { |
|
||||
width: $settings_dropdown_width; |
|
||||
|
|
||||
.mdl-menu__item, |
|
||||
a { |
|
||||
@include typo-dropdown-menu-li; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.search { |
|
||||
padding: 18px 0 !important; |
|
||||
} |
|
@ -1,10 +0,0 @@ |
|||||
import { Component, HostBinding } from '@angular/core'; |
|
||||
|
|
||||
@Component({ |
|
||||
selector: 'base-page-top', |
|
||||
styleUrls: ['./page-top.component.scss'], |
|
||||
template: `<ng-content></ng-content>`, |
|
||||
}) |
|
||||
export class PageTopComponent { |
|
||||
@HostBinding('class.mdl-layout__header-row') protected readonly mdlLayoutHeaderRow = true; |
|
||||
} |
|
@ -1 +0,0 @@ |
|||||
export { PaginationComponent } from './pagination.component'; |
|
@ -1,13 +0,0 @@ |
|||||
<span (click)="onChangePage(-1)"><i class="material-icons">chevron_left</i></span> |
|
||||
<span> {{ currentPage }} of {{ numPage }} </span> |
|
||||
<span (click)="onChangePage(1)"><i class="material-icons">chevron_right</i></span> |
|
||||
<div class="goto"> |
|
||||
<span> Go to </span> |
|
||||
<form (submit)="goToPage($event)"> |
|
||||
<input [(ngModel)]="inputNumPage" name="inputPage" |
|
||||
class="mdl-textfield__input" |
|
||||
type="number" |
|
||||
min="1" |
|
||||
[max]="numPage"> |
|
||||
</form> |
|
||||
</div> |
|
@ -1,36 +0,0 @@ |
|||||
@import '~theme/helpers'; |
|
||||
|
|
||||
.pagination { |
|
||||
font-family: Roboto, Helvetica, sans-serif; |
|
||||
color: $color-smooth-gray; |
|
||||
font-size: 14px; |
|
||||
position: relative; |
|
||||
line-height: 16px; |
|
||||
user-select: none; |
|
||||
|
|
||||
.material-icons { |
|
||||
cursor: pointer; |
|
||||
position: relative; |
|
||||
top: 0.5rem; |
|
||||
margin: 0 0.5rem; |
|
||||
} |
|
||||
|
|
||||
.goto { |
|
||||
display: inline-block; |
|
||||
margin: 0 1rem 0 2rem; |
|
||||
|
|
||||
form { |
|
||||
width: 30px; |
|
||||
display: inline-block; |
|
||||
|
|
||||
input { |
|
||||
text-align: center; |
|
||||
font-family: Roboto, Helvetica, sans-serif; |
|
||||
|
|
||||
&:focus { |
|
||||
outline: none; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue