From ab1b90d020e85f2dea1d01a674f7f97d40841521 Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Fri, 13 Nov 2020 19:04:45 +0100 Subject: [PATCH] Notification system in frontend - Heatmap worker now reports progress - Base for new altitude value - Phones can be marked active --- .../nicolasklier/livebeat/TrackerService.kt | 5 +- backend/endpoints/phone.ts | 8 +- backend/lib/rabbit.ts | 9 ++- backend/models/beat/beat.interface.ts | 1 + backend/models/phone/phone.interface.ts | 1 + backend/models/phone/phone.schema.ts | 3 +- frontend/angular.json | 3 +- frontend/package-lock.json | 45 +++++++++++ frontend/package.json | 5 ++ frontend/src/app/_alert/alert.service.spec.ts | 16 ++++ frontend/src/app/_alert/alert.service.ts | 64 ++++++++++++++++ .../src/app/_alert/alert/alert.component.html | 5 ++ .../src/app/_alert/alert/alert.component.scss | 64 ++++++++++++++++ .../app/_alert/alert/alert.component.spec.ts | 25 +++++++ .../src/app/_alert/alert/alert.component.ts | 75 +++++++++++++++++++ frontend/src/app/admin/admin.component.html | 1 + frontend/src/app/admin/admin.component.ts | 7 +- frontend/src/app/app.component.html | 3 + frontend/src/app/app.component.scss | 13 +++- frontend/src/app/app.component.ts | 6 +- frontend/src/app/app.module.ts | 11 ++- .../app/dashboard/dashboard.component.html | 14 +++- .../src/app/dashboard/dashboard.component.ts | 36 ++++++--- frontend/src/app/map.worker.ts | 8 ++ frontend/src/app/map/map.component.html | 2 +- frontend/src/app/map/map.component.ts | 13 +++- package-lock.json | 16 ++++ 27 files changed, 423 insertions(+), 36 deletions(-) create mode 100644 frontend/src/app/_alert/alert.service.spec.ts create mode 100644 frontend/src/app/_alert/alert.service.ts create mode 100644 frontend/src/app/_alert/alert/alert.component.html create mode 100644 frontend/src/app/_alert/alert/alert.component.scss create mode 100644 frontend/src/app/_alert/alert/alert.component.spec.ts create mode 100644 frontend/src/app/_alert/alert/alert.component.ts create mode 100644 package-lock.json diff --git a/android/app/src/main/java/de/nicolasklier/livebeat/TrackerService.kt b/android/app/src/main/java/de/nicolasklier/livebeat/TrackerService.kt index 568d668..2b71e3a 100644 --- a/android/app/src/main/java/de/nicolasklier/livebeat/TrackerService.kt +++ b/android/app/src/main/java/de/nicolasklier/livebeat/TrackerService.kt @@ -92,7 +92,10 @@ class TrackerService : Service() { level * 100 / scale.toFloat() } - val beat = Beat(androidId, arrayOf(location.latitude, location.longitude, location.accuracy.toDouble(), location.speed.toDouble()), batteryPct?.toInt(), location.time) + val beat = Beat( + androidId, + arrayOf(location.latitude, location.longitude, location.altitude, location.accuracy.toDouble(), location.speed.toDouble()), + batteryPct?.toInt(), location.time) val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter: JsonAdapter = moshi.adapter(Beat::class.java) diff --git a/backend/endpoints/phone.ts b/backend/endpoints/phone.ts index 2075555..c0c33d8 100644 --- a/backend/endpoints/phone.ts +++ b/backend/endpoints/phone.ts @@ -7,7 +7,7 @@ import { Phone } from "../models/phone/phone.model"; export async function GetPhone(req: LivebeatRequest, res: Response) { const phoneId: String = req.params['id']; - // If none id provided, return all. + // If no id provided, return all. if (phoneId === undefined) { const phone = await Phone.find({ user: req.user?._id }); res.status(200).send(phone); @@ -48,8 +48,7 @@ export async function PostPhone(req: LivebeatRequest, res: Response) { const phone = await Phone.findOne({ androidId, user: req.user?._id }); if (phone !== null) { - logger.debug("Request to /phone failed because phone already exists."); - res.status(409).send(); + res.status(409).send({ message: "This phone already exists." }); return; } @@ -60,7 +59,8 @@ export async function PostPhone(req: LivebeatRequest, res: Response) { modelName, operatingSystem, architecture, - user: req.user?._id + user: req.user?._id, + active: false }); logger.info(`New device (${displayName}) registered for ${req.user?.name}.`) diff --git a/backend/lib/rabbit.ts b/backend/lib/rabbit.ts index 19df5ed..0807b0b 100644 --- a/backend/lib/rabbit.ts +++ b/backend/lib/rabbit.ts @@ -30,13 +30,14 @@ export class RabbitMQ { return; } - logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`); + logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]}, ${msg.gpsLocation[3]}m height and accuracy and ${msg.battery}% battery`); const newBeat = await Beat.create({ phone: phone._id, - coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]], - accuracy: msg.gpsLocation[2], - speed: msg.gpsLocation[3], + // [latitude, longitude, altitude] + coordinate: [msg.gpsLocation[0], msg.gpsLocation[1], msg.gpsLocation[2]], + accuracy: msg.gpsLocation[3], + speed: msg.gpsLocation[4], battery: msg.battery, createdAt: msg.timestamp }); diff --git a/backend/models/beat/beat.interface.ts b/backend/models/beat/beat.interface.ts index 0f0e73d..1a69161 100644 --- a/backend/models/beat/beat.interface.ts +++ b/backend/models/beat/beat.interface.ts @@ -2,6 +2,7 @@ import { Document } from 'mongoose'; import { IPhone } from '../phone/phone.interface'; export interface IBeat extends Document { + // [latitude, longitude, altitude, accuracy, speed] coordinate?: number[], accuracy: number, speed: number, diff --git a/backend/models/phone/phone.interface.ts b/backend/models/phone/phone.interface.ts index 9c3fa66..8a65355 100644 --- a/backend/models/phone/phone.interface.ts +++ b/backend/models/phone/phone.interface.ts @@ -8,6 +8,7 @@ export interface IPhone extends Document { operatingSystem: String, architecture: String, user: IUser, + active: Boolean, updatedAt?: Date, createdAt?: Date } \ No newline at end of file diff --git a/backend/models/phone/phone.schema.ts b/backend/models/phone/phone.schema.ts index ec991c7..4a12cfc 100644 --- a/backend/models/phone/phone.schema.ts +++ b/backend/models/phone/phone.schema.ts @@ -7,7 +7,8 @@ const schemaPhone = new Schema({ modelName: { type: String, required: false }, operatingSystem: { type: String, required: false }, architecture: { type: String, required: false }, - user: { type: SchemaTypes.ObjectId, required: true } + user: { type: SchemaTypes.ObjectId, required: true }, + active: { type: Boolean, required: true } }, { timestamps: { createdAt: true, diff --git a/frontend/angular.json b/frontend/angular.json index 18c7fdb..2746210 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -31,7 +31,8 @@ "styles": [ "src/styles.scss", "./node_modules/mapbox-gl/dist/mapbox-gl.css", - "./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css" + "./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css", + "../node_modules/font-awesome/css/font-awesome.css" ], "scripts": [ "./node_modules/chart.js/dist/Chart.min.js" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 047c803..aeb4786 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1653,6 +1653,51 @@ "to-fast-properties": "^2.0.0" } }, + "@fortawesome/angular-fontawesome": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.7.0.tgz", + "integrity": "sha512-U+eHYbKuVYrrm9SnIfl+z+6KTiI4Pu+S2OKh34JIi7C1jHhDcrVeDZISP/cpswHY7LWWDOPYeKE+yuWFlL4aVw==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@fortawesome/fontawesome-common-types": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.32.tgz", + "integrity": "sha512-ux2EDjKMpcdHBVLi/eWZynnPxs0BtFVXJkgHIxXRl+9ZFaHPvYamAfCzeeQFqHRjuJtX90wVnMRaMQAAlctz3w==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "1.2.32", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.32.tgz", + "integrity": "sha512-XjqyeLCsR/c/usUpdWcOdVtWFVjPbDFBTQkn2fQRrWhhUoxriQohO2RWDxLyUM8XpD+Zzg5xwJ8gqTYGDLeGaQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.32" + } + }, + "@fortawesome/free-brands-svg-icons": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.15.1.tgz", + "integrity": "sha512-pkTZIWn7iuliCCgV+huDfZmZb2UjslalXGDA2PcqOVUYJmYL11y6ooFiMJkJvUZu+xgAc1gZgQe+Px12mZF0CA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.32" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.1.tgz", + "integrity": "sha512-eD9NWFy89e7SVVtrLedJUxIpCBGhd4x7s7dhesokjyo1Tw62daqN5UcuAGu1NrepLLq1IeAYUVfWwnOjZ/j3HA==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.32" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.1.tgz", + "integrity": "sha512-EFMuKtzRMNbvjab/SvJBaOOpaqJfdSap/Nl6hst7CgrJxwfORR1drdTV6q1Ib/JVzq4xObdTDcT6sqTaXMqfdg==", + "requires": { + "@fortawesome/fontawesome-common-types": "^0.2.32" + } + }, "@istanbuljs/schema": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 13fc2d8..3fde8e9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,11 @@ "@angular/platform-browser-dynamic": "~10.1.5", "@angular/router": "~10.1.5", "@angular/service-worker": "~10.1.5", + "@fortawesome/angular-fontawesome": "^0.7.0", + "@fortawesome/fontawesome-svg-core": "^1.2.28", + "@fortawesome/free-brands-svg-icons": "^5.13.0", + "@fortawesome/free-regular-svg-icons": "^5.13.0", + "@fortawesome/free-solid-svg-icons": "^5.13.0", "@types/chart.js": "^2.9.27", "@types/mapbox-gl": "^1.12.5", "@types/moment": "^2.13.0", diff --git a/frontend/src/app/_alert/alert.service.spec.ts b/frontend/src/app/_alert/alert.service.spec.ts new file mode 100644 index 0000000..b20ed8c --- /dev/null +++ b/frontend/src/app/_alert/alert.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AlertService } from './alert.service'; + +describe('AlertService', () => { + let service: AlertService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AlertService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_alert/alert.service.ts b/frontend/src/app/_alert/alert.service.ts new file mode 100644 index 0000000..f33641c --- /dev/null +++ b/frontend/src/app/_alert/alert.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; + +export class Alert { + id: string; + type: AlertType; + message: string; + title?: string; + duration = 5; + isClosing = false; + + constructor(init?: Partial) { + Object.assign(this, init); + } +} + +export enum AlertType { + Success, + Error, + Info, + Warning +} + +@Injectable({ providedIn: 'root' }) +export class AlertService { + private subject = new Subject(); + private defaultId = 'default-alert'; + + // enable subscribing to alerts observable + onAlert(id = this.defaultId): Observable { + return this.subject.asObservable(); + } + + // convenience methods + success(message: string, title = 'Success', options?: any): void { + this.alert(new Alert({ ...options, type: AlertType.Success, message, title })); + } + + error(message: string, title = 'Error!', options?: any): void { + this.alert(new Alert({ ...options, type: AlertType.Error, message, title })); + } + + info(message: string, title = 'Info', options?: any): void { + this.alert(new Alert({ ...options, type: AlertType.Info, message, title })); + } + + warn(message: string, title = 'Warning!', options?: any): void { + this.alert(new Alert({ ...options, type: AlertType.Warning, message, title })); + } + + // main alert method + alert(alert: Alert): void { + // Don't show empty alerts + if (alert.message === undefined) { return; } + + alert.id = alert.id || this.defaultId; + this.subject.next(alert); + } + + // clear alerts + clear(id = this.defaultId): void { + this.subject.next(new Alert({ id })); + } +} \ No newline at end of file diff --git a/frontend/src/app/_alert/alert/alert.component.html b/frontend/src/app/_alert/alert/alert.component.html new file mode 100644 index 0000000..465c6bb --- /dev/null +++ b/frontend/src/app/_alert/alert/alert.component.html @@ -0,0 +1,5 @@ +
+ {{alert.title}} + {{alert.message}} + +
\ No newline at end of file diff --git a/frontend/src/app/_alert/alert/alert.component.scss b/frontend/src/app/_alert/alert/alert.component.scss new file mode 100644 index 0000000..96901a5 --- /dev/null +++ b/frontend/src/app/_alert/alert/alert.component.scss @@ -0,0 +1,64 @@ +@keyframes appear { + from { + transform: translateY(-100px); + } + to { + transform: translateY(0); + } +} + +.alert { + display: flex; + background-color: #000; + width: 100%; + height: 3rem; + padding-left: 1rem; + padding-right: 1rem; + margin-bottom: 1.5rem; + line-height: 0rem; + border-radius: 15px; + vertical-align: middle; + transition: 0.25s ease; + animation: appear 0.5s ease; + + box-shadow: + 0 4.5px 5.3px rgba(0, 0, 0, 0.121), + 0 15px 17.9px rgba(0, 0, 0, 0.179), + 0 67px 80px rgba(0, 0, 0, 0.3); + + + & .title { + font-weight: bolder; + padding-right: 1rem; + white-space: nowrap; + transform: translateY(50%); + } + + & .message { + width: 100%; + text-align: center; + transform: translateY(50%); + } + + & fa-icon { + transform: translateY(30%); + scale: 1.2; + } +} + +.alert-success { + background-color: #0FAE4B !important; +} + +.alert-danger { + background-color: #FA2400 !important; +} + +.alert-warning { + background-color: #F3CC17 !important; +} + +.fadeOut { + transform: scale(0.9); + opacity: 0; +} \ No newline at end of file diff --git a/frontend/src/app/_alert/alert/alert.component.spec.ts b/frontend/src/app/_alert/alert/alert.component.spec.ts new file mode 100644 index 0000000..8e3decb --- /dev/null +++ b/frontend/src/app/_alert/alert/alert.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AlertComponent } from './alert.component'; + +describe('AlertComponent', () => { + let component: AlertComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AlertComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AlertComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/_alert/alert/alert.component.ts b/frontend/src/app/_alert/alert/alert.component.ts new file mode 100644 index 0000000..f9bed3d --- /dev/null +++ b/frontend/src/app/_alert/alert/alert.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { Router, NavigationStart } from '@angular/router'; +import { faWindowClose } from '@fortawesome/free-solid-svg-icons'; +import { Subscription } from 'rxjs'; +import { Alert, AlertService, AlertType } from '../alert.service'; + +/** + * ~~Stoled~~ inspired by https://jasonwatmore.com/post/2020/04/30/angular-9-alert-notifications + */ + +// tslint:disable-next-line: component-selector +@Component({ selector: 'alert', templateUrl: 'alert.component.html', styleUrls: ['./alert.component.scss'] }) +export class AlertComponent implements OnInit, OnDestroy { + @Input() id = 'default-alert'; + + // Icons + faClose = faWindowClose; + + alerts: Alert[] = []; + alertSubscription: Subscription; + routeSubscription: Subscription; + + constructor(private router: Router, private alertService: AlertService) { } + + ngOnInit(): void { + this.alertSubscription = this.alertService.onAlert(this.id) + .subscribe(alert => { + // Revert array back to original + this.alerts = this.alerts.reverse(); + this.alerts.push(alert); + this.alerts = this.alerts.reverse(); + + setTimeout(() => { + this.removeAlert(alert); + }, alert.duration * 1000); + }); + } + + ngOnDestroy(): void { + // unsubscribe to avoid memory leaks + this.alertSubscription.unsubscribe(); + this.routeSubscription.unsubscribe(); + } + + removeAlert(alert: Alert): void { + // check if already removed to prevent error on auto close + if (!this.alerts.includes(alert)) { return; } + + alert.isClosing = true; + + // remove alert after faded out + setTimeout(() => { + this.alerts = this.alerts.filter(x => x !== alert); + }, 250); + } + + cssClass(alert: Alert): string { + if (!alert) { return; } + + const classes = ['alert', 'alert-dismissable']; + + const alertTypeClass = { + [AlertType.Success]: 'alert alert-success', + [AlertType.Error]: 'alert alert-danger', + [AlertType.Info]: 'alert alert-info', + [AlertType.Warning]: 'alert alert-warning' + }; + + classes.push(alertTypeClass[alert.type]); + + if (alert.isClosing) { classes.push('fadeOut'); } + + return classes.join(' '); + } +} diff --git a/frontend/src/app/admin/admin.component.html b/frontend/src/app/admin/admin.component.html index 73c4ef2..2a0cf3a 100644 --- a/frontend/src/app/admin/admin.component.html +++ b/frontend/src/app/admin/admin.component.html @@ -1,4 +1,5 @@

Create new user

+


diff --git a/frontend/src/app/admin/admin.component.ts b/frontend/src/app/admin/admin.component.ts index 43e559c..d7c39bf 100644 --- a/frontend/src/app/admin/admin.component.ts +++ b/frontend/src/app/admin/admin.component.ts @@ -1,5 +1,6 @@ import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core'; import { APIService, UserType } from '../api.service'; +import { AlertService } from '../_alert/alert.service'; @Component({ selector: 'app-admin', @@ -14,7 +15,7 @@ export class AdminComponent implements AfterContentInit, OnDestroy { newType: UserType; invitationCode: string; - constructor(public api: APIService) { } + constructor(public api: APIService, private alertt: AlertService) { } ngAfterContentInit(): void { this.api.showFilter = false; @@ -29,4 +30,8 @@ export class AdminComponent implements AfterContentInit, OnDestroy { this.invitationCode = await this.api.createUser(this.newUsername, this.newPassword, this.newType); } + alert() { + this.alertt.info('This is a test from admin', 'Admin says'); + } + } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 56a392f..6a3aee8 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,3 +1,5 @@ + +