Frontend can now filter with timespan

This commit is contained in:
2020-10-23 22:08:54 +02:00
parent 13f8437f29
commit edaff8a3ec
29 changed files with 558 additions and 152 deletions

View File

@@ -1,5 +1,6 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
export interface ILogin {
token: string;
@@ -14,6 +15,11 @@ export interface IBeat {
createdAt?: Date;
}
export interface ITimespan {
from?: number;
to?: number;
}
@Injectable({
providedIn: 'root'
})
@@ -22,8 +28,17 @@ export class APIService {
private token: string;
username: string;
time: ITimespan | undefined;
API_ENDPOINT = 'http://192.168.178.26:8040'
// Cached data
beats: IBeat[];
// Events when new data got fetched
beatsEvent: BehaviorSubject<IBeat[]> = new BehaviorSubject([]);
loginEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
fetchingDataEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
API_ENDPOINT = 'http://192.168.178.26:8040';
constructor(private httpClient: HttpClient) { }
@@ -37,20 +52,34 @@ export class APIService {
this.token = (token as ILogin).token;
this.username = username;
this.loginEvent.next(true);
resolve(token as ILogin);
});
});
}
async getBeats(): Promise<IBeat[]> {
async getBeats(preset?: 'battery'): Promise<IBeat[]> {
return new Promise<IBeat[]>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
const headers = new HttpHeaders({ token: this.token });
this.fetchingDataEvent.next(true);
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers })
const headers = new HttpHeaders({ token: this.token });
let params = new HttpParams()
.set('preset', preset);
if (this.time !== undefined) {
params = params.set('from', this.time.from.toString());
params = params.set('to', this.time.to.toString());
}
console.log(params);
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers, params })
.subscribe(beats => {
console.log(beats);
this.beats = beats as IBeat[];
this.beatsEvent.next(beats as IBeat[]);
this.fetchingDataEvent.next(false);
resolve(beats as IBeat[]);
});
});

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NbAuthComponent, NbLoginComponent } from '@nebular/auth';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
const routes: Routes = [
{
@@ -17,6 +17,10 @@ const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent
},
{
path: 'map',
component: MapComponent
}
];

View File

@@ -1,12 +1,16 @@
<div id="header">
<p>Header</p>
<div class="left">
<span class="ident" *ngIf="this.api.hasSession()">Logged in as {{this.api.username}}</span>
</div>
<ul class="navbar">
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active" >Dashboard</a></li>
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active" >Map</a></li>
<li><a [routerLink]="['/settings']" routerLinkActive="router-link-active" >Settings</a></li>
<li class="navbar-right"><a href="#about">{{this.api.username}}</a></li>
</ul>
</div>
<div class="header-spacer"></div>
<app-filter></app-filter>
<router-outlet></router-outlet>
<!-- Display start page -->
<div id="startpage" *ngIf="false">
<h1>Livebeat</h1>
<div id="loadingOverlay" [ngClass]="{show: this.showOverlay, gone: !this.showOverlay}">
<div>
<img src="assets/oval.svg">
</div>
</div>

View File

@@ -1,10 +1,76 @@
#header {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: fit-content;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
padding-top: 0.8rem;
padding-bottom: 0.8rem;
background-color: #1d1d1dd9;
backdrop-filter: blur(20px);
backdrop-filter: blur(30px);
box-shadow: 10px 10px 50px 0px rgba(0,0,0,0.85);
& ul {
display: inline;
list-style: none;
& li {
float: left;
padding-left: 2rem;
& a {
text-decoration: none;
color: white;
}
}
& .navbar-right {
float: right !important;
padding-right: 2rem;
}
}
}
#loadingOverlay {
position: fixed;
display: none;
top: 30vh;
left: 0;
z-index: 9999;
backdrop-filter: blur(40px);
background-color: rgba(0,0,0,0.75);
width: 100vw;
height: 30vh;
vertical-align: middle;
align-items: center;
& div {
height: fit-content;
margin: 0 auto;
& img {
width: 5rem;
}
}
}
.show {
display: flex !important;
animation: showOverlay .2s ease-in-out;
}
@keyframes showOverlay {
from {
backdrop-filter: blur(0px);
background-color: rgba(0,0,0,0);
}
to {
backdrop-filter: blur(10px);
background-color: rgba(0,0,0,0.8);
}
}
.header-spacer {
height: 3rem;
}

View File

