Skip to content

Commit cea8ef0

Browse files
committed
Add new service for handling TCR messages in webapp
1 parent 8350770 commit cea8ef0

12 files changed

+166
-52
lines changed

webapp/src/app/components/tcr-console/tcr-console.component.ts

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import {Component, ViewChild} from '@angular/core';
2-
import {WebsocketService} from "../../services/websocket.service";
1+
import {Component, effect, OnInit, Signal, ViewChild} from '@angular/core';
32
import {NgTerminal, NgTerminalModule} from "ng-terminal";
43
import {TcrMessage, TcrMessageType} from "../../interfaces/tcr-message";
5-
import {bgDarkGray, cyan, green, lightCyan, lightYellow, red, yellow} from "ansicolor";
64
import {TcrRolesComponent} from "../tcr-roles/tcr-roles.component";
5+
import {TcrMessageService} from "../../services/tcr-message.service";
6+
import {toSignal} from "@angular/core/rxjs-interop";
7+
import {
8+
bgDarkGray,
9+
cyan,
10+
green,
11+
lightCyan,
12+
lightYellow,
13+
red,
14+
yellow
15+
} from "ansicolor";
716

817
@Component({
918
selector: 'app-tcr-console',
@@ -15,15 +24,30 @@ import {TcrRolesComponent} from "../tcr-roles/tcr-roles.component";
1524
templateUrl: './tcr-console.component.html',
1625
styleUrl: './tcr-console.component.css'
1726
})
18-
export class TcrConsoleComponent {
27+
export class TcrConsoleComponent implements OnInit {
1928
title = "TCR Console";
29+
tcrMessage: Signal<TcrMessage | undefined>;
30+
2031
@ViewChild('term', {static: false}) child!: NgTerminal;
2132

22-
constructor(private ws: WebsocketService) {
23-
this.ws.webSocket$.subscribe((m: TcrMessage) => this.printMessage(m));
33+
constructor(private messageService: TcrMessageService) {
34+
this.tcrMessage = toSignal(this.messageService.webSocket$);
35+
36+
effect(() => {
37+
// When receiving a message from the server
38+
// print it in the terminal
39+
this.printMessage(this.tcrMessage()!);
40+
});
41+
}
42+
43+
ngOnInit(): void {
44+
this.clear();
2445
}
2546

2647
private printMessage(message: TcrMessage): void {
48+
if (message === undefined) {
49+
return;
50+
}
2751
switch (message.type) {
2852
case TcrMessageType.SIMPLE:
2953
this.print(message.text);

webapp/src/app/components/tcr-role/tcr-role.component.css

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
.wrap {
33
display: flex;
4-
/*background: blue;*/
54
border-radius: 0.5rem;
65
box-shadow: 7px 7px 30px -5px rgba(0, 0, 0, 0.1);
76
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
<div *ngIf="role">
2-
<div (click)="toggleRole(role)"
3-
[ngClass]="{
1+
<div *ngIf="role" (click)="toggleRole(role)"
2+
[ngClass]="{
43
'navigator-on': role.active && role.name === 'navigator',
5-
'driver-on' :role.active && role.name === 'driver',
4+
'driver-on' : role.active && role.name === 'driver',
65
'role-off': !role.active}"
7-
class="wrap">
8-
<div class="ico-wrap">
9-
<span class="mbr-iconfont fa-compass fa" *ngIf="role.name === 'navigator'"></span>
10-
<span class="mbr-iconfont fa-keyboard-o fa" *ngIf="role.name === 'driver'"></span>
11-
</div>
12-
<div class="text-wrap vcenter">
13-
<h2 class="mbr-fonts-style mbr-bold mbr-section-title3 display-5">{{ role.description }}</h2>
14-
</div>
6+
class="wrap">
7+
<div class="ico-wrap">
8+
<span class="mbr-iconfont fa-compass fa" *ngIf="role.name === 'navigator'"></span>
9+
<span class="mbr-iconfont fa-keyboard-o fa" *ngIf="role.name === 'driver'"></span>
10+
</div>
11+
<div class="text-wrap vcenter">
12+
<h2 class="mbr-fonts-style mbr-bold mbr-section-title3 display-5">{{ role.description }}</h2>
1513
</div>
1614
</div>
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
import {ComponentFixture, TestBed} from '@angular/core/testing';
22

33
import {TcrRoleComponent} from './tcr-role.component';
4+
import {HttpClientTestingModule} from "@angular/common/http/testing";
5+
import {Observable, Subject} from "rxjs";
6+
import {TcrMessage} from "../../interfaces/tcr-message";
7+
import {TcrRolesService} from "../../services/trc-roles.service";
8+
import {TcrRole} from "../../interfaces/tcr-role";
49

5-
xdescribe('TcrRoleComponent', () => {
10+
class TcrRolesServiceFake implements Partial<TcrRolesService> {
11+
webSocket$: Subject<TcrMessage> = new Subject<TcrMessage>();
12+
13+
getRole(name: string): Observable<TcrRole> {
14+
return new Observable<TcrRole>();
15+
}
16+
}
17+
18+
describe('TcrRoleComponent', () => {
619
let component: TcrRoleComponent;
720
let fixture: ComponentFixture<TcrRoleComponent>;
21+
let serviceFake: TcrRolesService;
822

923
beforeEach(async () => {
1024
await TestBed.configureTestingModule({
11-
imports: [TcrRoleComponent]
12-
})
13-
.compileComponents();
25+
imports: [TcrRoleComponent, HttpClientTestingModule],
26+
providers: [
27+
{provide: TcrRolesService, useClass: TcrRolesServiceFake},
28+
]
29+
}).compileComponents();
1430

1531
fixture = TestBed.createComponent(TcrRoleComponent);
1632
component = fixture.componentInstance;
33+
serviceFake = TestBed.inject(TcrRolesService);
1734
fixture.detectChanges();
1835
});
1936

2037
it('should create', () => {
2138
expect(component).toBeTruthy();
2239
});
2340
});
41+

webapp/src/app/components/tcr-role/tcr-role.component.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@ import {toSignal} from "@angular/core/rxjs-interop";
1616
styleUrl: './tcr-role.component.css'
1717
})
1818
export class TcrRoleComponent implements OnInit {
19-
@Input() name = "";
20-
@Input() role?: TcrRole;
19+
@Input({required: true}) name = "";
20+
role?: TcrRole;
2121
roleMessage: Signal<TcrMessage | undefined>;
2222

23-
constructor(
24-
private rolesService: TcrRolesService) {
23+
constructor(private rolesService: TcrRolesService) {
2524
this.roleMessage = toSignal(this.rolesService.webSocket$);
2625

2726
effect(() => {
@@ -56,5 +55,8 @@ export class TcrRoleComponent implements OnInit {
5655
.subscribe(r => {
5756
console.log(r.name + ' set to ' + r.active);
5857
});
58+
// To make sure the role is updated in the UI even when
59+
// websocket connection is down
60+
this.getRole();
5961
}
6062
}

webapp/src/app/components/tcr-roles/tcr-roles.component.css

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
.wrap {
33
display: flex;
4-
/*background: blue;*/
54
border-radius: 0.5rem;
65
box-shadow: 7px 7px 30px -5px rgba(0, 0, 0, 0.1);
76
}

webapp/src/app/components/tcr-timer/tcr-timer.component.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {AfterViewInit, Component, effect, Input, OnInit, Signal} from '@angular/core';
1+
import {AfterViewInit, Component, effect, OnInit, Signal} from '@angular/core';
22
import {TcrMessage} from "../../interfaces/tcr-message";
33
import {TcrTimerService} from "../../services/tcr-timer.service";
44
import {toSignal} from "@angular/core/rxjs-interop";
@@ -19,17 +19,16 @@ import {NgClass, NgIf, NgStyle} from "@angular/common";
1919
styleUrl: './tcr-timer.component.css'
2020
})
2121
export class TcrTimerComponent implements OnInit, AfterViewInit {
22-
@Input() timer?: TcrTimer;
23-
@Input() progressRatio: number | undefined;
24-
@Input() remaining: number | undefined;
25-
@Input() timeout: number | undefined;
26-
@Input() fgColor: string | undefined;
22+
timer?: TcrTimer;
23+
progressRatio: number | undefined;
24+
remaining: number | undefined;
25+
timeout: number | undefined;
26+
fgColor: string | undefined;
2727
timerMessage: Signal<TcrMessage | undefined>;
2828
private syncCounter = 0;
2929
private SYNC_INTERVAL = 10;
3030

31-
constructor(
32-
private timerService: TcrTimerService) {
31+
constructor(private timerService: TcrTimerService) {
3332
this.timerMessage = toSignal(this.timerService.webSocket$);
3433

3534
effect(() => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {TestBed} from '@angular/core/testing';
2+
import {WebsocketService} from './websocket.service';
3+
import {TcrMessage, TcrMessageType} from "../interfaces/tcr-message";
4+
import {Subject} from "rxjs";
5+
import {TcrMessageService} from "./tcr-message.service";
6+
7+
class WebsocketServiceFake {
8+
webSocket$: Subject<TcrMessage> = new Subject<TcrMessage>();
9+
}
10+
11+
describe('TcrMessageService', () => {
12+
let service: TcrMessageService;
13+
let wsServiceFake: WebsocketService;
14+
15+
beforeEach(() => {
16+
TestBed.configureTestingModule({
17+
imports: [],
18+
providers: [
19+
TcrMessageService,
20+
{provide: WebsocketService, useClass: WebsocketServiceFake},
21+
]
22+
});
23+
24+
service = TestBed.inject(TcrMessageService);
25+
wsServiceFake = TestBed.inject(WebsocketService);
26+
});
27+
28+
describe('service instance', () => {
29+
30+
it('should be created', () => {
31+
expect(service).toBeTruthy();
32+
});
33+
});
34+
35+
describe('websocket message handler', () => {
36+
Object.values(TcrMessageType).forEach(type => {
37+
it(`should forward ${type} messages`, (done) => {
38+
const sampleMessage: TcrMessage = {
39+
type: type,
40+
emphasis: false,
41+
severity: "",
42+
text: "",
43+
timestamp: "",
44+
};
45+
let actual: TcrMessage | undefined;
46+
service.webSocket$.subscribe((msg) => {
47+
actual = msg;
48+
done();
49+
});
50+
wsServiceFake.webSocket$.next(sampleMessage);
51+
expect(actual).toEqual(sampleMessage);
52+
});
53+
});
54+
});
55+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {Injectable} from '@angular/core';
2+
import {Observable, retry} from "rxjs";
3+
import {TcrMessage} from "../interfaces/tcr-message";
4+
import {WebsocketService} from "./websocket.service";
5+
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
6+
7+
@Injectable({
8+
providedIn: 'root'
9+
})
10+
export class TcrMessageService {
11+
public webSocket$: Observable<TcrMessage>;
12+
13+
constructor(private ws: WebsocketService) {
14+
this.webSocket$ = this.ws.webSocket$.pipe(
15+
retry({delay: 5_000}),
16+
takeUntilDestroyed(),
17+
)
18+
}
19+
}

webapp/src/app/services/tcr-timer.service.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {Injectable} from '@angular/core';
2-
import {catchError, filter, Observable, of} from "rxjs";
2+
import {catchError, filter, Observable, of, retry} from "rxjs";
33
import {TcrMessage, TcrMessageType} from "../interfaces/tcr-message";
44
import {WebsocketService} from "./websocket.service";
55
import {HttpClient, HttpHeaders} from "@angular/common/http";
66
import {TcrTimer} from "../interfaces/tcr-timer";
7+
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
78

89
@Injectable({
910
providedIn: 'root'
@@ -12,11 +13,11 @@ export class TcrTimerService {
1213
private apiUrl = `/api`; // URL to web api
1314
public webSocket$: Observable<TcrMessage>;
1415

15-
constructor(
16-
private http: HttpClient,
17-
private ws: WebsocketService) {
16+
constructor(private http: HttpClient, private ws: WebsocketService) {
1817
this.webSocket$ = this.ws.webSocket$.pipe(
19-
filter(message => message.type === TcrMessageType.TIMER)
18+
filter(message => message.type === TcrMessageType.TIMER),
19+
retry({delay: 5_000}),
20+
takeUntilDestroyed(),
2021
)
2122
}
2223

webapp/src/app/services/trc-roles.service.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {Injectable} from '@angular/core';
22
import {HttpClient, HttpHeaders} from "@angular/common/http";
3-
import {catchError, filter, Observable, of} from "rxjs";
3+
import {catchError, filter, Observable, of, retry} from "rxjs";
44
import {TcrRole} from "../interfaces/tcr-role";
55
import {WebsocketService} from "./websocket.service";
66
import {TcrMessage, TcrMessageType} from "../interfaces/tcr-message";
7+
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
78

89
@Injectable({
910
providedIn: 'root'
@@ -12,11 +13,11 @@ export class TcrRolesService {
1213
private apiUrl = `/api`; // URL to web api
1314
public webSocket$: Observable<TcrMessage>;
1415

15-
constructor(
16-
private http: HttpClient,
17-
private ws: WebsocketService) {
16+
constructor(private http: HttpClient, private ws: WebsocketService) {
1817
this.webSocket$ = this.ws.webSocket$.pipe(
19-
filter(message => message.type === TcrMessageType.ROLE)
18+
filter(message => message.type === TcrMessageType.ROLE),
19+
retry({delay: 5_000}),
20+
takeUntilDestroyed(),
2021
)
2122
}
2223

@@ -28,10 +29,9 @@ export class TcrRolesService {
2829
})
2930
};
3031

31-
return this.http.get<TcrRole>(url, httpOptions)
32-
.pipe(
33-
catchError(this.handleError<TcrRole>('getRole'))
34-
);
32+
return this.http.get<TcrRole>(url, httpOptions).pipe(
33+
catchError(this.handleError<TcrRole>('getRole'))
34+
);
3535
}
3636

3737
activateRole(name: string, state: boolean): Observable<TcrRole> {
@@ -44,7 +44,7 @@ export class TcrRolesService {
4444
};
4545

4646
return this.http.post<TcrRole>(url, httpOptions).pipe(
47-
catchError(this.handleError<TcrRole>('activateRole'))
47+
catchError(this.handleError<TcrRole>('activateRole')),
4848
);
4949
}
5050

webapp/src/app/services/websocket.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class WebsocketService implements OnDestroy {
2626
return throwError(() => new Error(error));
2727
}),
2828
retry({delay: 5_000}),
29-
takeUntilDestroyed()
29+
takeUntilDestroyed(),
3030
);
3131
}
3232

0 commit comments

Comments
 (0)