Skip to content

Commit 278146b

Browse files
hoonjicopybara-github
authored andcommitted
Adds feature flags to show/hide features
Allows enabling/disabling: - token streaming toggle - message file upload - manual state updates - bidi streaming - trace features - trace, artifact, eval tabs PiperOrigin-RevId: 812660091
1 parent 8cd5de0 commit 278146b

14 files changed

+313
-50
lines changed

src/app/components/chat-panel/chat-panel.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@
355355
(click)="fileInput.click()"
356356
class="function-event-button"
357357
[matTooltip]="i18n.uploadFileTooltip"
358+
[disabled]="!(isMessageFileUploadEnabledObs | async)"
358359
>
359360
<mat-icon>attach_file</mat-icon>
360361
</button>
@@ -363,6 +364,7 @@
363364
[matMenuTriggerFor]="moreMenu"
364365
class="function-event-button"
365366
[matTooltip]="i18n.moreOptionsTooltip"
367+
[disabled]="!(isManualStateUpdateEnabledObs | async)"
366368
>
367369
<mat-icon>more_vert</mat-icon>
368370
</button>
@@ -379,6 +381,7 @@
379381
'background-color': isAudioRecording ? 'rgb(234, 67, 53)' : 'rgb(51, 53, 55)'
380382
}"
381383
[matTooltip]="isAudioRecording ? i18n.turnOffMicTooltip : i18n.useMicTooltip"
384+
[disabled]="!(isBidiStreamingEnabledObs | async)"
382385
>
383386
<mat-icon>mic</mat-icon>
384387
</button>
@@ -390,6 +393,7 @@
390393
'background-color': isVideoRecording ? 'rgb(234, 67, 53)' : 'rgb(51, 53, 55)'
391394
}"
392395
[matTooltip]="isVideoRecording ? i18n.turnOffCamTooltip : i18n.useCamTooltip"
396+
[disabled]="!(isBidiStreamingEnabledObs | async)"
393397
>
394398
<mat-icon>videocam</mat-icon>
395399
</button>

src/app/components/chat-panel/chat-panel.component.spec.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import {By} from '@angular/platform-browser';
2424
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
2525
import {of} from 'rxjs';
2626

