From 3cca0b8fb1f3b41428b21a19b04ffacde6b92349 Mon Sep 17 00:00:00 2001 From: "Darryl L. Pierce" Date: Sun, 19 Jul 2020 10:47:31 -0400 Subject: [PATCH] Added the frontend state wiring for the task audit log [#413] --- .../actions/task-audit-log.actions.ts | 53 +++++++ .../adaptors/task-audit-log.adaptor.spec.ts | 134 +++++++++++++++++ .../adaptors/task-audit-log.adaptor.ts | 77 ++++++++++ .../backend-status.constants.ts | 21 +++ .../backend-status/backend-status.fixtures.ts | 28 ++++ .../backend-status/backend-status.module.ts | 12 +- .../effects/task-audit-log.effects.spec.ts | 136 ++++++++++++++++++ .../effects/task-audit-log.effects.ts | 89 ++++++++++++ .../src/app/backend-status/index.ts | 8 +- .../models/task-audit-log-entry.fixtures.ts | 54 +++++++ .../models/task-audit-log-entry.ts | 24 ++++ .../reducers/task-audit-log.reducer.spec.ts | 122 ++++++++++++++++ .../reducers/task-audit-log.reducer.ts | 69 +++++++++ .../services/task-audit-log.service.spec.ts | 77 ++++++++++ .../services/task-audit-log.service.ts | 41 ++++++ .../src/assets/i18n/en/backend-status.json | 7 + .../src/assets/i18n/es/backend-status.json | 7 + .../src/assets/i18n/fr/backend-status.json | 7 + .../src/assets/i18n/pt/backend-status.json | 7 + 19 files changed, 969 insertions(+), 4 deletions(-) create mode 100644 comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts create mode 100644 comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts create mode 100644 comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts create mode 100644 comixed-frontend/src/app/backend-status/backend-status.constants.ts create mode 100644 comixed-frontend/src/app/backend-status/backend-status.fixtures.ts create mode 100644 comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts create mode 100644 comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts create mode 100644 comixed-frontend/src/app/backend-status/models/task-audit-log-entry.fixtures.ts create mode 100644 comixed-frontend/src/app/backend-status/models/task-audit-log-entry.ts create mode 100644 comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts create mode 100644 comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts create mode 100644 comixed-frontend/src/app/backend-status/services/task-audit-log.service.spec.ts create mode 100644 comixed-frontend/src/app/backend-status/services/task-audit-log.service.ts diff --git a/comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts b/comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts new file mode 100644 index 000000000..20361a367 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts @@ -0,0 +1,53 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Action } from '@ngrx/store'; +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; + +export enum TaskAuditLogActionTypes { + GetEntries = '[TASK AUDIT LOG] Get task audit log entries', + ReceivedEntries = '[TASK AUDIT LOG] Task audit log entries received', + GetEntriesFailed = '[TASK AUDIT LOG] Failed to get task audit log entries' +} + +export class GetTaskAuditLogEntries implements Action { + readonly type = TaskAuditLogActionTypes.GetEntries; + + constructor( + public payload: { + cutoff: number; + } + ) {} +} + +export class ReceivedTaskAuditLogEntries implements Action { + readonly type = TaskAuditLogActionTypes.ReceivedEntries; + + constructor(public payload: { entries: TaskAuditLogEntry[] }) {} +} + +export class GetTaskAuditLogEntriesFailed implements Action { + readonly type = TaskAuditLogActionTypes.GetEntriesFailed; + + constructor() {} +} + +export type TaskAuditLogActions = + | GetTaskAuditLogEntries + | ReceivedTaskAuditLogEntries + | GetTaskAuditLogEntriesFailed; diff --git a/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts b/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts new file mode 100644 index 000000000..5a4b2e595 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts @@ -0,0 +1,134 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { TaskAuditLogAdaptor } from './task-audit-log.adaptor'; +import { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/models/task-audit-log-entry.fixtures'; +import { TestBed } from '@angular/core/testing'; +import { Store, StoreModule } from '@ngrx/store'; +import { AppState } from 'app/backend-status'; +import { + GetTaskAuditLogEntries, + GetTaskAuditLogEntriesFailed, + ReceivedTaskAuditLogEntries +} from 'app/backend-status/actions/task-audit-log.actions'; +import { LoggerModule } from '@angular-ru/logger'; +import { + reducer, + TASK_AUDIT_LOG_FEATURE_KEY +} from 'app/backend-status/reducers/task-audit-log.reducer'; +import { EffectsModule } from '@ngrx/effects'; +import { TaskAuditLogEffects } from 'app/backend-status/effects/task-audit-log.effects'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MessageService } from 'primeng/api'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('TaskAuditLogAdaptor', () => { + const LAST_ENTRY_DATE = new Date().getTime(); + const LOG_ENTRIES = [ + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 + ]; + + let taskAuditLogAdaptor: TaskAuditLogAdaptor; + let store: Store; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + LoggerModule.forRoot(), + TranslateModule.forRoot(), + StoreModule.forRoot({}), + StoreModule.forFeature(TASK_AUDIT_LOG_FEATURE_KEY, reducer), + EffectsModule.forRoot([]), + EffectsModule.forFeature([TaskAuditLogEffects]) + ], + providers: [TaskAuditLogAdaptor, MessageService] + }); + + taskAuditLogAdaptor = TestBed.get(TaskAuditLogAdaptor); + store = TestBed.get(Store); + spyOn(store, 'dispatch').and.callThrough(); + }); + + it('should create an instance', () => { + expect(taskAuditLogAdaptor).toBeTruthy(); + }); + + describe('getting the list of task audit log entries', () => { + let lastEntryDate: number; + + beforeEach(() => { + lastEntryDate = taskAuditLogAdaptor.lastEntryDate; + taskAuditLogAdaptor.getEntries(); + }); + + it('fires an action', () => { + expect(store.dispatch).toHaveBeenCalledWith( + new GetTaskAuditLogEntries({ cutoff: lastEntryDate }) + ); + }); + + it('provides updates on fetching', () => { + taskAuditLogAdaptor.fetchingEntries$.subscribe(response => + expect(response).toBeTruthy() + ); + }); + + describe('success', () => { + beforeEach(() => { + store.dispatch( + new ReceivedTaskAuditLogEntries({ entries: LOG_ENTRIES }) + ); + }); + + it('provides updates on fetching', () => { + taskAuditLogAdaptor.fetchingEntries$.subscribe(response => + expect(response).toBeFalsy() + ); + }); + + it('provides updates on the log entries', () => { + taskAuditLogAdaptor.entries$.subscribe(response => + expect(response).toEqual(LOG_ENTRIES) + ); + }); + }); + + describe('failure', () => { + beforeEach(() => { + store.dispatch(new GetTaskAuditLogEntriesFailed()); + }); + + it('provides updates on fetching', () => { + taskAuditLogAdaptor.fetchingEntries$.subscribe(response => + expect(response).toBeFalsy() + ); + }); + }); + }); +}); diff --git a/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts b/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts new file mode 100644 index 000000000..8b427d93c --- /dev/null +++ b/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts @@ -0,0 +1,77 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import {Injectable} from '@angular/core'; +import {LoggerService} from '@angular-ru/logger'; +import {Store} from '@ngrx/store'; +import {AppState} from 'app/backend-status'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {TaskAuditLogEntry} from 'app/backend-status/models/task-audit-log-entry'; +import {GetTaskAuditLogEntries} from 'app/backend-status/actions/task-audit-log.actions'; +import {TASK_AUDIT_LOG_FEATURE_KEY, TaskAuditLogState} from 'app/backend-status/reducers/task-audit-log.reducer'; +import {filter} from 'rxjs/operators'; +import * as _ from 'lodash'; + +@Injectable({ + providedIn: 'root' +}) +export class TaskAuditLogAdaptor { + private _lastEntryDate$ = new BehaviorSubject(0); + private _fetchingEntries$ = new BehaviorSubject(false); + private _entries$ = new BehaviorSubject([]); + + constructor(private logger: LoggerService, private store: Store) { + this.store + .select(TASK_AUDIT_LOG_FEATURE_KEY) + .pipe(filter(state => !!state)) + .subscribe((state: TaskAuditLogState) => { + this.logger.debug('task audit log state changed:', state); + if (!_.isEqual(this._lastEntryDate$.getValue(), state.lastEntryDate)) { + this._lastEntryDate$.next(state.lastEntryDate); + } + if (this._fetchingEntries$.getValue() !== state.fetchingEntries) { + this._fetchingEntries$.next(state.fetchingEntries); + } + if (!_.isEqual(this._entries$.getValue(), state.entries)) { + this._entries$.next(state.entries); + } + }); + } + + getEntries(): void { + this.logger.debug( + 'getting task audit log entries:', + this._lastEntryDate$.getValue() + ); + this.store.dispatch( + new GetTaskAuditLogEntries({cutoff: this._lastEntryDate$.getValue()}) + ); + } + + get lastEntryDate(): number { + return this._lastEntryDate$.getValue(); + } + + get fetchingEntries$(): Observable { + return this._fetchingEntries$.asObservable(); + } + + get entries$(): Observable { + return this._entries$.asObservable(); + } +} diff --git a/comixed-frontend/src/app/backend-status/backend-status.constants.ts b/comixed-frontend/src/app/backend-status/backend-status.constants.ts new file mode 100644 index 000000000..69b47f2b4 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/backend-status.constants.ts @@ -0,0 +1,21 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { API_ROOT_URL } from 'app/app.functions'; + +export const GET_TASK_LOG_ENTRIES_URL = `${API_ROOT_URL}/tasks/entries/\${timestamp}`; diff --git a/comixed-frontend/src/app/backend-status/backend-status.fixtures.ts b/comixed-frontend/src/app/backend-status/backend-status.fixtures.ts new file mode 100644 index 000000000..80cc6c76d --- /dev/null +++ b/comixed-frontend/src/app/backend-status/backend-status.fixtures.ts @@ -0,0 +1,28 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +export { + BUILD_DETAILS +} from 'app/backend-status/models/build-details.fixtures'; +export { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/models/task-audit-log-entry.fixtures'; diff --git a/comixed-frontend/src/app/backend-status/backend-status.module.ts b/comixed-frontend/src/app/backend-status/backend-status.module.ts index c8299781f..5c0a54e0a 100644 --- a/comixed-frontend/src/app/backend-status/backend-status.module.ts +++ b/comixed-frontend/src/app/backend-status/backend-status.module.ts @@ -24,9 +24,13 @@ import { BackendStatusRoutingModule } from 'app/backend-status/backend-status-ro import { BuildDetailsAdaptor } from 'app/backend-status/adaptors/build-details.adaptor'; import { StoreModule } from '@ngrx/store'; import * as fromBuildDetails from './reducers/build-details.reducer'; +import * as fromTaskAuditLog from './reducers/task-audit-log.reducer'; import { EffectsModule } from '@ngrx/effects'; import { BuildDetailsEffects } from 'app/backend-status/effects/build-details.effects'; import { TranslateModule } from '@ngx-translate/core'; +import { TaskAuditLogEffects } from 'app/backend-status/effects/task-audit-log.effects'; +import { TaskAuditLogPageComponent } from './pages/task-audit-log-page/task-audit-log-page.component'; +import { TaskAuditLogAdaptor } from 'app/backend-status/adaptors/task-audit-log.adaptor'; @NgModule({ declarations: [BuildDetailsPageComponent], @@ -38,9 +42,13 @@ import { TranslateModule } from '@ngx-translate/core'; fromBuildDetails.BUILD_DETAILS_FEATURE_KEY, fromBuildDetails.reducer ), - EffectsModule.forFeature([BuildDetailsEffects]) + StoreModule.forFeature( + fromTaskAuditLog.TASK_AUDIT_LOG_FEATURE_KEY, + fromTaskAuditLog.reducer + ), + EffectsModule.forFeature([BuildDetailsEffects, TaskAuditLogEffects]) ], exports: [CommonModule], - providers: [BuildDetailsService, BuildDetailsAdaptor] + providers: [BuildDetailsService, BuildDetailsAdaptor, TaskAuditLogAdaptor] }) export class BackendStatusModule {} diff --git a/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts b/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts new file mode 100644 index 000000000..7771f633a --- /dev/null +++ b/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts @@ -0,0 +1,136 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import {TestBed} from '@angular/core/testing'; +import {provideMockActions} from '@ngrx/effects/testing'; +import {Observable, of, throwError} from 'rxjs'; + +import {TaskAuditLogEffects} from './task-audit-log.effects'; +import {TaskAuditLogService} from 'app/backend-status/services/task-audit-log.service'; +import { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/backend-status.fixtures'; +import { + GetTaskAuditLogEntries, + GetTaskAuditLogEntriesFailed, + ReceivedTaskAuditLogEntries +} from 'app/backend-status/actions/task-audit-log.actions'; +import {hot} from 'jasmine-marbles'; +import {HttpErrorResponse} from '@angular/common/http'; +import {MessageService} from 'primeng/api'; +import {LoggerModule} from '@angular-ru/logger'; +import {TranslateModule} from '@ngx-translate/core'; +import objectContaining = jasmine.objectContaining; + +describe('TaskAuditLogEffects', () => { + const LOG_ENTRIES = [ + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 + ]; + + let actions$: Observable; + let effects: TaskAuditLogEffects; + let taskAuditLogService: jasmine.SpyObj; + let messageService: MessageService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [LoggerModule.forRoot(), TranslateModule.forRoot()], + providers: [ + TaskAuditLogEffects, + provideMockActions(() => actions$), + { + provide: TaskAuditLogService, + useValue: { + getLogEntries: jasmine.createSpy( + 'TaskAuditLogService.getLogEntries()' + ) + } + }, + MessageService + ] + }); + + effects = TestBed.get(TaskAuditLogEffects); + taskAuditLogService = TestBed.get(TaskAuditLogService); + messageService = TestBed.get(MessageService); + spyOn(messageService, 'add'); + }); + + it('should be created', () => { + expect(effects).toBeTruthy(); + }); + + describe('getting the list of log entries', () => { + it('fires an action on success', () => { + const serviceResponse = LOG_ENTRIES; + const action = new GetTaskAuditLogEntries({ + cutoff: new Date().getTime() + }); + const outcome = new ReceivedTaskAuditLogEntries({entries: LOG_ENTRIES}); + + actions$ = hot('-a', {a: action}); + taskAuditLogService.getLogEntries.and.returnValue(of(serviceResponse)); + + const expected = hot('-b', {b: outcome}); + expect(effects.getLogEntries$).toBeObservable(expected); + }); + + it('fires an action on service failure', () => { + const serviceResponse = new HttpErrorResponse({}); + const action = new GetTaskAuditLogEntries({ + cutoff: new Date().getTime() + }); + const outcome = new GetTaskAuditLogEntriesFailed(); + + actions$ = hot('-a', {a: action}); + taskAuditLogService.getLogEntries.and.returnValue( + throwError(serviceResponse) + ); + + const expected = hot('-b', {b: outcome}); + expect(effects.getLogEntries$).toBeObservable(expected); + expect(messageService.add).toHaveBeenCalledWith( + objectContaining({severity: 'error'}) + ); + }); + + it('fires an action on general failure', () => { + const action = new GetTaskAuditLogEntries({ + cutoff: new Date().getTime() + }); + const outcome = new GetTaskAuditLogEntriesFailed(); + + actions$ = hot('-a', {a: action}); + taskAuditLogService.getLogEntries.and.throwError('expected'); + + const expected = hot('-(b|)', {b: outcome}); + expect(effects.getLogEntries$).toBeObservable(expected); + expect(messageService.add).toHaveBeenCalledWith( + objectContaining({severity: 'error'}) + ); + }); + }); +}); diff --git a/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts b/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts new file mode 100644 index 000000000..4032d107e --- /dev/null +++ b/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts @@ -0,0 +1,89 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { + GetTaskAuditLogEntriesFailed, + ReceivedTaskAuditLogEntries, + TaskAuditLogActions, + TaskAuditLogActionTypes +} from '../actions/task-audit-log.actions'; +import { Action } from '@ngrx/store'; +import { Observable, of } from 'rxjs'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { LoggerService } from '@angular-ru/logger'; +import { TaskAuditLogService } from 'app/backend-status/services/task-audit-log.service'; +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; +import { MessageService } from 'primeng/api'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable() +export class TaskAuditLogEffects { + constructor( + private actions$: Actions, + private logger: LoggerService, + private taskAuditLogService: TaskAuditLogService, + private messageService: MessageService, + private translateService: TranslateService + ) {} + + @Effect() + getLogEntries$: Observable = this.actions$.pipe( + ofType(TaskAuditLogActionTypes.GetEntries), + map(action => action.payload), + tap(action => + this.logger.debug('effect: getting task audit log entries:', action) + ), + switchMap(action => + this.taskAuditLogService.getLogEntries(action.cutoff).pipe( + tap(response => this.logger.debug('received response:', response)), + map( + (response: TaskAuditLogEntry[]) => + new ReceivedTaskAuditLogEntries({ entries: response }) + ), + catchError(error => { + this.logger.error( + 'service failure getting task audit log entries:', + error + ); + this.messageService.add({ + severity: 'error', + detail: this.translateService.instant( + 'task-audit-log-effects.get-log-entries.error.detail' + ) + }); + return of(new GetTaskAuditLogEntriesFailed()); + }) + ) + ), + catchError(error => { + this.logger.error( + 'general failure getting task audit log entries:', + error + ); + this.messageService.add({ + severity: 'error', + detail: this.translateService.instant( + 'general-message.error.general-service-failure' + ) + }); + return of(new GetTaskAuditLogEntriesFailed()); + }) + ); +} diff --git a/comixed-frontend/src/app/backend-status/index.ts b/comixed-frontend/src/app/backend-status/index.ts index 3d35c7211..05fd2f32c 100644 --- a/comixed-frontend/src/app/backend-status/index.ts +++ b/comixed-frontend/src/app/backend-status/index.ts @@ -18,11 +18,13 @@ import * as fromRouter from '@ngrx/router-store'; import * as fromBuildDetails from './reducers/build-details.reducer'; +import { BuildDetailsState } from './reducers/build-details.reducer'; +import * as fromTaskAuditLog from './reducers/task-audit-log.reducer'; import { Params } from '@angular/router'; -import { BuildDetailsState } from './reducers/build-details.reducer'; import { ActionReducerMap, MetaReducer } from '@ngrx/store'; import { environment } from '../../environments/environment'; +import { TaskAuditLogState } from 'app/backend-status/reducers/task-audit-log.reducer'; interface RouterStateUrl { url: string; @@ -33,13 +35,15 @@ interface RouterStateUrl { export interface AppState { router: fromRouter.RouterReducerState; build_details: BuildDetailsState; + task_audit_log_state: TaskAuditLogState; } export type State = AppState; export const reducers: ActionReducerMap = { router: fromRouter.routerReducer, - build_details: fromBuildDetails.reducer + build_details: fromBuildDetails.reducer, + task_audit_log_state: fromTaskAuditLog.reducer }; export const metaReducers: MetaReducer[] = !environment.production diff --git a/comixed-frontend/src/app/backend-status/models/task-audit-log-entry.fixtures.ts b/comixed-frontend/src/app/backend-status/models/task-audit-log-entry.fixtures.ts new file mode 100644 index 000000000..889f1afb1 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/models/task-audit-log-entry.fixtures.ts @@ -0,0 +1,54 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; + +export const TASK_AUDIT_LOG_ENTRY_1: TaskAuditLogEntry = { + startTime: new Date().getTime(), + endTime: new Date().getTime(), + successful: true, + description: 'The first task' +}; + +export const TASK_AUDIT_LOG_ENTRY_2: TaskAuditLogEntry = { + startTime: new Date().getTime(), + endTime: new Date().getTime(), + successful: true, + description: 'The second task' +}; + +export const TASK_AUDIT_LOG_ENTRY_3: TaskAuditLogEntry = { + startTime: new Date().getTime(), + endTime: new Date().getTime(), + successful: true, + description: 'The third task' +}; + +export const TASK_AUDIT_LOG_ENTRY_4: TaskAuditLogEntry = { + startTime: new Date().getTime(), + endTime: new Date().getTime(), + successful: true, + description: 'The fourth task' +}; + +export const TASK_AUDIT_LOG_ENTRY_5: TaskAuditLogEntry = { + startTime: new Date().getTime(), + endTime: new Date().getTime(), + successful: true, + description: 'The fifth task' +}; diff --git a/comixed-frontend/src/app/backend-status/models/task-audit-log-entry.ts b/comixed-frontend/src/app/backend-status/models/task-audit-log-entry.ts new file mode 100644 index 000000000..e8f5e0dcd --- /dev/null +++ b/comixed-frontend/src/app/backend-status/models/task-audit-log-entry.ts @@ -0,0 +1,24 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +export interface TaskAuditLogEntry { + startTime: number; + endTime: number; + successful: boolean; + description: string; +} diff --git a/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts b/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts new file mode 100644 index 000000000..fad1b6b71 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts @@ -0,0 +1,122 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { + initialState, + reducer, + TaskAuditLogState +} from './task-audit-log.reducer'; +import { + GetTaskAuditLogEntries, + GetTaskAuditLogEntriesFailed, + ReceivedTaskAuditLogEntries +} from 'app/backend-status/actions/task-audit-log.actions'; +import { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/models/task-audit-log-entry.fixtures'; + +describe('TaskAuditLog Reducer', () => { + const LOG_ENTRIES = [ + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 + ]; + + let state: TaskAuditLogState; + + beforeEach(() => { + state = initialState; + }); + + describe('the initial state', () => { + beforeEach(() => { + state = reducer(state, {} as any); + }); + + it('clears the fetching entries flag', () => { + expect(state.fetchingEntries).toBeFalsy(); + }); + + it('has an empty set of entries', () => { + expect(state.entries).toEqual([]); + }); + + it('has a last entry date of 0', () => { + expect(state.lastEntryDate).toEqual(0); + }); + }); + + describe('fetching a set of entries', () => { + beforeEach(() => { + state = reducer( + { ...state, fetchingEntries: false }, + new GetTaskAuditLogEntries({ cutoff: 0 }) + ); + }); + + it('sets the fetching flag', () => { + expect(state.fetchingEntries).toBeTruthy(); + }); + }); + + describe('receiving a set of entries', () => { + beforeEach(() => { + state = reducer( + { + ...state, + fetchingEntries: true, + entries: [TASK_AUDIT_LOG_ENTRY_2], + lastEntryDate: null + }, + new ReceivedTaskAuditLogEntries({ entries: LOG_ENTRIES }) + ); + }); + + it('clears the fetching flag', () => { + expect(state.fetchingEntries).toBeFalsy(); + }); + + it('merges the set of entries', () => { + expect( + LOG_ENTRIES.every(entry => state.entries.includes(entry)) + ).toBeTruthy(); + }); + + it('updates the last entry date', () => { + expect(state.lastEntryDate).not.toBeNull(); + }); + }); + + describe('failure to receive audit log entries', () => { + beforeEach(() => { + state = reducer( + { ...state, fetchingEntries: true }, + new GetTaskAuditLogEntriesFailed() + ); + }); + + it('clears the fetching flag', () => { + expect(state.fetchingEntries).toBeFalsy(); + }); + }); +}); diff --git a/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts b/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts new file mode 100644 index 000000000..e4316f17f --- /dev/null +++ b/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts @@ -0,0 +1,69 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { + TaskAuditLogActions, + TaskAuditLogActionTypes +} from '../actions/task-audit-log.actions'; +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; + +export const TASK_AUDIT_LOG_FEATURE_KEY = 'task_audit_log_state'; + +export interface TaskAuditLogState { + fetchingEntries: boolean; + entries: TaskAuditLogEntry[]; + lastEntryDate: number; +} + +export const initialState: TaskAuditLogState = { + fetchingEntries: false, + entries: [], + lastEntryDate: 0 +}; + +export function reducer( + state = initialState, + action: TaskAuditLogActions +): TaskAuditLogState { + switch (action.type) { + case TaskAuditLogActionTypes.GetEntries: + return { ...state, fetchingEntries: true }; + + case TaskAuditLogActionTypes.ReceivedEntries: { + const entries = state.entries.concat(action.payload.entries); + let lastEntryDate = state.lastEntryDate; + entries.forEach(entry => { + if (entry.startTime > lastEntryDate) { + lastEntryDate = entry.startTime; + } + }); + return { + ...state, + fetchingEntries: false, + entries: entries, + lastEntryDate: lastEntryDate + }; + } + + case TaskAuditLogActionTypes.GetEntriesFailed: + return { ...state, fetchingEntries: false }; + + default: + return state; + } +} diff --git a/comixed-frontend/src/app/backend-status/services/task-audit-log.service.spec.ts b/comixed-frontend/src/app/backend-status/services/task-audit-log.service.spec.ts new file mode 100644 index 000000000..0d3fe0d2b --- /dev/null +++ b/comixed-frontend/src/app/backend-status/services/task-audit-log.service.spec.ts @@ -0,0 +1,77 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { TestBed } from '@angular/core/testing'; + +import { TaskAuditLogService } from './task-audit-log.service'; +import { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/backend-status.fixtures'; +import { LoggerModule } from '@angular-ru/logger'; +import { + HttpClientTestingModule, + HttpTestingController +} from '@angular/common/http/testing'; +import { interpolate } from 'app/app.functions'; +import { GET_TASK_LOG_ENTRIES_URL } from 'app/backend-status/backend-status.constants'; + +describe('TaskAuditLogService', () => { + const LAST_ENTRY_DATE = new Date().getTime(); + const LOG_ENTRIES = [ + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 + ]; + + let taskAuditLogService: TaskAuditLogService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, LoggerModule.forRoot()], + providers: [TaskAuditLogService] + }); + + taskAuditLogService = TestBed.get(TaskAuditLogService); + httpMock = TestBed.get(HttpTestingController); + }); + + it('should be created', () => { + expect(taskAuditLogService).toBeTruthy(); + }); + + it('can get a list of log entries', () => { + taskAuditLogService + .getLogEntries(LAST_ENTRY_DATE) + .subscribe(response => expect(response).toEqual(LOG_ENTRIES)); + + const req = httpMock.expectOne( + interpolate(GET_TASK_LOG_ENTRIES_URL, { + timestamp: LAST_ENTRY_DATE + }) + ); + expect(req.request.method).toEqual('GET'); + req.flush(LOG_ENTRIES); + }); +}); diff --git a/comixed-frontend/src/app/backend-status/services/task-audit-log.service.ts b/comixed-frontend/src/app/backend-status/services/task-audit-log.service.ts new file mode 100644 index 000000000..60dd3954f --- /dev/null +++ b/comixed-frontend/src/app/backend-status/services/task-audit-log.service.ts @@ -0,0 +1,41 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LoggerService } from '@angular-ru/logger'; +import { HttpClient } from '@angular/common/http'; +import { GET_TASK_LOG_ENTRIES_URL } from 'app/backend-status/backend-status.constants'; +import { interpolate } from 'app/app.functions'; + +@Injectable({ + providedIn: 'root' +}) +export class TaskAuditLogService { + constructor(private logger: LoggerService, private http: HttpClient) {} + + getLogEntries(timestamp: number): Observable { + this.logger.debug( + '[GET] http request: get task audit log entries:', + timestamp + ); + return this.http.get( + interpolate(GET_TASK_LOG_ENTRIES_URL, { timestamp: timestamp }) + ); + } +} diff --git a/comixed-frontend/src/assets/i18n/en/backend-status.json b/comixed-frontend/src/assets/i18n/en/backend-status.json index 9ea2e83c6..0aec48e6e 100644 --- a/comixed-frontend/src/assets/i18n/en/backend-status.json +++ b/comixed-frontend/src/assets/i18n/en/backend-status.json @@ -20,5 +20,12 @@ "dirty": "Built From Uncommitted Code?", "remote-origin-url": "Remote Origin" } + }, + "task-audit-log-effects": { + "get-log-entries": { + "error": { + "detail": "Failed to get task audit log entries." + } + } } } diff --git a/comixed-frontend/src/assets/i18n/es/backend-status.json b/comixed-frontend/src/assets/i18n/es/backend-status.json index 1a9ef89b5..39fb1e728 100644 --- a/comixed-frontend/src/assets/i18n/es/backend-status.json +++ b/comixed-frontend/src/assets/i18n/es/backend-status.json @@ -20,5 +20,12 @@ "dirty": "¿Compilado desde código Uncommitted?", "remote-origin-url": "Remote Origin" } + }, + "task-audit-log-effects": { + "get-log-entries": { + "error": { + "detail": "Failed to get task audit log entries." + } + } } } diff --git a/comixed-frontend/src/assets/i18n/fr/backend-status.json b/comixed-frontend/src/assets/i18n/fr/backend-status.json index 1cc3938ab..2137619b0 100644 --- a/comixed-frontend/src/assets/i18n/fr/backend-status.json +++ b/comixed-frontend/src/assets/i18n/fr/backend-status.json @@ -20,5 +20,12 @@ "dirty": "Construit à partir d'un code non comité?", "remote-origin-url": "url du \"Remote Origin\"" } + }, + "task-audit-log-effects": { + "get-log-entries": { + "error": { + "detail": "Failed to get task audit log entries." + } + } } } diff --git a/comixed-frontend/src/assets/i18n/pt/backend-status.json b/comixed-frontend/src/assets/i18n/pt/backend-status.json index 9ea2e83c6..0aec48e6e 100644 --- a/comixed-frontend/src/assets/i18n/pt/backend-status.json +++ b/comixed-frontend/src/assets/i18n/pt/backend-status.json @@ -20,5 +20,12 @@ "dirty": "Built From Uncommitted Code?", "remote-origin-url": "Remote Origin" } + }, + "task-audit-log-effects": { + "get-log-entries": { + "error": { + "detail": "Failed to get task audit log entries." + } + } } }