Add No Server Connection Error message.
Increase width of test modes dropdown.
Truncate the test mode names to 15 characters.

Bug: b/433185544, b/433185869
Test: Manual
Flag: None

Change-Id: I0f80f9b069e75aedc79b4e57e0e23124e025f43f
diff --git a/tools/motion/motion_test_watcher_app/src/app/app.component.ts b/tools/motion/motion_test_watcher_app/src/app/app.component.ts
index bd22c19..4e75d45 100644
--- a/tools/motion/motion_test_watcher_app/src/app/app.component.ts
+++ b/tools/motion/motion_test_watcher_app/src/app/app.component.ts
@@ -1,12 +1,12 @@
 import { ProgressTracker } from './../util/progress';
 import { GoldensService } from './../service/goldens.service';
-import { Component, DoCheck, OnInit } from '@angular/core';
+import { Component, DoCheck, OnDestroy, OnInit } from '@angular/core';
 import { MatToolbarModule } from '@angular/material/toolbar';
 import { TestListComponent } from '../test-list/test-list.component';
 import { PreviewComponent } from '../preview/preview.component';
 import { TimelineComponent } from '../timeline/timeline.component';
 import { MotionGolden } from '../model/golden';
-import { finalize } from 'rxjs';
+import { finalize, Subscription } from 'rxjs';
 import { NgFor } from '@angular/common';
 import { JsonPipe, NgIf, NgStyle } from '@angular/common';
 import {
@@ -25,6 +25,7 @@
 import { MatMenuModule } from '@angular/material/menu';
 import { TestModeComponent } from '../testMode/test-mode.component';
 import { PreviewService } from '../service/preview.service';
+import { ErrorService } from '../service/error.service';
 import { MatSnackBar } from '@angular/material/snack-bar';
 @Component({
   selector: 'app-root',
@@ -91,15 +92,18 @@
     ])
   ]
 })