27+
import {FEATURE_FLAG_SERVICE} from '../../core/services/feature-flag.service';
2728
import {STRING_TO_COLOR_SERVICE} from '../../core/services/interfaces/string-to-color';
2829
import {StringToColorServiceImpl} from '../../core/services/string-to-color.service';
30+
import {MockFeatureFlagService} from '../../core/services/testing/mock-feature-flag.service';
2931
import {MockStringToColorService} from '../../core/services/testing/mock-string-to-color.service';
3032
import {ChatPanelComponent} from './chat-panel.component';
3133
import {MARKDOWN_COMPONENT} from '../markdown/markdown.component.interface';
@@ -37,25 +39,31 @@ const FUNC1_NAME = 'func1';
3739
describe('ChatPanelComponent', () => {
3840
let component: ChatPanelComponent;
3941
let fixture: ComponentFixture<ChatPanelComponent>;
42+
let mockFeatureFlagService: MockFeatureFlagService;
4043

4144
beforeEach(async () => {
45+
mockFeatureFlagService = new MockFeatureFlagService();
46+
47+
mockFeatureFlagService.isMessageFileUploadEnabledResponse.next(true);
48+
mockFeatureFlagService.isManualStateUpdateEnabledResponse.next(true);
49+
mockFeatureFlagService.isBidiStreamingEnabledResponse.next(true);
50+
4251
await TestBed
4352
.configureTestingModule({
4453
imports: [
4554
ChatPanelComponent,
4655
MatDialogModule,
4756
NoopAnimationsModule,
4857
HttpClientTestingModule,
49-
// BEGIN_EXTERNAL
50-
// MarkdownModule.forRoot(),
51-
// END_EXTERNAL
58+
MarkdownModule.forRoot(),
5259
],
5360
providers: [
5461
{
5562
provide: STRING_TO_COLOR_SERVICE,
5663
useClass: StringToColorServiceImpl,
5764
},
5865
{provide: MARKDOWN_COMPONENT, useValue: MarkdownComponent},
66+
{provide: FEATURE_FLAG_SERVICE, useValue: mockFeatureFlagService},
5967
],
6068
})
6169
.compileComponents();
@@ -329,4 +337,58 @@ describe('ChatPanelComponent', () => {
329337
expect(scrollContainerElement.scrollTo).toHaveBeenCalled();
330338
}));
331339
});
340+
341+
describe('disabled features', () => {
342+
it('should have the attach_file button disabled', () => {
343+
mockFeatureFlagService.isMessageFileUploadEnabledResponse.next(false);
344+
fixture.detectChanges();
345+
346+
const allButtons =
347+
fixture.debugElement.queryAll(By.css('button[mat-icon-button]'));
348+
const button = allButtons.find(
349+
b =>
350+
b.nativeElement.querySelector('mat-icon')?.textContent?.trim() ===
351+
'attach_file');
352+
expect(button!.nativeElement.disabled).toBeTrue();
353+
});
354+
355+
it('should have the more_vert button disabled', () => {
356+
mockFeatureFlagService.isManualStateUpdateEnabledResponse.next(false);
357+
fixture.detectChanges();
358+
359+
const allButtons =
360+
fixture.debugElement.queryAll(By.css('button[mat-icon-button]'));
361+
const button = allButtons.find(
362+
b =>
363+
b.nativeElement.querySelector('mat-icon')?.textContent?.trim() ===
364+
'more_vert');
365+
expect(button!.nativeElement.disabled).toBeTrue();
366+
});
367+
368+
it('should have the mic button disabled', () => {
369+
mockFeatureFlagService.isBidiStreamingEnabledResponse.next(false);
370+
fixture.detectChanges();
371+
372+
const allButtons =
373+
fixture.debugElement.queryAll(By.css('button[mat-icon-button]'));
374+
const button = allButtons.find(
375+
b =>
376+
b.nativeElement.querySelector('mat-icon')?.textContent?.trim() ===
377+
'mic');
378+
expect(button!.nativeElement.disabled).toBeTrue();
379+
});
380+
381+
it('should have the videocam button disabled', () => {
382+
mockFeatureFlagService.isBidiStreamingEnabledResponse.next(false);
383+
fixture.detectChanges();
384+
385+
const allButtons =
386+
fixture.debugElement.queryAll(By.css('button[mat-icon-button]'));
387+
const button = allButtons.find(
388+
b =>
389+
b.nativeElement.querySelector('mat-icon')?.textContent?.trim() ===
390+
'videocam');
391+
expect(button!.nativeElement.disabled).toBeTrue();
392+
});
393+
});
332394
});

src/app/components/chat-panel/chat-panel.component.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
3131
import {NgxJsonViewerModule} from 'ngx-json-viewer';
3232

3333
import type {EvalCase} from '../../core/models/Eval';
34+
import {FEATURE_FLAG_SERVICE} from '../../core/services/feature-flag.service';
3435
import {STRING_TO_COLOR_SERVICE} from '../../core/services/interfaces/string-to-color';
3536
import {MediaType,} from '../artifact-tab/artifact-tab.component';
3637
import {AudioPlayerComponent} from '../audio-player/audio-player.component';
@@ -104,13 +105,17 @@ export class ChatPanelComponent implements OnChanges, AfterViewInit {
104105
readonly markdownComponent: Type<MarkdownComponentInterface> = inject(
105106
MARKDOWN_COMPONENT,
106107
);
108+
private readonly featureFlagService = inject(FEATURE_FLAG_SERVICE);
107109
readonly MediaType = MediaType;
108110

109-
constructor(
110-
private sanitizer: DomSanitizer,
111-
@Inject(DOCUMENT) private document: Document,
112-
private renderer: Renderer2,
113-
) {}
111+
readonly isMessageFileUploadEnabledObs =
112+
this.featureFlagService.isMessageFileUploadEnabled();
113+
readonly isManualStateUpdateEnabledObs =
114+
this.featureFlagService.isManualStateUpdateEnabled();
115+
readonly isBidiStreamingEnabledObs =
116+
this.featureFlagService.isBidiStreamingEnabled();
117+
118+
constructor(private sanitizer: DomSanitizer) {}
114119

115120
ngAfterViewInit() {
116121
if (this.scrollContainer?.nativeElement) {

src/app/components/chat/chat.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
class="example-margin"
148148
[checked]="enableSseIndicator()"
149149
(change)="toggleSse()"
150+
[disabled]="!(isTokenStreamingEnabledObs | async)"
150151
>
151152
Token Streaming
152153
</mat-slide-toggle>

src/app/components/chat/chat.component.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ describe('ChatComponent', () => {
143143
mockFeatureFlagService.isImportSessionEnabledResponse.next(true);
144144
mockFeatureFlagService.isEditFunctionArgsEnabledResponse.next(true);
145145
mockFeatureFlagService.isSessionUrlEnabledResponse.next(true);
146+
mockFeatureFlagService.isApplicationSelectorEnabledResponse.next(true);
147+
mockFeatureFlagService.isTokenStreamingEnabledResponse.next(true);
146148

147149
mockDialog = jasmine.createSpyObj('MatDialog', ['open']);
148150
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']);
@@ -854,4 +856,18 @@ describe('ChatComponent', () => {
854856
});
855857
});
856858
});
859+
860+
describe('Feature Disabling', () => {
861+
describe('when token streaming is disabled', () => {
862+
beforeEach(() => {
863+
mockFeatureFlagService.isTokenStreamingEnabledResponse.next(false);
864+
fixture.detectChanges();
865+
});
866+
867+
it('should have the token streaming toggle disabled', () => {
868+
const slideToggle = fixture.debugElement.query(By.css('mat-slide-toggle'));
869+
expect(slideToggle.componentInstance.disabled).toBe(true);
870+
});
871+
});
872+
});
857873
});

