Skip to content

Commit 60e1393

Browse files
OmniLab Teamcopybara-github
authored andcommitted
Internal change
PiperOrigin-RevId: 932851189
1 parent 333e670 commit 60e1393

33 files changed

Lines changed: 1296 additions & 84 deletions

src/devtools/mobileharness/fe/v6/angular/app/core/models/device_action.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
import {ActionButtonState} from './action_common';
2+
/**
3+
* Reason code for a device being ineligible for remote control.
4+
*/
5+
export type IneligibilityReasonCode =
6+
| 'PERMISSION_DENIED' // User lacks permission for the device
7+
| 'DEVICE_NOT_IDLE' // Device state is not IDLE (e.g., BUSY, MISSING)
8+
| 'DEVICE_TYPE_NOT_SUPPORTED' // Device type is not supported (e.g., FailedDevice, AbnormalTestbedDevice, or non-AndroidRealDevice in multi-selection mode)
9+
| 'HOST_OS_NOT_SUPPORTED' // Host OS is MacOS
10+
| 'ACID_NOT_SUPPORTED' // Device does not support AcidRemoteDriver
11+
| 'DEVICE_NOT_FOUND'; // Device or sub-device not found in lab info
12+
213

314
/**
415
* Information about the host machine.
@@ -66,6 +77,8 @@ export declare interface DeviceHeaderInfo {
6677
export declare interface TakeScreenshotResponse {
6778
screenshotUrl: string;
6879
capturedAt: string;
80+
errorType?: IneligibilityReasonCode;
81+
errorMessage?: string;
6982
}
7083

7184
/**
@@ -74,6 +87,8 @@ export declare interface TakeScreenshotResponse {
7487
export declare interface GetLogcatResponse {
7588
logUrl: string;
7689
capturedAt: string;
90+
errorType?: IneligibilityReasonCode;
91+
errorMessage?: string;
7792
}
7893

7994
/**

src/devtools/mobileharness/fe/v6/angular/app/core/models/host_overview.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* uses this data to render the UI, including icons, colors, and layouts.
77
*/
88

9-
import {DeviceActions} from './device_action';
9+
import {DeviceActions, IneligibilityReasonCode} from './device_action';
1010
import {HealthState, SubDeviceInfo} from './device_overview';
1111
import {HostHeaderInfo, LabServerActions} from './host_action';
1212

@@ -280,16 +280,7 @@ export enum DeviceProxyType {
280280
VIDEO = 5,
281281
}
282282

283-
/**
284-
* Reason code for a device being ineligible for remote control.
285-
*/
286-
export type IneligibilityReasonCode =
287-
| 'PERMISSION_DENIED' // User lacks permission for the device
288-
| 'DEVICE_NOT_IDLE' // Device state is not IDLE (e.g., BUSY, MISSING)
289-
| 'DEVICE_TYPE_NOT_SUPPORTED' // Device type is not supported (e.g., FailedDevice, AbnormalTestbedDevice, or non-AndroidRealDevice in multi-selection mode)
290-
| 'HOST_OS_NOT_SUPPORTED' // Host OS is MacOS
291-
| 'ACID_NOT_SUPPORTED' // Device does not support AcidRemoteDriver
292-
| 'DEVICE_NOT_FOUND'; // Device or sub-device not found in lab info
283+
293284

