Commit 9cf8ce15 authored by Vitali Stupin's avatar Vitali Stupin

Merge pull request #6 in XTSS/xtss-catalogue from develop to release

* commit '5039f3f6':
  fix instance switching
  Adding catalogue history support
  Scroll to top, warn when more subsystems exist
  Update karma package
  Updating packages
  Creating component for displaying messages
  Getting filter limits from configuration
  Changing unit tests
  Loading configuration from assets
parents 0a2fe26f 5039f3f6
This diff is collapsed.
import { AppRoutingModule } from './app-routing.module';
describe('AppModule', () => {
describe('AppRoutingModule', () => {
let module: AppRoutingModule;
beforeEach(() => {
......
import { Injectable } from '@angular/core';
import { AppConfig } from './app.config';
@Injectable()
export class AppConfigMock extends AppConfig {
private configMock: any = {
MAX_LIMIT: 1000000,
DEFAULT_LIMIT: 10,
LIMITS: {
10: 10,
20: 20,
50: 50
},
INSTANCES: {
EE: 'https://www.x-tee.ee/catalogue/EE/wsdls/',
'ee-test': 'https://www.x-tee.ee/catalogue/ee-test/wsdls/',
'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'
},
PREVIEW_SIZE: 5,
// Smaller value for faster unit testing
FILTER_DEBOUNCE: 20
};
public getConfig(key: any) {
return this.configMock[key];
}
}
import { AppConfig } from './app.config';
import { of } from 'rxjs';
describe('AppConfig', () => {
let config: AppConfig;
let httpClientSpy: { get: jasmine.Spy };
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
config = new AppConfig(httpClientSpy as any);
});
it('should be created', () => {
expect(config).toBeTruthy();
});
it('should load configuration', async () => {
httpClientSpy.get.and.returnValue(of({TEST: 'OK'}));
await config.load();
expect(httpClientSpy.get).toHaveBeenCalledWith('./assets/config.json');
});
it('getConfig should work', async () => {
httpClientSpy.get.and.returnValue(of({TEST: 'OK'}));
await config.load();
expect(config.getConfig('TEST')).toBe('OK');
});
});
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable()
export class AppConfig {
private config: any = null;
constructor(private http: HttpClient) { }
/**
* Use to get the data found in the config file
*/
public getConfig(key: any) {
return this.config[key];
}
/**
* This method loads "config.json" to get all configuration variables
*/
public load() {
return new Promise(resolve => {
// Not handling errors. App cannot work without valid configuration
this.http.get<any>('./assets/config.json')
.subscribe(responseData => {
this.config = responseData;
resolve(true);
});
});
}
}
import { AppModule, HttpLoaderFactory } from './app.module';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { AppConfig } from './app.config';
import { TestBed } from '@angular/core/testing';
describe('AppModule', () => {
let module: AppModule;
let appModule: AppModule;
let httpClientSpy: { get: jasmine.Spy };
let appConfigSpy: { load: jasmine.Spy };
beforeEach(() => {
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
module = new AppModule();
appConfigSpy = jasmine.createSpyObj('HttpClient', ['load']);
appModule = new AppModule();
});
it('should be created', () => {
expect(module).toBeTruthy();
expect(appModule).toBeTruthy();
});
it('HttpLoaderFactory should work', () => {
expect(HttpLoaderFactory(httpClientSpy as any) instanceof TranslateHttpLoader).toBeTruthy();
});
it('AppConfig should be initialized', async () => {
TestBed.configureTestingModule({
imports: [ AppModule ],
providers: [
{ provide: AppConfig, useValue: appConfigSpy }
]
});
expect(TestBed.get(AppConfig)).toBeTruthy();
expect(appConfigSpy.load).toHaveBeenCalledTimes(1);
});
});
......@@ -11,6 +11,9 @@ import { SubsystemItemComponent } from './subsystem-list/subsystem-item/subsyste
import { AppRoutingModule } from './app-routing.module';
import { SubsystemComponent } from './subsystem/subsystem.component';
import { HeaderComponent } from './header/header.component';
import { APP_INITIALIZER } from '@angular/core';
import { AppConfig } from './app.config';
import { MessagesComponent } from './messages/messages.component';
@NgModule({
declarations: [
......@@ -19,7 +22,8 @@ import { HeaderComponent } from './header/header.component';
SearchComponent,
SubsystemItemComponent,
SubsystemComponent,
HeaderComponent
HeaderComponent,
MessagesComponent
],
imports: [
BrowserModule,
......@@ -34,7 +38,10 @@ import { HeaderComponent } from './header/header.component';
}
})
],
providers: [],
providers: [
AppConfig,
{ provide: APP_INITIALIZER, useFactory: (config: AppConfig) => () => config.load(), deps: [AppConfig], multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
......
export const MAX_LIMIT = 1000000;
export const DEFAULT_LIMIT = 10;
export const INSTANCES = {
EE: 'https://www.x-tee.ee/catalogue/EE/wsdls/',
'ee-test': 'https://www.x-tee.ee/catalogue/ee-test/wsdls/',
'ee-dev': 'https://www.x-tee.ee/catalogue/ee-dev/wsdls/'
};
export const API_SERVICE = 'index.json';
export const LANGUAGES = {
EST: 'est',
ENG: 'eng'
};
export const PREVIEW_SIZE = 5;
export const FILTER_DEBOUNCE = 200;
......@@ -3,6 +3,8 @@ import { TranslateModule } from '@ngx-translate/core';
import { HeaderComponent } from './header.component';
import { HttpClientModule } from '@angular/common/http';
import { LanguagesService } from '../languages.service';
import { AppConfigMock } from 'src/app/app.config-mock';
import { AppConfig } from 'src/app/app.config';
describe('HeaderComponent', () => {
let component: HeaderComponent;
......@@ -14,6 +16,9 @@ describe('HeaderComponent', () => {
imports: [
TranslateModule.forRoot(),
HttpClientModule
],
providers: [
{ provide: AppConfig, useClass: AppConfigMock }
]
})
.compileComponents();
......
export class InstanceVersion {
reportTime: string;
reportTimeCompact: string;
reportPath: string;
}
import { TestBed } from '@angular/core/testing';
import { LanguagesService } from './languages.service';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { AppConfigMock } from 'src/app/app.config-mock';
import { AppConfig } from 'src/app/app.config';
import { HttpClientModule } from '@angular/common/http';
describe('LanguagesService', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot()
TranslateModule.forRoot(),
HttpClientModule
],
providers: [
{ provide: AppConfig, useClass: AppConfigMock }
]
}));
......
......@@ -3,7 +3,7 @@ import {TranslateService} from '@ngx-translate/core';
import { Title } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { LANGUAGES } from './config';
import { AppConfig } from './app.config';
@Injectable({
providedIn: 'root'
......@@ -14,10 +14,11 @@ export class LanguagesService {
constructor(
private translate: TranslateService,
private title: Title
private title: Title,
private config: AppConfig
) {
this.selectedLang = this.getDefaultLang();
translate.setDefaultLang(LANGUAGES[this.selectedLang]);
translate.setDefaultLang(this.config.getConfig('LANGUAGES')[this.selectedLang]);
this.updateTitle();
}
......@@ -34,11 +35,11 @@ export class LanguagesService {
if (window && window.localStorage && window.localStorage.getItem('lang')) {
return window.localStorage.getItem('lang');
}
return Object.keys(LANGUAGES)[0];
return Object.keys(this.config.getConfig('LANGUAGES'))[0];
}
getLangs(): string[] {
return Object.keys(LANGUAGES);
return Object.keys(this.config.getConfig('LANGUAGES'));
}
getLang(): string {
......@@ -50,7 +51,7 @@ export class LanguagesService {
if (window && window.localStorage) {
window.localStorage.setItem('lang', this.selectedLang);
}
this.translate.use(LANGUAGES[this.selectedLang]);
this.translate.use(this.config.getConfig('LANGUAGES')[this.selectedLang]);
this.updateTitle();
}
}
<div class="alert alert-warning" role="alert" *ngIf="message">
{{message | translate:{"subsystem": subsystemId} }}
</div>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { HttpClientModule } from '@angular/common/http';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
MessagesComponent
],
imports: [
TranslateModule.forRoot(),
HttpClientModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html'
})
export class MessagesComponent implements OnInit {
@Input() message: '';
@Input() subsystemId: '';
constructor() { }
ngOnInit() {
}
}
......@@ -16,9 +16,7 @@
<label for="limitSelect">{{'search.limit' | translate}}</label>
<select class="form-control" id="limitSelect"
[(ngModel)]="limit" (change)="setLimit(limit)">
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option *ngFor="let key of getLimitKeys()" [ngValue]="key">{{limits[key]}}</option>
<option value="all">{{'search.allOption' | translate}}</option>
</select>
</div>
......@@ -32,4 +30,4 @@
</div>
</div>
</div>
</div>
\ No newline at end of file
</div>
......@@ -4,6 +4,8 @@ import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { SearchComponent } from './search.component';
import { SubsystemsService } from 'src/app/subsystems.service';
import { AppConfigMock } from 'src/app/app.config-mock';
import { AppConfig } from 'src/app/app.config';
describe('SearchComponent', () => {
let component: SearchComponent;
......@@ -17,6 +19,9 @@ describe('SearchComponent', () => {
FormsModule,
TranslateModule.forRoot(),
HttpClientModule
],
providers: [
{ provide: AppConfig, useClass: AppConfigMock }
]
})
.compileComponents();
......@@ -24,6 +29,7 @@ describe('SearchComponent', () => {
beforeEach(() => {
subsystemsService = TestBed.get(SubsystemsService);
spyOn(subsystemsService, 'getLimits').and.returnValue({10: 10, 20: 20});
spyOn(subsystemsService, 'setNonEmpty').and.returnValue(null);
spyOn(subsystemsService, 'setLimit').and.returnValue(null);
spyOn(subsystemsService, 'setFilter').and.returnValue(null);
......@@ -36,6 +42,11 @@ describe('SearchComponent', () => {
expect(component).toBeTruthy();
});
it('getLimitKeys should work', () => {
expect(component.getLimitKeys()).toEqual(['10', '20']);
expect(subsystemsService.getLimits).toHaveBeenCalledWith();
});
it('setNonEmpty should work', () => {
component.setNonEmpty(true);
expect(subsystemsService.setNonEmpty).toHaveBeenCalledWith(true);
......
......@@ -7,10 +7,18 @@ import { SubsystemsService } from '../../subsystems.service';
})
export class SearchComponent implements OnInit {
limit: string;
limits: object;
nonEmpty: boolean;
filter: string;
constructor(private subsystemsService: SubsystemsService) {}
constructor(private subsystemsService: SubsystemsService) {
this.limit = this.subsystemsService.getLimit();
this.limits = this.subsystemsService.getLimits();
}
getLimitKeys(): string[] {
return Object.keys(this.limits);
}
setNonEmpty(nonEmpty: boolean) {
this.subsystemsService.setNonEmpty(nonEmpty);
......
......@@ -5,8 +5,11 @@ import { SubsystemItemComponent } from './subsystem-item.component';
import { RouterTestingModule } from '@angular/router/testing';
import { SubsystemsService } from 'src/app/subsystems.service';
import { Router } from '@angular/router';
import { PREVIEW_SIZE } from '../../config';
import { Method } from 'src/app/method';
import { AppConfigMock } from 'src/app/app.config-mock';
import { AppConfig } from 'src/app/app.config';
const PREVIEW_SIZE = 5;
describe('SubsystemItemComponent', () => {
let component: SubsystemItemComponent;
......@@ -26,9 +29,9 @@ describe('SubsystemItemComponent', () => {
providers: [
{ provide: Router, useValue: {
navigateByUrl: jasmine.createSpy('navigateByUrl')
}}
}},
{ provide: AppConfig, useClass: AppConfigMock }
]
})
.compileComponents();
}));
......@@ -77,7 +80,13 @@ describe('SubsystemItemComponent', () => {
});
it('should go to detail view', () => {
const spy = TestBed.get(Router).navigateByUrl;
component.showDetail();
expect(spy).toHaveBeenCalledWith('/INST/CLASS/CODE/SUB');
spy.calls.reset();
spyOn(subsystemsService, 'getInstanceVersion').and.returnValue('12345');
component.showDetail();
expect(TestBed.get(Router).navigateByUrl).toHaveBeenCalledWith('/INST/CLASS/CODE/SUB');
expect(spy).toHaveBeenCalledWith('/INST/CLASS/CODE/SUB?at=12345');
});
});
......@@ -3,7 +3,7 @@ import { Subsystem } from '../../subsystem';
import { Method } from '../../method';
import { SubsystemsService } from '../../subsystems.service';
import { Router } from '@angular/router';
import { PREVIEW_SIZE } from '../../config';
import { AppConfig } from '../../app.config';
@Component({
selector: 'app-subsystem-item',
......@@ -15,7 +15,8 @@ export class SubsystemItemComponent implements OnInit {
constructor(
private subsystemsService: SubsystemsService,
private router: Router
private router: Router,
private config: AppConfig
) { }
getApiUrlBase(): string {
......@@ -23,14 +24,14 @@ export class SubsystemItemComponent implements OnInit {
}
getMethodsPreview(): Method[] {
return this.subsystem.methods.length ? this.subsystem.methods.slice(0, PREVIEW_SIZE) : [];
return this.subsystem.methods.length ? this.subsystem.methods.slice(0, this.config.getConfig('PREVIEW_SIZE')) : [];
}
getNotInPreview(): number {
if (this.subsystem.methods.length - PREVIEW_SIZE < 0) {
if (this.subsystem.methods.length - this.config.getConfig('PREVIEW_SIZE') < 0) {
return 0;
}
return this.subsystem.methods.length - PREVIEW_SIZE;
return this.subsystem.methods.length - this.config.getConfig('PREVIEW_SIZE');
}
showDetail() {
......@@ -39,6 +40,7 @@ export class SubsystemItemComponent implements OnInit {
+ '/' + this.subsystem.memberClass
+ '/' + this.subsystem.memberCode
+ '/' + this.subsystem.subsystemCode
+ (this.subsystemsService.getInstanceVersion() ? '?at=' + this.subsystemsService.getInstanceVersion() : '')
);
}
......
......@@ -7,9 +7,7 @@
<p [innerHTML]="'subsystemList.intro.p4' | translate:{'jsonUrl': getApiUrl()}"></p>
<p [innerHTML]="'subsystemList.intro.p5' | translate"></p>
<div class="alert alert-warning" role="alert" *ngIf="message">
{{message | translate}}
</div>
<app-messages [message]="message"></app-messages>
<div class="btn-group" role="group" [attr.aria-label]="'subsystemList.selectInstance' | translate">
<button *ngFor="let instance of getInstances()"
......@@ -17,9 +15,30 @@
(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>
<div *ngFor="let subsystem of filteredSubsystems | async">
<app-subsystem-