src/app/components/chat/chat.component.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,17 +251,12 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
251251
shareReplay(),
252252
);
253253

254-
// Import session
255-
importSessionEnabledObs: Observable<boolean>;
256-
257-
// Edit eval tool use
258-
isEditFunctionArgsEnabledObs: Observable<boolean>;
259-
260-
// Session url
261-
isSessionUrlEnabledObs: Observable<boolean>;
262-
263-
// Application selector
264-
isApplicationSelectorEnabledObs: Observable<boolean>;
254+
// Feature flag references for use in template.
255+
readonly importSessionEnabledObs: Observable<boolean>;
256+
readonly isEditFunctionArgsEnabledObs: Observable<boolean>;
257+
readonly isSessionUrlEnabledObs: Observable<boolean>;
258+
readonly isApplicationSelectorEnabledObs: Observable<boolean>;
259+
readonly isTokenStreamingEnabledObs: Observable<boolean>;
265260

266261
// Trace detail
267262
bottomPanelVisible = false;
@@ -294,6 +289,8 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy {
294289
this.isSessionUrlEnabledObs = this.featureFlagService.isSessionUrlEnabled();
295290
this.isApplicationSelectorEnabledObs =
296291
this.featureFlagService.isApplicationSelectorEnabled();
292+
this.isTokenStreamingEnabledObs =
293+
this.featureFlagService.isTokenStreamingEnabled();
297294
}
298295

