Commit 72dd2e4b authored by Vitali Stupin's avatar Vitali Stupin

Adding catalogue history support

parent 08a629f3
{
"name": "xtss-catalogue",
"version": "0.1.0",
"version": "0.2.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
......
......@@ -17,6 +17,8 @@ export class AppConfigMock extends AppConfig {
'ee-dev': 'https://www.x-tee.ee/catalogue/ee-dev/wsdls/'
},
API_SERVICE: 'index.json',
API_HISTORY: 'history.json',
HISTORY_LIMIT: 30,
LANGUAGES: {
EST: 'est',
ENG: 'eng'
......
export class InstanceVersion {
reportTime: string;
reportTimeCompact: string;
reportPath: string;
}
......@@ -80,7 +80,13 @@ describe('SubsystemItemComponent', () => {
});
it('should go to detail view', () => {
const spy = TestBed.get(Router).navigateByUrl;
component.showDetail();
expect(TestBed.get(Router).navigateByUrl).toHaveBeenCalledWith('/INST/CLASS/CODE/SUB');
expect(spy).toHaveBeenCalledWith('/INST/CLASS/CODE/SUB');
spy.calls.reset();
spyOn(subsystemsService, 'getInstanceVersion').and.returnValue('12345');
component.showDetail();
expect(spy).toHaveBeenCalledWith('/INST/CLASS/CODE/SUB?at=12345');
});
});
......@@ -40,6 +40,7 @@ export class SubsystemItemComponent implements OnInit {
+ '/' + this.subsystem.memberClass
+ '/' + this.subsystem.memberCode
+ '/' + this.subsystem.subsystemCode
+ (this.subsystemsService.getInstanceVersion() ? '?at=' + this.subsystemsService.getInstanceVersion() : '')
);
}
......
......@@ -15,6 +15,22 @@
(click)="switchInstance(instance)">{{instance}}</button>
</div>
<div class="card">
<div class="card-header">
{{'subsystemList.selectVersion' | translate}}
</div>
<div class="card-body">
<div class="row">
<div class="col-sm-6">
<select [(ngModel)]="instanceVersion" class="form-control" id="instanceVersion" (ngModelChange)="setInstanceVersion()">
<option value="">{{'subsystemList.latestVersion' | translate}}</option>
<option *ngFor="let version of instanceVersions | async" [ngValue]="version.reportTimeCompact">{{version.reportTime}}</option>
</select>
</div>
</div>
</div>
</div>
<app-search></app-search>
<br>
......
......@@ -10,6 +10,7 @@ import { SubsystemsService } from '../subsystems.service';
import { ViewportScroller } from '@angular/common';
import { AppConfigMock } from 'src/app/app.config-mock';
import { AppConfig } from 'src/app/app.config';
import { FormsModule } from '@angular/forms';
@Component({selector: 'app-header', template: ''})
class HeaderStubComponent {}
......@@ -41,6 +42,7 @@ describe('SubsystemListComponent', () => {
SubsystemItemStubComponent
],
imports: [
FormsModule,
TranslateModule.forRoot(),
HttpClientModule
],
......@@ -90,7 +92,7 @@ describe('SubsystemListComponent', () => {
fixture = TestBed.createComponent(SubsystemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST');
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST', '');
});
it('should detect change instance', () => {
......@@ -99,7 +101,7 @@ describe('SubsystemListComponent', () => {
fixture = TestBed.createComponent(SubsystemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST');
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST', '');
});
it('should scroll to position', () => {
......@@ -151,4 +153,81 @@ describe('SubsystemListComponent', () => {
getLimitSpy.and.returnValue(['3']);
expect(component.isPartialList()).toBeFalsy();
});
it('setInstanceVersion should work without instance version', () => {
fixture = TestBed.createComponent(SubsystemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.setInstanceVersion();
expect(TestBed.get(Router).navigateByUrl).toHaveBeenCalledWith('/INST');
});
});
describe('SubsystemListComponent (with instance version)', () => {
let component: SubsystemListComponent;
let fixture: ComponentFixture<SubsystemListComponent>;
let getInstanceSpy;
let getInstancesSpy;
let subsystemsService: SubsystemsService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
SubsystemListComponent,
HeaderStubComponent,
MessagesStubComponent,
SearchStubComponent,
SubsystemItemStubComponent
],
imports: [
FormsModule,
TranslateModule.forRoot(),
HttpClientModule
],
providers: [
{ provide: ActivatedRoute, useValue: {
params: of({
instance: 'INST'
}),
snapshot: {
queryParams: {
at: '12345'
}
}
}},
{ provide: Router, useValue: {
events: of(new Scroll(null, [11, 12], null)),
navigateByUrl: jasmine.createSpy('navigateByUrl')
}},
{ provide: AppConfig, useClass: AppConfigMock }
]
})
.compileComponents();
}));
beforeEach(() => {
subsystemsService = TestBed.get(SubsystemsService);
getInstanceSpy = spyOn(subsystemsService, 'getInstance').and.returnValue('INST');
getInstancesSpy = spyOn(subsystemsService, 'getInstances').and.returnValue(['INST']);
spyOn(TestBed.get(ViewportScroller), 'scrollToPosition');
spyOn(subsystemsService, 'setInstance').and.returnValue(null);
spyOn(subsystemsService, 'getDefaultInstance').and.returnValue('DEFINST');
spyOn(TestBed.get(SubsystemsService), 'getApiUrlBase').and.returnValue('base');
});
it('should create', () => {
fixture = TestBed.createComponent(SubsystemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component).toBeTruthy();
expect(component.instanceVersion).toBe('12345');
});
it('setInstanceVersion should work with instance version', () => {
fixture = TestBed.createComponent(SubsystemListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.setInstanceVersion();
expect(TestBed.get(Router).navigateByUrl).toHaveBeenCalledWith('/INST?at=12345');
});
});
......@@ -5,19 +5,22 @@ import { ActivatedRoute, Router, Scroll } from '@angular/router';
import { Subscription, BehaviorSubject } from 'rxjs';
import { ViewportScroller } from '@angular/common';
import { filter } from 'rxjs/operators';
import { InstanceVersion } from '../instance-version';
@Component({
selector: 'app-subsystem-list',
templateUrl: './subsystem-list.component.html'
})
export class SubsystemListComponent implements OnInit, AfterViewInit, OnDestroy {
message = '';
message: string;
scrollSubject: BehaviorSubject<any> = new BehaviorSubject(null);
routerScrollSubscription: Subscription;
routeSubscription: Subscription;
warningsSubscription: Subscription;
scrollSubjectSubscription: Subscription;
filteredSubsystems: BehaviorSubject<Subsystem[]>;
instanceVersions: BehaviorSubject<InstanceVersion[]>;
instanceVersion: string;
constructor(
private subsystemsService: SubsystemsService,
......@@ -68,10 +71,20 @@ export class SubsystemListComponent implements OnInit, AfterViewInit, OnDestroy
this.viewportScroller.scrollToPosition([0, 0]);
}
setInstanceVersion() {
// This will update URL without triggering route.params
this.router.navigateByUrl(
'/' + this.subsystemsService.getInstance()
+ (this.instanceVersion ? '?at=' + this.instanceVersion : ''));
this.subsystemsService.setInstance(this.subsystemsService.getInstance(), this.instanceVersion);
}
ngOnInit() {
// Reset message on page load
this.message = '';
this.filteredSubsystems = this.subsystemsService.filteredSubsystemsSubject;
this.instanceVersions = this.subsystemsService.instanceVersionsSubject;
// Service will tell when data loading failed!
this.warningsSubscription = this.subsystemsService.warnings.subscribe(signal => {
......@@ -87,9 +100,17 @@ export class SubsystemListComponent implements OnInit, AfterViewInit, OnDestroy
this.router.navigateByUrl('/' + this.subsystemsService.getDefaultInstance());
return;
}
// Set selected instance version
if (this.route.snapshot && this.route.snapshot.queryParams.at) {
this.instanceVersion = this.route.snapshot.queryParams.at;
} else {
this.instanceVersion = '';
}
// Only reload on switching of instance or when no instance is selected yet on service side
if (this.getInstance() === '' || this.getInstance() !== params.instance) {
this.subsystemsService.setInstance(params.instance);
this.subsystemsService.setInstance(params.instance, this.instanceVersion);
}
});
}
......
......@@ -95,7 +95,7 @@ describe('SubsystemComponent', () => {
fixture = TestBed.createComponent(SubsystemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST');
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST', '');
});
it('should detect change instance', () => {
......@@ -104,7 +104,7 @@ describe('SubsystemComponent', () => {
fixture = TestBed.createComponent(SubsystemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST');
expect(subsystemsService.setInstance).toHaveBeenCalledWith('INST', '');
});
it('should detect incorrect subsystem', () => {
......@@ -166,3 +166,75 @@ describe('SubsystemComponent', () => {
expect(spy).toHaveBeenCalledWith([0, 0]);
});
});
describe('SubsystemComponent (with instance version)', () => {
let component: SubsystemComponent;
let fixture: ComponentFixture<SubsystemComponent>;
let getInstanceSpy;
let getInstancesSpy;
let subsystemsService: SubsystemsService;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
SubsystemComponent,
HeaderStubComponent,
MessagesStubComponent
],
imports: [
TranslateModule.forRoot(),
HttpClientModule
],
providers: [
{ provide: ActivatedRoute, useValue: {
params: of({
instance: 'INST',
class: 'CLASS',
member: 'MEMBER',
subsystem: 'SYSTEM'
}),
snapshot: {
queryParams: {
at: '12345'
}
}
}},
{ provide: Router, useValue: {
events: of(new Scroll(null, [11, 12], null)),
navigateByUrl: jasmine.createSpy('navigateByUrl')
}},
{ provide: AppConfig, useClass: AppConfigMock }
]
})
.compileComponents();
}));
beforeEach(() => {
subsystemsService = TestBed.get(SubsystemsService);
getInstanceSpy = spyOn(subsystemsService, 'getInstance').and.returnValue('INST');
getInstancesSpy = spyOn(subsystemsService, 'getInstances').and.returnValue(['INST']);
spyOn(TestBed.get(ViewportScroller), 'scrollToPosition');
spyOn(subsystemsService, 'setInstance').and.returnValue(null);
spyOn(TestBed.get(SubsystemsService), 'getApiUrlBase').and.returnValue('base');
subsystemsService.subsystemsSubject = new BehaviorSubject([
{
memberClass: '',
subsystemCode: '',
xRoadInstance: '',
subsystemStatus: '',
memberCode: '',
fullSubsystemName: 'INST/CLASS/MEMBER/SYSTEM',
methods: []
}
]);
});
it('goToList should work with instance version', () => {
fixture = TestBed.createComponent(SubsystemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.goToList();
expect(TestBed.get(Router).navigateByUrl).toHaveBeenCalledWith('/INST?at=12345');
});
});
......@@ -21,6 +21,7 @@ export class SubsystemComponent implements OnInit, AfterViewInit, OnDestroy {
private warningsSubscription: Subscription;
private scrollSubjectSubscription: Subscription;
private subsystemsSubscription: Subscription;
private instanceVersion: string;
subsystemSubject: BehaviorSubject<Subsystem> = new BehaviorSubject(null);
constructor(
......@@ -54,7 +55,10 @@ export class SubsystemComponent implements OnInit, AfterViewInit, OnDestroy {
}
goToList(): void {
this.router.navigateByUrl('/' + this.subsystemsService.getInstance());
this.router.navigateByUrl(
'/' + this.subsystemsService.getInstance()
+ (this.instanceVersion ? '?at=' + this.instanceVersion : '')
);
}
scrollToTop() {
......@@ -77,12 +81,22 @@ export class SubsystemComponent implements OnInit, AfterViewInit, OnDestroy {
this.message = 'subsystem.incorrectInstanceWarning';
return;
}
// Set selected instance version
if (this.route.snapshot && this.route.snapshot.queryParams.at) {
this.instanceVersion = this.route.snapshot.queryParams.at;
} else {
this.instanceVersion = '';
}
this.subsystemId = params.instance + '/' + params.class + '/' + params.member + '/' + params.subsystem;
// Only reload on switching of instance or when no instance is selected yet on service side
if (this.getInstance() === '' || this.getInstance() !== params.instance) {
// this.subsystemsService.setInstance(params.instance ? params.instance : this.subsystemsService.getDefaultInstance());
this.subsystemsService.setInstance(params.instance);
this.subsystemsService.setInstance(params.instance, this.instanceVersion);
}
this.subsystemsSubscription = this.subsystemsService.subsystemsSubject.subscribe(subsystems => {
const subsystem = this.getSubsystem(subsystems, this.subsystemId);
if (!subsystem && !this.message && subsystems.length) {
......
......@@ -5,6 +5,7 @@ import { Method } from './method';
import { HttpErrorResponse } from '@angular/common/http';
import { tick, fakeAsync } from '@angular/core/testing';
import { AppConfigMock } from './app.config-mock';
import { InstanceVersion } from './instance-version';
describe('SubsystemsService', () => {
let httpClientSpy: { get: jasmine.Spy };
......@@ -21,7 +22,7 @@ describe('SubsystemsService', () => {
expect(service).toBeTruthy();
});
it('should set instance on HTTP OK', () => {
it('should set instance on HTTP OK', fakeAsync(() => {
const sourceSubsystems = [
{
memberClass: 'CLASS',
......@@ -79,12 +80,22 @@ describe('SubsystemsService', () => {
// Setting value to test resetting of values
service.subsystemsSubject.next([new Subsystem()]);
service.filteredSubsystemsSubject.next([new Subsystem()]);
service.setInstance('EE');
// Disabling updateInstanceVersions()
service.updateInstanceVersions = () => {};
service.setInstance('EE', '');
// Waiting for asynchronous work
tick();
expect(httpClientSpy.get).toHaveBeenCalledWith('https://www.x-tee.ee/catalogue/EE/wsdls/index.json');
expect(service.subsystemsSubject.value).toEqual(expectedSubsystems);
// No filters yet
expect(service.filteredSubsystemsSubject.value).toEqual(expectedSubsystems);
});
service.setInstance('EE', '12345');
// Waiting for asynchronous work
tick();
expect(httpClientSpy.get).toHaveBeenCalledWith('https://www.x-tee.ee/catalogue/EE/wsdls/index_12345.json');
}));
it('should set instance on HTTP ERROR', fakeAsync(() => {
const errorResponse = new HttpErrorResponse({error: 'error', status: 404, statusText: 'Not Found'});
......@@ -92,14 +103,80 @@ describe('SubsystemsService', () => {
// Setting value to test resetting of values
service.subsystemsSubject.next([new Subsystem()]);
service.filteredSubsystemsSubject.next([new Subsystem()]);
service.setInstance('EE');
// Waiting for some (unknown) asynchronous work
// Disabling updateInstanceVersions()
service.updateInstanceVersions = () => {};
service.setInstance('EE', '');
// Waiting for asynchronous work
tick();
expect(httpClientSpy.get).toHaveBeenCalledWith('https://www.x-tee.ee/catalogue/EE/wsdls/index.json');
expect(service.subsystemsSubject.value).toEqual([]);
expect(service.filteredSubsystemsSubject.value).toEqual([]);
}));
it('should set instance versions on HTTP OK', fakeAsync(() => {
const sourceHistory = [
{
reportTime: '2019-04-15 08:01:41',
reportPath: 'index_20190415080141.json'
},
{
reportTime: '2019-04-14 08:01:37',
reportPath: 'index_FAIL.json'
}
];
const expectedInstanceVersions = [
{
reportTime: '2019-04-15 08:01:41',
reportTimeCompact: '20190415080141',
reportPath: 'index_20190415080141.json'
}
];
httpClientSpy.get.and.returnValue(of(sourceHistory));
// Setting value to test resetting of values
service.instanceVersionsSubject.next([new InstanceVersion()]);
// Disabling updateSubsystems()
service.updateSubsystems = () => {};
service.setInstance('EE', '');
// Waiting for asynchronous work
tick();
expect(httpClientSpy.get).toHaveBeenCalledWith('https://www.x-tee.ee/catalogue/EE/wsdls/history.json');
expect(service.instanceVersionsSubject.value).toEqual(expectedInstanceVersions);
const longHistory = [];
for (let i = 0; i < config.getConfig('HISTORY_LIMIT') + 1; i++) {
longHistory.push(
{
reportTime: '2019-04-15 08:01:41',
reportPath: 'index_20190415080141.json'
}
);
}
httpClientSpy.get.and.returnValue(of(longHistory));
service.setInstance('EE');
// Waiting for asynchronous work
tick();
expect(longHistory.length).toBe(config.getConfig('HISTORY_LIMIT') + 1);
expect(service.instanceVersionsSubject.value.length).toEqual(config.getConfig('HISTORY_LIMIT'));
}));
it('should set instance versions on HTTP ERROR', fakeAsync(() => {
const errorResponse = new HttpErrorResponse({error: 'error', status: 404, statusText: 'Not Found'});
httpClientSpy.get.and.returnValue(defer(() => Promise.reject(errorResponse)));
// Setting value to test resetting of values
service.instanceVersionsSubject.next([new InstanceVersion()]);
// Disabling updateSubsystems()
service.updateSubsystems = () => {};
service.setInstance('EE');
// Waiting for asynchronous work
tick();
expect(httpClientSpy.get).toHaveBeenCalledWith('https://www.x-tee.ee/catalogue/EE/wsdls/history.json');
expect(service.instanceVersionsSubject.value).toEqual([]);
}));
it('should filter nonEmpty subsystems', () => {
const sourceSubsystems = [
{
......
......@@ -5,6 +5,7 @@ import { catchError, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { Subsystem } from './subsystem';
import { Method } from './method';
import { AppConfig } from './app.config';
import { InstanceVersion } from './instance-version';
@Injectable({
providedIn: 'root'
......@@ -15,8 +16,10 @@ export class SubsystemsService {
private nonEmpty = false;
private filter = '';
private instance = '';
private instanceVersion = '';
subsystemsSubject: BehaviorSubject<Subsystem[]> = new BehaviorSubject([]);
filteredSubsystemsSubject: BehaviorSubject<Subsystem[]> = new BehaviorSubject([]);
instanceVersionsSubject: BehaviorSubject<InstanceVersion[]> = new BehaviorSubject([]);
private updateFilter = new Subject<string>();
warnings: EventEmitter<string> = new EventEmitter();
......@@ -92,19 +95,6 @@ export class SubsystemsService {
return subsystems;
}
/**
* Handle Http operation that failed.
* Let the app continue.
* @param result - optional value to return as the observable result
*/
private handleError<T>(result?: T) {
return (error: any): Observable<T> => {
this.emitWarning('service.dataLoadingError');
// Let the app keep running by returning an empty result.
return of(result);
};
}
private emitWarning(msg: string) {
this.warnings.emit(msg);
}
......@@ -125,23 +115,60 @@ export class SubsystemsService {
return this.instance;
}
setInstance(instance: string) {
this.instance = instance;
this.apiUrlBase = this.config.getConfig('INSTANCES')[instance];
// Not "private" to be able to override in unit tests
updateSubsystems() {
// Reset only if has values (less refreshes)
if (this.subsystemsSubject.value.length) {
this.subsystemsSubject.next([]);
}
this.http.get<Subsystem[]>(this.apiUrlBase + this.config.getConfig('API_SERVICE'))
.pipe(
catchError(this.handleError([]))
this.http.get<Subsystem[]>(
this.apiUrlBase + (this.instanceVersion ? 'index_' + this.instanceVersion + '.json' : this.config.getConfig('API_SERVICE'))
).pipe(
catchError( () => {
this.emitWarning('service.dataLoadingError');
// Let the app keep running by returning an empty result.
return of([]);
})
).subscribe(subsystems => {
this.subsystemsSubject.next(this.setFullNames(subsystems));
this.updateFiltered();
});
}
// Not "private" to be able to override in unit tests
updateInstanceVersions() {
this.instanceVersionsSubject.next([]);
this.http.get<InstanceVersion[]>(this.apiUrlBase + this.config.getConfig('API_HISTORY'))
.pipe(
catchError(() => {
// Let the app keep running by returning an empty result.
return of([]);
})