@@ -7,9 +7,19 @@ import { APIService } from './api.service';
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
export class AppComponent implements OnInit{
title = 'Livebeat';
showOverlay = false;
constructor(public api: APIService, private router: Router) {
this.api.fetchingDataEvent.subscribe(status => {
this.showOverlay = status;
});
}
async ngOnInit(): Promise<void> {
await this.api.login('admin', '$1KDaNCDlyXAOg');
return;
}
}

View File

@@ -1,23 +1,25 @@
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NbEvaIconsModule } from '@nebular/eva-icons';
import { NbPasswordAuthStrategy, NbAuthModule, NbDummyAuthStrategy } from '@nebular/auth';
import { NbCardModule, NbLayoutModule, NbSidebarModule, NbThemeModule } from '@nebular/theme';
import { ChartsModule } from 'ng2-charts';
import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { FormsModule } from '@angular/forms';
import { DashboardComponent } from './dashboard/dashboard.component';
import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
import { FilterComponent } from './filter/filter.component';
import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
DashboardComponent
DashboardComponent,
MapComponent,
FilterComponent
],
imports: [
BrowserModule,
@@ -28,21 +30,7 @@ import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
NgxMapboxGLModule.withConfig({
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}),
NbThemeModule.forRoot({ name: 'dark' }),
NbLayoutModule,
NbEvaIconsModule,
NbLayoutModule,
NbCardModule,
NbSidebarModule,
NbAuthModule.forRoot({
strategies: [
NbDummyAuthStrategy.setup({
name: 'email',
alwaysFail: false
})
],
forms: {}
})
ChartsModule
],
providers: [],
bootstrap: [AppComponent]

View File

@@ -1,13 +1,12 @@
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-layer
id="locHisotryLines"
type="line"
source="locHistory"
[paint]="{
'line-color': '#ff0000',
'line-width': 4
}"
>
</mgl-layer>
</mgl-map>
<div id="dashboard">
<h1>Dashboard</h1>
<div class="chartjs-cointainer batteryChart">
<canvas baseChart
[chartType]="'line'"
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[legend]="true"
></canvas>
</div>
</div>

View File

@@ -1,8 +1,9 @@
mgl-map {
position: absolute;
z-index: -1;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
#dashboard {
margin-left: 3rem;
margin-right: 3rem;
}
.batteryChart {
max-height: 40rem;
max-width: 60rem;
}

View File