-export class AppComponent implements DoCheck, OnInit {
+export class AppComponent implements DoCheck, OnInit, OnDestroy {
   constructor(
     private goldenService: GoldensService,
     private progressTracker: ProgressTracker,
     public dialog: MatDialog,
+    private errorService: ErrorService,
     private snackBar: MatSnackBar,
     private previewService: PreviewService
     ) {}
 
+  private errorSubscription!: Subscription;
+
   isNullOrEmpty(obj : any) : Boolean {
     return (obj == null || obj.length == 0)
   }
@@ -193,6 +197,13 @@
     const leftLink = searchParams.get('leftLink') ?? ""
     const rightLink = searchParams.get('rightLink') ?? ""
 
+    this.errorSubscription = this.errorService.error$.subscribe(message => {
+      this.snackBar.open(message, undefined, {
+        horizontalPosition: 'left',
+        verticalPosition: 'bottom',
+      });
+    });
+
     if(leftLink || rightLink){
       this.testMode = "GERRIT"
       this.fetchGerritData(leftLink, rightLink)
@@ -275,4 +286,9 @@
     this.showPreviewComponent = !this.showPreviewComponent;
     this.previewService.setShowMarker(this.showPreviewComponent && this.isVideoPresent);
   }
+  ngOnDestroy() {
+    if (this.errorSubscription) {
+      this.errorSubscription.unsubscribe();
+    }
+  }
 }
diff --git a/tools/motion/motion_test_watcher_app/src/app/pipes/truncate.pipe.spec.ts b/tools/motion/motion_test_watcher_app/src/app/pipes/truncate.pipe.spec.ts
new file mode 100644
index 0000000..b16f3ef
--- /dev/null
+++ b/tools/motion/motion_test_watcher_app/src/app/pipes/truncate.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { TruncatePipe } from './truncate.pipe';
+
+describe('TruncatePipe', () => {
+  it('create an instance', () => {
+    const pipe = new TruncatePipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/tools/motion/motion_test_watcher_app/src/app/pipes/truncate.pipe.ts b/tools/motion/motion_test_watcher_app/src/app/pipes/truncate.pipe.ts
new file mode 100644
index 0000000..83ea2f0
--- /dev/null
+++ b/tools/motion/motion_test_watcher_app/src/app/pipes/truncate.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'truncate',
+  standalone: true,
+})
+export class TruncatePipe implements PipeTransform {
+  transform(value: string | null | undefined, limit: number = 15): string {
+    if (!value) {
+      return '';
+    }
+
+    return value.length > limit ? value.substring(0, limit) + '...' : value;
+  }
+}
\ No newline at end of file
diff --git a/tools/motion/motion_test_watcher_app/src/service/error.service.ts b/tools/motion/motion_test_watcher_app/src/service/error.service.ts
new file mode 100644
index 0000000..95fbf52
--- /dev/null
+++ b/tools/motion/motion_test_watcher_app/src/service/error.service.ts
@@ -0,0 +1,15 @@
+import { Injectable } from '@angular/core';
+import { Subject } from 'rxjs';
+
+@Injectable({
+  providedIn: 'root',
+})
+export class ErrorService {
+  private errorSubject = new Subject<string>();
+
+  error$ = this.errorSubject.asObservable();
+
+  handleError(message: string) {
+    this.errorSubject.next(message);
+  }
+}
\ No newline at end of file
diff --git a/tools/motion/motion_test_watcher_app/src/service/goldens.service.ts b/tools/motion/motion_test_watcher_app/src/service/goldens.service.ts
index a9ffe03..5e0d1f0 100644
--- a/tools/motion/motion_test_watcher_app/src/service/goldens.service.ts
+++ b/tools/motion/motion_test_watcher_app/src/service/goldens.service.ts
@@ -25,6 +25,8 @@
 import { VideoSource } from '../model/video-source';
 import { checkNotNull } from '../util/preconditions';
 import { Feature, recordedFeatureFactory } from '../model/feature';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { ErrorService } from './error.service';
 
 export const ACCESS_TOKEN = new InjectionToken<string>('token');
 export const SERVICE_PORT = new InjectionToken<string>('port');
@@ -36,6 +38,8 @@
 
   constructor(
     private http: HttpClient,
+    private snackBar: MatSnackBar,
+    private errorService: ErrorService,
     @Inject(ACCESS_TOKEN) config: string,
     @Inject(SERVICE_PORT) port: string
   ) {
@@ -233,7 +237,9 @@
   private handleError<T>(operation = 'operation', result?: T) {
     return (error: any): Observable<T> => {
       console.error(error);
-
+      if (error.status == 0){
+        this.errorService.handleError('Server is not connected. Run the server and try again.');
+      }
       // Let the app keep running by returning an empty result.
       return of(result as T);
     };
@@ -242,6 +248,9 @@
   private handleErrorForUpdatingGolden<T>(operation = 'operation', result?: T) {
     return (error: any): Observable<T> => {
       console.error(error);
+      if (error.status == 0){
+        this.errorService.handleError('Server is not connected. Run the server and try again.');
+      }
       const response = error.error;
       return of(response as T);
     };
diff --git a/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.html b/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.html
index a8ed01b..3a44ca2 100644
--- a/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.html
+++ b/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.html
@@ -3,7 +3,9 @@
   <div class="relative inline-block text-left">
     <div>
       <button (click)="toggleDropdown()" type="button" class="inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm text-gray-700 shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50">
-        <span class="block truncate pr-6" *ngIf="selectedMode && selectedMode.trim().length > 0">Test Mode: {{ selectedMode.toUpperCase() }}</span>
+        <span class="block truncate pr-6" *ngIf="selectedMode && selectedMode.trim().length > 0">
+          Test Mode: {{ selectedMode.toUpperCase() | truncate}}
+        </span>
         <svg class="-mr-1 size-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
           <path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
         </svg>
@@ -11,8 +13,10 @@
     </div>
 
 
-    <div *ngIf="showDropdown" class="absolute right-0 z-10 mt-2 w-56 origin-top-right divide-y divide-gray-600 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-hidden" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
-      <p *ngFor="let testMode of testModes" (click)="onActionSelected(testMode)" class="block px-4 py-2 cursor-pointer text-sm text-gray-700 bg-blue-50 hover:bg-blue-300 ">{{testMode.toUpperCase()}}</p>
+    <div *ngIf="showDropdown" class="absolute right-0 z-10 mt-2 w-100 origin-top-right divide-y divide-gray-600 rounded-md bg-white shadow-lg ring-1 ring-black/5 focus:outline-hidden" role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1">
+      <p *ngFor="let testMode of testModes" (click)="onActionSelected(testMode)" class="block px-4 py-2 cursor-pointer text-sm text-gray-700 bg-blue-50 hover:bg-blue-300 ">
+        {{testMode.toUpperCase() | truncate}}
+      </p>
     </div>
   </div>
 </div>
diff --git a/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.ts b/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.ts
index f773839..1231dfe 100644
--- a/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.ts
+++ b/tools/motion/motion_test_watcher_app/src/testMode/test-mode.component.ts
@@ -13,6 +13,7 @@
 import { MatExpansionModule } from '@angular/material/expansion';
 import { FormsModule } from '@angular/forms';
 import { MatMenuModule } from '@angular/material/menu';
+import { TruncatePipe } from '../app/pipes/truncate.pipe';
 
 @Component({
   selector: 'test-mode-list',
@@ -23,7 +24,8 @@
     FormsModule,
     MatIconModule,
     MatExpansionModule,
-    MatMenuModule
+    MatMenuModule,
+    TruncatePipe
   ],
   templateUrl: './test-mode.component.html',
 })