Notification system enhanced
- Base for notifcation center - Show text in map if no data is available
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div id="notifications">
|
||||
<h1>Notifications</h1>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
#notifications {
|
||||
margin-left: 3rem;
|
||||
margin-right: 3rem;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
25
frontend/src/app/notifications/notifications.component.ts
Normal file
25
frontend/src/app/notifications/notifications.component.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user