@@ -1,6 +1,7 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl';
import { APIService } from '../api.service';
import { ChartDataSets, ChartOptions } from 'chart.js';
import { Label } from 'ng2-charts';
import { APIService, IBeat } from '../api.service';
@Component({
selector: 'app-dashboard',
@@ -8,25 +9,68 @@ import { APIService } from '../api.service';
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements AfterViewInit {
map: Map;
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection', features: [
{
type: 'Feature',
properties: null,
geometry: { type: 'LineString', coordinates: [] }
}]
// Array of different segments in chart
lineChartData: ChartDataSets[] = [
{ data: [], label: 'Battery' }
];
// Labels shown on the x-axis
lineChartLabels: Label[] = [];
// Define chart options
lineChartOptions: ChartOptions = {
responsive: true,
scales: {
xAxes: [{
type: 'time',
stacked: false,
time: {
parser: 'MM/DD/YYYY HH:mm:ss',
round: 'minute',
tooltipFormat: 'll HH:mm'
},
scaleLabel: {
display: true,
labelString: 'Date'
}
}]
}
};
constructor(private api: APIService) { }
constructor(private api: APIService) {
this.api.beatsEvent.subscribe(beats => {
this.lineChartData[0].data = [];
this.lineChartLabels = [];
async ngAfterViewInit(): Promise<void> {
const beats = await this.api.getBeats();
beats.forEach((beat) => {
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
const batteryLevels: number[] = [];
const finalBeats = beats.filter((val, i, array) => {
if (batteryLevels.indexOf(val.battery) === -1) {
batteryLevels.push(val.battery);
return true;
} else {
return false;
}
});
finalBeats.forEach((beat) => {
this.lineChartData[0].data.push(beat.battery);
this.lineChartLabels.push(this.formatDateTime(new Date(beat.createdAt)));
});
});
console.log("Now:", this.data.features);
}
async fetchData(): Promise<void> {
await this.api.getBeats();
}
private formatDateTime(date: Date): string {
return `${date.getMonth()}/${date.getDay()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
ngAfterViewInit(): void {
this.fetchData();
}
}

View File

@@ -0,0 +1,11 @@
<div id="filter">
<h3>Filter</h3>
<select (change)="update($event.target.value)">
<option value="-1">Today</option>
<option value="3">Last 3h</option>
<option value="12">Last 12h</option>
<option value="24">Last 24h</option>
<option value="48">Last 48h</option>
<option value="0">All time</option>
</select>
</div>

View File

@@ -0,0 +1,30 @@
@import "../../styles.scss";
#filter {
position: fixed;
display: inline-flex;
bottom: 0;
width: 30vw;
height: 2.5rem;
min-width: 30rem;
max-width: 90vw;
align-items: center;
padding: 0.5rem;
background-color: $header-background;
backdrop-filter: blur(30px);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.9);
left: calc(50% - 30vw / 2);
}
h3 {
text-align: center;
height: fit-content;
padding-right: 1rem;
padding-left: 1rem;
}
select {
height: 60%;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterComponent } from './filter.component';
describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FilterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { APIService, ITimespan } from '../api.service';
import * as moment from 'moment';
@Component({
selector: 'app-filter',
templateUrl: './filter.component.html',
styleUrls: ['./filter.component.scss']
})
export class FilterComponent implements OnInit {
constructor(private api: APIService) { }
ngOnInit(): void {
}
update(value: number) {
let result: ITimespan | undefined = { to: 0, from: 0 };
if (value == -1) {
result.from = moment().startOf('day').unix();
result.to = Math.floor(moment.now() / 1000);
} else if (value == 0) {
result = undefined;
} else {
result.from = moment().subtract(value, 'hours').unix();
result.to = Math.floor(moment.now() / 1000);
}
console.log(result);
this.api.time = result;
this.api.getBeats();
}
}

View File

@@ -22,7 +22,7 @@ export class LoginComponent implements OnInit {
if ((await this.api.login(this.username, this.password)).token !== undefined) {
console.log('Login was successful!');
this.router.navigate(['dashboard']);
this.router.navigate(['map']);
} else {
console.log('Login was not successful!');
}

View File

@@ -0,0 +1,14 @@
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-layer
id="locHisotryLines"
type="line"
source="locHistory"
[zoom]="3"
[paint]="{
'line-color': '#ff0000',
'line-width': 4
}"
>
</mgl-layer>
</mgl-map>

View File

@@ -0,0 +1,8 @@
mgl-map {
position: absolute;
z-index: -1;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MapComponent } from './map.component';
describe('MapComponent', () => {
let component: MapComponent;
let fixture: ComponentFixture<MapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MapComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(MapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,36 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl';
import { APIService } from '../api.service';
@Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.scss']
})
export class MapComponent implements AfterViewInit {
map: Map;
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection', features: [
{
type: 'Feature',
properties: null,
geometry: { type: 'LineString', coordinates: [] }
}]
};
constructor(private api: APIService) {
this.api.beatsEvent.subscribe(beats => {
this.data.features[0].geometry.coordinates = [];
beats.forEach((beat) => {
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
this.data = {... this.data};
});
});
}
async ngAfterViewInit(): Promise<void> {
}
}

View File

@@ -0,0 +1,17 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width="2">
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@@ -1,17 +1,18 @@
@import 'themes';
@import '~@nebular/auth/styles/globals';
@import '~@nebular/theme/styles/globals';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700;900&display=swap');
@include nb-install() {
@include nb-auth-global();
@include nb-theme-global();
};
/* Color palette */
$primary-color: #1d1d1d;
$secondary-color: #1c1c1c;
$foreground-color: #fff;
/* Misc */
$header-background: #1d1d1d9f;
body {
background-color: #1d1d1d;
color: #fff;
background-color: $primary-color;
color: $foreground-color;
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;

View File

@@ -1,18 +0,0 @@
@import '~@nebular/theme/styles/theming';
@import '~@nebular/theme/styles/themes/dark';
$nb-themes: nb-register-theme((
// add your variables here like:
// color-primary-100: #f2f6ff,
// color-primary-200: #d9e4ff,
// color-primary-300: #a6c1ff,
// color-primary-400: #598bff,
// color-primary-500: #3366ff,
// color-primary-600: #274bdb,
// color-primary-700: #1a34b8,
// color-primary-800: #102694,
// color-primary-900: #091c7a,
), dark, dark);