Notification system enhanced

- Base for notifcation center
- Show text in map if no data is available
This commit is contained in:
2020-11-21 14:11:26 +01:00
parent 8b54431449
commit 533749c7c8
26 changed files with 323 additions and 76 deletions

View File

@@ -16,10 +16,10 @@ export class Alert {
}
export enum AlertType {
Success,
Error,
Info,
Warning
INFO = 0,
SUCCESS = 1,
WARN = 2,
ERROR = 3
}
@Injectable({ providedIn: 'root' })
@@ -36,23 +36,30 @@ export class AlertService {
// convenience methods
success(message: string, title = 'Success', options?: any): void {
this.alert(new Alert({ ...options, type: AlertType.Success, message, title }));
this.alert(new Alert({ ...options, type: AlertType.SUCCESS, message, title }));
}
error(message: string, title = 'Error!', options?: any): void {
const audio = new Audio(this.errorSoundPath);
audio.load();
audio.play();
audio.play().catch(error => {});
this.alert(new Alert({ ...options, type: AlertType.Error, message, title }));
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 }));
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 }));
this.alert(new Alert({ ...options, type: AlertType.WARN, message, title }));
}
/**
* Use this to use a variable as alert type.
*/
dynamic(message: string, type: AlertType, title = 'Dynamic', options?: any) {
this.alert(new Alert({ ...options, type, message, title }));
}
// main alert method

View File

@@ -62,10 +62,10 @@ export class AlertComponent implements OnInit, OnDestroy {
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'
[AlertType.SUCCESS]: 'alert alert-success',
[AlertType.ERROR]: 'alert alert-danger',
[AlertType.INFO]: 'alert alert-info',
[AlertType.WARN]: 'alert alert-warning'
};
classes.push(alertTypeClass[alert.type]);

View File

