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 f85aa36 commit e0a3927
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 2 deletions.
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,19 @@
export interface EventMetric {
eventType: string;
exception?: string;
id: string;
payload: { [key: string]: any };
projectId?: string;
result?: any;
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,37 @@
<ng-container *transloco="let t; read: 'event_metrics'">
@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="successful">
<th mat-header-cell *matHeaderCellDef>Successful</th>
<td mat-cell *matCellDef="let row">
{{ t(row.successful ? "successful" : "failed") }}
</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>
}
</ng-container>
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,113 @@
import { Component, OnInit } from '@angular/core';
import { TranslocoModule } from '@ngneat/transloco';
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;
successful: boolean;
}

@Component({
selector: 'app-event-metrics-log',
templateUrl: './event-metrics-log.component.html',
styleUrls: ['./event-metrics-log.component.scss'],
standalone: true,
imports: [OwnerComponent, TranslocoModule, UICommonModule]
})
export class EventMetricsLogComponent extends DataLoadingComponent implements OnInit {
columnsToDisplay: string[] = ['scope', 'eventType', 'successful', '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,
successful: eventMetric.exception == null,
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.
// I have not localized at this time because these strings are likely to change based on feedback.
// When this feature is mature, these should be localized to help Project Administrators.
switch (eventType) {
case 'CancelPreTranslationBuildAsync':
return 'Cancel draft generation';

Check warning on line 92 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L92

Added line #L92 was not covered by tests
case 'CancelSyncAsync':
return 'Cancel synchronization with Paratext';

Check warning on line 94 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L94

Added line #L94 was not covered by tests
case 'SetDraftAppliedAsync':
return "Updated the chapter's draft applied status";

Check warning on line 96 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L96

Added line #L96 was not covered by tests
case 'SetIsValidAsync':
return 'Marked chapter as valid/invalid';

Check warning on line 98 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L98

Added line #L98 was not covered by tests
case 'SetPreTranslateAsync':
return 'Set drafting as enabled/disabled for the project';

Check warning on line 100 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L100

Added line #L100 was not covered by tests
case 'SetServalConfigAsync':
return 'Manually update drafting configuration for the project';

Check warning on line 102 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L102

Added line #L102 was not covered by tests
case 'StartBuildAsync':
return 'Begin training translation suggestions';

Check warning on line 104 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L104

Added line #L104 was not covered by tests
case 'StartPreTranslationBuildAsync':
return 'Start draft generation';

Check warning on line 106 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L106

Added line #L106 was not covered by tests
case 'SyncAsync':
return 'Start synchronization with Paratext';

Check warning on line 108 in src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts

View check run for this annotation

Codecov / codecov/patch

src/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L108

Added line #L108 was not covered by tests
default:
return eventType;
}
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
<h1>Event Log</h1>
<ng-container *transloco="let t; read: 'event_metrics'">
<h1>{{ t("event_log") }}</h1>
<app-event-metrics-log></app-event-metrics-log>
</ng-container>
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Component } from '@angular/core';
import { TranslocoModule } from '@ngneat/transloco';
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, TranslocoModule]
})
export class EventMetricsComponent {}
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@
"default_resource_tab_header": "Resource",
"draft_tab_header": "Auto Draft"
},
"event_metrics": {
"event_log": "Event Log",
"failed": "Failed",
"successful": "Successful"
},
"font_unsupported_message": {
"change_font_or_contact_for_help": "If this does not meet the needs of this project, please select a different font in the Paratext project settings, or { 1 }contact us for help.{ 2 }",
"contact_for_help": "If this does not meet the needs of this project, please { 1 }contact us for help.{ 2 }",
Expand Down

0 comments on commit e0a3927

Please sign in to comment.