Skip to content

Commit 5d46d99

Browse files
OmniLab Teamcopybara-github
authored andcommitted
Internal change
PiperOrigin-RevId: 923319177
1 parent c82a06b commit 5d46d99

19 files changed

Lines changed: 1358 additions & 138 deletions

src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.spec.ts

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import {ComponentFixture, TestBed} from '@angular/core/testing';
2-
import {MAT_DIALOG_DATA} from '@angular/material/dialog';
3-
import {
4-
MatTestDialogOpener,
5-
MatTestDialogOpenerModule,
6-
} from '@angular/material/dialog/testing';
2+
import {MatDialog} from '@angular/material/dialog';
3+
import {MatTestDialogOpener} from '@angular/material/dialog/testing';
74
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
8-
import {of} from 'rxjs';
5+
import {of, Subject} from 'rxjs';
96

107
import {
118
PopularFlag,
@@ -22,8 +19,10 @@ import {FlagsDialog, FlagsDialogData} from './flags_dialog';
2219
describe('FlagsDialog', () => {
2320
let component: FlagsDialog;
2421
let fixture: ComponentFixture<MatTestDialogOpener<FlagsDialog>>;
22+
let opener: MatTestDialogOpener<FlagsDialog>;
2523
let hostService: jasmine.SpyObj<HostService>;
2624
let snackBarService: jasmine.SpyObj<SnackBarService>;
25+
let dialog: jasmine.SpyObj<MatDialog>;
2726

2827
const dialogData: FlagsDialogData = {
2928
hostName: 'test-host',
@@ -39,6 +38,7 @@ describe('FlagsDialog', () => {
3938
'showSuccess',
4039
'showError',
4140
]);
41+
dialog = jasmine.createSpyObj('MatDialog', ['open']);
4242
const configService = jasmine.createSpyObj('CONFIG_SERVICE', [
4343
'checkDeviceWritePermission',
4444
'checkHostWritePermission',
@@ -53,20 +53,26 @@ describe('FlagsDialog', () => {
5353
hostService.getPopularFlags.and.returnValue(of({flags: []}));
5454

5555
await TestBed.configureTestingModule({
56-
imports: [FlagsDialog, NoopAnimationsModule, MatTestDialogOpenerModule],
56+
imports: [NoopAnimationsModule],
5757
providers: [
58-
{provide: MAT_DIALOG_DATA, useValue: dialogData},
5958
{provide: HOST_SERVICE, useValue: hostService},
6059
{provide: SnackBarService, useValue: snackBarService},
6160
{provide: CONFIG_SERVICE, useValue: configService},
6261
],
63-
}).compileComponents();
62+
})
63+
.overrideComponent(FlagsDialog, {
64+
set: {
65+
providers: [{provide: MatDialog, useValue: dialog}],
66+
},
67+
})
68+
.compileComponents();
6469

6570
fixture = TestBed.createComponent(
6671
MatTestDialogOpener.withComponent(FlagsDialog, {data: dialogData}),
6772
);
73+
opener = fixture.componentInstance;
74+
component = opener.dialogRef.componentInstance;
6875
fixture.detectChanges();
69-
component = fixture.componentInstance.dialogRef.componentInstance;
7076
});
7177

7278
it('should create', () => {
@@ -127,12 +133,81 @@ describe('FlagsDialog', () => {
127133
};
128134
hostService.updatePassThroughFlags.and.returnValue(of(response));
129135

136+
const mockConfirmDialogRef = jasmine.createSpyObj('MatDialogRef', [
137+
'afterClosed',
138+
]);
139+
mockConfirmDialogRef.afterClosed.and.returnValue(of('primary'));
140+
dialog.open.and.returnValue(mockConfirmDialogRef);
141+
130142
component.save();
131143

144+
expect(dialog.open).toHaveBeenCalled();
145+
expect(hostService.updatePassThroughFlags).toHaveBeenCalledWith(
146+
'test-host',
147+
'--flag1 --flag2',
148+
);
132149
expect(component.isSaving()).toBeFalse();
133150
expect(snackBarService.showError).toHaveBeenCalledWith('Error message');
134151
});
135152

153+
it('should handle save success response', () => {
154+
const response: UpdatePassThroughFlagsResponse = {
155+
success: true,
156+
};
157+
hostService.updatePassThroughFlags.and.returnValue(of(response));
158+
159+
const mockConfirmDialogRef = jasmine.createSpyObj('MatDialogRef', [
160+
'afterClosed',
161+
]);
162+
mockConfirmDialogRef.afterClosed.and.returnValue(of('primary'));
163+
dialog.open.and.returnValue(mockConfirmDialogRef);
164+
165+
spyOn(opener.dialogRef, 'close');
166+
167+
component.save();
168+
169+
expect(dialog.open).toHaveBeenCalled();
170+
expect(hostService.updatePassThroughFlags).toHaveBeenCalledWith(
171+
'test-host',
172+
'--flag1 --flag2',
173+
);
174+
expect(opener.dialogRef.close).toHaveBeenCalledWith('--flag1 --flag2');
175+
});
176+
177+
it('should not save flags if confirmation is cancelled', () => {
178+
const mockConfirmDialogRef = jasmine.createSpyObj('MatDialogRef', [
179+
'afterClosed',
180+
]);
181+
mockConfirmDialogRef.afterClosed.and.returnValue(of('cancel'));
182+
dialog.open.and.returnValue(mockConfirmDialogRef);
183+
184+
component.save();
185+
186+
expect(dialog.open).toHaveBeenCalled();
187+
expect(hostService.updatePassThroughFlags).not.toHaveBeenCalled();
188+
expect(component.isSaving()).toBeFalse();
189+
});
190+
191+
it('should set isSaving to true while saving', () => {
192+
const subject = new Subject<UpdatePassThroughFlagsResponse>();
193+
hostService.updatePassThroughFlags.and.returnValue(subject);
194+
195+
const mockConfirmDialogRef = jasmine.createSpyObj('MatDialogRef', [
196+
'afterClosed',
197+
]);
198+
mockConfirmDialogRef.afterClosed.and.returnValue(of('primary'));
199+
dialog.open.and.returnValue(mockConfirmDialogRef);
200+
201+
component.save();
202+
203+
expect(component.isSaving()).toBeTrue();
204+
205+
subject.next({success: true});
206+
subject.complete();
207+
208+
expect(component.isSaving()).toBeFalse();
209+
});
210+
136211
it('should update hasPermission when permission changes', () => {
137212
component.handlePermissionChange({hasPermission: false});
138213
expect(component.hasPermission()).toBeFalse();

src/devtools/mobileharness/fe/v6/angular/app/features/host_detail/components/host_overview/flags_dialog/flags_dialog.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import {FormsModule} from '@angular/forms';
1212
import {MatButtonModule} from '@angular/material/button';
1313
import {
1414
MAT_DIALOG_DATA,
15+
MatDialog,
1516
MatDialogModule,
1617
MatDialogRef,
1718
} from '@angular/material/dialog';
1819
import {MatFormFieldModule} from '@angular/material/form-field';
1920
import {MatIconModule} from '@angular/material/icon';
2021
import {MatInputModule} from '@angular/material/input';
2122
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
22-
import {take} from 'rxjs/operators';
23+
import {finalize, take} from 'rxjs/operators';
2324

2425
import {SnackBarService} from '@deviceinfra/app/shared/services/snackbar_service';
2526
import {WritePermissionResult} from '../../../../../core/models/action_common';
@@ -29,6 +30,8 @@ import {
2930
} from '../../../../../core/models/host_action';
3031
import {HOST_SERVICE} from '../../../../../core/services/host/host_service';
3132
import {Dialog} from '../../../../../shared/components/config_common/dialog/dialog';
33+
import {ConfirmDialog} from '../../../../../shared/components/confirm_dialog/confirm_dialog';
34+
import {SaveFlagsConfirmContent} from './save_flags_confirm_content/save_flags_confirm_content';
3235

3336
/**
3437
* Structure for passing data including host identity and current flags to FlagsDialog.
@@ -62,6 +65,7 @@ export interface FlagsDialogData {
6265
export class FlagsDialog implements OnInit {
6366
readonly data = inject<FlagsDialogData>(MAT_DIALOG_DATA);
6467
private readonly dialogRef = inject(MatDialogRef<FlagsDialog>);
68+
private readonly dialog = inject(MatDialog);
6569
private readonly hostService = inject(HOST_SERVICE);
6670
private readonly snackBarService = inject(SnackBarService);
6771

@@ -249,30 +253,51 @@ export class FlagsDialog implements OnInit {
249253
this.currentFlagsArray.set(flags);
250254
}
251255

252-
this.isSaving.set(true);
253-
254-
this.hostService
255-
.updatePassThroughFlags(this.data.hostName, finalString)
256-
.pipe(take(1))
257-
.subscribe({
258-
next: (response: UpdatePassThroughFlagsResponse) => {
259-
if (response.success) {
260-
this.isSaving.set(false);
261-
this.dialogRef.close(finalString);
262-
} else {
263-
this.isSaving.set(false);
264-
const msg =
265-
response.error?.message ||
266-
response.error?.code ||
267-
'Failed to save flags';
268-
this.snackBarService.showError(msg);
269-
}
270-
},
271-
error: () => {
272-
this.isSaving.set(false);
273-
this.snackBarService.showError('Failed to save flags');
256+
const confirmDialogRef = this.dialog.open(ConfirmDialog, {
257+
data: {
258+
title: 'Save Pass-through Flags?',
259+
contentComponent: SaveFlagsConfirmContent,
260+
contentComponentInputs: {
261+
flags: finalString,
274262
},
275-
});
263+
type: 'warning',
264+
customIcon: 'warning',
265+
primaryButtonLabel: 'Save Anyway',
266+
secondaryButtonLabel: 'Cancel',
267+
},
268+
panelClass: 'confirm-dialog-panel',
269+
autoFocus: false,
270+
});
271+
272+
confirmDialogRef.afterClosed().subscribe((result) => {
273+
if (result === 'primary') {
274+
this.isSaving.set(true);
275+
this.hostService
276+
.updatePassThroughFlags(this.data.hostName, finalString)
277+
.pipe(
278+
take(1),
279+
finalize(() => {
280+
this.isSaving.set(false);
281+
}),
282+
)
283+
.subscribe({
284+
next: (response: UpdatePassThroughFlagsResponse) => {
285+
if (response.success) {
286+
this.dialogRef.close(finalString);
287+
} else {
288+
const msg =
289+
response.error?.message ||
290+
response.error?.code ||
291+
'Failed to save flags';
292+
this.snackBarService.showError(msg);
293+
}
294+
},
295+
error: () => {
296+
this.snackBarService.showError('Failed to save flags');
297+
},
298+
});
299+
}
300+
});
276301
}
277302

278303
handlePermissionChange(result: WritePermissionResult) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<div class="save-flags-confirm-box">
2+
<div class="warning-callout">
3+
<p class="warning-title">
4+
<mat-icon class="info-icon-small">info</mat-icon>
5+
Release Impact Warning
6+
</p>
7+
<p class="warning-text">
8+
These flags will take effect during the <b>next release or restart</b>. Invalid flags will prevent the Lab Server from starting successfully.
9+
</p>
10+
</div>
11+
12+
<div class="flags-to-save-container">
13+
<span class="section-title">Flags to be saved</span>
14+
<div class="flags-scroll-box custom-scrollbar">
15+
@if (flags) {
16+
<code class="flags-code">{{ flags }}</code>
17+
} @else {
18+
<span class="italic text-gray-400">No flags (default configuration)</span>
19+
}
20+
</div>
21+
</div>
22+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
.save-flags-confirm-box {
2+
display: flex;
3+
flex-direction: column;
4+
gap: 1rem;
5+
text-align: left;
6+
7+
.warning-callout {
8+
background-color: #fffbeb; // amber-50
9+
border: 1px solid #fef3c7; // amber-100
10+
border-radius: 0.75rem; // rounded-xl
11+
padding: 1rem;
12+
font-size: 0.75rem; // text-xs
13+
line-height: 1.25rem;
14+
color: #78350f; // amber-900 (approx text-amber-850)
15+
}
16+
17+
.warning-title {
18+
font-weight: bold;
19+
display: flex;
20+
align-items: center;
21+
gap: 0.25rem;
22+
margin: 0 0 0.25rem 0;
23+
color: #78350f; // text-amber-900
24+
25+
.info-icon-small {
26+
font-size: 0.875rem; // 14px
27+
width: 0.875rem;
28+
height: 0.875rem;
29+
}
30+
}
31+
32+
.warning-text {
33+
margin: 0;
34+
}
35+
36+
.flags-to-save-container {
37+
display: flex;
38+
flex-direction: column;
39+
gap: 0.25rem;
40+
}
41+
42+
.section-title {
43+
font-size: 10px;
44+
font-weight: bold;
45+
color: #9ca3af; // text-gray-400
46+
text-transform: uppercase;
47+
letter-spacing: 0.05em;
48+
}
49+
50+
.flags-scroll-box {
51+
font-family: monospace;
52+
font-size: 11px;
53+
color: #4b5563; // text-gray-600
54+
background-color: #f9fafb; // bg-gray-50
55+
padding: 0.5rem;
56+
border-radius: 0.25rem;
57+
border: 1px solid #e5e7eb; // border-gray-200
58+
margin-top: 0.25rem;
59+
word-break: break-all;
60+
line-height: 1.375;
61+
max-height: 6rem; // max-h-24
62+
overflow-y: auto;
63+
}
64+
65+
.flags-code {
66+
white-space: pre-wrap;
67+
}
68+
69+
.italic {
70+
font-style: italic;
71+
}
72+
73+
.text-gray-400 {
74+
color: #9ca3af;
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {SaveFlagsConfirmContent} from './save_flags_confirm_content';
3+
4+
describe('SaveFlagsConfirmContent', () => {
5+
let component: SaveFlagsConfirmContent;
6+
let fixture: ComponentFixture<SaveFlagsConfirmContent>;
7+
8+
beforeEach(async () => {
9+
await TestBed.configureTestingModule({
10+
imports: [SaveFlagsConfirmContent],
11+
}).compileComponents();
12+
13+
fixture = TestBed.createComponent(SaveFlagsConfirmContent);
14+
component = fixture.componentInstance;
15+
fixture.detectChanges();
16+
});
17+
18+
it('should create', () => {
19+
expect(component).toBeTruthy();
20+
});
21+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {CommonModule} from '@angular/common';
2+
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
3+
import {MatIconModule} from '@angular/material/icon';
4+
5+
/**
6+
* Component to display confirmation content when saving pass-through flags.
7+
*/
8+
@Component({
9+
selector: 'app-save-flags-confirm-content',
10+
standalone: true,
11+
imports: [CommonModule, MatIconModule],
12+
templateUrl: './save_flags_confirm_content.ng.html',
13+
styleUrls: ['./save_flags_confirm_content.scss'],
14+
changeDetection: ChangeDetectionStrategy.OnPush,
15+
})
16+
export class SaveFlagsConfirmContent {
17+
@Input() flags = '';
18+
}

0 commit comments

Comments
 (0)