294285
/**
295286
* The eligibility result for a sub-device.

src/devtools/mobileharness/fe/v6/angular/app/core/services/device/fake_device_service.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {GoogleDate} from '../../../shared/utils/date_utils';
66
import {
77
DeviceHeaderInfo,
88
GetLogcatResponse,
9+
IneligibilityReasonCode,
910
QuarantineDeviceRequest,
1011
QuarantineDeviceResponse,
1112
QuarantineInfo,
@@ -150,6 +151,28 @@ export class FakeDeviceService extends DeviceService {
150151

151152
override takeScreenshot(id: string): Observable<TakeScreenshotResponse> {
152153
console.log(`FakeService: Taking screenshot for ${id}`);
154+
if (id.includes('permission-denied')) {
155+
return of({
156+
screenshotUrl: '',
157+
capturedAt: '',
158+
errorType: 'PERMISSION_DENIED' as IneligibilityReasonCode,
159+
errorMessage: 'Lacks permission to take screenshot',
160+
}).pipe(delay(1000));
161+
}
162+
if (id.includes('logical-error')) {
163+
return of({
164+
screenshotUrl: '',
165+
capturedAt: '',
166+
errorType: 'DEVICE_NOT_FOUND' as IneligibilityReasonCode,
167+
errorMessage: 'Device was disconnected during screenshot',
168+
}).pipe(delay(1000));
169+
}
170+
if (id.includes('rpc-error')) {
171+
return throwError(
172+
() => new Error('RPC Failure: Connection reset by peer'),
173+
).pipe(delay(1000));
174+
}
175+
153176
return of({
154177
screenshotUrl:
155178
'http://0.0.0.0:8000/device_detail/action_bar/resource/screenshot-demo.png',
@@ -159,6 +182,28 @@ export class FakeDeviceService extends DeviceService {
159182

160183
override getLogcat(id: string): Observable<GetLogcatResponse> {
161184
console.log(`FakeService: Getting logcat for ${id}`);
185+
if (id.includes('permission-denied')) {
186+
return of({
187+
logUrl: '',
188+
capturedAt: '',
189+
errorType: 'PERMISSION_DENIED' as IneligibilityReasonCode,
190+
errorMessage: 'Lacks permission to get logcat',
191+
}).pipe(delay(1000));
192+
}
193+
if (id.includes('logical-error')) {
194+
return of({
195+
logUrl: '',
196+
capturedAt: '',
197+
errorType: 'DEVICE_NOT_FOUND' as IneligibilityReasonCode,
198+
errorMessage: 'Device was disconnected while retrieving logcat',
199+
}).pipe(delay(1000));
200+
}
201+
if (id.includes('rpc-error')) {
202+
return throwError(
203+
() => new Error('RPC Failure: Connection timed out'),
204+
).pipe(delay(1000));
205+
}
206+
162207
return of({
163208
logUrl:
164209
'http://0.0.0.0:8000/device_detail/action_bar/resource/logcat-demo.log',
@@ -244,6 +289,7 @@ export class FakeDeviceService extends DeviceService {
244289
const remoteControlVisible =
245290
scenario.actionVisibility?.remoteControl ?? true;
246291
const quarantineVisible = scenario.actionVisibility?.quarantine ?? true;
292+
const decommissionVisible = isMissing;
247293

248294
return {
249295
id: overview.id,
@@ -312,7 +358,7 @@ export class FakeDeviceService extends DeviceService {
312358
},
313359
decommission: {
314360
enabled: true,
315-
visible: true,
361+
visible: decommissionVisible,
316362
tooltip: 'Decommission device',
317363
isReady: !scenario.allActionsNotReady,
318364
},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {MockDeviceScenario} from '../models';
2+
import {SCENARIO_IN_SERVICE_IDLE} from './01_in_service_idle';
3+
4+
/** Mock scenario for a device that triggers a Permission Denied error. */
5+
export const SCENARIO_ERROR_PERMISSION_DENIED: MockDeviceScenario = {
6+
...SCENARIO_IN_SERVICE_IDLE,
7+
id: 'android-permission-denied-01',
8+
scenarioName: '27. Error: Permission Denied (Demo)',
9+
overview: {
10+
...SCENARIO_IN_SERVICE_IDLE.overview,
11+
id: 'android-permission-denied-01',
12+
healthAndActivity: {
13+
...SCENARIO_IN_SERVICE_IDLE.overview.healthAndActivity,
14+
subtitle:
15+
'This device is configured to trigger errors for demo: Permission Denied',
16+
},
17+
},
18+
};
19+
20+
/** Mock scenario for a device that triggers a logical action error. */
21+
export const SCENARIO_ERROR_LOGICAL: MockDeviceScenario = {
22+
...SCENARIO_IN_SERVICE_IDLE,
23+
id: 'android-logical-error-01',
24+
scenarioName: '28. Error: Logical Action Failed (Demo)',
25+
overview: {
26+
...SCENARIO_IN_SERVICE_IDLE.overview,
27+
id: 'android-logical-error-01',
28+
healthAndActivity: {
29+
...SCENARIO_IN_SERVICE_IDLE.overview.healthAndActivity,
30+
subtitle:
31+
'This device is configured to trigger errors for demo: Logical Error',
32+
},
33+
},
34+
};
35+
36+
/** Mock scenario for a device that triggers an RPC error. */
37+
export const SCENARIO_ERROR_RPC: MockDeviceScenario = {
38+
...SCENARIO_IN_SERVICE_IDLE,
39+
id: 'android-rpc-error-01',
40+
scenarioName: '29. Error: RPC Exception (Demo)',
41+
overview: {
42+
...SCENARIO_IN_SERVICE_IDLE.overview,
43+
id: 'android-rpc-error-01',
44+
healthAndActivity: {
45+
...SCENARIO_IN_SERVICE_IDLE.overview.healthAndActivity,
46+
subtitle:
47+
'This device is configured to trigger errors for demo: RPC Exception',
48+
},
49+
},
50+
};

src/devtools/mobileharness/fe/v6/angular/app/core/services/mock_data/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ import {OVERVIEW_11} from './hosts/overview_11';
6565
import {OVERVIEW_12} from './hosts/overview_12';
6666
import {OVERVIEW_13} from './hosts/overview_13';
6767
import {OVERVIEW_14} from './hosts/overview_14';
68+
import {
69+
SCENARIO_ERROR_LOGICAL,
70+
SCENARIO_ERROR_PERMISSION_DENIED,
71+
SCENARIO_ERROR_RPC,
72+
} from './devices/27_error_scenarios';
6873
import {SCENARIO_RC_PERMISSIONS_ALL} from './hosts/remote_control_permissions';
6974
import {MockDeviceScenario, MockHostScenario} from './models';
7075