299296
ngOnInit(): void {

src/app/components/event-tab/event-tab.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
@if (traceData()) {
2828
<mat-button-toggle-group name="fontStyle" aria-label="Font Style" style="scale: 0.8" [(ngModel)]="view">
2929
<mat-button-toggle value="events">Events</mat-button-toggle>
30-
<mat-button-toggle value="trace">Trace</mat-button-toggle>
30+
@if (isTraceEnabledObs | async) {
31+
<mat-button-toggle value="trace">Trace</mat-button-toggle>
32+
}
3133
</mat-button-toggle-group>
3234
}
3335
</div>

src/app/components/event-tab/event-tab.component.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,29 @@ import {ComponentFixture, TestBed} from '@angular/core/testing';
1919
import {MatDialogModule, MatDialogRef} from '@angular/material/dialog';
2020

2121

22+
import {FEATURE_FLAG_SERVICE} from '../../core/services/feature-flag.service';
23+
import {MockFeatureFlagService} from '../../core/services/testing/mock-feature-flag.service';
2224
import {EventTabComponent} from './event-tab.component';
2325

2426
describe('EventTabComponent', () => {
2527
let component: EventTabComponent;
2628
let fixture: ComponentFixture<EventTabComponent>;
29+
let featureFlagService: MockFeatureFlagService;
2730
const mockDialogRef = {
2831
close: jasmine.createSpy('close'),
2932
};
3033

3134
beforeEach(async () => {
35+
featureFlagService = new MockFeatureFlagService();
36+
37+
featureFlagService.isTraceEnabledResponse.next(true);
38+
3239
await TestBed.configureTestingModule({
3340
imports: [MatDialogModule, EventTabComponent],
34-
providers: [{ provide: MatDialogRef, useValue: mockDialogRef }],
41+
providers: [
42+
{provide: MatDialogRef, useValue: mockDialogRef},
43+
{provide: FEATURE_FLAG_SERVICE, useValue: featureFlagService},
44+
],
3545
}).compileComponents();
3646

3747
fixture = TestBed.createComponent(EventTabComponent);
@@ -43,3 +53,46 @@ describe('EventTabComponent', () => {
4353
expect(component).toBeTruthy();
4454
});
4555
});
56+
57+
describe('EventTabComponent feature disabling', () => {
58+
let component: EventTabComponent;
59+
let fixture: ComponentFixture<EventTabComponent>;
60+
let featureFlagService: MockFeatureFlagService;
61+
const mockDialogRef = {
62+
close: jasmine.createSpy('close'),
63+
};
64+
65+
beforeEach(async () => {
66+
featureFlagService = new MockFeatureFlagService();
67+
68+
featureFlagService.isTraceEnabledResponse.next(false);
69+
70+
await TestBed.configureTestingModule({
71+
imports: [MatDialogModule, EventTabComponent],
72+
providers: [
73+
{provide: MatDialogRef, useValue: mockDialogRef},
74+
{provide: FEATURE_FLAG_SERVICE, useValue: featureFlagService},
75+
],
76+
}).compileComponents();
77+
78+
fixture = TestBed.createComponent(EventTabComponent);
79+
component = fixture.componentInstance;
80+
fixture.componentRef.setInput('traceData', [
81+
{
82+
trace_id: '1',
83+
span_id: '1',
84+
start_time: 1,
85+
end_time: 2,
86+
name: 'test',
87+
},
88+
]);
89+
fixture.detectChanges();
90+
});
91+
92+
it('should hide the Trace mat-button-toggle', () => {
93+
const traceToggle = fixture.nativeElement.querySelector(
94+
'mat-button-toggle[value="trace"]',
95+
);
96+
expect(traceToggle).toBeNull();
97+
});
98+
});

src/app/components/event-tab/event-tab.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, comput
1919
import {MatDialog} from '@angular/material/dialog';
2020

2121
import {Span} from '../../core/models/Trace';
22+
import {FEATURE_FLAG_SERVICE} from '../../core/services/feature-flag.service';
2223
import {TraceChartComponent} from './trace-chart/trace-chart.component';
2324
import { MatButtonToggleGroup, MatButtonToggle } from '@angular/material/button-toggle';
2425
import { FormsModule } from '@angular/forms';
2526
import { MatList, MatListItem } from '@angular/material/list';
26-
import { KeyValuePipe } from '@angular/common';
27+
import {AsyncPipe, KeyValuePipe} from '@angular/common';
2728
import {InvocIdPipe} from './invoc-id.pipe';
2829

2930
@Component({
@@ -38,13 +39,15 @@ import {InvocIdPipe} from './invoc-id.pipe';
3839
MatListItem,
3940
KeyValuePipe,
4041
InvocIdPipe,
42+
AsyncPipe,
4143
],
4244
})
4345
export class EventTabComponent {
4446
readonly eventsMap = input<Map<string, any>>(new Map<string, any>());
4547
readonly traceData = input<Span[]>([]);
4648
@Output() selectedEvent = new EventEmitter<string>();
4749
private readonly dialog = inject(MatDialog);
50+
private readonly featureFlagService = inject(FEATURE_FLAG_SERVICE);
4851

4952
readonly view = signal<string>('events');
5053
readonly isTraceView = computed(() => this.view() === 'trace');
@@ -67,6 +70,7 @@ export class EventTabComponent {
6770
});
6871

6972
showJson: boolean[] = Array(this.eventsMap().size).fill(false);
73+
readonly isTraceEnabledObs = this.featureFlagService.isTraceEnabled();
7074

7175
toggleJson(index: number) {
7276
this.showJson[index] = !this.showJson[index];

0 commit comments

Comments
 (0)