Notification system enhanced
- Base for notifcation center - Show text in map if no data is available
This commit is contained in:
@@ -10,6 +10,7 @@ import * as winston from 'winston';
|
||||
|
||||
import { config } from './config';
|
||||
import { GetBeat, GetBeatStats } from './endpoints/beat';
|
||||
import { getNotification } from './endpoints/notification';
|
||||
import { GetPhone, PostPhone } from './endpoints/phone';
|
||||
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, PostUser, Resource, Topic, VHost } from './endpoints/user';
|
||||
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
||||
@@ -145,17 +146,20 @@ async function run() {
|
||||
app.get('/user/resource', (req, res) => Resource(req, res));
|
||||
app.get('/user/topic', (req, res) => Topic(req, res));
|
||||
|
||||
// Basic user actions
|
||||
// CRUD user
|
||||
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
|
||||
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
||||
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
||||
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
||||
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
|
||||
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
|
||||
|
||||
// Phones
|
||||
app.get('/phone/:id', MW_User, (req, res) => GetPhone(req, res));
|
||||
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
|
||||
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
|
||||
|
||||
// Beats
|
||||
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
|
||||
app.get('/beat/stats', MW_User, (req, res) => GetBeatStats(req, res));
|
||||
|
||||
|
||||
22
backend/endpoints/notification.ts
Normal file
22
backend/endpoints/notification.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Response } from "express";
|
||||
import { LivebeatRequest } from "../lib/request";
|
||||
import { Notification } from "../models/notifications/notification.model";
|
||||
|
||||
export async function getNotification(req: LivebeatRequest, res: Response) {
|
||||
let limit = req.query.limit;
|
||||
let skip = req.query.skip;
|
||||
|
||||
if (limit === undefined) { limit = '100' };
|
||||
if (skip === undefined) { skip = '0' };
|
||||
|
||||
try {
|
||||
const notifications = await Notification.find({ user: req.user?._id }).limit(Number(limit)).sort({ _id: -1 });
|
||||
res.status(200).send(notifications);
|
||||
} catch(error: any) {
|
||||
if (error instanceof TypeError) {
|
||||
res.status(400).send({ message: "'Limit' has to be a number" });
|
||||
} else {
|
||||
res.status(500).send({ message: "An error occured while processing your request" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import * as amqp from 'amqplib';
|
||||
import { Schema, SchemaType } from 'mongoose';
|
||||
import { logger, RABBITMQ_URI } from '../app';
|
||||
import { Beat } from '../models/beat/beat.model.';
|
||||
import { ISeverity, NotificationType } from '../models/notifications/notification.interface';
|
||||
import { addNotification, Notification } from '../models/notifications/notification.model';
|
||||
import { IPhone } from '../models/phone/phone.interface';
|
||||
import { Phone } from '../models/phone/phone.model';
|
||||
import { User } from '../models/user/user.model';
|
||||
|
||||
interface IBeat {
|
||||
token: string,
|
||||
@@ -14,7 +19,7 @@ export class RabbitMQ {
|
||||
connection: amqp.Connection | null = null;
|
||||
channel: amqp.Channel | null = null;
|
||||
|
||||
timeouts: Map<string, Timeout> = new Map<string, Timeout>();
|
||||
timeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
async init() {
|
||||
this.connection = await amqp.connect(RABBITMQ_URI);
|
||||
@@ -46,16 +51,15 @@ export class RabbitMQ {
|
||||
|
||||
// Broadcast if device became active
|
||||
if (this.timeouts.has(phone.id)) {
|
||||
clearTimeout(this.timeouts.get(phone.id));
|
||||
clearTimeout(this.timeouts.get(phone.id)!!);
|
||||
} else {
|
||||
logger.debug('Set phone active');
|
||||
phone.active = true;
|
||||
await phone.save();
|
||||
this.publish(phone.user.toString(), phone.toJSON(), 'phone_alive');
|
||||
this.publish(phone.user.toString(), phone.toJSON(), 'phone_alive', ISeverity.SUCCESS);
|
||||
}
|
||||
|
||||
const timeoutTimer = setTimeout(async () => {
|
||||
this.publish(phone.user.toString(), phone.toJSON(), 'phone_dead');
|
||||
this.publish(phone.user.toString(), phone.toJSON(), 'phone_dead', ISeverity.WARN);
|
||||
this.timeouts.delete(phone.id);
|
||||
phone.active = false;
|
||||
await phone.save();
|
||||
@@ -66,10 +70,22 @@ export class RabbitMQ {
|
||||
}, { noAck: true });
|
||||
}
|
||||
|
||||
async publish(userId: string, data: any, type: 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic' = 'beat') {
|
||||
if (this.connection == undefined) await this.init()
|
||||
async publish(userId: string, data: any, type: NotificationType, severity = ISeverity.INFO) {
|
||||
if (this.connection == undefined) await this.init();
|
||||
|
||||
const user = await User.findById(userId);
|
||||
if (user === null) return;
|
||||
|
||||
/* Manage notifications */
|
||||
if (type != 'beat') {
|
||||
if (type == 'phone_alive' || type == 'phone_dead') {
|
||||
addNotification(type, severity, ((data as IPhone)._id), user);
|
||||
}
|
||||
}
|
||||
|
||||
data = { type, severity, ...data };
|
||||
console.log('Send:', data);
|
||||
|
||||
data = { type, ...data };
|
||||
this.channel?.publish('amq.topic', userId, Buffer.from(JSON.stringify(data)));
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const schemaBeat = new Schema({
|
||||
accuracy: { type: Number, required: false },
|
||||
speed: { type: Number, required: false },
|
||||
battery: { type: Number, required: false },
|
||||
phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' },
|
||||
phone: { type: SchemaTypes.ObjectId, required: true },
|
||||
createdAt: { type: SchemaTypes.Date, required: false }
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
18
backend/models/notifications/notification.interface.ts
Normal file
18
backend/models/notifications/notification.interface.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Document } from "mongoose";
|
||||
import { IUser } from "../user/user.interface";
|
||||
|
||||
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;
|
||||
}
|
||||
11
backend/models/notifications/notification.model.ts
Normal file
11
backend/models/notifications/notification.model.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Model, model } from 'mongoose';
|
||||
import { IUser } from '../user/user.interface';
|
||||
import { INotification, ISeverity, NotificationType } from './notification.interface';
|
||||
import { schemaNotification } from './notification.schema';
|
||||
|
||||
const modelNotification: Model<INotification> = model<INotification>('Notification', schemaNotification, 'Notification');
|
||||
export { modelNotification as Notification };
|
||||
|
||||
export function addNotification(type: NotificationType, severity: ISeverity, message: any, user: IUser) {
|
||||
return modelNotification.create({ type, severity, message, user });
|
||||
}
|
||||
15
backend/models/notifications/notification.schema.ts
Normal file
15
backend/models/notifications/notification.schema.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Schema, SchemaTypes } from 'mongoose';
|
||||
const schemaNotification = new Schema({
|
||||
type: { type: String, required: true },
|
||||
severity: { type: Number, required: true },
|
||||
message: { type: Object, required: true },
|
||||
user: { type: SchemaTypes.ObjectId }
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: true,
|
||||
updatedAt: false
|
||||
},
|
||||
versionKey: false
|
||||
});
|
||||
|
||||
export { schemaNotification };
|
||||
7
backend/package-lock.json
generated
7
backend/package-lock.json
generated
@@ -188,10 +188,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.10.tgz",
|
||||
"integrity": "sha512-yV1nWZPlMFpoXyoknm4S56y2nlTAuFYaJuQtYRAOU7xA/FJ9RY0Xm7QOkaYMMmr8ESdHIuUb6oQgR/0+2NqlyA==",
|
||||
"dev": true
|
||||
"version": "14.14.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.9.tgz",
|
||||
"integrity": "sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw=="
|
||||
},
|
||||
"@types/qs": {
|
||||
"version": "6.9.5",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"author": "Mondei1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@types/node": "^14.14.9",
|
||||
"amqplib": "^0.6.0",
|
||||
"argon2": "^0.27.0",
|
||||
"body-parser": "^1.19.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES5",
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
|
||||
@@ -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 (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++;
|
||||
}
|
||||
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;
|
||||
} 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);
|
||||
});
|
||||
|
||||
@@ -37,3 +37,7 @@
|
||||
></mgl-control>
|
||||
</div>
|
||||
</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>
|
||||
@@ -6,3 +6,12 @@ mgl-map {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
#noData {
|
||||
position: fixed;
|
||||
top: 35vh;
|
||||
text-align: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
@@ -161,10 +161,14 @@ 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 !== undefined) {
|
||||
if (this.api.beats.length === 0) {
|
||||
console.warn('Build map method was called while there are no beats!');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this.data.features[0].geometry.coordinates = [];
|
||||
this.mostVisitData.features[0].geometry.coordinates = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
8
frontend/src/assets/message.svg
Normal file
8
frontend/src/assets/message.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="19" height="19" viewBox="0 0 24 24" stroke-width="2" stroke="#fff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" />
|
||||
<line x1="8" y1="9" x2="16" y2="9" />
|
||||
<line x1="8" y1="13" x2="14" y2="13" />
|
||||
</svg>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 446 B |
Reference in New Issue
Block a user