-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
- Loading branch information
There are no files selected for viewing
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> |
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L92
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L94
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L96
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L98
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L100
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L102
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L104
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L106
|
||
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 Codecov / codecov/patchsrc/SIL.XForge.Scripture/ClientApp/src/app/event-metrics/event-metrics-log.component.ts#L108
|
||
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 {} |