Skip to content

Commit

Permalink
WIP: Event Metrics Log Frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
pmachapman committed Jan 6, 2025
1 parent cce3083 commit 5151ef6
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ describe('SFProjectService', () => {
}));
});

describe('onlineEventMetrics', () => {
it('should invoke the command service', fakeAsync(async () => {
const env = new TestEnvironment();
const projectId = 'project01';
const pageIndex = 0;
const pageSize = 20;
await env.service.onlineEventMetrics(projectId, pageIndex, pageSize);
verify(mockedCommandService.onlineInvoke(anything(), 'eventMetrics', anything())).once();
expect().nothing();
}));
});

class TestEnvironment {
readonly httpTestingController: HttpTestingController;
readonly service: SFProjectService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { QueryParameters } from 'xforge-common/query-parameters';
import { RealtimeService } from 'xforge-common/realtime.service';
import { RetryingRequest, RetryingRequestService } from 'xforge-common/retrying-request.service';
import { TransceleratorQuestion } from '../checking/import-questions-dialog/import-questions-dialog.component';
import { EventMetric } from '../event-metrics/event-metric';
import { ShareLinkType } from '../shared/share/share-dialog.component';
import { InviteeStatus } from '../users/collaborators/collaborators.component';
import { BiblicalTermDoc } from './models/biblical-term-doc';
Expand Down Expand Up @@ -309,4 +310,8 @@ export class SFProjectService extends ProjectService<SFProject, SFProjectDoc> {
isValid
});
}

