Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/components/event-tab/event-tab.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
@if (isTraceView()) {
<p>Trace</p>
}
@if (traceData()) {
@if (traceData().length > 0) {
<mat-button-toggle-group name="fontStyle" aria-label="Font Style" style="scale: 0.8" [(ngModel)]="view">
<mat-button-toggle value="events">Events</mat-button-toggle>
<mat-button-toggle value="trace">Trace</mat-button-toggle>
Expand Down
219 changes: 211 additions & 8 deletions src/app/components/event-tab/event-tab.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,234 @@
* limitations under the License.
*/

import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatDialogModule, MatDialogRef} from '@angular/material/dialog';
import {MatButtonToggleHarness} from '@angular/material/button-toggle/testing';
import {MatDialog} from '@angular/material/dialog';
import {MatListHarness} from '@angular/material/list/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';

import {Span} from '../../core/models/Trace';

import {EventTabComponent} from './event-tab.component';
import {TraceChartComponent} from './trace-chart/trace-chart.component';

const MOCK_TRACE_DATA: Span[] = [
{
name: 'agent.act',
start_time: 1733084700000000000,
end_time: 1733084760000000000,
span_id: 'span-1',
trace_id: 'trace-1',
attributes: {
'event_id': 1,
'gcp.vertex.agent.invocation_id': '21332-322222',
'gcp.vertex.agent.llm_request':
'{"contents":[{"role":"user","parts":[{"text":"Hello"}]},{"role":"agent","parts":[{"text":"Hi. What can I help you with?"}]},{"role":"user","parts":[{"text":"I need help with my project."}]}]}',
},
},
{
name: 'tool.invoke',
start_time: 1733084705000000000,
end_time: 1733084755000000000,
span_id: 'span-2',
parent_span_id: 'span-1',
trace_id: 'trace-1',
attributes: {
'tool_name': 'project_helper',
},
children: [
{
name: 'sub-tool-1.invoke',
start_time: 1733084710000000000,
end_time: 1733084750000000000,
span_id: 'span-3',
parent_span_id: 'span-2',
trace_id: 'trace-1',
attributes: {
'sub_tool_name': 'sub_project_helper_1',
},
children: [
{
name: 'sub-tool-2.invoke',
start_time: 1733084715000000000,
end_time: 1733084745000000000,
span_id: 'span-4',
parent_span_id: 'span-3',
trace_id: 'trace-1',
attributes: {
'sub_tool_name': 'sub_project_helper_2',
},
children: [
{
name: 'sub-tool-3.invoke',
start_time: 1733084720000000000,
end_time: 1733084740000000000,
span_id: 'span-5',
parent_span_id: 'span-4',
trace_id: 'trace-1',
attributes: {
'sub_tool_name': 'sub_project_helper_3',
},
children: [],
},
],
},
],
},
],
}
] as Span[];

const MOCK_EVENTS_MAP = new Map<string, any>([
['event1', {title: 'Event 1 Title'}],
['event2', {title: 'Event 2 Title'}],
]);

describe('EventTabComponent', () => {
let component: EventTabComponent;
let fixture: ComponentFixture<EventTabComponent>;
const mockDialogRef = {
close: jasmine.createSpy('close'),
};
let loader: HarnessLoader;
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MatDialogModule, EventTabComponent],
providers: [{ provide: MatDialogRef, useValue: mockDialogRef }],
}).compileComponents();
await TestBed
.configureTestingModule({
imports: [EventTabComponent, NoopAnimationsModule],
providers: [{provide: MatDialog, useValue: matDialogSpy}],
})
.compileComponents();

fixture = TestBed.createComponent(EventTabComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
matDialogSpy.open.calls.reset();
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should display "No conversations" if eventsMap is empty', () => {
expect(fixture.nativeElement.textContent).toContain('No conversations');
});

describe('with events', () => {
beforeEach(async () => {
fixture.componentRef.setInput('eventsMap', MOCK_EVENTS_MAP);
fixture.detectChanges();
await fixture.whenStable();
});

it('should display events list by default', async () => {
const list = await loader.getHarness(MatListHarness);
const items = await list.getItems();
expect(items.length).toBe(2);
expect(await items[0].getFullText()).toContain('Event 1 Title');
expect(await items[1].getFullText()).toContain('Event 2 Title');
});

it('should emit selectedEvent on event click', async () => {
spyOn(component.selectedEvent, 'emit');
const list = await loader.getHarness(MatListHarness);
const items = await list.getItems();
await (await items[0].host()).click();
expect(component.selectedEvent.emit).toHaveBeenCalledWith('event1');
});

it('should not show toggle if traceData is empty', async () => {
const hasToggleGroup = fixture.nativeElement.querySelector(
'mat-button-toggle-group',
);
expect(hasToggleGroup).toBeNull();
});
});

