From 7276ae3d6ef4f119d1ce558ef0f3351e5e4dc004 Mon Sep 17 00:00:00 2001 From: DeviceInfra Date: Thu, 7 May 2026 02:57:16 -0700 Subject: [PATCH] Internal change PiperOrigin-RevId: 911851595 --- .../v6/angular/app/core/models/host_action.ts | 107 ++- .../services/config/fake_config_service.ts | 7 +- .../core/services/host/fake_host_service.ts | 104 +-- .../app/core/services/host/host_service.ts | 20 +- .../services/host/http_host_service.spec.ts | 27 +- .../core/services/host/http_host_service.ts | 23 +- .../services/mock_data/hosts/01_no_config.ts | 3 + .../mock_data/hosts/02_basic_editable.ts | 2 + .../hosts/04_pusher_properties_only.ts | 3 + .../mock_data/hosts/12_no_valid_versions.ts | 21 + .../mock_data/hosts/multi_remote_control.ts | 9 +- .../services/mock_data/hosts/overview_13.ts | 5 + .../services/mock_data/hosts/overview_14.ts | 20 + .../hosts/remote_control_permissions.ts | 11 +- .../mock_data/hosts/ui_status_utils.ts | 316 ++++++++- .../app/core/services/mock_data/index.ts | 2 + .../app/core/services/mock_data/models.ts | 6 +- .../action_no_permission_content.ng.html | 4 + .../action_no_permission_content.scss | 19 + .../action_no_permission_content.spec.ts | 51 ++ .../action_no_permission_content.ts | 31 + .../flags_dialog/flags_dialog.ng.html | 178 +++++ .../flags_dialog/flags_dialog.scss | 575 ++++++++++++++++ .../flags_dialog/flags_dialog.ts | 251 +++++++ .../host_overview/host_overview.ng.html | 59 +- .../components/host_overview/host_overview.ts | 141 +++- .../no_valid_versions_content.ng.html | 4 + .../no_valid_versions_content.scss | 19 + .../no_valid_versions_content.spec.ts | 33 + .../no_valid_versions_content.ts | 17 + .../release_dialog/release_dialog.ng.html | 310 +++++++++ .../release_dialog/release_dialog.scss | 612 ++++++++++++++++++ .../release_dialog/release_dialog.ts | 156 +++++ .../config_common/dialog/dialog.ng.html | 5 +- .../components/config_common/dialog/dialog.ts | 1 + .../confirm_dialog/confirm_dialog.ng.html | 4 +- .../confirm_dialog/confirm_dialog.scss | 4 +- .../confirm_dialog/confirm_dialog.ts | 17 +- .../service/proto/host/host_resources.proto | 55 +- .../v6/service/proto/host/host_service.proto | 27 +- .../v6/service/host/HostServiceGrpcImpl.java | 14 +- .../fe/v6/service/host/HostServiceLogic.java | 7 +- .../v6/service/host/HostServiceLogicImpl.java | 40 +- .../service/host/{builder => builders}/BUILD | 5 +- .../builders/NoOpPopularFlagsBuilder.java | 36 ++ .../NoOpRemoteControlUrlBuilder.java | 2 +- .../host/builders/PopularFlagsBuilder.java | 30 + .../RemoteControlUrlBuilder.java | 2 +- .../fe/v6/service/host/handlers/BUILD | 2 +- .../host/handlers/GetPopularFlagsHandler.java | 42 ++ .../handlers/NoOpPopularFlagsBuilder.java | 15 + ...PreflightLabServerReleaseActionHelper.java | 44 ++ ...PreflightLabServerReleaseActionHelper.java | 30 + .../PreflightLabServerReleaseHandler.java | 44 ++ .../handlers/RemoteControlDevicesHandler.java | 2 +- .../mobileharness/fe/v6/service/shared/BUILD | 3 +- .../fe/v6/service/shared/OssStubsModule.java | 14 +- .../fe/v6/service/util/FeatureReadiness.java | 2 +- .../mobileharness/fe/v6/service/host/BUILD | 3 +- .../host/HostServiceLogicImplTest.java | 53 +- .../fe/v6/service/host/handlers/BUILD | 2 +- .../PreflightLabServerReleaseHandlerTest.java | 80 +++ .../RemoteControlDevicesHandlerTest.java | 2 +- .../v6/service/util/FeatureReadinessTest.java | 5 + 64 files changed, 3509 insertions(+), 229 deletions(-) create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/12_no_valid_versions.ts create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ng.html create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.scss create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.spec.ts create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ts create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ng.html create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.scss create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ts create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ng.html create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.scss create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.spec.ts create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ts create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ng.html create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.scss create mode 100644 src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ts rename src/java/com/google/devtools/mobileharness/fe/v6/service/host/{builder => builders}/BUILD (85%) create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpPopularFlagsBuilder.java rename src/java/com/google/devtools/mobileharness/fe/v6/service/host/{builder => builders}/NoOpRemoteControlUrlBuilder.java (99%) create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/PopularFlagsBuilder.java rename src/java/com/google/devtools/mobileharness/fe/v6/service/host/{builder => builders}/RemoteControlUrlBuilder.java (99%) create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/GetPopularFlagsHandler.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPopularFlagsBuilder.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPreflightLabServerReleaseActionHelper.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseActionHelper.java create mode 100644 src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandler.java create mode 100644 src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandlerTest.java diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/models/host_action.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/models/host_action.ts index 3ca6231613..ec306c2f28 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/models/host_action.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/models/host_action.ts @@ -55,23 +55,47 @@ export declare interface PopularFlag { } /** - * A preset configuration for pass-through flags. + * Response for GetPopularFlags API. */ -export declare interface FlagPreset { - readonly label: string; - readonly value: string; - readonly description: string; +export declare interface GetPopularFlagsResponse { + readonly flags: PopularFlag[]; } +/** + * Response for PreflightLabServerRelease API. + */ +export declare interface PreflightLabServerReleaseResponse { + readonly permissionDenied?: PermissionDenied; + readonly ready?: ReleaseReady; +} + +/** + * No deploy permission. + */ +export declare interface PermissionDenied {} + +/** + * All checks passed. Here are the available versions to deploy. + */ +export declare interface ReleaseReady { + readonly versions: DeployableVersion[]; +} + +/** + * Represents the status of a Lab Server release. + */ +export type ReleaseStatus = 'Latest' | 'Current' | 'Deprecated' | ''; + /** * Configuration for a specific Lab Server release. */ -export declare interface HostReleaseConfig { +export declare interface DeployableVersion { readonly name: string; readonly version: string; - readonly port: ReleasePort; - readonly syncCMD: string[]; - readonly asyncCMD: string[]; + readonly status: ReleaseStatus; + readonly buildTime: string; + readonly ports?: ReleasePort[]; + readonly releaseDetails?: ReleaseDetails; } /** @@ -82,11 +106,76 @@ export declare interface ReleasePort { readonly portNumber: number; } +/** + * Details about a specific release. + */ +export declare interface ReleaseDetails { + readonly changeLogs?: ChangeLogGroup[]; + readonly files?: FileRecord[]; + readonly syncCommands?: CommandRecord[]; + readonly asyncCommands?: CommandRecord[]; +} + +/** + * Represents a group of changes or bugs related to a release. + */ +export declare interface ChangeLogGroup { + readonly name: string; + readonly items: ChangeLogItem[]; +} + +/** + * Represents a single change record or bug record for a release. + */ +export declare interface ChangeLogItem { + readonly change?: ChangeRecord; + readonly bug?: BugRecord; +} + +/** + * Represents a single change record for a release. + */ +export declare interface ChangeRecord { + readonly cl: number; + readonly author: string; + readonly text: string; + readonly bugs: number[]; +} + +/** + * Represents a single bug record for a release. + */ +export declare interface BugRecord { + readonly bug: number; + readonly text: string; +} + +/** + * Represents a single file record for a release. + */ +export declare interface FileRecord { + readonly name: string; + readonly path: string; +} + +/** + * Represents a single command record for a release. + */ +export declare interface CommandRecord { + readonly name: string; + readonly command: string; +} + /** * Response for DecommissionHost API. */ export declare interface DecommissionHostResponse {} +/** + * Response for UpdatePassThroughFlags API. + */ +export declare interface UpdatePassThroughFlagsResponse {} + /** * Response for those rollout action */ diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/config/fake_config_service.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/config/fake_config_service.ts index 6bd5cadc5e..cd872be708 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/config/fake_config_service.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/config/fake_config_service.ts @@ -63,7 +63,12 @@ export class FakeConfigService extends ConfigService { const isHostManaged = scenario.overview.host.name === 'host-x.example.com'; const hostName = scenario.overview.host.name; - let uiStatus: Partial | undefined; + let uiStatus: Partial | undefined = { + permissions: {visible: true, editability: {editable: true}}, + wifi: {visible: true, editability: {editable: true}}, + dimensions: {visible: true, editability: {editable: true}}, + settings: {visible: true, editability: {editable: true}}, + }; if (deviceId === 'WIFI_DIMENSIONS_ONLY_DEVICE') { uiStatus = { permissions: {visible: false}, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/fake_host_service.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/fake_host_service.ts index bb274e53ca..2ba0d5dfb0 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/fake_host_service.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/fake_host_service.ts @@ -4,14 +4,15 @@ import {Observable, of, throwError} from 'rxjs'; import { DecommissionHostResponse, GetHostDebugInfoResponse, + GetPopularFlagsResponse, HostHeaderInfo, - HostReleaseConfig, - PopularFlag, + PreflightLabServerReleaseResponse, ReleaseLabServerRequest, ReleaseLabServerResponse, RestartLabServerResponse, StartLabServerResponse, StopLabServerResponse, + UpdatePassThroughFlagsResponse, } from '../../models/host_action'; import { CheckRemoteControlEligibilityResponse, @@ -25,7 +26,10 @@ import { RemoteControlDevicesResponse, } from '../../models/host_overview'; import {MOCK_HOST_SCENARIOS} from '../mock_data'; -import {createHostActions} from '../mock_data/hosts/ui_status_utils'; +import { + createDefaultReleaseResponse, + createHostActions, +} from '../mock_data/hosts/ui_status_utils'; import {HostService} from './host_service'; /** @@ -110,34 +114,58 @@ export class FakeHostService extends HostService { }); } - override getPopularFlags(hostName: string): Observable { - return of([ - { - name: 'No Mute Android', - description: 'Disables muting of Android devices', - cmd: '--nomute_android', - }, - { - name: 'No Binary Log', - description: 'Disables binary logging to save space', - cmd: '--nobinarylog', - }, - { - name: 'Enable Linux Device', - description: 'Enables support for Linux devices', - cmd: '--enable_linux_device', - }, - ]); + override getPopularFlags( + hostName: string, + ): Observable { + return of({ + flags: [ + { + name: 'Standard Satellite', + cmd: '--nomute_android --noandroid_device_daemon', + description: 'Default configuration for Android Satellite Labs', + }, + { + name: 'Linux Support', + cmd: '--enable_linux_device', + description: 'Enables detection of Linux devices', + }, + { + name: 'Debug Mode', + cmd: '--debug_mode=true --verbose', + description: 'Enables verbose logging for debugging', + }, + { + name: 'Flashstation Cache', + cmd: '--flashstation_cache_dir=/tmp/fs_cache', + description: 'Custom cache directory for Flashstation', + }, + { + name: 'No Binary Log', + cmd: '--nobinarylog', + description: 'Disables binary logging to save space', + }, + { + name: 'Custom Flag', + cmd: `--my_message=":text: field_a: 'test' field_b: 123"`, + description: 'Custom flag for testing', + }, + { + name: 'Custom Flag 2', + cmd: `--flagD="some value"`, + description: 'Custom flag for testing 2', + }, + ], + }); } override updatePassThroughFlags( hostName: string, flags: string, - ): Observable { + ): Observable { const scenario = MOCK_HOST_SCENARIOS.find((s) => s.hostName === hostName); if (scenario && scenario.overview) { scenario.overview.labServer.passThroughFlags = flags; - return of(undefined); + return of({}); } else { return throwError( () => @@ -148,31 +176,13 @@ export class FakeHostService extends HostService { } } - override getReleaseConfigs( + override preflightLabServerRelease( hostName: string, - ): Observable { - return of([ - { - name: 'MH_SATELLITE_LAB', - version: '4.349.0', - port: {protocol: 'grpc', portNumber: 9994}, - syncCMD: [ - 'sudo systemctl stop mobileharness-lab', - 'sudo /usr/bin/mh_lab_installer --version 4.349.0', - ], - asyncCMD: ['sudo systemctl start mobileharness-lab'], - }, - { - name: 'MH_SATELLITE_LAB', - version: '4.348.0', - port: {protocol: 'grpc', portNumber: 9994}, - syncCMD: [ - 'sudo systemctl stop mobileharness-lab', - 'sudo /usr/bin/mh_lab_installer --version 4.348.0', - ], - asyncCMD: ['sudo systemctl start mobileharness-lab'], - }, - ]); + ): Observable { + const scenario = MOCK_HOST_SCENARIOS.find((s) => s.hostName === hostName); + const preflightLabServerReleaseResponse = + scenario?.releaseResponse || createDefaultReleaseResponse(); + return of(preflightLabServerReleaseResponse); } override decommissionMissingDevices( diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/host_service.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/host_service.ts index 961c2e94d9..5fff24553e 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/host_service.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/host_service.ts @@ -1,16 +1,18 @@ import {InjectionToken} from '@angular/core'; import {Observable} from 'rxjs'; + import { DecommissionHostResponse, - ReleaseLabServerRequest, - ReleaseLabServerResponse, GetHostDebugInfoResponse, + GetPopularFlagsResponse, HostHeaderInfo, - HostReleaseConfig, - PopularFlag, + PreflightLabServerReleaseResponse, + ReleaseLabServerRequest, + ReleaseLabServerResponse, RestartLabServerResponse, StartLabServerResponse, StopLabServerResponse, + UpdatePassThroughFlagsResponse, } from '../../models/host_action'; import { CheckRemoteControlEligibilityResponse, @@ -57,7 +59,9 @@ export abstract class HostService { /** * Retrieves popular pass-through flags for a host. */ - abstract getPopularFlags(hostName: string): Observable; + abstract getPopularFlags( + hostName: string, + ): Observable; /** * Updates the pass through flags for a specific host. @@ -65,12 +69,14 @@ export abstract class HostService { abstract updatePassThroughFlags( hostName: string, flags: string, - ): Observable; + ): Observable; /** * Retrieves release configurations for a host. */ - abstract getReleaseConfigs(hostName: string): Observable; + abstract preflightLabServerRelease( + hostName: string, + ): Observable; /** * Decommissions missing devices on a specific host. diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.spec.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.spec.ts index de0e31fd56..fd5d09732e 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.spec.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.spec.ts @@ -8,10 +8,12 @@ import {TestBed} from '@angular/core/testing'; import {APP_DATA, AppData} from '../../models/app_data'; import { DecommissionHostResponse, + DeployableVersion, GetHostDebugInfoResponse, + GetPopularFlagsResponse, HostHeaderInfo, - HostReleaseConfig, PopularFlag, + PreflightLabServerReleaseResponse, ReleaseLabServerRequest, ReleaseLabServerResponse, RestartLabServerResponse, @@ -133,14 +135,15 @@ describe('HttpHostService', () => { it('should retrieve popular flags', () => { const mockPopularFlags: PopularFlag[] = [{name: 'flag1'} as PopularFlag]; - service.getPopularFlags('test-host').subscribe((flags) => { - expect(flags).toEqual(mockPopularFlags); + const mockResponse: GetPopularFlagsResponse = {flags: mockPopularFlags}; + service.getPopularFlags('test-host').subscribe((response) => { + expect(response).toEqual(mockResponse); }); const req = httpMock.expectOne( 'http://testdomain.com/v6/hosts/test-host/popular-flags', ); expect(req.request.method).toBe('GET'); - req.flush(mockPopularFlags); + req.flush(mockResponse); }); it('should update pass-through flags', () => { @@ -154,17 +157,19 @@ describe('HttpHostService', () => { }); it('should retrieve release configs', () => { - const mockConfigs: HostReleaseConfig[] = [ - {name: 'config1'} as HostReleaseConfig, - ]; - service.getReleaseConfigs('test-host').subscribe((configs) => { - expect(configs).toEqual(mockConfigs); + const mockResponse: PreflightLabServerReleaseResponse = { + ready: { + versions: [{name: 'config1'} as DeployableVersion], + }, + }; + service.preflightLabServerRelease('test-host').subscribe((response) => { + expect(response).toEqual(mockResponse); }); const req = httpMock.expectOne( - 'http://testdomain.com/v6/hosts/test-host/release-configs', + `http://testdomain.com/v6/hosts/test-host/preflightLabServerRelease`, ); expect(req.request.method).toBe('GET'); - req.flush(mockConfigs); + req.flush(mockResponse); }); it('should decommission host', () => { diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.ts index 75dbf00e14..1f4a6c5564 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/host/http_host_service.ts @@ -8,14 +8,15 @@ import {APP_DATA, AppData} from '../../models/app_data'; import { DecommissionHostResponse, GetHostDebugInfoResponse, + GetPopularFlagsResponse, HostHeaderInfo, - HostReleaseConfig, - PopularFlag, + PreflightLabServerReleaseResponse, ReleaseLabServerRequest, ReleaseLabServerResponse, RestartLabServerResponse, StartLabServerResponse, StopLabServerResponse, + UpdatePassThroughFlagsResponse, } from '../../models/host_action'; import { CheckRemoteControlEligibilityResponse, @@ -67,8 +68,10 @@ export class HttpHostService extends HostService { ); } - override getPopularFlags(hostName: string): Observable { - return this.http.get( + override getPopularFlags( + hostName: string, + ): Observable { + return this.http.get( `${this.apiUrl}/${hostName}/popular-flags`, ); } @@ -76,8 +79,8 @@ export class HttpHostService extends HostService { override updatePassThroughFlags( hostName: string, flags: string, - ): Observable { - return this.http.post( + ): Observable { + return this.http.post( `${this.apiUrl}/${hostName}/updatePassThroughFlags`, { flags, @@ -85,11 +88,11 @@ export class HttpHostService extends HostService { ); } - override getReleaseConfigs( + override preflightLabServerRelease( hostName: string, - ): Observable { - return this.http.get( - `${this.apiUrl}/${hostName}/release-configs`, + ): Observable { + return this.http.get( + `${this.apiUrl}/${hostName}/preflightLabServerRelease`, ); } diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/01_no_config.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/01_no_config.ts index a207055e84..ba227a58e5 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/01_no_config.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/01_no_config.ts @@ -18,4 +18,7 @@ export const SCENARIO_HOST_NO_CONFIG: MockHostScenario = { }, defaultDeviceConfig: null, actions: createHostActions(), + releaseResponse: { + permissionDenied: {}, + }, }; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/02_basic_editable.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/02_basic_editable.ts index cc612c2a85..4ff47ef106 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/02_basic_editable.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/02_basic_editable.ts @@ -5,6 +5,7 @@ import {HostConfig} from '../../../models/host_config_models'; import {MockHostScenario} from '../models'; import { createDefaultHostOverview, + createDefaultReleaseResponse, createDefaultUiStatus, createHostActions, } from './ui_status_utils'; @@ -44,4 +45,5 @@ export const SCENARIO_HOST_BASIC_EDITABLE: MockHostScenario = { }, defaultDeviceConfig: DEFAULT_DEVICE_CONFIG, actions: createHostActions(), + releaseResponse: createDefaultReleaseResponse(), }; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/04_pusher_properties_only.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/04_pusher_properties_only.ts index 1b9cc87a57..71eba0a95b 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/04_pusher_properties_only.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/04_pusher_properties_only.ts @@ -55,4 +55,7 @@ export const SCENARIO_HOST_PUSHER_PROPERTIES: MockHostScenario = { }, defaultDeviceConfig: DEFAULT_DEVICE_CONFIG, actions: createHostActions(), + releaseResponse: { + permissionDenied: {}, + }, }; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/12_no_valid_versions.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/12_no_valid_versions.ts new file mode 100644 index 0000000000..8e422bcdcb --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/12_no_valid_versions.ts @@ -0,0 +1,21 @@ +/** @fileoverview Mock host scenario with no valid versions for release. */ + +import {MockHostScenario} from '../models'; +import {SCENARIO_HOST_BASIC_EDITABLE} from './02_basic_editable'; +import {createDefaultHostOverview} from './ui_status_utils'; + +/** + * Represents a mock host scenario where there are no valid versions for release. + */ +export const SCENARIO_HOST_NO_VALID_VERSIONS: MockHostScenario = { + ...SCENARIO_HOST_BASIC_EDITABLE, + hostName: 'no-valid-versions.host.example.com', + scenarioName: '12. No Valid Versions', + overview: createDefaultHostOverview('no-valid-versions.host.example.com'), + deviceSummaries: [], + releaseResponse: { + ready: { + versions: [], + }, + }, +}; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/multi_remote_control.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/multi_remote_control.ts index e983d3a08f..c3c8b77bee 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/multi_remote_control.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/multi_remote_control.ts @@ -6,7 +6,11 @@ import { HostOverview, } from '../../../models/host_overview'; import {MockHostScenario} from '../models'; -import {createDefaultUiStatus, createHostActions} from './ui_status_utils'; +import { + createDefaultUiStatus, + createDeviceActions, + createHostActions, +} from './ui_status_utils'; // Helper to create a basic HostOverview function createHostOverview( @@ -278,6 +282,7 @@ const DEVICES_PROXY_MISMATCH: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 8', version: '14', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'RC-PROXY-MISMATCH-2', @@ -292,6 +297,7 @@ const DEVICES_PROXY_MISMATCH: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 8 Pro', version: '21', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'RC-NO-PROXY', @@ -302,6 +308,7 @@ const DEVICES_PROXY_MISMATCH: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 8 Pro', version: '21', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'RC-TESTBED-NO-PROXY', diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_13.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_13.ts index 93adfa7333..7b3aa4995e 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_13.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_13.ts @@ -4,6 +4,7 @@ import {MockHostScenario} from '../models'; import { createDefaultHostOverview, createDefaultUiStatus, + createDeviceActions, createHostActions, } from './ui_status_utils'; @@ -55,6 +56,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:missing', model: 'Pixel 6', version: '13', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'MISSING-DEV-2', @@ -69,6 +71,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:missing', model: 'Pixel 6 Pro', version: '13', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'MISSING-DEV-3', @@ -83,6 +86,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:missing', model: 'Pixel 7', version: '14', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'ACTIVE-DEV-4', @@ -97,6 +101,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:active', model: 'Pixel 8', version: '14', + actions: createDeviceActions('ALL_PERMISSIONS'), }, ]; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_14.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_14.ts index 8c182855ed..4491bbec18 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_14.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/overview_14.ts @@ -4,6 +4,7 @@ import {MockHostScenario} from '../models'; import { createDefaultHostOverview, createDefaultUiStatus, + createDeviceActions, createHostActions, } from './ui_status_utils'; @@ -122,6 +123,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2A311FDH300A2H', @@ -156,6 +158,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2A311FDH300AAZ', @@ -190,6 +193,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B011FDH300169', @@ -224,6 +228,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B011FDH300FDY', @@ -258,6 +263,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B011FDH300FFC', @@ -292,6 +298,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH30015J', @@ -326,6 +333,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH3007X2', @@ -360,6 +368,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH300833', @@ -394,6 +403,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH30087D', @@ -428,6 +438,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH300891', @@ -462,6 +473,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH3008EP', @@ -496,6 +508,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH3008GN', @@ -513,6 +526,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: '', version: '', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH3008GP', @@ -547,6 +561,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: '2B021FDH3008MN', @@ -581,6 +596,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: 'pixel 7 pro', version: '36', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'at1-ab7.atc.google.com:33487', @@ -598,6 +614,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: '', version: '', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'at1-ab7.atc.google.com:35137', @@ -615,6 +632,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: '', version: '', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'at1-ab7.atc.google.com:35555', @@ -632,6 +650,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: '', version: '', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'at1-ab7.atc.google.com:36735', @@ -649,6 +668,7 @@ const deviceSummaries: DeviceSummary[] = [ requiredDims: 'pool:android-test-executor', model: '', version: '', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'at1-ab7.atc.google.com:38191', diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/remote_control_permissions.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/remote_control_permissions.ts index 0e6f9485b5..ef3e78bb4c 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/remote_control_permissions.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/remote_control_permissions.ts @@ -6,7 +6,11 @@ import { HostOverview, } from '../../../models/host_overview'; import {MockHostScenario} from '../models'; -import {createDefaultUiStatus, createHostActions} from './ui_status_utils'; +import { + createDefaultUiStatus, + createDeviceActions, + createHostActions, +} from './ui_status_utils'; // Helper to create a basic HostOverview function createHostOverview( @@ -58,6 +62,7 @@ const DEVICES_PERMISSIONS_ALL: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 8', version: '14', + actions: createDeviceActions('NO_PERMISSION'), }, { id: 'RC-NO-PERM-2', @@ -68,6 +73,7 @@ const DEVICES_PERMISSIONS_ALL: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 8 Pro', version: '14', + actions: createDeviceActions('NO_PERMISSION'), }, // 2. Device with USER permission only @@ -80,6 +86,7 @@ const DEVICES_PERMISSIONS_ALL: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 7', version: '13', + actions: createDeviceActions('USER_PERMISSION'), }, // 3. Device with GROUP permission only @@ -92,6 +99,7 @@ const DEVICES_PERMISSIONS_ALL: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 7 Pro', version: '13', + actions: createDeviceActions('GROUP_PERMISSION'), }, // 4. Device with BOTH/ALL permissions @@ -104,6 +112,7 @@ const DEVICES_PERMISSIONS_ALL: DeviceSummary[] = [ requiredDims: '', model: 'Pixel 6', version: '12', + actions: createDeviceActions('ALL_PERMISSIONS'), }, { id: 'RC-INVALID-ineligible-busy', diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/ui_status_utils.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/ui_status_utils.ts index 644cd6b2e4..0d0189edcc 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/ui_status_utils.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/hosts/ui_status_utils.ts @@ -2,7 +2,13 @@ * @fileoverview Utility functions for creating UI status objects for mock data. */ -import {HostActions, LabServerActions} from '../../../models/host_action'; +import {DeviceActions} from '../../../models/device_action'; +import { + HostActions, + LabServerActions, + PreflightLabServerReleaseResponse, + ReleaseDetails, +} from '../../../models/host_action'; import { Editability, HostConfigUiStatus, @@ -48,6 +54,251 @@ export function createPartStatus( }; } +function createDefaultReleaseDetails(): ReleaseDetails { + return { + changeLogs: [ + { + name: 'Bug Fixes', + items: [ + { + change: { + cl: 858423150, + author: 'test_author', + text: 'test_text', + bugs: [123456789], + }, + }, + ], + }, + ], + syncCommands: [ + { + name: 'test_sync_command', + command: 'test_command', + }, + ], + }; +} + +/** + * Creates default release configs for mock scenarios. + */ +export function createDefaultReleaseResponse(): PreflightLabServerReleaseResponse { + return { + ready: { + versions: [ + { + name: 'mobileharness_lab_server', + version: 'v4.349.0', + status: 'Latest', + buildTime: '2024-03-15 21:30:00', + ports: [{protocol: 'grpc', portNumber: 9994}], + releaseDetails: { + changeLogs: [ + { + name: 'Bug Fixes', + items: [ + { + change: { + cl: 858423150, + author: 'huiliu', + text: 'Prevent lab-resolved files from being escaped in KickOffTestRequest.', + bugs: [443716027, 468693971], + }, + }, + { + change: { + cl: 858945464, + author: 'huiliu', + text: 'Resolve test files cannot be sent issue.', + bugs: [468693971], + }, + }, + ], + }, + { + name: 'Incremental Changes', + items: [ + { + change: { + cl: 858343140, + author: 'huiliu', + text: 'Support to set different master spec for different jobs in one session.', + bugs: [476920060], + }, + }, + ], + }, + { + name: 'Deprecated', + items: [ + { + change: { + cl: 858854515, + author: 'zeek', + text: 'Remove the logic to limit iOS simulator count based on disk type.', + bugs: [473739492], + }, + }, + ], + }, + ], + syncCommands: [ + { + name: 'copy_tf_config', + command: + 'cp -f @android_tradefed_test_config /usr/local/google/mobileharness/android_tradefed_test_config_key', + }, + { + name: 'copy_tf_credential', + command: + 'cp -f @android_tradefed_credential_key /usr/local/google/mobileharness/android_tradefed_test_key', + }, + { + name: 'prepare_dual_home_lab_dir', + command: + 'mkdir -p /etc/dual_home_lab/agent && chmod -R 775 /etc/dual_home_lab/', + }, + { + name: 'copy_credential', + command: + 'cp -f @udcluster_daemon_credential /etc/dual_home_lab/agent/dual-home-lab.json', + }, + { + name: 'remove_mh_default_credential', + command: + 'rm -f /usr/local/google/mobileharness/mh_default_credential.json', + }, + { + name: 'copy_robo_credential', + command: + 'cp -f @default_robo_credential /usr/local/google/mobileharness/default_robo_credential.json', + }, + { + name: 'copy_file_transfer_credential', + command: + 'cp -f @file_transfer_credential /usr/local/google/mobileharness/file_transfer_credential.json', + }, + { + name: 'copy_web_flashstation_key', + command: + 'cp -f @web_flashstation_key /usr/local/google/mobileharness/web_flashstation_key.json', + }, + { + name: 'add_read_permission_of_credential', + command: 'chmod +r /etc/dual_home_lab/agent/dual-home-lab.json', + }, + { + name: 'process_before_killing', + command: 'ps xao pid,ppid,pgid,command', + }, + { + name: 'kill_zombie_lab_script', + command: '@legacy_daemon_killer', + }, + { + name: 'process_after_killing', + command: 'ps xao pid,ppid,pgid,command', + }, + { + name: 'clean_previous_lab_server', + command: + '/usr/local/buildtools/java/jdk21/bin/java --uid= -loas_pwd_fallback_in_corp -jar @cleaner', + }, + ], + asyncCommands: [ + { + name: 'start_command_server', + command: + '@command_server_binary --launcher_javabase=/usr/local/buildtools/java/jdk21 --uid= -loas_pwd_fallback_in_corp run', + }, + { + name: 'start_mobileharness_lab_server', + command: + '@binary --launcher_javabase=/usr/local/buildtools/java/jdk21 --uid= -loas_pwd_fallback_in_corp -Xms3g -Xmx8g run ...', + }, + ], + files: [ + { + name: 'kill_zombie_lab_script', + path: 'gs://release-resource/kill_zombie_lab.sh', + }, + { + name: 'command_server_binary', + path: 'gs://release-resource/mobileharness/command_server_deploy.jar', + }, + { + name: 'binary', + path: 'gs://release-resource/mobileharness/lab_server_deploy_4.349.0.jar', + }, + { + name: 'cleaner', + path: 'gs://release-resource/mobileharness/lab_cleaner_deploy_4.349.0.jar', + }, + { + name: 'web_flashstation_key', + path: 'gs://release-resource/keys/mh_web_flashstation.json', + }, + { + name: 'legacy_daemon_killer', + path: 'gs://release-resource/kill_legacy_daemon_server.sh', + }, + { + name: 'android_tradefed_test_config', + path: 'gs://release-resource/keys/android_tradefed_test_config_key', + }, + ], + }, + }, + { + name: 'release_configs', + version: 'v4.358.0', + status: '', + buildTime: '2025-03-14 12:00:00', + ports: [ + { + protocol: 'TCP', + portNumber: 8080, + }, + { + protocol: 'TCP', + portNumber: 8081, + }, + ], + releaseDetails: createDefaultReleaseDetails(), + }, + { + name: 'release_configs', + version: 'v4.357.0', + status: 'Current', + buildTime: '2025-03-13 12:00:00', + ports: [ + { + protocol: 'TCP', + portNumber: 8080, + }, + ], + releaseDetails: createDefaultReleaseDetails(), + }, + { + name: 'release_configs', + version: 'v4.356.0', + status: '', + buildTime: '2025-03-12 12:00:00', + releaseDetails: createDefaultReleaseDetails(), + }, + { + name: 'Host Release Config', + version: 'v4.355.0', + status: '', + buildTime: '2025-03-11 12:00:00', + releaseDetails: createDefaultReleaseDetails(), + }, + ], + }, + }; +} + /** * Creates a default HostOverview with basic running status. */ @@ -103,6 +354,67 @@ export function createDefaultHostOverview(hostName: string): HostOverview { }; } +/** + * Creates default device actions for mock scenarios. + */ +export function createDeviceActions( + permissionState: + | 'NO_PERMISSION' + | 'USER_PERMISSION' + | 'GROUP_PERMISSION' + | 'ALL_PERMISSIONS' = 'ALL_PERMISSIONS', +): DeviceActions { + const enabled = permissionState !== 'NO_PERMISSION'; + return { + remoteControl: { + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + decommission: { + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + configuration: { + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + screenshot: { + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + logcat: { + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + flash: { + params: { + deviceType: 'AndroidRealDevice', + requiredDimensions: 'required_dimensions', + }, + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + quarantine: { + enabled, + visible: true, + tooltip: permissionState, + isReady: true, + }, + }; +} + /** * Creates HostActions based on the host state. * @param status The status of the host (RUNNING, STOPPED, MISSING, ERROR, etc.) @@ -164,7 +476,7 @@ export function createLabServerActions( : isCoreLab ? 'Cannot release in a Shared Lab' : '', - isReady: false, + isReady: true, }, start: { enabled: !isRunning && !isMissing && actionEnabled, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/index.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/index.ts index d50b3cea22..13432aab96 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/index.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/index.ts @@ -42,6 +42,7 @@ import {SCENARIO_HOST_DISCOVERY_HIDDEN} from './hosts/08_device_discovery_hidden import {SCENARIO_HOST_DEVICE_CONFIG_HIDDEN} from './hosts/09_device_config_hidden'; import {SCENARIO_HOST_DEVICE_CONFIG_WIFI_DIMENSIONS_ONLY} from './hosts/10_device_config_wifi_dimensions_only'; import {SCENARIO_HOST_COMING_SOON} from './hosts/11_coming_soon'; +import {SCENARIO_HOST_NO_VALID_VERSIONS} from './hosts/12_no_valid_versions'; import {SCENARIO_HOST_X_PROD} from './hosts/host_x_prod'; import {SCENARIO_HOST_Z_PROD} from './hosts/host_z_prod'; import { @@ -117,6 +118,7 @@ export const MOCK_HOST_SCENARIOS: MockHostScenario[] = [ SCENARIO_HOST_X_PROD, SCENARIO_HOST_Z_PROD, SCENARIO_HOST_COMING_SOON, + SCENARIO_HOST_NO_VALID_VERSIONS, SCENARIO_RC_ALL_VALID, SCENARIO_RC_MIXED_ALL, SCENARIO_RC_PROXY_MISMATCH, diff --git a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/models.ts b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/models.ts index 83e1a801ba..ecdb2e9e97 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/models.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/models.ts @@ -9,7 +9,10 @@ import { RecoveryTaskStats, TestResultStats, } from '../../models/device_stats'; -import {HostActions} from '../../models/host_action'; +import { + HostActions, + PreflightLabServerReleaseResponse, +} from '../../models/host_action'; import {GetHostConfigResult} from '../../models/host_config_models'; import {DeviceSummary, HostOverview} from '../../models/host_overview'; @@ -51,4 +54,5 @@ export interface MockHostScenario { overview?: HostOverview; deviceSummaries?: DeviceSummary[]; actions: HostActions; + releaseResponse?: PreflightLabServerReleaseResponse; } diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ng.html new file mode 100644 index 0000000000..e8e62dc90e --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ng.html @@ -0,0 +1,4 @@ +
+

You don't have the permission to perform this action on {{hostName}}.

+

Please check access in the host permission page or contact the host administrator.

+
diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.scss b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.scss new file mode 100644 index 0000000000..cf0dafe97e --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.scss @@ -0,0 +1,19 @@ +.permission-box { + display: flex; + flex-direction: column; + gap: 1.25rem; + + + p { + text-align: center; + color: #4b5563; + font-size: 0.875rem; + line-height: 1.625; + margin: 0; + max-width: 28rem; + } + + .permission-page-link { + cursor: pointer; + } +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.spec.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.spec.ts new file mode 100644 index 0000000000..0b95144277 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.spec.ts @@ -0,0 +1,51 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialog} from '@angular/material/dialog'; +import { + MatTestDialogOpener, + MatTestDialogOpenerModule, +} from '@angular/material/dialog/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; + +import {ActionNoPermissionContent} from './action_no_permission_content'; + +describe('ActionNoPermissionContent', () => { + let component: ActionNoPermissionContent; + let fixture: ComponentFixture>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ActionNoPermissionContent, + NoopAnimationsModule, + MatTestDialogOpenerModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent( + MatTestDialogOpener.withComponent(ActionNoPermissionContent), + ); + fixture.detectChanges(); + component = fixture.componentInstance.dialogRef.componentInstance; + component.hostName = 'test-host'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display a no permission message', () => { + const contentEl = document.querySelector( + 'app-action-no-permission-content', + ); + expect(contentEl?.textContent).toContain("don't have the permission"); + }); + + it('should close dialog when navigating to permissions page', () => { + const matDialog = TestBed.inject(MatDialog); + spyOn(matDialog, 'open'); + spyOn(fixture.componentInstance.dialogRef, 'close'); + component.navigateToPermissionsPage(); + expect(fixture.componentInstance.dialogRef.close).toHaveBeenCalled(); + }); +}); diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ts new file mode 100644 index 0000000000..55d835bd23 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/action_no_permission_content/action_no_permission_content.ts @@ -0,0 +1,31 @@ +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; +import {HostConfig} from '../../host_config/host_config'; + +/** + * Component to display "no permission" message for host actions. + */ +@Component({ + selector: 'app-action-no-permission-content', + standalone: true, + imports: [CommonModule, MatIconModule], + templateUrl: './action_no_permission_content.ng.html', + styleUrls: ['./action_no_permission_content.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ActionNoPermissionContent { + @Input({required: true}) hostName!: string; + + private readonly dialog = inject(MatDialog); + private readonly dialogRef = inject(MatDialogRef); + + navigateToPermissionsPage() { + this.dialog.open(HostConfig, { + data: {hostName: this.hostName}, + autoFocus: false, + }); + this.dialogRef.close(); + } +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ng.html new file mode 100644 index 0000000000..cdd0aaa2b6 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ng.html @@ -0,0 +1,178 @@ + +
+ tune +
+ +
+ +
+ +
+

Configure additional startup arguments for the Lab Server binary.

+
+ + +
+
+ +
+ + @if (isListMode()) { +
+ +
+ + add + + + +
+ + +
+ Current Flags + +
+ +
+ @if (currentFlagsArray().length === 0) { +
+
+ data_array +
+

No flags configured

+

Use the input above or choose a preset from the side to get started.

+
+ } @else { +
    + @for (flag of displayedFlags(); track $index) { +
  • +
    + {{ flag.name }} + @if (flag.description) { +
    + subdirectory_arrow_right + {{ flag.description }} +
    + } +
    + +
  • + } +
+ } +
+
+ } @else { + +
+ +
+ } +
+
+ + +
+
+
+ auto_awesome +

Quick Presets

+
+

Common configurations for rapid input.

+
+
+ @if (isLoadingPresets()) { +
+ +
+ } @else if (presets().length === 0) { +
+ No recommended flags found. +
+ } @else if (filteredPresets().length === 0) { +
+ No matching presets found. +
+ } @else { + @for (preset of displayedPresets(); track preset.name) { +
+
+ {{ preset.name }} + +
+
{{ preset.cmd }}
+
{{ preset.description }}
+
+ } + } +
+
+
+
+ + + + +
diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.scss b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.scss new file mode 100644 index 0000000000..ed646dd446 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.scss @@ -0,0 +1,575 @@ +@use '@angular/material' as mat; +@use '../../../../../shared/styles/common.scss' as common; + +.header-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 0.75rem; + background-color: #eff6ff; + color: #2563eb; + margin-right: 12px; + border: 1px solid #dbeafe; + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } +} + +.flags-dialog-wrapper { + @include common.shared-scrollbar-style(); + + .flags-dialog-body { + display: flex; + height: 100%; + overflow: hidden; + letter-spacing: normal; + } + + .flags-dialog-editor-col { + flex: 1; + display: flex; + flex-direction: column; + padding: 1.5rem; + overflow: hidden; + } + + .flags-dialog-mode-switcher { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + + .description-text { + font-size: 0.8125rem; + color: #4b5563; + font-weight: 500; + } + + .mode-buttons-group { + display: flex; + border: 1px solid #d1d5db; + border-radius: 9999px; + overflow: hidden; + height: 2.25rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + + .mode-btn { + flex: 1; + min-width: 7.5rem; + padding: 0 1.25rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + white-space: nowrap; + font-weight: 500; + transition: all 0.2s; + background: transparent; + border: none; + cursor: pointer; + color: #4b5563; + + mat-icon { + font-size: 1.125rem; + width: 1.125rem; + height: 1.125rem; + margin-right: 0.25rem; + } + + &:first-child { + border-right: 1px solid #d1d5db; + } + + &:hover:not(.active) { + background-color: #f9fafb; + } + + &.active { + background-color: #dbeafe; + color: #1e3a8a; + } + } + } + } + + .editor-container { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + background-color: rgba(248, 250, 252, 0.5); + border-radius: 1rem; + border: 1px solid #e2e8f0; + overflow: hidden; + position: relative; + } + + .list-view-container { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + padding: 1rem; + + .add-input-row { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + + .flag-form-field { + flex: 1; + + mat-icon { + font-size: 1.125rem; + width: 1.125rem; + height: 1.125rem; + padding-right: 0.25rem; + } + } + + .add-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.75rem; + min-width: 2.75rem; + padding: 0; + background-color: #eff6ff; + color: #2563eb; + border-radius: 0.5rem; + border: 1px solid #dbeafe; + cursor: pointer; + transition: all 0.2s; + + &:hover:not(:disabled) { + background-color: #2563eb; + color: white; + } + + &:disabled { + background-color: #f9fafb; + color: #d1d5db; + border-color: #e5e7eb; + cursor: not-allowed; + } + + mat-icon { + margin: 0; + } + } + } + + .list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + padding: 0 0.25rem; + + .title { + font-size: 0.75rem; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .clear-btn { + font-size: 0.6875rem; + font-weight: 700; + color: #dc2626; + background: transparent; + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + + &:hover { + background-color: #fee2e2; + } + + mat-icon { + font-size: 0.875rem; + width: 0.875rem; + height: 0.875rem; + } + } + } + + .flags-list-wrapper { + flex: 1; + overflow-y: auto; + border-radius: 0.75rem; + border: 1px solid #e5e7eb; + background-color: white; + + .empty-state { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + letter-spacing: normal; + + .icon-circle { + width: 3.5rem; + height: 3.5rem; + background-color: #f9fafb; + color: #d1d5db; + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + + mat-icon { + font-size: 1.75rem; + width: 1.75rem; + height: 1.75rem; + } + } + + .empty-title { + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 500; + color: #111827; + } + + .empty-subtitle { + font-size: 0.75rem; + line-height: 1rem; + color: #6b7280; + margin-top: 0.25rem; + max-width: 12.5rem; + } + } + + .divide-y { + list-style: none; + padding: 0; + margin: 0; + + .flag-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #f3f4f6; + + &:last-child { + border-bottom: none; + } + + &:hover { + background-color: rgba(239, 246, 255, 0.3); + } + + .flag-content { + min-width: 0; + flex-grow: 1; + padding-right: 1rem; + } + + .flag-text { + font-family: monospace; + font-size: 0.8125rem; + color: #111827; + font-weight: 500; + word-break: break-all; + } + + .flag-desc { + font-size: 0.625rem; + color: #6b7280; + display: flex; + align-items: center; + gap: 0.25rem; + + mat-icon { + font-size: 0.875rem; + width: 0.875rem; + height: 0.875rem; + color: #9ca3af; + } + + span { + line-height: normal; + } + } + + .delete-btn { + color: #d1d5db; + background: transparent; + border: none; + cursor: pointer; + padding: 0.375rem; + border-radius: 0.5rem; + transition: all 0.2s; + height: 2.5rem; + + &:hover { + color: #ef4444; + background-color: #fee2e2; + } + + mat-icon { + font-size: 1.125rem; + width: 1.125rem; + height: 1.125rem; + } + } + } + } + } + } + + .textarea-container { + flex: 1; + display: flex; + flex-direction: column; + padding: 1rem; + height: 100%; + + .flags-textarea { + height: 100%; + padding: 1.25rem; + background-color: #0f172a; + color: #93c5fd; + font-family: monospace; + font-size: 0.875rem; + border-radius: 0.75rem; + border: 1px solid #1e293b; + resize: none; + line-height: 1.625; + + &:focus { + outline: none; + border-color: #2563eb; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); + } + } + } + + .flags-dialog-presets-col { + width: 20rem; + background-color: rgba(249, 250, 251, 0.8); + border-left: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + overflow: hidden; + + .presets-header { + padding: 1.25rem; + border-bottom: 1px solid #e5e7eb; + background-color: rgba(255, 255, 255, 0.5); + backdrop-filter: blur(4px); + flex-shrink: 0; + + .header-title { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; + + h3 { + font-weight: 700; + color: #1f2937; + font-size: 0.75rem; + margin: 0; + } + + mat-icon { + color: #f59e0b; + font-size: 1.125rem; + width: 1.125rem; + height: 1.125rem; + } + } + + .header-subtitle { + font-size: 0.625rem; + color: #6b7280; + line-height: 1.25; + margin-bottom: 0; + } + } + + .presets-list { + padding: 1rem; + overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + gap: 0.75rem; + + .loading-spinner { + display: flex; + justify-content: center; + padding: 2rem 0; + } + + .empty-presets { + text-align: center; + font-size: 0.75rem; + color: #9ca3af; + padding: 3rem 1rem; + font-style: italic; + } + + .preset-card { + background-color: white; + border: 1px solid #e5e7eb; + border-radius: 0.75rem; + padding: 0.75rem; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + transition: all 0.2s; + + &:hover { + border-color: #bfdbfe; + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.5rem; + + .preset-name { + font-weight: 700; + color: #1f2937; + font-size: 0.6875rem; + line-height: 1.25; + padding-right: 0.5rem; + } + + .append-btn { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + background-color: #eff6ff; + color: #2563eb; + border-radius: 0.5rem; + border: 1px solid #dbeafe; + cursor: pointer; + transition: all 0.2s; + + &:hover:not(:disabled) { + background-color: #2563eb; + color: white; + } + + &:disabled { + background-color: #f3f4f6; + color: #9ca3af; + border-color: #e5e7eb; + cursor: not-allowed; + } + + mat-icon { + font-size: 0.875rem; + width: 0.875rem; + height: 0.875rem; + } + } + } + + .preset-cmd { + font-family: monospace; + font-size: 0.5625rem; + color: #4b5563; + background-color: #f9fafb; + padding: 0.5rem; + border-radius: 0.25rem; + border: 1px solid #f3f4f6; + margin-bottom: 0.5rem; + word-break: break-all; + line-height: 1.4; + } + + .preset-desc { + font-size: 0.5625rem; + color: #9ca3af; + line-height: 1.25; + font-style: italic; + } + } + } + } +} + +.footer-actions { + display: flex; + justify-content: flex-end; + align-items: center; + width: 100%; + gap: 0.5rem; + + button { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; + } + + .primary-button { + background-color: #0b57d0; + color: white; + border: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + + &:hover:not(:disabled) { + background-color: #0a4cb8; + } + + &:disabled { + background-color: #e2e8f0; + color: #94a3b8; + cursor: not-allowed; + } + } + + .secondary-button { + background-color: white; + color: #0b57d0; + border: 1px solid #c2c2c2; + + &:hover:not(:disabled) { + background-color: #f0f5fe; + } + + &:disabled { + background-color: white; + color: #94a3b8; + border-color: #e2e8f0; + cursor: not-allowed; + } + } + + .tertiary-button { + background-color: transparent; + color: #475569; + border: none; + + &:hover { + background-color: #f1f5f9; + } + } +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ts new file mode 100644 index 0000000000..9ae58b97f1 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ts @@ -0,0 +1,251 @@ +import {CommonModule} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + OnInit, + signal, + ViewEncapsulation, +} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef, +} from '@angular/material/dialog'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {take} from 'rxjs/operators'; + +import {PopularFlag} from '../../../../../core/models/host_action'; +import {HOST_SERVICE} from '../../../../../core/services/host/host_service'; +import {Dialog} from '../../../../../shared/components/config_common/dialog/dialog'; + +/** + * Structure for passing data including host identity and current flags to FlagsDialog. + */ +export interface FlagsDialogData { + hostName: string; + currentFlags: string; +} +/** + * Handles configuration and synchronization operations for Pass-through Flags in the Lab Console. + */ +@Component({ + selector: 'app-flags-dialog', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatProgressSpinnerModule, + Dialog, + ], + templateUrl: './flags_dialog.ng.html', + styleUrl: './flags_dialog.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, +}) +export class FlagsDialog implements OnInit { + readonly data = inject(MAT_DIALOG_DATA); + private readonly dialogRef = inject(MatDialogRef); + private readonly hostService = inject(HOST_SERVICE); + + readonly isListMode = signal(true); + readonly currentFlagsArray = signal([]); + readonly initialFlagsArray = signal([]); + readonly presets = signal([]); + readonly isLoadingPresets = signal(false); + readonly isSaving = signal(false); + readonly errorMessage = signal(''); + readonly addInput = signal(''); + readonly filterText = signal(''); + readonly rawTextFlags = signal(''); + + readonly isDirty = computed(() => { + const currentFlags = this.isListMode() + ? this.currentFlagsArray().join(' ') + : this.rawTextFlags(); + const normalizedCurrentStr = this.splitFlags(currentFlags).join(' '); + const normalizedInitialStr = this.initialFlagsArray().join(' '); + return normalizedCurrentStr !== normalizedInitialStr; + }); + + readonly filteredPresets = computed(() => { + const filter = (this.addInput() || '').toLowerCase(); + if (!filter) return this.presets(); + return this.presets().filter( + (p) => + (p.name || '').toLowerCase().includes(filter) || + (p.description || '').toLowerCase().includes(filter) || + (p.cmd || '').toLowerCase().includes(filter), + ); + }); + + private splitFlags(input: string): string[] { + if (!input) return []; + return input + .trim() + .split(/\s+(?=--)/) + .filter(Boolean); + } + + readonly flagDescriptions = computed(() => { + const entries = this.presets().flatMap((p) => { + const flags = this.splitFlags(p.cmd); + const desc = flags.length === 1 ? p.description : `Part of: ${p.name}`; + return flags.map((f) => [f, desc] as [string, string]); + }); + return new Map(entries); + }); + + readonly displayedFlags = computed(() => { + const map = this.flagDescriptions(); + return this.currentFlagsArray().map((flag) => { + let description = map.get(flag) || ''; + if (!description) { + const baseFlag = flag.split('=')[0]; + description = map.get(baseFlag) || ''; + } + return {name: flag, description}; + }); + }); + + readonly displayedPresets = computed(() => { + const activeFlags = this.isListMode() + ? this.currentFlagsArray() + : this.splitFlags(this.rawTextFlags()); + const currentSet = new Set(activeFlags); + return this.filteredPresets().map((p) => { + const presetFlags = this.splitFlags(p.cmd); + const isAdded = + presetFlags.length > 0 && presetFlags.every((f) => currentSet.has(f)); + return {...p, isAdded}; + }); + }); + + ngOnInit() { + this.parseInitialFlags(); + this.loadPresets(); + } + + parseInitialFlags() { + const flags = this.data.currentFlags + ? this.splitFlags(this.data.currentFlags) + : []; + this.rawTextFlags.set(this.data.currentFlags); + this.currentFlagsArray.set([...flags]); + this.initialFlagsArray.set([...flags]); + } + + updateFlagsFromText(text: string) { + this.rawTextFlags.set(text); + } + + loadPresets() { + this.isLoadingPresets.set(true); + this.hostService + .getPopularFlags(this.data.hostName) + .pipe(take(1)) + .subscribe({ + next: (response) => { + this.presets.set(response?.flags || []); + this.isLoadingPresets.set(false); + }, + error: () => { + this.isLoadingPresets.set(false); + }, + }); + } + + toggleMode(isList: boolean) { + if (this.isListMode() && !isList) { + // Switching from List to Text: Update rawTextFlags from current array + this.rawTextFlags.set(this.currentFlagsArray().join(' ')); + } else if (!this.isListMode() && isList) { + // Switching from Text to List: Parse rawTextFlags into currentFlagsArray + const flags = this.rawTextFlags() + ? this.splitFlags(this.rawTextFlags()) + : []; + this.currentFlagsArray.set(flags); + } + this.isListMode.set(isList); + } + + addFlag() { + const val = this.addInput().trim(); + if (val) { + const newFlags = this.splitFlags(val); + this.currentFlagsArray.update((flags) => [...flags, ...newFlags]); + this.addInput.set(''); + } + } + + removeFlag(index: number) { + this.currentFlagsArray.update((flags) => + flags.filter((_, i) => i !== index), + ); + } + + appendPreset(preset: PopularFlag) { + if (this.isListMode()) { + const newFlags = this.splitFlags(preset.cmd); + this.currentFlagsArray.update((flags) => [...flags, ...newFlags]); + } else { + this.rawTextFlags.update((text) => { + const trimmed = text.trim(); + return trimmed ? `${trimmed} ${preset.cmd}` : preset.cmd; + }); + } + } + + clearAll() { + this.currentFlagsArray.set([]); + } + + discardChanges() { + this.rawTextFlags.set(this.initialFlagsArray().join(' ')); + this.currentFlagsArray.set([...this.initialFlagsArray()]); + } + + save() { + const finalString = this.isListMode() + ? this.currentFlagsArray().join(' ').trim() + : this.rawTextFlags().trim(); + + // If in text mode, re-parse to ensure currentFlagsArray is in sync + if (!this.isListMode()) { + const flags = finalString ? this.splitFlags(finalString) : []; + this.currentFlagsArray.set(flags); + } + + this.isSaving.set(true); + this.errorMessage.set(''); + + this.hostService + .updatePassThroughFlags(this.data.hostName, finalString) + .pipe(take(1)) + .subscribe({ + next: () => { + this.isSaving.set(false); + this.dialogRef.close(finalString); + }, + error: (err: Error) => { + this.isSaving.set(false); + this.errorMessage.set(err.message || 'Failed to save flags'); + }, + }); + } + + close() { + this.dialogRef.close(); + } +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ng.html index 5af7fecef3..9c60c9544d 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ng.html +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ng.html @@ -47,10 +47,14 @@ @if (actions.release.visible) { } @@ -141,43 +145,22 @@ >help_outline -
- @if(isEditingFlags()) { -
- -
- - -
-
+
+ @if (passThroughFlags()) { +
{{ passThroughFlags() }}
} @else { - @if (host.labServer.passThroughFlags) { -
{{ host.labServer.passThroughFlags }}
- } @else { - None - } - @if (updateFlags.visible) { - - } + None + } + + @if (updateFlags.visible) { + }
} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ts index 5f02a049fb..2f25ed267b 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/host_overview.ts @@ -69,7 +69,11 @@ import {RemoteControlService} from '../../../../shared/services/remote_control_s import {SnackBarService} from '../../../../shared/services/snackbar_service'; import {dateUtils} from '../../../../shared/utils/date_utils'; import {objectUtils} from '../../../../shared/utils/object_utils'; +import {ActionNoPermissionContent} from './action_no_permission_content/action_no_permission_content'; import {DecommissionContent} from './decommission_content/decommission_content'; +import {FlagsDialog} from './flags_dialog/flags_dialog'; +import {NoValidVersionsContent} from './no_valid_versions_content/no_valid_versions_content'; +import {ReleaseDialog} from './release_dialog/release_dialog'; const HEALTH_SEMANTIC_MAP: Record< string, @@ -197,9 +201,7 @@ export class HostOverviewPage implements OnChanges { ); }); - isEditingFlags = signal(false); - editedFlags = ''; - isSavingFlags = signal(false); + readonly passThroughFlags = signal(''); deviceDataSource = new MatTableDataSource(); selection = new SelectionModel(true, []); @@ -220,6 +222,7 @@ export class HostOverviewPage implements OnChanges { deviceFilterValue = ''; isDeviceLoading = signal(false); expandedElement = signal(null); + isOpeningReleaseDialog = signal(false); // --- Dimensions Overlay State --- activeOverlay = signal<{ @@ -355,6 +358,7 @@ export class HostOverviewPage implements OnChanges { ngOnChanges(changes: SimpleChanges) { if (changes['host']) { this.selection.clear(); + this.passThroughFlags.set(this.host.labServer.passThroughFlags); this.loadDevices(); } } @@ -398,50 +402,45 @@ export class HostOverviewPage implements OnChanges { } // Flags editing - startEditFlags() { - this.editedFlags = this.host.labServer.passThroughFlags; - this.isEditingFlags.set(true); - } - - cancelEditFlags() { - this.isEditingFlags.set(false); - } - - saveFlags() { - if (this.editedFlags === this.host.labServer.passThroughFlags) { - this.isEditingFlags.set(false); - return; - } + openFlagsDialog() { + const dialogRef = this.dialog.open(FlagsDialog, { + data: { + hostName: this.host.hostName, + currentFlags: this.passThroughFlags(), + }, + width: '72rem', + maxHeight: '90vh', + autoFocus: false, + }); - this.isSavingFlags.set(true); - this.hostService - .updatePassThroughFlags(this.host.hostName, this.editedFlags) + dialogRef + .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - next: () => { - this.host.labServer.passThroughFlags = this.editedFlags; - this.isEditingFlags.set(false); - this.isSavingFlags.set(false); + .subscribe((result) => { + if ( + typeof result === 'string' && + result !== 'close' && + result !== 'cancel' + ) { + this.passThroughFlags.set(result); + this.host.labServer.passThroughFlags = result; this.showRestartDialog(); - }, - error: (err) => { - this.snackBar.showError(`Failed to save flags: ${err.message}`); - this.isSavingFlags.set(false); - }, + } }); } showRestartDialog() { const dialogData = { - title: 'Restart Required', - content: `Pass through flags for ${this.host.hostName} have been updated. A Lab Server restart is required for changes to take effect. Restart now?`, - type: 'warning', - primaryButtonLabel: 'Restart Now', + title: 'Flags Updated', + content: `Pass-through flags have been updated successfully. Would you like to perform a release now to apply these changes?`, + customIcon: 'rocket_launch', + primaryButtonLabel: 'Release Now', secondaryButtonLabel: 'Later', }; const restartDialogRef = this.dialog.open(ConfirmDialog, { data: dialogData, + panelClass: 'confirm-dialog-panel', disableClose: true, }); @@ -456,7 +455,79 @@ export class HostOverviewPage implements OnChanges { } onRelease() { - this.showComingSoonPopup('Release'); + this.isOpeningReleaseDialog.set(true); + this.hostService + .preflightLabServerRelease(this.host.hostName) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (response) => { + this.isOpeningReleaseDialog.set(false); + if (response.permissionDenied) { + const dialogData = { + title: 'No Access', + contentComponent: ActionNoPermissionContent, + contentComponentInputs: { + hostName: this.host.hostName, + }, + type: 'error', + customIcon: 'lock_outline', + primaryButtonLabel: 'Close', + }; + + this.dialog.open(ConfirmDialog, { + data: dialogData, + panelClass: 'confirm-dialog-panel', + }); + + return; + } + + const versions = response.ready?.versions; + if (!versions || versions.length === 0) { + const dialogData = { + title: 'No Valid Versions', + contentComponent: NoValidVersionsContent, + contentComponentInputs: { + hostName: this.host.hostName, + }, + type: 'warning', + customIcon: 'warning_amber', + primaryButtonLabel: 'Close', + }; + + this.dialog.open(ConfirmDialog, { + data: dialogData, + panelClass: 'confirm-dialog-panel', + }); + + return; + } + + const releaseDialogRef = this.dialog.open(ReleaseDialog, { + data: { + hostName: this.host.hostName, + releaseConfigs: versions, + passThroughFlags: this.passThroughFlags, + }, + autoFocus: false, + }); + + releaseDialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result) => { + if (!result) { + return; + } + }); + }, + error: (err) => { + this.isOpeningReleaseDialog.set(false); + this.snackBar.showError( + `Failed to load release info: ${err.message}`, + ); + }, + }); } onDeploy() { diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ng.html new file mode 100644 index 0000000000..dcdbc135d4 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ng.html @@ -0,0 +1,4 @@ +
+

No valid release versions were found for {{hostName}}, or the version data could not be retrieved.

+

Please try again later or contact the host administrator if the issue persists.

+
diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.scss b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.scss new file mode 100644 index 0000000000..e58e2e7a32 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.scss @@ -0,0 +1,19 @@ +.no-valid-versions-content { + display: flex; + flex-direction: column; + gap: 1.25rem; + + + p { + text-align: center; + color: #4b5563; + font-size: 0.875rem; + line-height: 1.625; + margin: 0; + max-width: 28rem; + } + + .no-valid-versions-content-link { + cursor: pointer; + } +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.spec.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.spec.ts new file mode 100644 index 0000000000..4218bd318a --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.spec.ts @@ -0,0 +1,33 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; + +import {NoValidVersionsContent} from './no_valid_versions_content'; + +describe('NoValidVersionsContent', () => { + let component: NoValidVersionsContent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NoValidVersionsContent, NoopAnimationsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(NoValidVersionsContent); + component = fixture.componentInstance; + component.hostName = 'test-host'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display the no valid versions message with hostname', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain( + 'No valid release versions were found for test-host', + ); + const strongEl = compiled.querySelector('strong'); + expect(strongEl?.textContent).toContain('test-host'); + }); +}); diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ts new file mode 100644 index 0000000000..faa3479018 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/no_valid_versions_content/no_valid_versions_content.ts @@ -0,0 +1,17 @@ +import {CommonModule} from '@angular/common'; +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; + +/** + * Component to display "no valid release versions" message. + */ +@Component({ + selector: 'app-no-valid-versions-content', + standalone: true, + imports: [CommonModule], + templateUrl: './no_valid_versions_content.ng.html', + styleUrls: ['./no_valid_versions_content.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NoValidVersionsContent { + @Input({required: true}) hostName!: string; +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ng.html new file mode 100644 index 0000000000..1464d81e9e --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ng.html @@ -0,0 +1,310 @@ + + + @if (currentStep() === 1) { + +
+
+ + info_outline + + + Select a release candidate to proceed. + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ Version + +
+ {{ element.version }} +
+
+ {{ element.name }} +
+
+ Build Date + + {{ element.buildTime | date:'shortDate' }} + + Status + + @if (element.status?.toLowerCase() === 'latest') { + + Latest + + } + @if (element.status?.toLowerCase() === 'current') { + + Current + + } + + +
+
+ +
+
+ + Build Manifest + + +
+
+ + + +
+
+ @if (currentTab() === 'metadata') { + + } + @if (currentTab() === 'files') { +
+ @defer (on idle) { + @for (file of selectedVersion()?.releaseDetails?.files; track file.name) { +
+
{{ file.name }}
+ @if (file.path) { +
{{ file.path }}
+ } @else { +
Not set
+ } +
+ } @empty { +
No Files
+ } + } @placeholder { +
+ Loading files... +
+ } +
+ } + @if (currentTab() === 'commands') { +
+ @if (hasCommands()) { + @if (hasSyncCommands()) { +
Sync Commands
+ @for (cmd of selectedVersion()?.releaseDetails?.syncCommands; track cmd.name) { + {{ cmd.name }} + {{ cmd.command }} + } + } + @if (hasAsyncCommands()) { +
Async Commands
+ @for (cmd of selectedVersion()?.releaseDetails?.asyncCommands; track cmd.name) { + {{ cmd.name }} + {{ cmd.command }} + } + } + } @else { +
No Commands
+ } +
+ } +
+
+
+
+ } @else if (currentStep() === 2) { + +
+
+
rocket_launch
+
+

Ready to Deploy

+

Please review your deployment configuration before proceeding.

+
+
+
+
+
label
+
+
Selected Version
+
+ {{ selectedVersion()?.version }} + @if (selectedVersion()?.status?.toLowerCase() === 'latest') { + Latest + } +
+
+
+
+
Build Date
+
{{ selectedVersion()?.buildTime | date:'mediumDate' }}
+
+
+
+
+
+ Pass-through Flags + Optional +
+ +
+ @if (tempFlags()) { +
+ {{ tempFlags() }} + @if (flagsModifiedThisSession()) { +
+ info + Modified this session +
+ } +
+ } @else { +
+
not_interested
+

No flags specified.

+ Click here to use example args (Optional) +
+ } +
+
+ } +
+ + + +
diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.scss b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.scss new file mode 100644 index 0000000000..38f52d49e2 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.scss @@ -0,0 +1,612 @@ +@use '@angular/material' as mat; +@use '../../../../../shared/styles/common.scss' as common; +@use '../../../../../shared/styles/typography.scss' as typography; + +:host { + @include mat.table-overrides(( + header-container-height: 3rem, + )); +} + +.dialog-content { + @include common.shared-scrollbar-style(); + flex-grow: 1; + overflow-y: auto; + padding: 1.5rem; + background-color: #f8fafc; +} + +.info-alert { + display: flex; + align-items: center; + gap: 0.5rem; + background-color: #eff6ff; + color: #1d4ed8; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid #dbeafe; + margin-bottom: 1rem; + + span { + font-weight: 500; + font-size: 0.875rem; + line-height: 1.25rem; + } +} + +.content-splitter { + display: flex; + align-items: stretch; + overflow: hidden; + height: 500px; +} + +.versions-panel { + flex-grow: 1; + overflow-y: auto; + border: 1px solid #e2e8f0; + border-radius: 0.75rem; + background-color: white; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + + .versions-table { + width: 100%; + border-collapse: collapse; + text-align: left; + + th { + background-color: #f8fafc; + padding: 0.75rem 1rem; + font-size: 0.75rem; + font-weight: 600; + color: #64748b; + border-bottom: 1px solid #e2e8f0; + position: sticky; + top: 0; + z-index: 10; + } + + td { + padding: 0.75rem 1rem; + border-bottom: 1px solid #f1f5f9; + font-size: 0.875rem; + } + + .version-row { + cursor: pointer; + transition: background-color 0.2s; + + &:hover { + background-color: #f8fafc; + } + + &.selected { + background-color: #eff6ff; + + .version-label { + color: #1e40af; + } + } + } + + .version-label { + font-weight: 700; + color: #0f172a; + } + + .version-name { + font-size: 0.75rem; + color: #94a3b8; + margin-top: 0.125rem; + } + + .status-badge { + font-size: 0.75rem; + font-weight: 700; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + border: 1px solid transparent; + + &.latest { + background-color: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } + + &.current { + background-color: #dbeafe; + color: #1d4ed8; + border-color: #bfdbfe; + } + } + + .row-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.5rem; + + button[mat-icon-button] { + color: #2563eb; + opacity: 1; + } + + button[mat-flat-button] { + background-color: #2563eb; + color: white; + } + } + } +} + +.manifest-panel { + width: 0; + opacity: 0; + border: 1px solid #e2e8f0; + border-radius: 0.75rem; + background-color: white; + display: flex; + flex-direction: column; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + letter-spacing: normal; + + &.open { + transform: translateX(0); + width: 400px; + margin-left: 1rem; + opacity: 1; + } + + .manifest-header { + padding: 0 0 0 1rem; + background-color: #f8fafc; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; + font-weight: 700; + color: #334155; + flex-shrink: 0; + } + + .manifest-tabs { + display: flex; + border-bottom: 1px solid #e2e8f0; + flex-shrink: 0; + + .tab-btn { + flex-grow: 1; + padding: 0.5rem; + font-size: 0.75rem; + font-weight: 700; + color: #94a3b8; + border: none; + border-bottom: 2px solid transparent; + background: none; + cursor: pointer; + transition: all 0.2s; + + &:hover { + color: #64748b; + } + + &.active { + color: #1d4ed8; + border-bottom-color: #2563eb; + background-color: #eff6ff; + } + } + } + + .manifest-content { + flex-grow: 1; + overflow-y: auto; + padding: 1rem; + } + + .metadata-content { + display: flex; + flex-direction: column; + gap: 1rem; + + .detail-item { + display: flex; + gap: 0.75rem; + align-items: flex-start; + + mat-icon { + color: #2563eb; + font-size: 1.25rem; + width: 1.25rem; + height: 1.25rem; + } + + .label { + font-size: 0.75rem; + font-weight: 700; + color: #94a3b8; + } + + .value { + font-size: 0.875rem; + font-weight: 700; + color: #334155; + } + } + + .changelist-section { + margin-top: 0.5rem; + border-top: 1px solid #f1f5f9; + padding-top: 0.75rem; + + .section-title { + font-size: 0.75rem; + font-weight: 700; + color: #94a3b8; + margin-bottom: 0.5rem; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + + li { + font-size: 0.75rem; + line-height: 1.4; + color: #334155; + } + + .cl-number { + font-family: monospace; + font-weight: 700; + color: #2563eb; + } + + .bug-number { + color: #94a3b8; + } + } + } + } + + .files-content { + display: flex; + flex-direction: column; + gap: 0.75rem; + + .file-item { + padding: 0.75rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + + .file-key { + font-size: 0.75rem; + font-family: monospace; + font-weight: 700; + color: #2563eb; + margin-bottom: 0.25rem; + } + + .file-value { + font-size: 0.75rem; + font-family: monospace; + color: #475569; + word-break: break-all; + + &.not-set { + color: #d1d5db; + font-style: italic; + } + } + } + } + + .commands-content { + .section-title { + font-size: 0.75rem; + font-weight: 700; + color: #94a3b8; + } + + .command-name { + font-size: 0.75rem; + font-weight: 700; + color: #6b7280; + margin-bottom: 0.25rem; + } + + .command-block { + display: block; + background-color: #0f172a; + color: #93c5fd; + padding: 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-family: monospace; + word-break: break-all; + margin-bottom: 0.5rem; + border-left: 2px solid #2563eb; + + &.async { + border-left-color: #8b5cf6; + color: #c7d2fe; + } + } + } + + .empty-text { + font-size: 0.875rem; + font-weight: 500; + color: #9ca3af; + text-align: center; + } +} + +.step-2-container { + max-width: 600px; + margin: 0 auto; + padding-top: 1rem; + letter-spacing: normal; + + .step-2-header { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 2rem; + + .header-icon { + width: 3rem; + height: 3rem; + background-color: #eff6ff; + color: #2563eb; + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #dbeafe; + + mat-icon { + font-size: 1.5rem; + width: 1.5rem; + height: 1.5rem; + } + } + + h3 { + font-size: 1.5rem; + font-weight: 700; + color: #0f172a; + margin: 0; + } + + p { + font-size: 0.875rem; + color: #64748b; + margin: 0.25rem 0 0 0; + } + } +} + + +.version-summary-card { + background-color: white; + border: 1px solid #e2e8f0; + border-radius: 1rem; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + + .left-section { + display: flex; + gap: 1rem; + align-items: center; + } + + .icon-bg { + width: 2.5rem; + height: 2.5rem; + background-color: #f1f5f9; + color: #94a3b8; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + } + + .label { + font-size: 0.625rem; + font-weight: 700; + color: #9ca3af; + letter-spacing: normal; + } + + .value { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.125rem; + + .version-str { + font-size: 1.25rem; + font-family: monospace; + font-weight: 700; + color: #0f172a; + } + } + + .right-section { + text-align: right; + + .value { + justify-content: flex-end; + font-size: 0.875rem; + font-weight: 600; + color: #475569; + } + } +} + +.flags-section { + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + } + + + + .title-section { + display: flex; + gap: 0.5rem; + align-items: center; + } + + .header-title { + font-size: 0.75rem; + line-height: 1rem; + font-weight: 700; + color: #374151; + } + + .optional-badge { + font-size: 0.625rem; + color: #9ca3af; + height: 21px; + padding: 0 0.375rem; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; + border: 1px solid #e2e8f0; + background-color: #f9fafb; + box-sizing: border-box; + font-weight: 500; + } + + .add-flags-button { + color: #2563eb; + font-size: 0.75rem; + line-height: 1rem; + letter-spacing: normal; + font-weight: 700; + } + + .flags-code-block { + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.75rem; + padding: 1rem; + font-family: monospace; + font-size: 0.75rem; + color: #475569; + word-break: break-all; + max-height: 160px; + overflow-y: auto; + + .modified-label { + display: flex; + align-items: center; + gap: 0.25rem; + color: #d97706; + font-weight: 500; + font-size: 0.685rem; + font-family: typography.$labconsole-font; + margin-top: 0.5rem; + border-top: 1px solid #e2e8f066; + padding-top: 0.5rem; + + mat-icon { + font-size: 0.685rem; + height: 0.685rem; + width: 0.685rem; + color: #d97706; + } + } + } + + .empty-flags-box { + padding: 2rem; + border: 2px dashed #e2e8f0; + border-radius: 1rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + cursor: pointer; + transition: border-color 0.2s; + + &:hover { + border-color: #cbd5e1; + } + + .icon-circle { + width: 2.5rem; + height: 2.5rem; + background-color: #f8fafc; + color: #cbd5e1; + border-radius: 9999px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.75rem; + } + + p { + font-size: 0.875rem; + font-weight: 500; + color: #94a3b8; + margin: 0; + } + + a { + color: #3b82f6; + font-weight: 700; + font-size: 0.75rem; + text-decoration: none; + } + } +} + +.footer-actions { + display: flex; + gap: 0.75rem; + align-items: center; + + .primary-button { + @include common.shared-button('primary'); + + mat-icon { + height: 1rem; + width: 1rem; + font-size: 1rem; + margin-left: 0.25rem; + } + } + + .secondary-button { + @include common.shared-button('secondary'); + } + + .tertiary-button { + @include common.shared-button('tertiary'); + border-color: transparent; + } + + .mr-2 { + margin-right: 0.5rem; + } +} + + diff --git a/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ts b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ts new file mode 100644 index 0000000000..5a24e9d019 --- /dev/null +++ b/src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/release_dialog/release_dialog.ts @@ -0,0 +1,156 @@ +import {CommonModule} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + OnInit, + signal, + WritableSignal, +} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogModule, +} from '@angular/material/dialog'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTableModule} from '@angular/material/table'; + +import {DeployableVersion} from 'app/core/models/host_action'; +import {FlagsDialog} from 'app/features/host_detail/components/host_overview/flags_dialog/flags_dialog'; +import {Dialog} from 'app/shared/components/config_common/dialog/dialog'; +import {SnackBarService} from 'app/shared/services/snackbar_service'; + +/** + * Data passed to the ReleaseDialog component. + */ +export interface ReleaseDialogData { + hostName: string; + releaseConfigs?: DeployableVersion[]; + passThroughFlags: WritableSignal; +} + +/** + * Component for displaying and managing host releases. + */ +@Component({ + selector: 'app-release-dialog', + standalone: true, + templateUrl: './release_dialog.ng.html', + styleUrl: './release_dialog.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + Dialog, + MatTableModule, + CommonModule, + FormsModule, + MatDialogModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatButtonModule, + MatProgressSpinnerModule, + ], +}) +export class ReleaseDialog implements OnInit { + readonly data: ReleaseDialogData = inject(MAT_DIALOG_DATA); + private readonly snackBar = inject(SnackBarService); + private readonly dialog = inject(MatDialog); + + displayedColumns: string[] = ['version', 'buildTime', 'status', 'actions']; + + readonly currentStep = signal(1); + readonly selectedVersion = signal(null); + + readonly hasSyncCommands = computed(() => { + return !!this.selectedVersion()?.releaseDetails?.syncCommands?.length; + }); + + readonly hasAsyncCommands = computed(() => { + return !!this.selectedVersion()?.releaseDetails?.asyncCommands?.length; + }); + + readonly hasCommands = computed(() => { + return this.hasSyncCommands() || this.hasAsyncCommands(); + }); + readonly currentTab = signal('metadata'); + readonly tempFlags = signal(''); + readonly flagsModifiedThisSession = signal(false); + readonly showDetails = signal(false); + readonly isDeploying = signal(false); + + availableVersions: DeployableVersion[] = []; + + ngOnInit() { + this.availableVersions = this.data.releaseConfigs || []; + this.tempFlags.set(this.data.passThroughFlags()); + } + + selectVersion(version: DeployableVersion) { + this.selectedVersion.set(version); + } + + viewDetails(version: DeployableVersion) { + const current = this.selectedVersion(); + if (current?.version === version.version && this.showDetails()) { + this.showDetails.set(false); + } else { + this.selectedVersion.set(version); + this.showDetails.set(true); + } + } + + switchTab(tab: string) { + this.currentTab.set(tab); + } + + proceed() { + if (this.selectedVersion()) { + this.currentStep.set(2); + } + } + + back() { + this.currentStep.set(1); + } + + deploy() { + this.isDeploying.set(true); + // Simulate deployment delay + setTimeout(() => { + this.isDeploying.set(false); + this.snackBar.showSuccess( + `Deployed ${this.selectedVersion()?.version} successfully`, + ); + }, 1500); + } + + openFlagsDialog() { + const dialogRef = this.dialog.open(FlagsDialog, { + data: { + hostName: this.data.hostName, + currentFlags: this.tempFlags(), + }, + width: '72rem', + maxHeight: '90vh', + autoFocus: false, + }); + + dialogRef.afterClosed().subscribe((result) => { + // Only update if the result is a string and not the explicit 'close' signal. + if (typeof result === 'string' && result !== 'close') { + if (result !== this.tempFlags()) { + this.flagsModifiedThisSession.set(true); + this.snackBar.showSuccess('Pass-through flags updated successfully'); + } + this.data.passThroughFlags.set(result); + this.tempFlags.set(result); + } + }); + } +} diff --git a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ng.html index e7edd4bc42..8ef9f9de52 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ng.html +++ b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ng.html @@ -1,6 +1,7 @@
-
+
+

{{ title }}

@@ -9,7 +10,7 @@

{{ title }}

-
diff --git a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ts b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ts index 4e855d226b..ab87d3faae 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/config_common/dialog/dialog.ts @@ -38,6 +38,7 @@ import {Footer} from '../footer/footer'; export class Dialog implements OnInit { @Input() width = '48rem'; @Input() height = ''; + @Input() maxHeight = ''; @Input() title = ''; @Input() subtitle = ''; diff --git a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ng.html b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ng.html index 4353cc6e01..65df80a570 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ng.html +++ b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ng.html @@ -1,7 +1,7 @@
- - {{ iconUI.icon }} + + {{ iconUI().icon }}

{{ data.title }}

diff --git a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.scss b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.scss index 4fb807eda0..6383568b0b 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.scss +++ b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.scss @@ -7,7 +7,7 @@ @include mat.dialog-overrides(( container-shape: .5rem, - container-max-width: 35rem, + container-max-width: fit-content, )); } @@ -431,7 +431,7 @@ } .info-icon { - color: #1976d2; + color: #2563eb; } .warning-icon { diff --git a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ts b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ts index 89ea6f64ec..8589658eb4 100644 --- a/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ts +++ b/src/devtools/mobileharness/fe/v6/angular/app/shared/components/confirm_dialog/confirm_dialog.ts @@ -2,6 +2,7 @@ import {CommonModule} from '@angular/common'; import { ChangeDetectionStrategy, Component, + computed, inject, OnInit, signal, @@ -46,6 +47,7 @@ export class ConfirmDialog implements OnInit { primaryButtonIcon?: string; secondaryButtonLabel?: string; onConfirm?: () => Observable; + customIcon?: string; }>(MAT_DIALOG_DATA); private readonly dialogRef = inject(MatDialogRef); @@ -78,8 +80,17 @@ export class ConfirmDialog implements OnInit { } } - get iconUI() { - switch (this.data.type) { + readonly iconUI = computed(() => { + const type = this.data.type || 'info'; + if (this.data.customIcon) { + return { + icon: this.data.customIcon, + iconColorClass: `${type}-icon`, + iconBgColorClass: `bg-${type === 'error' ? 'red' : type === 'warning' ? 'yellow' : type === 'success' ? 'green' : 'gray'}-100`, + }; + } + + switch (type) { case 'info': return { icon: 'info', @@ -106,5 +117,5 @@ export class ConfirmDialog implements OnInit { iconBgColorClass: 'bg-red-100', }; } - } + }); } diff --git a/src/devtools/mobileharness/fe/v6/service/proto/host/host_resources.proto b/src/devtools/mobileharness/fe/v6/service/proto/host/host_resources.proto index 870bcb1fd0..7bc27301f7 100644 --- a/src/devtools/mobileharness/fe/v6/service/proto/host/host_resources.proto +++ b/src/devtools/mobileharness/fe/v6/service/proto/host/host_resources.proto @@ -111,20 +111,53 @@ message PopularFlag { string cmd = 3; } -// A preset configuration for pass-through flags. -message FlagPreset { - string label = 1; - string value = 2; - string description = 3; +message ChangeRecord { + int64 cl = 1; + string author = 2; + string text = 3; + repeated int32 bugs = 4; +} + +message BugRecord { + int32 bug = 1; + string text = 2; } -// Configuration for a specific Lab Server release. -message HostReleaseConfig { +message ChangeLogGroup { string name = 1; - string version = 2; - ReleasePort port = 3; - repeated string sync_cmd = 4; - repeated string async_cmd = 5; + message Item { + oneof type { + ChangeRecord change = 1; + BugRecord bug = 2; + } + } + repeated Item items = 2; +} + +message FileRecord { + string name = 1; + string path = 2; +} + +message CommandRecord { + string name = 1; + string command = 2; +} + +message ReleaseDetails { + repeated ChangeLogGroup change_logs = 1; + repeated FileRecord files = 2; + repeated CommandRecord sync_commands = 3; + repeated CommandRecord async_commands = 4; +} + +message DeployableVersion { + string name = 1; // Release name + string version = 2; // Version string (e.g. "v4.358.0") + string status = 3; // "Latest", "Current", or empty + string build_time = 4; // Build timestamp + repeated ReleasePort ports = 5; + ReleaseDetails release_details = 6; } // Port configuration for a release. diff --git a/src/devtools/mobileharness/fe/v6/service/proto/host/host_service.proto b/src/devtools/mobileharness/fe/v6/service/proto/host/host_service.proto index 68be9ac7e5..1f52f94039 100644 --- a/src/devtools/mobileharness/fe/v6/service/proto/host/host_service.proto +++ b/src/devtools/mobileharness/fe/v6/service/proto/host/host_service.proto @@ -76,10 +76,10 @@ service HostService { } // Retrieves release configurations for a host. - rpc GetReleaseConfigs(GetReleaseConfigsRequest) - returns (GetReleaseConfigsResponse) { + rpc PreflightLabServerRelease(PreflightLabServerReleaseRequest) + returns (PreflightLabServerReleaseResponse) { option (google.api.http) = { - get: "/v6/hosts/{host_name}/release-configs" + get: "/v6/hosts/{host_name}/preflightLabServerRelease" }; } @@ -229,17 +229,28 @@ message UpdatePassThroughFlagsRequest { // Response message for UpdatePassThroughFlags. message UpdatePassThroughFlagsResponse {} -// Request message for GetReleaseConfigs. -message GetReleaseConfigsRequest { +// Request message for PreflightLabServerRelease. +message PreflightLabServerReleaseRequest { string host_name = 1; // The universe of the host. string universe = 2; } -// Response message for GetReleaseConfigs. -message GetReleaseConfigsResponse { - repeated HostReleaseConfig configs = 1; +// Response message for PreflightLabServerRelease. +message PreflightLabServerReleaseResponse { + // No deploy permission. + message PermissionDenied {} + + // All checks passed. Here are the available versions to deploy. + message ReleaseReady { + repeated DeployableVersion versions = 1; + } + + oneof result { + PermissionDenied permission_denied = 1; + ReleaseReady ready = 2; + } } // Request message for DecommissionMissingDevices. diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceGrpcImpl.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceGrpcImpl.java index fd31b2ae74..38160425e2 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceGrpcImpl.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceGrpcImpl.java @@ -32,11 +32,11 @@ import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetHostOverviewRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; -import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetReleaseConfigsRequest; -import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetReleaseConfigsResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostHeaderInfo; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostOverviewPageData; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostServiceGrpc; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.ReleaseLabServerRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.ReleaseLabServerResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDevicesRequest; @@ -140,16 +140,16 @@ public void updatePassThroughFlags( } @Override - public void getReleaseConfigs( - GetReleaseConfigsRequest request, - StreamObserver responseObserver) { + public void preflightLabServerRelease( + PreflightLabServerReleaseRequest request, + StreamObserver responseObserver) { GrpcServiceUtil.invokeAsync( request, responseObserver, - logic::getReleaseConfigs, + req -> logic.preflightLabServerRelease(req, Optional.empty()), executor, HostServiceGrpc.getServiceDescriptor(), - HostServiceGrpc.getGetReleaseConfigsMethod()); + HostServiceGrpc.getPreflightLabServerReleaseMethod()); } @Override diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogic.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogic.java index f18afc9bce..62383d80ea 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogic.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogic.java @@ -31,10 +31,10 @@ import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetHostOverviewRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; -import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetReleaseConfigsRequest; -import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetReleaseConfigsResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostHeaderInfo; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostOverviewPageData; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.ReleaseLabServerRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.ReleaseLabServerResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDevicesRequest; @@ -65,7 +65,8 @@ ListenableFuture getHostDeviceSummaries( ListenableFuture updatePassThroughFlags( UpdatePassThroughFlagsRequest request); - ListenableFuture getReleaseConfigs(GetReleaseConfigsRequest request); + ListenableFuture preflightLabServerRelease( + PreflightLabServerReleaseRequest request, Optional username); ListenableFuture decommissionMissingDevices( DecommissionMissingDevicesRequest request); diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImpl.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImpl.java index a50eba6fe0..d8e7b62f01 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImpl.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImpl.java @@ -25,6 +25,8 @@ import com.google.devtools.mobileharness.fe.v6.service.host.handlers.GetHostDeviceSummariesHandler; import com.google.devtools.mobileharness.fe.v6.service.host.handlers.GetHostHeaderInfoHandler; import com.google.devtools.mobileharness.fe.v6.service.host.handlers.GetHostOverviewHandler; +import com.google.devtools.mobileharness.fe.v6.service.host.handlers.GetPopularFlagsHandler; +import com.google.devtools.mobileharness.fe.v6.service.host.handlers.PreflightLabServerReleaseHandler; import com.google.devtools.mobileharness.fe.v6.service.host.handlers.RemoteControlDevicesHandler; import com.google.devtools.mobileharness.fe.v6.service.proto.host.CheckRemoteControlEligibilityRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.CheckRemoteControlEligibilityResponse; @@ -40,10 +42,10 @@ import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetHostOverviewRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; -import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetReleaseConfigsRequest; -import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetReleaseConfigsResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostHeaderInfo; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostOverviewPageData; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.ReleaseLabServerRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.ReleaseLabServerResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDevicesRequest; @@ -72,6 +74,8 @@ public final class HostServiceLogicImpl implements HostServiceLogic { private final RemoteControlDevicesHandler remoteControlDevicesHandler; private final GetHostHeaderInfoHandler getHostHeaderInfoHandler; private final DecommissionMissingDevicesHandler decommissionMissingDevicesHandler; + private final PreflightLabServerReleaseHandler preflightLabServerReleaseHandler; + private final GetPopularFlagsHandler getPopularFlagsHandler; private final UniverseFactory universeFactory; @Inject @@ -82,6 +86,8 @@ public final class HostServiceLogicImpl implements HostServiceLogic { RemoteControlDevicesHandler remoteControlDevicesHandler, GetHostHeaderInfoHandler getHostHeaderInfoHandler, DecommissionMissingDevicesHandler decommissionMissingDevicesHandler, + PreflightLabServerReleaseHandler preflightLabServerReleaseHandler, + GetPopularFlagsHandler getPopularFlagsHandler, UniverseFactory universeFactory) { this.getHostOverviewHandler = getHostOverviewHandler; this.getHostDeviceSummariesHandler = getHostDeviceSummariesHandler; @@ -89,6 +95,8 @@ public final class HostServiceLogicImpl implements HostServiceLogic { this.remoteControlDevicesHandler = remoteControlDevicesHandler; this.getHostHeaderInfoHandler = getHostHeaderInfoHandler; this.decommissionMissingDevicesHandler = decommissionMissingDevicesHandler; + this.preflightLabServerReleaseHandler = preflightLabServerReleaseHandler; + this.getPopularFlagsHandler = getPopularFlagsHandler; this.universeFactory = universeFactory; } @@ -138,11 +146,13 @@ public ListenableFuture getHostDebugInfo( @Override public ListenableFuture getPopularFlags(GetPopularFlagsRequest request) { - // TODO: Use the universe parameter. - @SuppressWarnings("unused") - String universe = request.getUniverse(); - // TODO: Implement this method. - return immediateFuture(GetPopularFlagsResponse.getDefaultInstance()); + UniverseScope universe; + try { + universe = universeFactory.create(request.getUniverse()); + } catch (IllegalArgumentException e) { + return immediateFailedFuture(e); + } + return getPopularFlagsHandler.getPopularFlags(request, universe); } @Override @@ -156,13 +166,15 @@ public ListenableFuture updatePassThroughFlags( } @Override - public ListenableFuture getReleaseConfigs( - GetReleaseConfigsRequest request) { - // TODO: Use the universe parameter. - @SuppressWarnings("unused") - String universe = request.getUniverse(); - // TODO: Implement this method. - return immediateFuture(GetReleaseConfigsResponse.getDefaultInstance()); + public ListenableFuture preflightLabServerRelease( + PreflightLabServerReleaseRequest request, Optional username) { + UniverseScope universe; + try { + universe = universeFactory.create(request.getUniverse()); + } catch (IllegalArgumentException e) { + return immediateFailedFuture(e); + } + return preflightLabServerReleaseHandler.preflightLabServerRelease(request, universe, username); } @Override diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/BUILD b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/BUILD similarity index 85% rename from src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/BUILD rename to src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/BUILD index 61fff8b0b3..960de7cd90 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/BUILD +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/BUILD @@ -21,11 +21,14 @@ package( ) java_library( - name = "builder", + name = "builders", srcs = glob(["*.java"]), deps = [ "//src/devtools/mobileharness/api/query/proto:lab_query_java_proto", "//src/devtools/mobileharness/fe/v6/service/proto/host:host_resources_java_proto", "//src/devtools/mobileharness/fe/v6/service/proto/host:host_service_java_proto", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/util", + "@maven//:com_google_guava_guava", + "@maven//:javax_inject_jsr330_api", ], ) diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpPopularFlagsBuilder.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpPopularFlagsBuilder.java new file mode 100644 index 0000000000..9bdcd672b4 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpPopularFlagsBuilder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.builders; + +import static com.google.common.util.concurrent.Futures.immediateFuture; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; +import javax.inject.Singleton; + +/** No-op implementation of {@link PopularFlagsBuilder} returning empty flags. */ +@Singleton +public final class NoOpPopularFlagsBuilder implements PopularFlagsBuilder { + + @Override + public ListenableFuture getPopularFlags( + GetPopularFlagsRequest request, UniverseScope universe) { + return immediateFuture(GetPopularFlagsResponse.getDefaultInstance()); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/NoOpRemoteControlUrlBuilder.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpRemoteControlUrlBuilder.java similarity index 99% rename from src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/NoOpRemoteControlUrlBuilder.java rename to src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpRemoteControlUrlBuilder.java index 88c08330d0..1670013bc7 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/NoOpRemoteControlUrlBuilder.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/NoOpRemoteControlUrlBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.devtools.mobileharness.fe.v6.service.host.builder; +package com.google.devtools.mobileharness.fe.v6.service.host.builders; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.DeviceInfo; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDeviceConfig; diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/PopularFlagsBuilder.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/PopularFlagsBuilder.java new file mode 100644 index 0000000000..e73bfaff7b --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/PopularFlagsBuilder.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.builders; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; + +/** Builder for popular pass-through flags. */ +public interface PopularFlagsBuilder { + + /** Builds popular pass-through flags for the host. */ + ListenableFuture getPopularFlags( + GetPopularFlagsRequest request, UniverseScope universe); +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/RemoteControlUrlBuilder.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/RemoteControlUrlBuilder.java similarity index 99% rename from src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/RemoteControlUrlBuilder.java rename to src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/RemoteControlUrlBuilder.java index d3899c437c..b499036da2 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder/RemoteControlUrlBuilder.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders/RemoteControlUrlBuilder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.devtools.mobileharness.fe.v6.service.host.builder; +package com.google.devtools.mobileharness.fe.v6.service.host.builders; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.DeviceInfo; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDeviceConfig; diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD index 2051443241..e5f8cc3254 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD @@ -34,7 +34,7 @@ java_library( "//src/devtools/mobileharness/infra/master/rpc/proto:lab_sync_service_java_proto", "//src/devtools/mobileharness/shared/labinfo/proto:lab_info_service_java_proto", "//src/java/com/google/devtools/mobileharness/fe/v6/service/device/handlers", - "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/provider", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/util:daemon_statuses", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/util:host_connectivity_statuses", diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/GetPopularFlagsHandler.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/GetPopularFlagsHandler.java new file mode 100644 index 0000000000..f0f85d8b6e --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/GetPopularFlagsHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.handlers; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.PopularFlagsBuilder; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Handler for the GetPopularFlags RPC. */ +@Singleton +public final class GetPopularFlagsHandler { + + private final PopularFlagsBuilder popularFlagsBuilder; + + @Inject + GetPopularFlagsHandler(PopularFlagsBuilder popularFlagsBuilder) { + this.popularFlagsBuilder = popularFlagsBuilder; + } + + public ListenableFuture getPopularFlags( + GetPopularFlagsRequest request, UniverseScope universe) { + return popularFlagsBuilder.getPopularFlags(request, universe); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPopularFlagsBuilder.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPopularFlagsBuilder.java new file mode 100644 index 0000000000..d840d55938 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPopularFlagsBuilder.java @@ -0,0 +1,15 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPreflightLabServerReleaseActionHelper.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPreflightLabServerReleaseActionHelper.java new file mode 100644 index 0000000000..6672a17ee2 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/NoOpPreflightLabServerReleaseActionHelper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.handlers; + +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; +import java.util.Optional; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** No-op implementation of {@link PreflightLabServerReleaseActionHelper}. */ +@Singleton +public final class NoOpPreflightLabServerReleaseActionHelper + implements PreflightLabServerReleaseActionHelper { + + @Inject + NoOpPreflightLabServerReleaseActionHelper() {} + + @Override + public ListenableFuture preflightLabServerRelease( + PreflightLabServerReleaseRequest request, UniverseScope universe, Optional username) { + return immediateFailedFuture( + new UnsupportedOperationException( + "Preflight lab server release is not supported in the current environment.")); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseActionHelper.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseActionHelper.java new file mode 100644 index 0000000000..7c70fa1fe4 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseActionHelper.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.handlers; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; +import java.util.Optional; + +/** Helper for {@link PreflightLabServerReleaseHandler}. */ +public interface PreflightLabServerReleaseActionHelper { + + ListenableFuture preflightLabServerRelease( + PreflightLabServerReleaseRequest request, UniverseScope universe, Optional username); +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandler.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandler.java new file mode 100644 index 0000000000..e1445c2d47 --- /dev/null +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.handlers; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; +import java.util.Optional; +import javax.inject.Inject; +import javax.inject.Singleton; + +/** Handler for the PreflightLabServerRelease RPC. */ +@Singleton +public final class PreflightLabServerReleaseHandler { + + private final PreflightLabServerReleaseActionHelper preflightLabServerReleaseActionHelper; + + @Inject + PreflightLabServerReleaseHandler( + PreflightLabServerReleaseActionHelper preflightLabServerReleaseActionHelper) { + this.preflightLabServerReleaseActionHelper = preflightLabServerReleaseActionHelper; + } + + public ListenableFuture preflightLabServerRelease( + PreflightLabServerReleaseRequest request, UniverseScope universe, Optional username) { + return preflightLabServerReleaseActionHelper.preflightLabServerRelease( + request, universe, username); + } +} diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandler.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandler.java index fd14447efb..7be6faa208 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandler.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandler.java @@ -26,7 +26,7 @@ import com.google.devtools.mobileharness.api.query.proto.FilterProto; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.DeviceInfo; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.LabQuery; -import com.google.devtools.mobileharness.fe.v6.service.host.builder.RemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.RemoteControlUrlBuilder; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDeviceConfig; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDevicesRequest; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDevicesResponse; diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/BUILD b/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/BUILD index 9b7f3f4321..80928e1ea6 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/BUILD +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/BUILD @@ -59,7 +59,8 @@ java_library( "//src/java/com/google/devtools/mobileharness/fe/v6/service/device/handlers", "//src/java/com/google/devtools/mobileharness/fe/v6/service/device/handlers:logcat_action_helper", "//src/java/com/google/devtools/mobileharness/fe/v6/service/device/handlers:screenshot_action_helper", - "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/provider", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared/auth", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared/providers:configuration_provider", diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/OssStubsModule.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/OssStubsModule.java index 8d03f07e04..baa11101fe 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/OssStubsModule.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/shared/OssStubsModule.java @@ -27,8 +27,12 @@ import com.google.devtools.mobileharness.fe.v6.service.device.handlers.OssTestbedConfigBuilderImpl; import com.google.devtools.mobileharness.fe.v6.service.device.handlers.ScreenshotActionHelper; import com.google.devtools.mobileharness.fe.v6.service.device.handlers.TestbedConfigBuilder; -import com.google.devtools.mobileharness.fe.v6.service.host.builder.NoOpRemoteControlUrlBuilder; -import com.google.devtools.mobileharness.fe.v6.service.host.builder.RemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.NoOpPopularFlagsBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.NoOpRemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.PopularFlagsBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.RemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.handlers.NoOpPreflightLabServerReleaseActionHelper; +import com.google.devtools.mobileharness.fe.v6.service.host.handlers.PreflightLabServerReleaseActionHelper; import com.google.devtools.mobileharness.fe.v6.service.host.provider.HostAuxiliaryInfoProvider; import com.google.devtools.mobileharness.fe.v6.service.host.provider.OssHostAuxiliaryInfoProviderImpl; import com.google.devtools.mobileharness.fe.v6.service.shared.auth.GroupMembershipProvider; @@ -91,8 +95,12 @@ protected void configure() { .in(Singleton.class); bind(HostAuxiliaryInfoProvider.class).to(OssHostAuxiliaryInfoProviderImpl.class); + bind(PreflightLabServerReleaseActionHelper.class) + .to(NoOpPreflightLabServerReleaseActionHelper.class) + .in(Singleton.class); + bind(PopularFlagsBuilder.class).to(NoOpPopularFlagsBuilder.class).in(Singleton.class); bind(LogcatActionHelper.class).to(NoOpLogcatActionHelper.class).in(Singleton.class); - bind(RemoteControlUrlBuilder.class).to(NoOpRemoteControlUrlBuilder.class); + bind(RemoteControlUrlBuilder.class).to(NoOpRemoteControlUrlBuilder.class).in(Singleton.class); bind(ScreenshotActionHelper.class).to(NoOpScreenshotActionHelper.class).in(Singleton.class); bind(GroupMembershipProvider.class).to(NoOpGroupMembershipProvider.class).in(Singleton.class); bind(TestbedConfigBuilder.class).to(OssTestbedConfigBuilderImpl.class).in(Singleton.class); diff --git a/src/java/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadiness.java b/src/java/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadiness.java index ffaa76b310..ea7e69f72d 100644 --- a/src/java/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadiness.java +++ b/src/java/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadiness.java @@ -63,7 +63,7 @@ public boolean isLabServerStopReady() { } public boolean isLabServerReleaseReady() { - return false; + return true; } public boolean isLabServerDeployReady() { diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/BUILD b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/BUILD index e9e7949cba..0bfa9e7862 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/BUILD +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/BUILD @@ -29,7 +29,8 @@ java_library( "//src/devtools/mobileharness/fe/v6/service/proto/host:host_service_java_proto", "//src/devtools/mobileharness/shared/labinfo/proto:lab_info_service_java_proto", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host:host_service_logic_impl", - "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/provider", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared/providers:lab_info_provider", diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImplTest.java b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImplTest.java index 912e7d20d1..d42c0d66fe 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImplTest.java +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/HostServiceLogicImplTest.java @@ -25,10 +25,16 @@ import static org.mockito.Mockito.when; import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.devtools.mobileharness.fe.v6.service.host.builder.RemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.PopularFlagsBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.RemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.handlers.PreflightLabServerReleaseActionHelper; import com.google.devtools.mobileharness.fe.v6.service.host.provider.HostAuxiliaryInfoProvider; import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetHostHeaderInfoRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.GetPopularFlagsResponse; import com.google.devtools.mobileharness.fe.v6.service.proto.host.HostHeaderInfo; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; import com.google.devtools.mobileharness.fe.v6.service.shared.SubDeviceInfoListFactory; import com.google.devtools.mobileharness.fe.v6.service.shared.providers.LabInfoProvider; import com.google.devtools.mobileharness.fe.v6.service.shared.remotecontrol.RemoteControlEligibilityChecker; @@ -68,6 +74,8 @@ public final class HostServiceLogicImplTest { @Bind @Mock private InstantSource instantSource; @Bind @Mock private FeatureManagerFactory featureManagerFactory; @Bind @Mock private LabSyncStub labSyncStub; + @Bind @Mock private PopularFlagsBuilder popularFlagsBuilder; + @Bind @Mock private PreflightLabServerReleaseActionHelper preflightLabServerReleaseActionHelper; @Mock private FeatureManager featureManager; private HostServiceLogicImpl hostServiceLogicImpl; @@ -109,4 +117,47 @@ public void getHostHeaderInfo_invalidUniverse_fails() throws Exception { ExecutionException.class, () -> hostServiceLogicImpl.getHostHeaderInfo(request).get()); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); } + + @Test + public void preflightLabServerRelease_success() throws Exception { + PreflightLabServerReleaseRequest request = + PreflightLabServerReleaseRequest.newBuilder().setUniverse("universe").build(); + PreflightLabServerReleaseResponse response = + PreflightLabServerReleaseResponse.getDefaultInstance(); + + when(preflightLabServerReleaseActionHelper.preflightLabServerRelease(any(), any(), any())) + .thenReturn(immediateFuture(response)); + + PreflightLabServerReleaseResponse actualResponse = + hostServiceLogicImpl.preflightLabServerRelease(request, Optional.of("user")).get(); + + assertThat(actualResponse).isEqualTo(response); + } + + @Test + public void preflightLabServerRelease_invalidUniverse_fails() throws Exception { + PreflightLabServerReleaseRequest request = + PreflightLabServerReleaseRequest.newBuilder().setUniverse("invalid").build(); + + when(universeFactory.create("invalid")).thenThrow(new IllegalArgumentException("invalid")); + + ExecutionException e = + assertThrows( + ExecutionException.class, + () -> + hostServiceLogicImpl.preflightLabServerRelease(request, Optional.of("user")).get()); + assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getPopularFlags_success() throws Exception { + GetPopularFlagsRequest request = GetPopularFlagsRequest.getDefaultInstance(); + GetPopularFlagsResponse response = GetPopularFlagsResponse.getDefaultInstance(); + + when(popularFlagsBuilder.getPopularFlags(any(), any())).thenReturn(immediateFuture(response)); + + GetPopularFlagsResponse actualResponse = hostServiceLogicImpl.getPopularFlags(request).get(); + + assertThat(actualResponse).isEqualTo(response); + } } diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD index 147d616329..df478e2cde 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/BUILD @@ -37,7 +37,7 @@ java_library( "//src/devtools/mobileharness/fe/v6/service/proto/host:host_service_java_proto", "//src/devtools/mobileharness/infra/master/rpc/proto:lab_sync_service_java_proto", "//src/devtools/mobileharness/shared/labinfo/proto:lab_info_service_java_proto", - "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builder", + "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/builders", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/handlers", "//src/java/com/google/devtools/mobileharness/fe/v6/service/host/provider", "//src/java/com/google/devtools/mobileharness/fe/v6/service/shared", diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandlerTest.java b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandlerTest.java new file mode 100644 index 0000000000..f609189cd8 --- /dev/null +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/PreflightLabServerReleaseHandlerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.devtools.mobileharness.fe.v6.service.host.handlers; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseRequest; +import com.google.devtools.mobileharness.fe.v6.service.proto.host.PreflightLabServerReleaseResponse; +import com.google.devtools.mobileharness.fe.v6.service.util.UniverseScope; +import com.google.inject.Guice; +import com.google.inject.testing.fieldbinder.Bind; +import com.google.inject.testing.fieldbinder.BoundFieldModule; +import java.util.Optional; +import javax.inject.Inject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public final class PreflightLabServerReleaseHandlerTest { + + private static final String USERNAME = "test-user"; + private static final String HOST_NAME = "test-host"; + private static final PreflightLabServerReleaseRequest REQUEST = + PreflightLabServerReleaseRequest.newBuilder().setHostName(HOST_NAME).build(); + private static final UniverseScope UNIVERSE = new UniverseScope.SelfUniverse(); + + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Bind @Mock private PreflightLabServerReleaseActionHelper actionHelper; + + @Inject private PreflightLabServerReleaseHandler handler; + + @Before + public void setUp() { + Guice.createInjector(BoundFieldModule.of(this)).injectMembers(this); + } + + @Test + public void preflight_delegatesToActionHelper() throws Exception { + PreflightLabServerReleaseResponse expectedResponse = + PreflightLabServerReleaseResponse.newBuilder() + .setReady(PreflightLabServerReleaseResponse.ReleaseReady.getDefaultInstance()) + .build(); + + when(actionHelper.preflightLabServerRelease( + eq(REQUEST), eq(UNIVERSE), eq(Optional.of(USERNAME)))) + .thenReturn(immediateFuture(expectedResponse)); + + ListenableFuture resultFuture = + handler.preflightLabServerRelease(REQUEST, UNIVERSE, Optional.of(USERNAME)); + + PreflightLabServerReleaseResponse response = Futures.getDone(resultFuture); + assertThat(response).isEqualTo(expectedResponse); + } +} diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandlerTest.java b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandlerTest.java index fc3320d4eb..55732fa16e 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandlerTest.java +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/host/handlers/RemoteControlDevicesHandlerTest.java @@ -31,7 +31,7 @@ import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.LabData; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.LabQueryResult; import com.google.devtools.mobileharness.api.query.proto.LabQueryProto.LabQueryResult.LabView; -import com.google.devtools.mobileharness.fe.v6.service.host.builder.RemoteControlUrlBuilder; +import com.google.devtools.mobileharness.fe.v6.service.host.builders.RemoteControlUrlBuilder; import com.google.devtools.mobileharness.fe.v6.service.proto.host.DeviceProxyType; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDeviceConfig; import com.google.devtools.mobileharness.fe.v6.service.proto.host.RemoteControlDevicesRequest; diff --git a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadinessTest.java b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadinessTest.java index f0e86f19c0..965c71250a 100644 --- a/src/javatests/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadinessTest.java +++ b/src/javatests/com/google/devtools/mobileharness/fe/v6/service/util/FeatureReadinessTest.java @@ -72,4 +72,9 @@ public void isHostDecommissionReady_returnsFalse() { public void isLabServerStartReady_returnsTrue() { assertThat(featureReadiness.isLabServerStartReady()).isTrue(); } + + @Test + public void isLabServerReleaseReady_returnsTrue() { + assertThat(featureReadiness.isLabServerReleaseReady()).isTrue(); + } }