async onlineEventMetrics(projectId: string, pageIndex: number, pageSize: number): Promise<EventMetric[]> {
return await this.onlineInvoke<EventMetric[]>('eventMetrics', { projectId, pageIndex, pageSize });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface EventMetric {
id: string;
eventType: string;
payload: { [key: string]: any };
projectId?: string;
scope: EventScope;
timeStamp: string;
userId?: string;
}

export enum EventScope {
None = 'None',
Settings = 'Settings',
Sync = 'Sync',
Drafting = 'Drafting',
Checking = 'Checking'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@if (!isLoading) {
<table mat-table id="event-metrics-log-table" [dataSource]="rows">
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToDisplay"></tr>
<ng-container matColumnDef="scope">
<th mat-header-cell *matHeaderCellDef>Scope</th>
<td mat-cell *matCellDef="let row">
{{ row.scope }}
</td>
</ng-container>
<ng-container matColumnDef="eventType">
<th mat-header-cell *matHeaderCellDef>Event</th>
<td mat-cell *matCellDef="let row">
{{ row.eventType }}
</td>
</ng-container>
<ng-container matColumnDef="author">
<th mat-header-cell *matHeaderCellDef>Author</th>
<td mat-cell *matCellDef="let row">
<app-owner
[ownerRef]="row.userId"
[includeAvatar]="true"
[layoutStacked]="false"
[dateTime]="row.timeStamp"
></app-owner>
</td>
</ng-container>
</table>
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { DebugElement, getDebugNode } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { anything, mock, when } from 'ts-mockito';
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
import { AuthService } from 'xforge-common/auth.service';
import { NoticeService } from 'xforge-common/notice.service';
import { TestRealtimeModule } from 'xforge-common/test-realtime.module';
import { configureTestingModule, TestTranslocoModule } from 'xforge-common/test-utils';
import { UICommonModule } from 'xforge-common/ui-common.module';
import { UserService } from 'xforge-common/user.service';
import { SF_TYPE_REGISTRY } from '../core/models/sf-type-registry';
import { SFProjectService } from '../core/sf-project.service';
import { EventMetric, EventScope } from './event-metric';
import { EventMetricsLogComponent } from './event-metrics-log.component';

const mockedActivatedProjectService = mock(ActivatedProjectService);
const mockedAuthService = mock(AuthService);
const mockedNoticeService = mock(NoticeService);
const mockedProjectService = mock(SFProjectService);
const mockedUserService = mock(UserService);

describe('EventMetricsLogComponent', () => {
configureTestingModule(() => ({
imports: [
EventMetricsLogComponent,
NoopAnimationsModule,
RouterModule.forRoot([]),
UICommonModule,
TestTranslocoModule,
TestRealtimeModule.forRoot(SF_TYPE_REGISTRY)
],
providers: [
{ provide: AuthService, useMock: mockedAuthService },
{ provide: ActivatedProjectService, useMock: mockedActivatedProjectService },
{ provide: NoticeService, useMock: mockedNoticeService },
{ provide: SFProjectService, useMock: mockedProjectService },
{ provide: UserService, useMock: mockedUserService },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting()
]
}));

it('should not display table if no event metrics', fakeAsync(() => {
const env = new TestEnvironment();
env.wait();
env.wait();

expect(env.table).toBeNull();
}));

it('should display event metrics', fakeAsync(() => {
const env = new TestEnvironment();
env.populateEventMetrics();
env.wait();
env.wait();

expect(env.rows.length).toEqual(1);
}));
});

class TestEnvironment {
readonly component: EventMetricsLogComponent;
readonly fixture: ComponentFixture<EventMetricsLogComponent>;

mockProjectId = 'project01';

constructor() {
const mockProjectId$ = new BehaviorSubject<string>(this.mockProjectId);
when(mockedActivatedProjectService.projectId).thenReturn(this.mockProjectId);
when(mockedActivatedProjectService.projectId$).thenReturn(mockProjectId$);
when(mockedUserService.currentUserId).thenReturn('user01');
when(mockedProjectService.onlineEventMetrics(anything(), anything(), anything())).thenReturn(null);

this.fixture = TestBed.createComponent(EventMetricsLogComponent);
this.component = this.fixture.componentInstance;
}

get table(): DebugElement {
return this.fixture.debugElement.query(By.css('#event-metrics-log-table'));
}

get rows(): DebugElement[] {
return Array.from(this.table.nativeElement.querySelectorAll('tbody tr')).map(r => getDebugNode(r) as DebugElement);
}

populateEventMetrics(): void {
when(mockedProjectService.onlineEventMetrics(anything(), anything(), anything())).thenReturn(
Promise.resolve([{ scope: EventScope.Settings, timeStamp: new Date().toISOString() } as EventMetric])
);
}

wait(): void {
this.fixture.detectChanges();
tick();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Component, OnInit } from '@angular/core';
import { switchMap } from 'rxjs';
import { ActivatedProjectService } from 'xforge-common/activated-project.service';
import { DataLoadingComponent } from 'xforge-common/data-loading-component';
import { I18nService } from 'xforge-common/i18n.service';
import { NoticeService } from 'xforge-common/notice.service';
import { OwnerComponent } from 'xforge-common/owner/owner.component';
import { UICommonModule } from 'xforge-common/ui-common.module';
import { filterNullish } from 'xforge-common/util/rxjs-util';
import { SFProjectService } from '../core/sf-project.service';
import { EventMetric } from './event-metric';

interface Row {
eventType: string;
scope: string;
timeStamp: string;
userId: string;
}

@Component({
selector: 'app-event-metrics-log',
templateUrl: './event-metrics-log.component.html',
styleUrls: ['./event-metrics-log.component.scss'],
standalone: true,
imports: [UICommonModule, OwnerComponent]
})
export class EventMetricsLogComponent extends DataLoadingComponent implements OnInit {
columnsToDisplay: string[] = ['scope', 'eventType', 'author'];
rows: Row[] = [];

pageIndex: number = 0;
pageSize: number = 50;

private eventMetrics?: Readonly<EventMetric[]>;

constructor(
noticeService: NoticeService,
private readonly i18n: I18nService,
private readonly activatedProjectService: ActivatedProjectService,
private readonly projectService: SFProjectService
) {
super(noticeService);
}

get isLoading(): boolean {
return this.eventMetrics == null;
}

ngOnInit(): void {
this.loadingStarted();
this.subscribe(
this.activatedProjectService.projectId$.pipe(
filterNullish(),
switchMap(async projectId => {
this.eventMetrics = await this.projectService.onlineEventMetrics(projectId, this.pageIndex, this.pageSize);
this.generateRows();
this.loadingFinished();
})
)
);
}

private generateRows(): void {
if (this.eventMetrics == null) {
return;
}

const rows: Row[] = [];
for (const eventMetric of this.eventMetrics) {
rows.push({
eventType: this.getEventType(eventMetric.eventType),
scope: eventMetric.scope,
timeStamp: this.i18n.formatDate(new Date(eventMetric.timeStamp), { showTimeZone: true }),
userId: eventMetric.userId
});
}
this.rows = rows;
}

private getEventType(eventType: string): string {
// These values are the functions that have the LogEventMetric attribute, where:
// - The case is the name of the method
// - The return value is a user friendly description of what the method does
// NOTE: These values are not localized, but can be localized if needed.
switch (eventType) {
case 'CancelPreTranslationBuildAsync':
return 'Cancel draft generation';
case 'CancelSyncAsync':
return 'Cancel synchronization with Paratext';
case 'SetDraftAppliedAsync':
return "Updated the chapter's draft applied status";
case 'SetIsValidAsync':
return 'Marked chapter as valid/invalid';
case 'SetPreTranslateAsync':
return 'Set drafting as enabled/disabled for the project';
case 'SetServalConfigAsync':
return 'Manually update drafting configuration for the project';
case 'StartBuildAsync':
return 'Begin training translation suggestions';
case 'StartPreTranslationBuildAsync':
return 'Start draft generation';
case 'SyncAsync':
return 'Synchronize with Paratext';
default:
return eventType;
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
<h1>Event Log</h1>
<app-event-metrics-log></app-event-metrics-log>
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { EventMetricsLogComponent } from './event-metrics-log.component';

@Component({
selector: 'app-event-metrics',
templateUrl: './event-metrics.component.html',
styleUrls: ['./event-metrics.component.scss'],
standalone: true,
imports: []
imports: [EventMetricsLogComponent]
})
export class EventMetricsComponent {}

0 comments on commit 5151ef6

Please sign in to comment.