describe('with trace data', () => {
beforeEach(async () => {
fixture.componentRef.setInput('eventsMap', MOCK_EVENTS_MAP);
fixture.componentRef.setInput('traceData', MOCK_TRACE_DATA);
fixture.detectChanges();
await fixture.whenStable();
});

it('should show toggle buttons', async () => {
const toggles = await loader.getAllHarnesses(MatButtonToggleHarness);
expect(toggles.length).toBe(2);
expect(await toggles[0].getText()).toBe('Events');
expect(await toggles[1].getText()).toBe('Trace');
});

it('should switch to trace view and display traces', async () => {
const traceToggle = await loader.getHarness(
MatButtonToggleHarness.with({text: 'Trace'}),
);
await traceToggle.check();
fixture.detectChanges();

const list = await loader.getHarness(MatListHarness);
const items = await list.getItems();
expect(items.length).toBe(1);
expect(await items[0].getFullText()).toContain('Invocation 21332-322222');
});

it('should open dialog when trace item is clicked', async () => {
const traceToggle = await loader.getHarness(
MatButtonToggleHarness.with({text: 'Trace'}),
);
await traceToggle.check();
fixture.detectChanges();

const list = await loader.getHarness(MatListHarness);
const items = await list.getItems();
await (await items[0].host()).click();

expect(matDialogSpy.open).toHaveBeenCalledWith(TraceChartComponent, {
width: 'auto',
maxWidth: '90vw',
data: {
spans: component.spansByTraceId().get('trace-1'),
invocId: '21332-322222',
},
});
});

it('should display multiple traces if present', async () => {
const MOCK_TRACE_DATA_WITH_MULTIPLE_TRACES: Span[] = [
...MOCK_TRACE_DATA,
{
name: 'agent.act-2',
start_time: 1733084700000000000,
end_time: 1733084760000000000,
span_id: 'span-10',
trace_id: 'trace-2',
attributes: {
'event_id': 10,
'gcp.vertex.agent.invocation_id': 'invoc-2',
'gcp.vertex.agent.llm_request': '{}',
},
},
];
fixture.componentRef.setInput(
'traceData',
MOCK_TRACE_DATA_WITH_MULTIPLE_TRACES,
);
fixture.detectChanges();
await fixture.whenStable();

const traceToggle = await loader.getHarness(
MatButtonToggleHarness.with({text: 'Trace'}),
);
await traceToggle.check();
fixture.detectChanges();

const list = await loader.getHarness(MatListHarness);
const items = await list.getItems();
expect(items.length).toBe(2);
expect(await items[0].getFullText()).toContain('Invocation 21332-322222');
expect(await items[1].getFullText()).toContain('Invocation invoc-2');
});
});
});
8 changes: 1 addition & 7 deletions src/app/components/event-tab/event-tab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class EventTabComponent {
readonly view = signal<string>('events');
readonly isTraceView = computed(() => this.view() === 'trace');
readonly spansByTraceId = computed(() => {
if (!this.traceData || this.traceData.length == 0) {
if (!this.traceData() || this.traceData().length == 0) {
return new Map<string, Span[]>();
}
return this.traceData().reduce((map, span) => {
Expand All @@ -66,12 +66,6 @@ export class EventTabComponent {
}, new Map<string, Span[]>());
});

showJson: boolean[] = Array(this.eventsMap().size).fill(false);

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

selectEvent(key: string) {
this.selectedEvent.emit(key);
}
Expand Down
9 changes: 6 additions & 3 deletions src/app/components/session-tab/session-tab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
* limitations under the License.
*/

import {Component, EventEmitter, Input, OnInit, Output, Inject} from '@angular/core';
import {NgClass} from '@angular/common';
import {ChangeDetectorRef, Component, EventEmitter, Inject, inject, Input, OnInit, Output} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {Subject} from 'rxjs';
import {switchMap} from 'rxjs/operators';

import {Session} from '../../core/models/Session';
import {SessionService, SESSION_SERVICE} from '../../core/services/session.service';
import { NgClass } from '@angular/common';
import {SESSION_SERVICE, SessionService} from '../../core/services/session.service';

@Component({
selector: 'app-session-tab',
Expand All @@ -40,6 +41,7 @@ export class SessionTabComponent implements OnInit {
sessionList: any[] = [];

private refreshSessionsSubject = new Subject<void>();
private readonly changeDetectorRef = inject(ChangeDetectorRef);

constructor(
@Inject(SESSION_SERVICE) private sessionService: SessionService,
Expand All @@ -58,6 +60,7 @@ export class SessionTabComponent implements OnInit {
Number(b.lastUpdateTime) - Number(a.lastUpdateTime),
);
this.sessionList = res;
this.changeDetectorRef.detectChanges();
});
}

Expand Down
Loading