@@ -6,6 +6,9 @@ import * as moment from 'moment';
import { error } from 'protractor';
import { AlertService } from './_alert/alert.service';
/*
* DEFINITION OF TYPE
*/
export interface ILogin {
token: string;
}
@@ -65,6 +68,26 @@ export interface ITimespan {
to?: number;
}
export enum ISeverity {
INFO = 0,
SUCCESS = 1,
WARN = 2,
ERROR = 3
}
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic';
export interface INotification extends Document {
type: NotificationType;
severity: ISeverity;
message: any;
user: IUser;
}
/*
* END OF THE DEFINITION OF TYPE
*/
@Injectable({
providedIn: 'root'
})
@@ -74,10 +97,6 @@ export class APIService {
username: string;
rabbitmq: any;
time: ITimespan | undefined = {
from: moment().subtract(1, 'day').unix(),
to: moment().unix()
};
// Passthough data (not useful for api but a way for components to share data)
showFilter = true;
@@ -99,6 +118,7 @@ export class APIService {
createdAt: new Date(),
twoFASecret: ''
};
notifications: INotification[];
// Events when new data got fetched
beatsEvent: BehaviorSubject<IBeat[]> = new BehaviorSubject([]);
@@ -121,30 +141,26 @@ export class APIService {
password: this.user.brokerToken
});
this.mqtt.observe(this.user._id).subscribe(message => {
this.mqtt.observe(this.user._id).subscribe(async message => {
if (message !== undefined || message !== null) {
const obj = JSON.parse(message.payload.toString());
console.log('Received message:', obj);
switch (obj.type) {
case 'beat':
if (this.beats !== undefined) {
this.beats.push(obj);
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
this.beatStats.totalBeats++;
}
break;
case 'phone_available':
this.alert.info(`Device ${obj.name} is now online`, 'Device');
break;
case 'phone_register':
this.alert.success(`New device "${obj.displayName}"`, 'New device');
break;
case 'phone_alive':
this.alert.info('Device is now active', obj.displayName.toString());
break;
case 'phone_dead':
this.alert.warn('Device is now offline', obj.displayName.toString());
break;
if (obj.type === 'beat') {
if (this.beats !== undefined) {
this.beats.push(obj);
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
this.beatStats.totalBeats++;
}
} else if (obj.type === 'phone_available') {
this.alert.dynamic(`Device ${obj.displayName} is now online`, obj.severity, 'Device');
} else if (obj.type === 'phone_register') {
await this.getPhones();
this.alert.dynamic(`New device "${obj.displayName}"`, obj.severity, 'New device');
} else if (obj.type === 'phone_alive') {
this.alert.dynamic('Device is now active', obj.severity, obj.displayName);
} else if (obj.type === 'phone_dead') {
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
}
}
});
@@ -163,6 +179,7 @@ export class APIService {
this.username = username;
await this.getPhones();
await this.getUserInfo();
await this.getNotifications();
this.mqttInit();
@@ -208,20 +225,25 @@ export class APIService {
/*
BEATS
*/
async getBeats(): Promise<IBeat[]> {
async getBeats(time?: ITimespan): Promise<IBeat[]> {
return new Promise<IBeat[]>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
this.fetchingDataEvent.next(true);
// If time is not specified, default to 'today'
if (time === undefined) {
time = { from: moment().startOf('day').unix(), to: moment().unix() };
}
const headers = new HttpHeaders({ token: this.token });
let params = new HttpParams();
if (this.time !== undefined) {
params = params.set('from', this.time.from.toString());
if (time !== undefined) {
params = params.set('from', time.from.toString());
if (this.time.to !== 0) {
params = params.set('to', this.time.to.toString());
if (time.to !== 0) {
params = params.set('to', time.to.toString());
}
}
@@ -314,6 +336,10 @@ export class APIService {
});
}
/**
* Fetch information about one phone from API and cache them locally.
* @param phoneId Object id of target phone
*/
async getPhone(phoneId: string): Promise<{ IPhone, IBeat }> {
return new Promise<{ IPhone, IBeat }>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
@@ -330,6 +356,35 @@ export class APIService {
});
}
/**
* This function searchs for the target phone in local array.
*
* **Notice:** If you want to fetch data again from the server consider using `getPhone(...)`.
* @param phoneId Object id of target phone
*/
getPhoneFromCache(phoneId: string): IPhone | undefined {
return this.phones.find((phone, i, array) => {
if (phone._id === phoneId) { return true; }
});
}
getNotifications(): Promise<INotification[]> {
return new Promise<INotification[]>((resolve, reject) => {
if (!this.hasSession()) { resolve([]); }
const headers = new HttpHeaders({ token: this.token });
this.fetchingDataEvent.next(true);
this.httpClient.get(this.API_ENDPOINT + '/user/notification', { responseType: 'json', headers })
.subscribe((notifications: INotification[]) => {
this.notifications = notifications;
this.fetchingDataEvent.next(false);
resolve(notifications);
});
});
}
/* HELPER CLASSES */
degreesToRadians(degrees: number): number {
return degrees * Math.PI / 180;

View File

@@ -5,6 +5,7 @@ import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';
import { MapComponent } from './map/map.component';
import { NotificationsComponent } from './notifications/notifications.component';
import { UserComponent } from './user/user.component';
const routes: Routes = [
@@ -24,6 +25,10 @@ const routes: Routes = [
path: 'map',
component: MapComponent
},
{
path: 'notifications',
component: NotificationsComponent
},
{
path: 'user/:id',
component: UserComponent

View File

@@ -5,6 +5,9 @@
<li><a [routerLink]="['/dashboard']" routerLinkActive="router-link-active">Dashboard</a></li>
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active">Map</a></li>
<li class="navbar-right"><a [routerLink]="['/notifications']">
<img src="assets/message.svg">
</a></li>
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]"
routerLinkActive="router-link-active">
<fa-icon [icon]="faUser"></fa-icon>

View File

@@ -20,6 +20,7 @@ import { environment } from '../environments/environment';
import { AdminComponent } from './admin/admin.component';
import { AlertComponent } from './_alert/alert/alert.component';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { NotificationsComponent } from './notifications/notifications.component';
@NgModule({
declarations: [
@@ -31,7 +32,8 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
UserComponent,
DashboardWidgetComponent,
AdminComponent,
AlertComponent
AlertComponent,
NotificationsComponent
],
imports: [
BrowserModule,

View File

@@ -12,40 +12,47 @@ export class FilterComponent implements OnInit {
presetHours = -1;
customRange: any;
customUnit: moment.unitOfTime.DurationConstructor;
timeRange: ITimespan; // Differs to customRange since this is the actual object used for query.
constructor(public api: APIService) { }
ngOnInit(): void {
}
update(value: number): void {
let result: ITimespan | undefined = { to: 0, from: 0 };
/**
* @param value Time in hours.
* Except: `-1`: Today,
* `-2`: Custom time range,
* `0`: All time
*/
update(): void {
let time: ITimespan = { from: 0, to: 0 };
console.log(this.customRange, this.customUnit, this.presetHours);
if (this.presetHours == -2) {
if (this.customRange !== undefined && this.customUnit !== undefined) {
result.from = moment().subtract(this.customRange, this.customUnit).unix();
console.log(result.from);
time.from = moment().subtract(this.customRange, this.customUnit).unix();
}
} else if (this.presetHours == -1) {
result.from = moment().startOf('day').unix();
time.from = moment().startOf('day').unix();
} else if (this.presetHours == 0) {
result = undefined;
// Do nothing since we want `from` to stay zero.
} else {
result.from = moment().subtract(this.presetHours, 'hours').unix();
time.from = moment().subtract(this.presetHours, 'hours').unix();
}
console.log(result);
this.api.time = result;
this.refresh();
time.to = moment().unix();
this.timeRange = time;
this.refresh(time);
}
updateAccuracy(val: any): void {
this.api.maxAccuracy.next(Number(val.target.value));
}
async refresh(): Promise<void> {
await this.api.getBeats();
async refresh(time: ITimespan): Promise<void> {
await this.api.getBeats(time);
await this.api.getBeatStats();
await this.api.getPhones();
await this.api.getUserInfo();

View File

@@ -25,7 +25,7 @@ addEventListener('message', ({ data }) => {
progress++;
// Report progress every fifth loop
if (Math.trunc(progress / data.length * 100) % 5 === 0) {
if (Math.trunc(progress / data.length * 100) % 3 === 0) {
postMessage({ progress: (progress / data.length) * 100 });
}
@@ -49,6 +49,5 @@ addEventListener('message', ({ data }) => {
}
});
console.log(`worker response to`, mostVisit);
postMessage(mostVisit);
});

View File

@@ -36,4 +36,8 @@
position="top-right"
></mgl-control>
</div>
</mgl-map>
</mgl-map>
<div id="noData" *ngIf="!this.showMap">
<h1>There is no data</h1><br>
<p>Selected time span does not contain any coordiantes.</p>
</div>

View File

@@ -5,4 +5,13 @@ mgl-map {
left: 0;
height: 100vh;
width: 100vw;
}
#noData {
position: fixed;
top: 35vh;
text-align: center;
height: 100vh;
width: 100%;
z-index: -1;
}

View File

@@ -161,8 +161,12 @@ export class MapComponent {
buildMap(isUpdate: boolean, maxAccuracy: number = 30): void {
// If this is an update don't rebuild entire map.
if (!isUpdate) {
if (this.api.beats.length === 0) {
console.warn('Build map method was called while there are no beats!');
if (this.api.beats !== undefined) {
if (this.api.beats.length === 0) {
console.warn('Build map method was called while there are no beats!');
return;
}
} else {
return;
}

View File

@@ -0,0 +1,3 @@
<div id="notifications">
<h1>Notifications</h1>
</div>

View File

@@ -0,0 +1,4 @@
#notifications {
margin-left: 3rem;
margin-right: 3rem;
}

View File

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

View File

@@ -0,0 +1,25 @@
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
import { APIService } from '../api.service';
@Component({
selector: 'app-notifications',
templateUrl: './notifications.component.html',
styleUrls: ['./notifications.component.scss']
})
export class NotificationsComponent implements OnInit, AfterContentInit, OnDestroy {
constructor(private api: APIService) { }
ngOnInit(): void {
}
ngAfterContentInit(): void {
this.api.showFilter = false;
console.log(this.api.showFilter);
}
ngOnDestroy(): void {
this.api.showFilter = true;
}
}