diff --git a/.gitignore b/.gitignore index 942f2fb1..009d0818 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,9 @@ testem.log # System Files .DS_Store Thumbs.db + +# UI config files +config-editor/config-editor-ui/src/config/* + +# REST config files +config-editor/config-editor-rest/src/main/resources/application.properties diff --git a/config-editor/config-editor-ui/eslint-common.json b/config-editor/config-editor-ui/eslint-common.json index 2ca49255..9ade9c5d 100644 --- a/config-editor/config-editor-ui/eslint-common.json +++ b/config-editor/config-editor-ui/eslint-common.json @@ -7,6 +7,7 @@ "plugin:prettier/recommended" ], "rules": { + "@typescript-eslint/explicit-member-accessibility": ["error", { "accessibility": "no-public" }], "@angular-eslint/component-selector": [ "error", { "type": "element", "prefix": ["re", "formly"], "style": "kebab-case" } @@ -113,7 +114,7 @@ { "selector": "enumMember", "format": [ - "PascalCase" + "UPPER_CASE" ] } ], diff --git a/config-editor/config-editor-ui/package.json b/config-editor/config-editor-ui/package.json index e8f295ba..85defba6 100644 --- a/config-editor/config-editor-ui/package.json +++ b/config-editor/config-editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "rule-editor.ui", - "version": "1.0.0", + "version": "1.0.1-dev", "license": "MIT", "scripts": { "ng": "ng", @@ -46,7 +46,6 @@ "rxjs": "^6.6.6", "rxjs-compat": "^6.6.7", "tslib": "^2.2.0", - "typescript": "^4.1.5", "zone.js": "^0.11.4" }, "devDependencies": { @@ -83,7 +82,7 @@ "protractor": "^7.0.0", "rimraf": "^3.0.2", "ts-node": "^9.1.1", - "typescript": "4.1.5", + "typescript": "^4.1.5", "webpack": "^4.41.6" }, "peerDependencies": { diff --git a/config-editor/config-editor-ui/src/app/components/admin/admin.component.ts b/config-editor/config-editor-ui/src/app/components/admin/admin.component.ts index 44f696e9..e17241c1 100644 --- a/config-editor/config-editor-ui/src/app/components/admin/admin.component.ts +++ b/config-editor/config-editor-ui/src/app/components/admin/admin.component.ts @@ -12,96 +12,98 @@ import { takeUntil, take, skip } from 'rxjs/operators'; import { Router } from '@angular/router'; import { SubmitDialogComponent } from '../submit-dialog/submit-dialog.component'; import { BlockUI, NgBlockUI } from 'ng-block-ui'; +import { AppConfigService } from '@app/services/app-config.service'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 're-admin-editor', - styleUrls: ['./admin.component.scss'], - templateUrl: './admin.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 're-admin-editor', + styleUrls: ['./admin.component.scss'], + templateUrl: './admin.component.html', }) export class AdminComponent implements OnInit, OnDestroy { - public ngUnsubscribe = new Subject(); - public configData: ConfigData = {}; - public options: FormlyFormOptions = {}; - public form: FormGroup = new FormGroup({}); - public adminConfig$: Observable; - public config: AdminConfig; - public serviceName: string; - public adminPullRequestPending$: Observable; - private readonly PR_OPEN_MESSAGE = 'A pull request is already open'; - private readonly BLOCKING_TIMEOUT = 30000; + @Input() fields: FormlyFieldConfig[]; + @BlockUI() blockUI: NgBlockUI; + ngUnsubscribe = new Subject(); + configData: ConfigData = {}; + options: FormlyFormOptions = {}; + form: FormGroup = new FormGroup({}); + adminConfig$: Observable; + config: AdminConfig; + serviceName: string; + adminPullRequestPending$: Observable; + private readonly PR_OPEN_MESSAGE = 'A pull request is already open'; + constructor( + public dialog: MatDialog, + public snackbar: PopupService, + private editorService: EditorService, + private router: Router, + private configService: AppConfigService + ) { + this.adminConfig$ = editorService.configStore.adminConfig$; + this.adminPullRequestPending$ = this.editorService.configStore.adminPullRequestPending$; + this.serviceName = editorService.serviceName; + } - @Input() fields: FormlyFieldConfig[]; - @BlockUI() blockUI: NgBlockUI; - constructor(public dialog: MatDialog, public snackbar: PopupService, - private editorService: EditorService, private router: Router) { - this.adminConfig$ = editorService.configStore.adminConfig$; - this.adminPullRequestPending$ = this.editorService.configStore.adminPullRequestPending$; - this.serviceName = editorService.serviceName; - } + ngOnInit() { + this.adminConfig$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(config => { + this.config = config; + this.configData = this.editorService.adminSchema.wrapConfig(config.configData); + this.editorService.adminSchema.wrapAdminConfig(this.configData); + this.options.formState = { + mainModel: this.configData, + rawObjects: {}, + }; + }); + } - ngOnInit() { - this.adminConfig$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(config => { - this.config = config; - this.configData = this.editorService.adminSchema.wrapConfig(config.configData); - this.editorService.adminSchema.wrapAdminConfig(this.configData); - this.options.formState = { - mainModel: this.configData, - rawObjects: {}, - } + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + updateConfigInStore() { + const configToClean = cloneDeep(this.config) as AdminConfig; + configToClean.configData = cloneDeep(this.form.value); + configToClean.configData = this.editorService.adminSchema.cleanRawObjects( + configToClean.configData, + this.options.formState.rawObjects + ); + const configToUpdate = this.editorService.adminSchema.unwrapAdminConfig(configToClean); + + this.editorService.configStore.updateAdmin(configToUpdate); + } + + onSubmit() { + this.updateConfigInStore(); + this.adminPullRequestPending$.pipe(skip(1), take(1)).subscribe(a => { + if (!a.pull_request_pending) { + const dialogRef = this.dialog.open(SubmitDialogComponent, { + data: { + type: Type.ADMIN_TYPE, + validate: () => this.editorService.configStore.validateAdminConfig(), + submit: () => this.editorService.configStore.submitAdminConfig(), + }, + disableClose: true, }); - } - ngOnDestroy() { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); - } - - updateConfigInStore() { - const configToClean = cloneDeep(this.config) as AdminConfig; - configToClean.configData = cloneDeep(this.form.value); - configToClean.configData = this.editorService.adminSchema.cleanRawObjects(configToClean.configData, this.options.formState.rawObjects); - let configToUpdate = this.editorService.adminSchema.unwrapAdminConfig(configToClean); - - this.editorService.configStore.updateAdmin(configToUpdate); - } - - onSubmit() { - this.updateConfigInStore(); - this.adminPullRequestPending$.pipe(skip(1), take(1)).subscribe(a => { - if (!a.pull_request_pending) { - const dialogRef = this.dialog.open(SubmitDialogComponent, - { - data: { - type: Type.ADMIN_TYPE, - validate: () => this.editorService.configStore.validateAdminConfig(), - submit: () => this.editorService.configStore.submitAdminConfig() - }, - disableClose: true - }); - - dialogRef.afterClosed().subscribe( - success => { - if (success) { - this.router.navigate( - [this.editorService.serviceName, 'admin'] - ); - } - } - ); - } else { - this.snackbar.openNotification(this.PR_OPEN_MESSAGE); - } + dialogRef.afterClosed().subscribe(success => { + if (success) { + this.router.navigate([this.editorService.serviceName, 'admin']); + } }); - } + } else { + this.snackbar.openNotification(this.PR_OPEN_MESSAGE); + } + }); + } - public onSyncWithGit() { - this.blockUI.start("loading admin config"); - this.editorService.configStore.reloadAdminConfig().subscribe(() => { - this.blockUI.stop(); - }); - setTimeout(() => { - this.blockUI.stop(); - }, this.BLOCKING_TIMEOUT); - } + onSyncWithGit() { + this.blockUI.start('loading admin config'); + this.editorService.configStore.reloadAdminConfig().subscribe(() => { + this.blockUI.stop(); + }); + setTimeout(() => { + this.blockUI.stop(); + }, this.configService.blockingTimeout); + } } diff --git a/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.html b/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.html index 3bdd65ab..909177f6 100644 --- a/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.html +++ b/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.html @@ -21,7 +21,7 @@
+ cdkDrag [cdkDragData]="config" [config]="config" [notDeployed]="i>=filteredDeployment.configs.length" (edit)="onEdit(i)" (view)="onView(i)" (clone)="onClone(i)" (addToDeployment)="addToDeployment(i)" (deleteFromStore)="deleteConfigFromStore(i)">
@@ -44,7 +44,7 @@ history - + diff --git a/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.ts b/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.ts index 6c5945ae..6d8cd324 100644 --- a/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.ts +++ b/config-editor/config-editor-ui/src/app/components/config-manager/config-manager.component.ts @@ -1,9 +1,5 @@ import { animate, query, stagger, style, transition, trigger } from '@angular/animations'; -import { - CdkDrag, - CdkDragDrop, - CdkDropList, -} from '@angular/cdk/drag-drop'; +import { CdkDrag, CdkDragDrop, CdkDropList } from '@angular/cdk/drag-drop'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { EditorService } from '@services/editor.service'; @@ -18,205 +14,218 @@ import { FileHistory } from '../../model'; import { ConfigStoreService } from '../../services/store/config-store.service'; import { Router } from '@angular/router'; import { BlockUI, NgBlockUI } from 'ng-block-ui'; +import { AppConfigService } from '@app/services/app-config.service'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 're-config-manager', - styleUrls: ['./config-manager.component.scss'], - templateUrl: './config-manager.component.html', - animations: [ - trigger('list', [ - transition(':enter', [ - transition('* => *', []), - query(':enter', [ - style({ opacity: 0 }), - stagger(10, [ - style({ transform: 'scale(0.8)', opacity: 0 }), - animate('.6s cubic-bezier(.8,-0.6,0.7,1.5)', - style({ transform: 'scale(1)', opacity: 1 })), - ]), - ], { optional: true }), + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 're-config-manager', + styleUrls: ['./config-manager.component.scss'], + templateUrl: './config-manager.component.html', + animations: [ + trigger('list', [ + transition(':enter', [ + transition('* => *', []), + query( + ':enter', + [ + style({ opacity: 0 }), + stagger(10, [ + style({ transform: 'scale(0.8)', opacity: 0 }), + animate('.6s cubic-bezier(.8,-0.6,0.7,1.5)', style({ transform: 'scale(1)', opacity: 1 })), ]), - ]), - ], + ], + { optional: true } + ), + ]), + ]), + ], }) export class ConfigManagerComponent implements OnInit, OnDestroy { - private ngUnsubscribe = new Subject(); - private configStore: ConfigStoreService; - public allConfigs$: Observable; - public filteredConfigs$: Observable; - public deployment$: Observable; - public deployment: Deployment; - public configs: Config[]; - public selectedConfig$: Observable; - public selectedConfig: number; - public pullRequestPending$: Observable; - public releaseSubmitInFlight$: Observable; - public searchTerm$: Observable; - public filteredDeployment: Deployment; - public filteredDeployment$: Observable; - private filteredConfigs: Config[]; - public filterMyConfigs$: Observable; - public filterUndeployed$: Observable; - public filterUpgradable$: Observable; - public deploymentHistory$: Observable; - public deploymentHistory; + @BlockUI() blockUI: NgBlockUI; + allConfigs$: Observable; + filteredConfigs$: Observable; + deployment$: Observable; + deployment: Deployment; + configs: Config[]; + selectedConfig$: Observable; + selectedConfig: number; + pullRequestPending$: Observable; + releaseSubmitInFlight$: Observable; + searchTerm$: Observable; + filteredDeployment: Deployment; + filteredDeployment$: Observable; + filterMyConfigs$: Observable; + filterUndeployed$: Observable; + filterUpgradable$: Observable; + deploymentHistory$: Observable; + deploymentHistory; - private readonly BLOCKING_TIMEOUT = 30000; - private readonly PR_OPEN_MESSAGE = 'A pull request is already open'; - @BlockUI() blockUI: NgBlockUI; - constructor(public dialog: MatDialog, private snackbar: PopupService, - private editorService: EditorService, private router: Router) { - this.configStore = editorService.configStore; - this.allConfigs$ = this.configStore.allConfigs$; + private ngUnsubscribe = new Subject(); + private filteredConfigs: Config[]; + private configStore: ConfigStoreService; + private readonly PR_OPEN_MESSAGE = 'A pull request is already open'; + constructor( + public dialog: MatDialog, + private snackbar: PopupService, + private editorService: EditorService, + private router: Router, + private configService: AppConfigService + ) { + this.configStore = editorService.configStore; + this.allConfigs$ = this.configStore.allConfigs$; - this.filteredConfigs$ = this.configStore.filteredConfigs$; + this.filteredConfigs$ = this.configStore.filteredConfigs$; - this.pullRequestPending$ = this.configStore.pullRequestPending$; - this.releaseSubmitInFlight$ = this.configStore.releaseSubmitInFlight$; - this.deployment$ = this.configStore.deployment$; + this.pullRequestPending$ = this.configStore.pullRequestPending$; + this.releaseSubmitInFlight$ = this.configStore.releaseSubmitInFlight$; + this.deployment$ = this.configStore.deployment$; - this.searchTerm$ = this.configStore.searchTerm$; - this.filteredDeployment$ = this.configStore.filteredDeployment$; - this.filterMyConfigs$ = this.configStore.filterMyConfigs$; + this.searchTerm$ = this.configStore.searchTerm$; + this.filteredDeployment$ = this.configStore.filteredDeployment$; + this.filterMyConfigs$ = this.configStore.filterMyConfigs$; - this.filterUndeployed$ = this.configStore.filterUndeployed$; - this.filterUpgradable$ = this.configStore.filterUpgradable$; + this.filterUndeployed$ = this.configStore.filterUndeployed$; + this.filterUpgradable$ = this.configStore.filterUpgradable$; - this.deploymentHistory$ = this.configStore.deploymentHistory$; + this.deploymentHistory$ = this.configStore.deploymentHistory$; + } + + ngOnInit() { + this.deployment$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => { + this.deployment = cloneDeep(s); + }); + this.allConfigs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => { + this.configs = cloneDeep(r); + }); + + this.filteredConfigs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => (this.filteredConfigs = s)); + + this.filteredDeployment$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => { + this.filteredDeployment = cloneDeep(s); + }); + + this.deploymentHistory$ + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(h => (this.deploymentHistory = { fileHistory: h })); + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + onSearch(searchTerm: string) { + this.configStore.updateSearchTerm(searchTerm); + } + + upgrade(index: number) { + this.configStore.upgradeConfigInDeployment(index); + } + + drop(event: CdkDragDrop) { + if (event.container.id === 'deployment-list') { + if (event.previousContainer.id === 'store-list') { + this.configStore.addConfigToDeploymentInPosition(event.previousIndex, event.currentIndex); + } else if (event.previousContainer.id === 'deployment-list' && event.currentIndex !== event.previousIndex) { + this.configStore.moveConfigInDeployment(event.previousIndex, event.currentIndex); + } } + } - ngOnInit() { - this.deployment$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => { - this.deployment = cloneDeep(s); + onView(id: number, releaseId: number = undefined) { + this.dialog.open(JsonViewerComponent, { + data: { + config1: releaseId === undefined ? undefined : this.filteredDeployment.configs[releaseId].configData, + config2: this.filteredConfigs[id].configData, + }, + }); + } + + onEdit(id: number) { + this.router.navigate([this.editorService.serviceName, 'edit'], { + queryParams: { configName: this.filteredConfigs[id].name }, + }); + } + + addToDeployment(id: number) { + this.configStore.addConfigToDeployment(id); + } + + onClone(id: number) { + this.router.navigate([this.editorService.serviceName, 'edit'], { + queryParams: { newConfig: true, cloneConfig: this.filteredConfigs[id].name }, + }); + } + + onRemove(id: number) { + this.configStore.removeConfigFromDeployment(id); + } + + onClickCreate() { + this.router.navigate([this.editorService.serviceName, 'edit'], { queryParams: { newConfig: true } }); + } + + onDeploy() { + this.configStore.loadPullRequestStatus(); + this.pullRequestPending$.pipe(skip(1), take(1)).subscribe(a => { + if (!a.pull_request_pending) { + const dialogRef = this.dialog.open(DeployDialogComponent, { + data: cloneDeep(this.deployment), }); - this.allConfigs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => { - this.configs = cloneDeep(r); - }); - - this.filteredConfigs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => this.filteredConfigs = s); - - this.filteredDeployment$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => { - this.filteredDeployment = cloneDeep(s); - }); - - this.deploymentHistory$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(h => this.deploymentHistory = { fileHistory: h }); - } - - ngOnDestroy() { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); - } - - public onSearch(searchTerm: string) { - this.configStore.updateSearchTerm(searchTerm); - } - - public upgrade(index: number) { - this.configStore.upgradeConfigInDeployment(index); - } - - public drop(event: CdkDragDrop) { - if (event.container.id === 'deployment-list') { - if (event.previousContainer.id === 'store-list') { - this.configStore.addConfigToDeploymentInPosition(event.previousIndex, event.currentIndex); - } else if (event.previousContainer.id === 'deployment-list' && event.currentIndex !== event.previousIndex) { - this.configStore.moveConfigInDeployment(event.previousIndex, event.currentIndex); + dialogRef.afterClosed().subscribe((results: Deployment) => { + if (results && results.configs.length > 0) { + if (results.deploymentVersion >= 0) { + this.configStore.submitRelease(results); } - } - } - - public onView(id: number, releaseId: number = undefined) { - this.dialog.open(JsonViewerComponent, { - data: { - config1: releaseId === undefined - ? undefined - : this.filteredDeployment.configs[releaseId].configData, - config2: this.filteredConfigs[id].configData, - }, + } }); - } + } else { + this.snackbar.openNotification(this.PR_OPEN_MESSAGE); + } + }); + } - public onEdit(id: number) { - this.router.navigate( - [this.editorService.serviceName, 'edit'], - { queryParams: { configName: this.filteredConfigs[id].name } } - ); - } + onFilterMine($event: boolean) { + this.configStore.updateFilterMyConfigs($event); + } - public addToDeployment(id: number) { - this.configStore.addConfigToDeployment(id); - } + onSyncWithGit() { + this.blockUI.start('loading store and deployments'); + this.configStore.reloadStoreAndDeployment().subscribe(() => { + this.blockUI.stop(); + }); + setTimeout(() => { + this.blockUI.stop(); + }, this.configService.blockingTimeout); + } - public onClone(id: number) { - this.router.navigate( - [this.editorService.serviceName, 'edit'], - { queryParams: { newConfig: true, cloneConfig: this.filteredConfigs[id].name } } - ); - } + duplicateItemCheck(item: CdkDrag, deployment: CdkDropList) { + return deployment.data.find(d => d.name === item.data.name) === undefined ? true : false; + } - public onRemove(id: number) { - this.configStore.removeConfigFromDeployment(id); - } + noReturnPredicate() { + return false; + } - public onClickCreate() { - this.router.navigate( - [this.editorService.serviceName, 'edit'], - { queryParams: { newConfig: true } }); - } + onFilterUpgradable($event: boolean) { + this.configStore.updateFilterUpgradable($event); + } - public onDeploy() { - this.configStore.loadPullRequestStatus(); - this.pullRequestPending$.pipe(skip(1), take(1)).subscribe(a => { - if (!a.pull_request_pending) { - const dialogRef = this.dialog.open(DeployDialogComponent, { - data: cloneDeep(this.deployment), - }); - dialogRef.afterClosed().subscribe((results: Deployment) => { - if (results && results.configs.length > 0) { - if (results.deploymentVersion >= 0) { - this.configStore.submitRelease(results); - } - } - }); - } else { - this.snackbar.openNotification(this.PR_OPEN_MESSAGE); - } - }); - } + onFilterUndeployed($event: boolean) { + this.configStore.updateFilterUndeployed($event); + } - public onFilterMine($event: boolean) { - this.configStore.updateFilterMyConfigs($event); - } + trackConfigByName(index: number, item: Config) { + return item.name; + } - public onSyncWithGit() { - this.blockUI.start("loading store and deployments"); - this.configStore.reloadStoreAndDeployment().subscribe(() => { - this.blockUI.stop(); - }); - setTimeout(() => { - this.blockUI.stop(); - }, this.BLOCKING_TIMEOUT); - } - - public duplicateItemCheck(item: CdkDrag, deployment: CdkDropList) { - return deployment.data.find(d => d.name === item.data.name) === undefined - ? true : false; - } - - public noReturnPredicate() { - return false; - } - - public onFilterUpgradable($event: boolean) { - this.configStore.updateFilterUpgradable($event); - } - - public onFilterUndeployed($event: boolean) { - this.configStore.updateFilterUndeployed($event); - } - - public trackConfigByName(index: number, item: Config) { - return item.name; - } + deleteConfigFromStore(index: number) { + this.blockUI.start('deleting config'); + this.configStore.deleteConfig(this.filteredConfigs[index].name).subscribe(() => { + this.blockUI.stop(); + }); + setTimeout(() => { + this.blockUI.stop(); + }, this.configService.blockingTimeout); + } } diff --git a/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.spec.ts b/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.spec.ts new file mode 100644 index 00000000..d0350f33 --- /dev/null +++ b/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.spec.ts @@ -0,0 +1,81 @@ +import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CONFIG_TAB, TEST_CASE_TAB } from '@app/model/test-case'; +import { EditorService } from '@app/services/editor.service'; +import { FormlyJsonschema } from '@ngx-formly/core/json-schema'; +import { of } from 'rxjs'; +import { EditorComponent } from '../editor/editor.component'; +import { EditorViewComponent } from './editor-view.component'; + +const MockEditorService = { + serviceName: 'test', + configSchema: { schema: {} }, + configStore: { + editedConfig$: of({}), + editingTestCase$: of(true), + }, + metaDataMap: { + testing: { + perConfigTesting: true, + testCaseEnabled: true, + }, + }, +}; + +@Component({ + selector: 're-generic-editor', + template: '', + providers: [ + { + provide: EditorComponent, + useClass: EditorStubComponent, + }, + ], +}) +class EditorStubComponent { + form = { valid: true }; +} + +describe('EditorViewComponent', () => { + const routerSpy = { navigate: jasmine.createSpy('navigate') }; + const formlySpy = { toFieldConfig: jasmine.createSpy('navigate') }; + let component: EditorViewComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [EditorViewComponent, EditorStubComponent], + providers: [ + { provide: EditorService, useValue: MockEditorService }, + { provide: Router, useValue: routerSpy }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { params: { configName: 'test' } }, + }, + }, + { provide: ChangeDetectorRef, useValue: {} }, + { provide: FormlyJsonschema, useValue: formlySpy }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EditorViewComponent); + component = fixture.componentInstance; + component.editorComponent = TestBed.createComponent(EditorStubComponent).componentInstance as EditorComponent; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it(`should navigate to testCase`, fakeAsync(() => { + expect(component.selectedTab).toBe(CONFIG_TAB.index); + component.ngOnInit(); + tick(); + expect(component.selectedTab).toBe(TEST_CASE_TAB.index); + })); +}); diff --git a/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.ts b/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.ts index cffdfd37..b4a5d0ca 100644 --- a/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.ts +++ b/config-editor/config-editor-ui/src/app/components/editor-view/editor-view.component.ts @@ -1,5 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Component } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { Config } from '@app/model'; import { CONFIG_TAB, TESTING_TAB, TEST_CASE_TAB } from '@app/model/test-case'; @@ -18,73 +17,66 @@ import { SchemaService } from '@app/services/schema/schema.service'; changeDetection: ChangeDetectionStrategy.OnPush, selector: 're-editor-view', styleUrls: ['./editor-view.component.scss'], - templateUrl: './editor-view.component.html' + templateUrl: './editor-view.component.html', }) -export class EditorViewComponent implements OnInit, OnDestroy { - @ViewChild(EditorComponent, { static: false }) editorComponent: EditorComponent; +export class EditorViewComponent implements OnInit, OnDestroy, AfterViewInit { + @ViewChild(EditorComponent) editorComponent: EditorComponent; readonly TEST_CASE_TAB = TEST_CASE_TAB; readonly TESTING_TAB = TESTING_TAB; readonly CONFIG_TAB = CONFIG_TAB; readonly NO_TAB = -1; ngUnsubscribe = new Subject(); - - testCaseEnabled: () => boolean = () => false; - testingEnabled: () => boolean = () => false; configData: any; serviceName: string; schema: JSONSchema7; - selectedTab = this.NO_TAB; + selectedTab = this.CONFIG_TAB.index; previousTab = this.NO_TAB; testingType = TestingType.CONFIG_TESTING; - fields: FormlyFieldConfig[] = []; - editedConfig$: Observable; constructor( private formlyJsonschema: FormlyJsonschema, private editorService: EditorService, private router: Router, - private activeRoute: ActivatedRoute, - private cd: ChangeDetectorRef + private activeRoute: ActivatedRoute ) { this.serviceName = editorService.serviceName; this.schema = editorService.configSchema.schema; this.editedConfig$ = editorService.configStore.editedConfig$; this.fields = [ - this.formlyJsonschema.toFieldConfig(cloneDeep(this.schema), {map: SchemaService.renameDescription}), + this.formlyJsonschema.toFieldConfig(cloneDeep(this.schema), { map: SchemaService.renameDescription }), ]; } + testCaseEnabled: () => boolean = () => false; + testingEnabled: () => boolean = () => false; + ngOnInit() { this.editorService.configStore.editingTestCase$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(e => { if (e) { this.selectedTab = TEST_CASE_TAB.index; - if (this.previousTab === this.NO_TAB) { - this.previousTab = this.selectedTab; - } + } + if (this.previousTab === this.NO_TAB) { + this.previousTab = this.selectedTab; } }); } - public ngAfterViewInit() { + ngAfterViewInit() { this.editedConfig$.pipe(takeUntil(this.ngUnsubscribe)).subscribe((config: Config) => { this.fields = [ - this.formlyJsonschema.toFieldConfig(cloneDeep(this.schema), {map: SchemaService.renameDescription}), + this.formlyJsonschema.toFieldConfig(cloneDeep(this.schema), { map: SchemaService.renameDescription }), ]; - this.testingEnabled = () => this.editorService.metaDataMap.testing.perConfigTestEnabled - && this.editorComponent.form.valid; + this.testingEnabled = () => + this.editorService.metaDataMap.testing.perConfigTestEnabled && this.editorComponent.form.valid; - - this.testCaseEnabled = () => this.editorService.metaDataMap.testing.testCaseEnabled - && this.editorComponent.form.valid - && !config.isNew; + this.testCaseEnabled = () => + this.editorService.metaDataMap.testing.testCaseEnabled && this.editorComponent.form.valid && !config.isNew; this.configData = config.configData; - - this.cd.markForCheck(); }); } @@ -95,15 +87,13 @@ export class EditorViewComponent implements OnInit, OnDestroy { onTabChange() { if (this.previousTab === TEST_CASE_TAB.index) { - this.router.navigate( - [], - { - relativeTo: this.activeRoute, - queryParams: { testCaseName: null }, - queryParamsHandling: 'merge', + this.router.navigate([], { + relativeTo: this.activeRoute, + queryParams: { testCaseName: null }, + queryParamsHandling: 'merge', }); } else if (this.previousTab === CONFIG_TAB.index) { - this.editorComponent.updateConfigInStore() + this.editorComponent.updateConfigInStore(); } this.previousTab = this.selectedTab; } @@ -111,5 +101,4 @@ export class EditorViewComponent implements OnInit, OnDestroy { changeRoute() { this.router.navigate([this.serviceName]); } - } diff --git a/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.html b/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.html index 9a51f67b..6c39ce87 100644 --- a/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.html +++ b/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.html @@ -1,95 +1,101 @@ - - -

Test Suite

-
- - -
-
- - -

{{ testCase.testCase.test_case_name }} v{{ testCase.testCase.version }}

-
- - - - {{testCase.testCaseResult?.evaluationResult?.number_matched_assertions}}done{{testCase.testCaseResult?.evaluationResult?.number_failed_assertions}}clear - {{testCase.testCaseResult?.evaluationResult?.number_skipped_assertions}}? -
-
-
-
-
-
- - - - - v{{testCase?.testCase?.version || '0' }} - -

{{testCase?.testCase?.author}}

-
- -
-

{{ testCase?.testCase?.test_case_name }}

-
- {{ testCase?.testCase?.description }} Assertions: {{ testCase?.testCase?.assertions?.length }} + + + + +

Test Suite

+
+ + +
+
+ + +

{{ testCase.testCase.test_case_name }} v{{ testCase.testCase.version }}

+
+ + + + {{testCase.testCaseResult?.evaluationResult?.number_matched_assertions}}done{{testCase.testCaseResult?.evaluationResult?.number_failed_assertions}}clear + {{testCase.testCaseResult?.evaluationResult?.number_skipped_assertions}}? +
+
+
+
+
+
+ + + + + v{{testCase?.testCase?.version || '0' }} + +

{{testCase?.testCase?.author}}

-
-
-
- - - edit - - - content_copy - - - -
- - - - {{testCase?.testCaseResult?.evaluationResult?.number_matched_assertions}}done - {{testCase?.testCaseResult?.evaluationResult?.number_failed_assertions}}clear - {{testCase?.testCaseResult?.evaluationResult?.number_skipped_assertions}}? -
-
+ +
+

{{ testCase?.testCase?.test_case_name }}

+
+ {{ testCase?.testCase?.description }} Assertions: {{ testCase?.testCase?.assertions?.length }} +
+
+
+
+ + + edit + + + content_copy + + + delete + + + +
+ + + + {{testCase?.testCaseResult?.evaluationResult?.number_matched_assertions}}done + {{testCase?.testCaseResult?.evaluationResult?.number_failed_assertions}}clear + {{testCase?.testCaseResult?.evaluationResult?.number_skipped_assertions}}? +
+
+
-
- - - -
\ No newline at end of file + + + + +
\ No newline at end of file diff --git a/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.ts b/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.ts index a80f8f92..b3d0cb43 100644 --- a/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.ts +++ b/config-editor/config-editor-ui/src/app/components/testing/test-centre/test-centre.component.ts @@ -7,108 +7,119 @@ import { TestCaseWrapper } from '@model/test-case'; import { TestStoreService } from '../../../services/store/test-store.service'; import { TestCaseResult } from '../../../model/test-case'; import { Router, ActivatedRoute } from '@angular/router'; +import { BlockUI, NgBlockUI } from 'ng-block-ui'; +import { AppConfigService } from '@app/services/app-config.service'; @Component({ - selector: 're-test-centre', - templateUrl: './test-centre.component.html', - styleUrls: ['./test-centre.component.scss'], + selector: 're-test-centre', + templateUrl: './test-centre.component.html', + styleUrls: ['./test-centre.component.scss'], }) export class TestCentreComponent implements OnInit, OnDestroy { - private testStoreService: TestStoreService; - private ngUnsubscribe = new Subject(); + @BlockUI() blockUI: NgBlockUI; + testCases$: Observable; + editingTestCase$: Observable; + editedTestCase$: Observable; - public testCases$: Observable; - public editingTestCase$: Observable; - public editedTestCase$: Observable; + testCases: TestCaseWrapper[]; + testCase: TestCaseWrapper; - public testCases: TestCaseWrapper[]; - public testCase: TestCaseWrapper; + private testStoreService: TestStoreService; + private ngUnsubscribe = new Subject(); + constructor( + private editorService: EditorService, + public snackbar: PopupService, + private router: Router, + private activeRoute: ActivatedRoute, + private configService: AppConfigService + ) { + this.testCases$ = this.editorService.configStore.editedConfigTestCases$; + this.editingTestCase$ = this.editorService.configStore.editingTestCase$; + this.testStoreService = this.editorService.configStore.testService; + this.editedTestCase$ = this.editorService.configStore.editedTestCase$; + } - constructor(private editorService: EditorService, - public snackbar: PopupService, - private router: Router, - private activeRoute: ActivatedRoute) { - this.testCases$ = this.editorService.configStore.editedConfigTestCases$; - this.editingTestCase$ = this.editorService.configStore.editingTestCase$; - this.testStoreService = this.editorService.configStore.testService; - this.editedTestCase$ = this.editorService.configStore.editedTestCase$; + ngOnInit() { + if (this.editorService.metaDataMap.testing.testCaseEnabled) { + this.testCases$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(testCases => { + this.testCases = testCases; + }); + this.editedTestCase$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(testCaseWrapper => { + this.testCase = testCaseWrapper; + }); + } + } + + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + onAddTestCase() { + this.router.navigate([], { + relativeTo: this.activeRoute, + queryParams: { newTestCase: true }, + queryParamsHandling: 'merge', + }); + } + + onEditTestCase(index: number) { + const name = this.testCases[index].testCase.test_case_name; + this.router.navigate([], { + relativeTo: this.activeRoute, + queryParams: { testCaseName: name }, + queryParamsHandling: 'merge', + }); + } + + onCloneTestCase(index: number) { + const name = this.testCases[index].testCase.test_case_name; + this.router.navigate([], { + relativeTo: this.activeRoute, + queryParams: { newTestCase: true, cloneTestCase: name }, + queryParamsHandling: 'merge', + }); + } + + onRunTestSuite() { + this.testStoreService.runEditedConfigTestSuite(); + } + + onCancelEditing() { + this.router.navigate([], { + relativeTo: this.activeRoute, + queryParams: { testCaseName: null, newTestCase: null, cloneTestCase: null }, + queryParamsHandling: 'merge', + }); + } + + getTestBadge(testCaseResult: TestCaseResult): string { + if (!testCaseResult) { + return 'test-default'; } - ngOnInit() { - if (this.editorService.metaDataMap.testing.testCaseEnabled) { - this.testCases$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(testCases => { - this.testCases = testCases; - }); - this.editedTestCase$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(testCaseWrapper => { - this.testCase = testCaseWrapper; - }); - } + if (testCaseResult.isRunning) { + return 'test-running'; } - ngOnDestroy() { - this.ngUnsubscribe.next(); - this.ngUnsubscribe.complete(); - } + return !testCaseResult.evaluationResult + ? 'test-skipped' + : testCaseResult.evaluationResult.number_failed_assertions > 0 + ? 'test-fail' + : testCaseResult.evaluationResult.number_skipped_assertions > 0 + ? 'test-skipped' + : 'test-success'; + } - onAddTestCase() { - this.router.navigate( - [], - { - relativeTo: this.activeRoute, - queryParams: { newTestCase: true }, - queryParamsHandling: 'merge', - }); - } - - onEditTestCase(index: number) { - const name = this.testCases[index].testCase.test_case_name; - this.router.navigate( - [], - { - relativeTo: this.activeRoute, - queryParams: { testCaseName: name }, - queryParamsHandling: 'merge', - }); - } - - onCloneTestCase(index: number) { - const name = this.testCases[index].testCase.test_case_name; - this.router.navigate( - [], - { - relativeTo: this.activeRoute, - queryParams: { newTestCase: true, cloneTestCase: name }, - queryParamsHandling: 'merge', - }); - } - - onRunTestSuite() { - this.testStoreService.runEditedConfigTestSuite(); - } - - onCancelEditing() { - this.router.navigate( - [], - { - relativeTo: this.activeRoute, - queryParams: { testCaseName: null, newTestCase: null, cloneTestCase: null}, - queryParamsHandling: 'merge', - }); - } - - getTestBadge(testCaseResult: TestCaseResult): string { - if (!testCaseResult) { - return 'test-default'; - } - - if (testCaseResult.isRunning) { - return 'test-running' - } - - return !testCaseResult.evaluationResult - ? 'test-skipped' : testCaseResult.evaluationResult.number_failed_assertions > 0 - ? 'test-fail' : testCaseResult.evaluationResult.number_skipped_assertions > 0 - ? 'test-skipped' : 'test-success'; - - } + onDeleteTestCase(index: number) { + this.blockUI.start('deleting test case'); + this.editorService.configStore + .deleteTestCase(this.testCases[index].testCase.config_name, this.testCases[index].testCase.test_case_name) + .subscribe(() => { + this.blockUI.stop(); + }); + setTimeout(() => { + this.blockUI.stop(); + }, this.configService.blockingTimeout); + } } diff --git a/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.html b/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.html index eed710ca..a3161362 100644 --- a/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.html +++ b/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.html @@ -40,7 +40,10 @@ content_copy - + + delete + + arrow_forward diff --git a/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.ts b/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.ts index 37495017..45bec3a0 100644 --- a/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.ts +++ b/config-editor/config-editor-ui/src/app/components/tile/config-tile.component.ts @@ -1,38 +1,39 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { ConfigData, Config } from '../../model/config-model'; +import { Config } from '../../model/config-model'; @Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - selector: 're-config-tile', - styleUrls: ['./config-tile.component.scss'], - templateUrl: './config-tile.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 're-config-tile', + styleUrls: ['./config-tile.component.scss'], + templateUrl: './config-tile.component.html', }) export class ConfigTileComponent { + @Input() config: Config; + @Input() notDeployed: boolean; - @Input() config: Config; - @Input() hideAddDeployment: boolean; + @Output() readonly edit = new EventEmitter(); + @Output() readonly view = new EventEmitter(); + @Output() readonly addToDeployment = new EventEmitter(); + @Output() readonly clone = new EventEmitter(); + @Output() readonly deleteFromStore = new EventEmitter(); - @Output() onEdit = new EventEmitter(); - @Output() onView = new EventEmitter(); - @Output() onAddToDeployment = new EventEmitter(); - @Output() onClone = new EventEmitter(); + editConfig() { + this.edit.emit(); + } - constructor() {} + viewConfig() { + this.view.emit(); + } - editConfig() { - this.onEdit.emit(); - } + addConfigToDeployment() { + this.addToDeployment.emit(); + } - viewConfig() { - this.onView.emit(); - } - - addToDeployment() { - this.onAddToDeployment.emit(); - } - - cloneConfig() { - this.onClone.emit(); - } + cloneConfig() { + this.clone.emit(); + } + deleteConfigFromStore() { + this.deleteFromStore.emit(); + } } diff --git a/config-editor/config-editor-ui/src/app/model/app-config.ts b/config-editor/config-editor-ui/src/app/model/app-config.ts index 93909b05..bc81a597 100644 --- a/config-editor/config-editor-ui/src/app/model/app-config.ts +++ b/config-editor/config-editor-ui/src/app/model/app-config.ts @@ -1,36 +1,37 @@ -import { UserSettings } from "oidc-client"; +import { UserSettings } from 'oidc-client'; export interface BuildInfo { - appName: string; - appVersion: number; - buildDate: Date; - angularVersion: string; + appName: string; + appVersion: number; + buildDate: Date; + angularVersion: string; } export enum AuthenticationType { - Disabled = "disabled", - Kerberos = "kerberos", - Oauth2 = "oauth2", + Disabled = 'disabled', + Kerberos = 'kerberos', + Oauth2 = 'oauth2', } export interface AppConfig { - environment: string; - serviceRoot: string; - aboutApp: BuildInfo; - authType: AuthenticationType, - authAttributes: Oauth2Attributes | any - homeHelpLinks?: HomeHelpLink[]; - historyMaxSize?: number; + environment: string; + serviceRoot: string; + aboutApp: BuildInfo; + authType: AuthenticationType; + authAttributes: Oauth2Attributes | any; + homeHelpLinks?: HomeHelpLink[]; + historyMaxSize?: number; + blockingTimeout?: number; } export interface HomeHelpLink { - title: string; - icon: string; - link: string; + title: string; + icon: string; + link: string; } export interface Oauth2Attributes { - callbackPath: string; - expiresIntervalMinimum: number; - oidcSettings: UserSettings; -} \ No newline at end of file + callbackPath: string; + expiresIntervalMinimum: number; + oidcSettings: UserSettings; +} diff --git a/config-editor/config-editor-ui/src/app/model/config-model.ts b/config-editor/config-editor-ui/src/app/model/config-model.ts index 60cea3b4..b44267db 100644 --- a/config-editor/config-editor-ui/src/app/model/config-model.ts +++ b/config-editor/config-editor-ui/src/app/model/config-model.ts @@ -1,168 +1,176 @@ -import { TestCase, TestCaseWrapper, TestCaseEvaluationResult } from './test-case'; +import { TestCase, TestCaseWrapper, TestCaseEvaluationResult, TestCaseMap } from './test-case'; import { JSONSchema7 } from 'json-schema'; import { Observable } from 'rxjs'; -export const NAME_REGEX = "^[a-zA-Z0-9_\\-]+$"; +export const NAME_REGEX = '^[a-zA-Z0-9_\\-]+$'; -export const repoNames = { - store_directory_name: "Config Store Folder", - release_directory_name: "Config Deployment Folder", - testcase_store_directory_name: "Config Testcase Folder", - admin_config_store_directory_name: "Admin Config Folder" -} +export const repoNames = { + store_directory_name: 'Config Store Folder', + release_directory_name: 'Config Deployment Folder', + testcase_store_directory_name: 'Config Testcase Folder', + admin_config_store_directory_name: 'Admin Config Folder', +}; export enum TestingType { - DEPLOYMENT_TESTING = 'deployment_testing', - CONFIG_TESTING = 'config_testing' + DEPLOYMENT_TESTING = 'deployment_testing', + CONFIG_TESTING = 'config_testing', } export enum Type { - CONFIG_TYPE = 'Config', - TESTCASE_TYPE = 'TestCase', - ADMIN_TYPE = 'Admin' + CONFIG_TYPE = 'Config', + TESTCASE_TYPE = 'TestCase', + ADMIN_TYPE = 'Admin', } export enum UserRole { - SERVICE_USER = 'service_user', - SERVICE_ADMIN = 'service_admin' + SERVICE_USER = 'service_user', + SERVICE_ADMIN = 'service_admin', } export interface SubmitDialogData { - name: string, - type: string, - validate: () => Observable; - submit: () => Observable; + name: string; + type: string; + validate: () => Observable; + submit: () => Observable; } export interface GitFiles { - files: T[]; + files: T[]; +} + +export interface GitFilesDelete { + configs_files: T[]; + test_cases_files?: T[]; } export interface AdminConfigGitFiles extends GitFiles { - config_version: number; + config_version: number; } export interface DeploymentGitFiles extends GitFiles { - rules_version: number; + rules_version: number; } export interface TestCaseEvaluation { - files: Content[]; - test_result_raw_output: string; + files: Content[]; + test_result_raw_output: string; } export interface GeneralRule { - file_name?: string; + file_name?: string; } export interface Content extends GeneralRule { - content: T; + content: T; } export interface ServiceInfo { - name: string; - type: string; - user_roles: UserRole[]; + name: string; + type: string; + user_roles: UserRole[]; } export interface UserInfo { - user_name: string; - services: ServiceInfo[]; - + user_name: string; + services: ServiceInfo[]; } export interface RepositoryLinksWrapper { - rules_repositories: RepositoryLinks; + rules_repositories: RepositoryLinks; } export interface RepositoryLinks { - rule_store_directory_url: string; - rules_release_directory_url: string; - test_case_store_directory_url: string; - admin_config_directory_url: string; - service_name: string; + rule_store_directory_url: string; + rules_release_directory_url: string; + test_case_store_directory_url: string; + admin_config_directory_url: string; + service_name: string; } export interface SchemaInfo { - rules_schema: JSONSchema7; + rules_schema: JSONSchema7; } export interface AdminSchemaInfo { - admin_config_schema: JSONSchema7; + admin_config_schema: JSONSchema7; } export interface TestSchemaInfo { - test_schema: JSONSchema7; + test_schema: JSONSchema7; } export interface PullRequestInfo { - pull_request_pending: boolean; - pull_request_url: string; + pull_request_pending: boolean; + pull_request_url: string; } export interface Config { - versionFlag?: number; - isDeployed?: boolean; - isNew: boolean; - configData: ConfigData; - savedInBackend: boolean; - name: string; - author: string; - version: number; - description: string; - tags?: string[]; - fileHistory?: FileHistory[]; - testCases?: TestCaseWrapper[]; + versionFlag?: number; + isDeployed?: boolean; + isNew: boolean; + configData: ConfigData; + savedInBackend: boolean; + name: string; + author: string; + version: number; + description: string; + tags?: string[]; + fileHistory?: FileHistory[]; + testCases?: TestCaseWrapper[]; } -export interface AdminConfig{ - configData: ConfigData; - version: number; - fileHistory?: FileHistory[]; +export interface AdminConfig { + configData: ConfigData; + version: number; + fileHistory?: FileHistory[]; } export interface FileHistory { - author: string; - date: string; - removed: number; - added: number; + author: string; + date: string; + removed: number; + added: number; } export interface ConfigTestDto { - files: Content[], - test_specification: string, + files: Content[]; + test_specification: string; } export type ConfigData = any; export interface Deployment { - configs: Config[]; - deploymentVersion: number; + configs: Config[]; + deploymentVersion: number; } export interface ConfigTestResult { - exception?: string; - message?: string; - test_result_output?: string; - test_result_complete?: boolean; - test_result_raw_output?: object; + exception?: string; + message?: string; + test_result_output?: string; + test_result_complete?: boolean; + test_result_raw_output?: object; } export interface DeploymentWrapper { - storedDeployment: Deployment; - deploymentHistory: FileHistory[]; + storedDeployment: Deployment; + deploymentHistory: FileHistory[]; } export interface TestCaseResultAttributes { - exception?: string; - message?: string; - test_case_result?: TestCaseEvaluationResult; + exception?: string; + message?: string; + test_case_result?: TestCaseEvaluationResult; } export interface UrlInfo { - service?: string, - mode?: string, - configName?: string, - testCaseName?: string + service?: string; + mode?: string; + configName?: string; + testCaseName?: string; } +export interface ConfigAndTestCases { + configs: Config[]; + testCases?: TestCaseMap; +} diff --git a/config-editor/config-editor-ui/src/app/model/index.ts b/config-editor/config-editor-ui/src/app/model/index.ts index cb5cbe2a..4d7a162f 100644 --- a/config-editor/config-editor-ui/src/app/model/index.ts +++ b/config-editor/config-editor-ui/src/app/model/index.ts @@ -1,7 +1,16 @@ export { - GitFiles, SchemaInfo, Content, - PullRequestInfo, Config, ConfigData, Deployment, - RepositoryLinks, RepositoryLinksWrapper, FileHistory, NAME_REGEX + GitFiles, + SchemaInfo, + Content, + PullRequestInfo, + Config, + ConfigData, + Deployment, + RepositoryLinks, + RepositoryLinksWrapper, + FileHistory, + NAME_REGEX, + ConfigAndTestCases, } from './config-model'; export { BuildInfo, AuthenticationType, AppConfig } from './app-config'; export { StatusCode } from './status-code'; diff --git a/config-editor/config-editor-ui/src/app/ngx-formly/tab-array.type.component.ts b/config-editor/config-editor-ui/src/app/ngx-formly/tab-array.type.component.ts index a76ef703..83f46eb4 100644 --- a/config-editor/config-editor-ui/src/app/ngx-formly/tab-array.type.component.ts +++ b/config-editor/config-editor-ui/src/app/ngx-formly/tab-array.type.component.ts @@ -64,7 +64,7 @@ import { FieldArrayType } from '@ngx-formly/core'; ], }) export class TabArrayTypeComponent extends FieldArrayType { - public selectedIndex = 0; + selectedIndex = 0; getUnionType(model): string { const keys = Object.keys(model); @@ -72,7 +72,8 @@ export class TabArrayTypeComponent extends FieldArrayType { } add(i: number) { - super.add(this.model.length); + const modelLength = this.model ? this.model.length : 0; + super.add(modelLength); this.selectedIndex = i; for (let j = this.model.length - 1; j >= i; j--) { this.moveDown(j); diff --git a/config-editor/config-editor-ui/src/app/services/app-config.service.ts b/config-editor/config-editor-ui/src/app/services/app-config.service.ts index 39e42e9a..edd9bb8e 100644 --- a/config-editor/config-editor-ui/src/app/services/app-config.service.ts +++ b/config-editor/config-editor-ui/src/app/services/app-config.service.ts @@ -26,13 +26,13 @@ export class AppConfigService { this._authenticationService = new DefaultAuthenticationService(); } - public loadConfigAndMetadata(): Promise { + loadConfigAndMetadata(): Promise { return this.loadConfig() .then(() => this.loadUiMetadata()) .then(() => this.createAuthenticationService()); } - public loadBuildInfo(): Promise { + loadBuildInfo(): Promise { return this.http .get('assets/build-info.json') .toPromise() @@ -43,49 +43,53 @@ export class AppConfigService { .catch(err => console.info(`could not load build info: ${err}`)); } - public isHomePath(path: string): boolean { + isHomePath(path: string): boolean { if (path === '/home' || path === '/') { return true; } return false; } - public get adminPath(): string { + get adminPath(): string { return '/admin'; } - public get config(): AppConfig { + get config(): AppConfig { return this._config; } - public get buildInfo(): BuildInfo { + get buildInfo(): BuildInfo { return this._buildInfo; } - public get environment(): string { + get environment(): string { return this._config.environment; } - public get serviceRoot(): string { + get serviceRoot(): string { return this._config.serviceRoot; } - public get uiMetadata(): UiMetadataMap { + get uiMetadata(): UiMetadataMap { return this._uiMetadata; } - public get authenticationService(): IAuthenticationService { + get authenticationService(): IAuthenticationService { return this._authenticationService; } - public get homeHelpLinks(): HomeHelpLink[] { + get homeHelpLinks(): HomeHelpLink[] { return this._config.homeHelpLinks; } - public get historyMaxSize(): number { + get historyMaxSize(): number { return this._config.historyMaxSize ? this._config.historyMaxSize : 5; } + get blockingTimeout(): number { + return this._config.blockingTimeout ? this._config.blockingTimeout : 30000; + } + private loadConfig(): Promise { return this.http .get('config/ui-config.json') diff --git a/config-editor/config-editor-ui/src/app/services/config-loader.service.spec.ts b/config-editor/config-editor-ui/src/app/services/config-loader.service.spec.ts index 9b89cd4b..4ea4a5ac 100644 --- a/config-editor/config-editor-ui/src/app/services/config-loader.service.spec.ts +++ b/config-editor/config-editor-ui/src/app/services/config-loader.service.spec.ts @@ -77,7 +77,7 @@ describe('ConfigLoaderService', () => { }); it('should convert testcase files to map', () => { - expect(service['testCaseFilesToMap'](mockTestCaseFiles)).toEqual(mockTestCaseMap); + expect(service['testCaseFilesToMap'](mockTestCaseFiles.files)).toEqual(mockTestCaseMap); }); it('should submit config', () => { diff --git a/config-editor/config-editor-ui/src/app/services/config-loader.service.ts b/config-editor/config-editor-ui/src/app/services/config-loader.service.ts index f1a901a0..b80995cc 100644 --- a/config-editor/config-editor-ui/src/app/services/config-loader.service.ts +++ b/config-editor/config-editor-ui/src/app/services/config-loader.service.ts @@ -16,6 +16,8 @@ import { AdminConfig, AdminConfigGitFiles, DeploymentGitFiles, + ConfigAndTestCases, + GitFilesDelete, } from '@model/config-model'; import { TestCase, TestCaseMap, TestCaseResult, TestCaseWrapper } from '@model/test-case'; import { ADMIN_VERSION_FIELD_NAME, UiMetadata } from '@model/ui-metadata-map'; @@ -45,7 +47,7 @@ export class ConfigLoaderService { } } - public getConfigFromFile(file: any): Config { + getConfigFromFile(file: any): Config { return { author: file.content[this.uiMetadata.author], configData: file.content, @@ -62,7 +64,7 @@ export class ConfigLoaderService { }; } - public getConfigs(): Observable { + getConfigs(): Observable { return this.http .get>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/configs`) .map(result => { @@ -74,13 +76,13 @@ export class ConfigLoaderService { }); } - public getTestSpecificationSchema(): Observable { + getTestSpecificationSchema(): Observable { return this.http .get(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/testschema`) .pipe(map(x => x.test_schema)); } - public getSchema(): Observable { + getSchema(): Observable { return this.http.get(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/schema`).map(x => { try { return x.rules_schema; @@ -90,7 +92,7 @@ export class ConfigLoaderService { }); } - public getAdminSchema(): Observable { + getAdminSchema(): Observable { return this.http .get(`${this.config.serviceRoot}api/v1/${this.serviceName}/adminconfig/schema`) .map(x => { @@ -102,19 +104,19 @@ export class ConfigLoaderService { }); } - public getPullRequestStatus(): Observable { + getPullRequestStatus(): Observable { return this.http.get( `${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release/status` ); } - public getAdminPullRequestStatus(): Observable { + getAdminPullRequestStatus(): Observable { return this.http.get( `${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/adminconfig/status` ); } - public getRelease(): Observable { + getRelease(): Observable { return this.http .get>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release`) .pipe( @@ -160,7 +162,7 @@ export class ConfigLoaderService { ); } - public getAdminConfig(): Observable { + getAdminConfig(): Observable { return this.http .get>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/adminconfig`) .pipe( @@ -185,13 +187,13 @@ export class ConfigLoaderService { ); } - public getTestCases(): Observable { + getTestCases(): Observable { return this.http .get>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases`) - .pipe(map(result => this.testCaseFilesToMap(result))); + .pipe(map(result => this.testCaseFilesToMap(result.files))); } - public validateConfig(config: Config): Observable { + validateConfig(config: Config): Observable { const json = JSON.stringify(config.configData, null, 2); return this.http.post( @@ -200,27 +202,27 @@ export class ConfigLoaderService { ); } - public validateRelease(deployment: Deployment): Observable { + validateRelease(deployment: Deployment): Observable { const validationFormat = this.marshalDeploymentFormat(deployment); const json = JSON.stringify(validationFormat, null, 2); return this.http.post(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/validate`, json); } - public validateAdminConfig(config: AdminConfig): Observable { + validateAdminConfig(config: AdminConfig): Observable { const json = JSON.stringify(config.configData, null, 2); return this.http.post(`${this.config.serviceRoot}api/v1/${this.serviceName}/adminconfig/validate`, json); } - public submitRelease(deployment: Deployment): Observable { + submitRelease(deployment: Deployment): Observable { const releaseFormat = this.marshalDeploymentFormat(deployment); const json = JSON.stringify(releaseFormat, null, 2); return this.http.post(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release`, json); } - public submitConfig(config: Config): Observable { + submitConfig(config: Config): Observable { const fun = config.isNew ? this.submitNewConfig(config) : this.submitConfigEdit(config); return fun.map(result => { if (result.files && result.files.length > 0) { @@ -231,7 +233,7 @@ export class ConfigLoaderService { }); } - public submitConfigEdit(config: Config): Observable> { + submitConfigEdit(config: Config): Observable> { const json = JSON.stringify(config.configData, null, 2); return this.http.put>( @@ -240,7 +242,7 @@ export class ConfigLoaderService { ); } - public submitNewConfig(config: Config): Observable> { + submitNewConfig(config: Config): Observable> { const json = JSON.stringify(config.configData, null, 2); return this.http.post>( @@ -249,7 +251,7 @@ export class ConfigLoaderService { ); } - public submitAdminConfig(config: AdminConfig): Observable> { + submitAdminConfig(config: AdminConfig): Observable> { const json = JSON.stringify(config.configData, null, 2); return this.http.post>( @@ -258,7 +260,7 @@ export class ConfigLoaderService { ); } - public testDeploymentConfig(deployment: Deployment, testSpecification: any): Observable { + testDeploymentConfig(deployment: Deployment, testSpecification: any): Observable { const testDto: ConfigTestDto = { files: [ { @@ -276,7 +278,7 @@ export class ConfigLoaderService { ); } - public testSingleConfig(configData: any, testSpecification: any): Observable { + testSingleConfig(configData: any, testSpecification: any): Observable { const testDto: ConfigTestDto = { files: [ { @@ -292,27 +294,27 @@ export class ConfigLoaderService { ); } - public submitTestCase(testCase: TestCaseWrapper): Observable { + submitTestCase(testCase: TestCaseWrapper): Observable { return isNewTestCase(testCase) ? this.submitNewTestCase(testCase) : this.submitTestCaseEdit(testCase); } - public submitTestCaseEdit(testCase: TestCaseWrapper): Observable { + submitTestCaseEdit(testCase: TestCaseWrapper): Observable { const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2); return this.http .put>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases`, json) - .pipe(map(result => this.testCaseFilesToMap(result))); + .pipe(map(result => this.testCaseFilesToMap(result.files))); } - public submitNewTestCase(testCase: TestCaseWrapper): Observable { + submitNewTestCase(testCase: TestCaseWrapper): Observable { const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2); return this.http .post>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases`, json) - .pipe(map(result => this.testCaseFilesToMap(result))); + .pipe(map(result => this.testCaseFilesToMap(result.files))); } - public validateTestCase(testcase: TestCase): Observable { + validateTestCase(testcase: TestCase): Observable { const outObj = { files: [ { @@ -325,7 +327,7 @@ export class ConfigLoaderService { return this.http.post(`${this.config.serviceRoot}api/v1/testcases/validate`, json); } - public evaluateTestCase(configData: any, testCaseWrapper: TestCaseWrapper): Observable { + evaluateTestCase(configData: any, testCaseWrapper: TestCaseWrapper): Observable { const ret = {} as TestCaseResult; return this.testSingleConfig(configData, testCaseWrapper.testCase.test_specification) @@ -345,7 +347,7 @@ export class ConfigLoaderService { ); } - public evaluateTestCaseFromResult(testcase: TestCase, testResult: any): Observable { + evaluateTestCaseFromResult(testcase: TestCase, testResult: any): Observable { const outObj: TestCaseEvaluation = { files: [ { @@ -362,10 +364,40 @@ export class ConfigLoaderService { .pipe(map(x => x.test_case_result)); } - private testCaseFilesToMap(result: GitFiles): TestCaseMap { + deleteConfig(configName: string): Observable { + return this.http + .post>( + `${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/configs/delete?configName=${configName}`, + null + ) + .map(result => { + if (!result.configs_files || (!result.test_cases_files && this.uiMetadata.testing.testCaseEnabled)) { + throw new DOMException('bad format response when deleting config'); + } + let configAndTestCases = { + configs: result.configs_files.map(file => this.getConfigFromFile(file)), + testCases: {}, + }; + if (result.test_cases_files) { + configAndTestCases['testCases'] = this.testCaseFilesToMap(result.test_cases_files); + } + return configAndTestCases; + }); + } + + deleteTestCase(configName: string, testCaseName: string): Observable { + return this.http + .post>>( + `${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases/delete?configName=${configName}&testCaseName=${testCaseName}`, + null + ) + .pipe(map(result => this.testCaseFilesToMap(result.files))); + } + + private testCaseFilesToMap(files: any[]): TestCaseMap { const testCaseMap: TestCaseMap = {}; - if (result.files && result.files.length > 0) { - result.files.forEach(file => { + if (files && files.length > 0) { + files.forEach(file => { if (!testCaseMap.hasOwnProperty(file.content.config_name)) { testCaseMap[file.content.config_name] = []; } diff --git a/config-editor/config-editor-ui/src/app/services/store/config-store.service.ts b/config-editor/config-editor-ui/src/app/services/store/config-store.service.ts index d17c25fd..ec3b7f54 100644 --- a/config-editor/config-editor-ui/src/app/services/store/config-store.service.ts +++ b/config-editor/config-editor-ui/src/app/services/store/config-store.service.ts @@ -64,7 +64,7 @@ export class ConfigStoreService { private metaDataMap: UiMetadata; private testStoreService: TestStoreService; - public get testService(): TestStoreService { + get testService(): TestStoreService { return this.testStoreService; } @@ -369,6 +369,34 @@ export class ConfigStoreService { this.store.next(newState); } + deleteConfig(configName: string): Observable { + return this.configLoaderService.deleteConfig(configName).map(data => { + const newState = new ConfigStoreStateBuilder(this.store.getValue()) + .testCaseMap(data.testCases) + .configs(data.configs) + .updateTestCasesInConfigs() + .detectOutdatedConfigs() + .reorderConfigsByDeployment() + .computeFiltered(this.user) + .build(); + + this.store.next(newState); + }); + } + + deleteTestCase(configName: string, testCaseName: string): Observable { + return this.configLoaderService.deleteTestCase(configName, testCaseName).map(testCaseMap => { + const newState = new ConfigStoreStateBuilder(this.store.getValue()) + .testCaseMap(testCaseMap) + .updateTestCasesInConfigs() + .editedConfigByName(configName) + .computeFiltered(this.user) + .build(); + + this.store.next(newState); + }); + } + private updateReleaseSubmitInFlight(releaseSubmitInFlight: boolean) { const newState = new ConfigStoreStateBuilder(this.store.getValue()) .releaseSubmitInFlight(releaseSubmitInFlight) diff --git a/config/config-editor-ui/ui-config.json b/config/config-editor-ui/ui-config.json index 677b6716..72eadf94 100644 --- a/config/config-editor-ui/ui-config.json +++ b/config/config-editor-ui/ui-config.json @@ -3,6 +3,8 @@ "serviceRoot": "https://config-editor/", "uiBootstrapPath": "./ui-bootstrap.json", "authType": "disabled", + "historyMaxSize": 5, + "blockingTimeout": "30000", "homeLinks": [ { "icon": "library_books", diff --git a/docs/siembol_ui/siembol_ui.md b/docs/siembol_ui/siembol_ui.md index f356aea3..4a1ba623 100644 --- a/docs/siembol_ui/siembol_ui.md +++ b/docs/siembol_ui/siembol_ui.md @@ -11,7 +11,8 @@ On the home page all services are listed alphabetically by name on the left side

### Recently visited -Your recently visited pages are saved in your browser and can be accessed with only one click from the home page. +Your recently visited pages are saved in your browser and can be accessed with only one click from the home page. The default number of pages shown is 5 but can be configured in the `ui-config.json` file using the "historyMaxSize" key. + ### Explore Siembol The 'Explore Siembol' section of the home page is for quick access to useful resources such as documentation, ticket tracking systems etc... By default there is a link to the documentation and to the issues page on the git repo. This can be customised from the `ui-config.json` config file. @@ -23,6 +24,8 @@ Below is the default config file provided. The two default links are in "homeLin "serviceRoot": "https://config-editor/", "uiBootstrapPath": "./ui-bootstrap.json", "authType": "disabled", + "historyMaxSize": 5, + "blockingTimeout": 30000, "homeLinks": [ { "icon": "library_books", @@ -36,6 +39,9 @@ Below is the default config file provided. The two default links are in "homeLin ] } +## Blocking timeout +The blocking timeout is a property that can be configured in `ui-config.json` (as shown in the default config above), it can be omitted in `ui-config.json`, the default value is 30 seconds. This value is used for certain operations that require blocking the entire UI (eg. deleting a config), the UI will be blocked for this maximum amount of time after which an error will occur if the operation hasn't yet finished. + ## Service configurations After selecting a service to edit you will be redirected to the service configuration page. An example is shown below.