@@ -99,6 +104,9 @@ export const MOCK_DEVICE_SCENARIOS: MockDeviceScenario[] = [
99104
SCENARIO_TESTBED_SINGLE_ELIGIBLE,
100105
SCENARIO_TESTBED_MIXED_ELIGIBILITY,
101106
SCENARIO_COMING_SOON,
107+
SCENARIO_ERROR_PERMISSION_DENIED,
108+
SCENARIO_ERROR_LOGICAL,
109+
SCENARIO_ERROR_RPC,
102110
];
103111

104112
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<div class="action-error-dialog-container">
2+
<div class="dialog-header">
3+
<mat-icon>error</mat-icon>
4+
<h3 class="dialog-title">Action Failed</h3>
5+
</div>
6+
7+
<mat-dialog-content class="dialog-content">
8+
<div class="error-message-container">
9+
<span class="message-label">Message:</span>
10+
<div class="error-message">{{ errorMessage }}</div>
11+
</div>
12+
13+
@if (errorDetails) {
14+
<div class="error-details-container">
15+
<span class="details-label">Details:</span>
16+
<pre class="error-details">{{ errorDetails }}</pre>
17+
</div>
18+
}
19+
</mat-dialog-content>
20+
21+
<mat-dialog-actions class="dialog-actions">
22+
<div class="left-actions">
23+
<button mat-stroked-button (click)="copyError()">
24+
<mat-icon>content_copy</mat-icon>
25+
Copy Error
26+
</button>
27+
<button mat-stroked-button (click)="reportBug()">
28+
<mat-icon>bug_report</mat-icon>
29+
Report Bug
30+
</button>
31+
</div>
32+
<button class="close-button" [mat-dialog-close]="'close'">
33+
Close
34+
</button>
35+
</mat-dialog-actions>
36+
</div>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
@use '@angular/material' as mat;
2+
@use '../../styles/common';
3+
@use '../../styles/typography';
4+
5+
.action-error-dialog-container {
6+
padding: 1.5rem;
7+
min-width: 24rem;
8+
max-width: 32rem;
9+
font-family: typography.$labconsole-font;
10+
}
11+
12+
.dialog-header {
13+
display: flex;
14+
align-items: center;
15+
flex-direction: column;
16+
gap: 1rem;
17+
margin-bottom: 1rem;
18+
19+
mat-icon {
20+
font-size: 2.5rem;
21+
width: 2.5rem;
22+
height: 2.5rem;
23+
color: #c5221f;
24+
}
25+
26+
.dialog-title {
27+
font-weight: 600;
28+
margin: 0;
29+
font-size: 1.125rem;
30+
line-height: 1.75rem;
31+
color: #1f2937;
32+
margin-bottom: 1rem;
33+
}
34+
}
35+
36+
.dialog-content {
37+
padding: 0;
38+
margin-bottom: 24px;
39+
display: flex;
40+
flex-direction: column;
41+
gap: 16px;
42+
max-height: 60vh;
43+
overflow-y: auto;
44+
@include common.shared-scrollbar-style();
45+
}
46+
47+
.error-message-container {
48+
display: flex;
49+
flex-direction: column;
50+
gap: 8px;
51+
}
52+
53+
.message-label {
54+
font-weight: 500;
55+
font-size: 14px;
56+
color: #5f6368;
57+
}
58+
59+
.error-message {
60+
margin: 0;
61+
padding: 12px;
62+
background-color: #f8f9fa;
63+
border-radius: 4px;
64+
border: 1px solid #dadce0;
65+
font-size: 14px;
66+
color: #3c4043;
67+
overflow-wrap: anywhere;
68+
word-break: break-word;
69+
}
70+
71+
.error-details-container {
72+
display: flex;
73+
flex-direction: column;
74+
gap: 8px;
75+
}
76+
77+
.details-label {
78+
font-weight: 500;
79+
font-size: 14px;
80+
color: #5f6368;
81+
}
82+
83+
.error-details {
84+
margin: 0;
85+
padding: 12px;
86+
background-color: #f8f9fa;
87+
border-radius: 4px;
88+
border: 1px solid #dadce0;
89+
font-family: monospace;
90+
font-size: 12px;
91+
white-space: pre-wrap;
92+
word-break: break-all;
93+
max-height: 200px;
94+
overflow-y: auto;
95+
@include common.shared-scrollbar-style();
96+
}
97+
98+
.dialog-actions {
99+
display: flex;
100+
justify-content: space-between;
101+
align-items: center;
102+
padding: 0;
103+
min-height: fit-content;
104+
105+
.left-actions {
106+
display: flex;
107+
gap: 8px;
108+
}
109+
110+
.close-button {
111+
@include common.shared-button('destructive');
112+
}
113+
}

0 commit comments

Comments
 (0)