Config-editor-ui: add delete config / testcase functionality and fix tab change bug (#51)

This commit is contained in:
Celie Valentiny
2021-05-11 16:43:07 +01:00
committed by GitHub
parent be6908d870
commit 24d0e5880a
22 changed files with 868 additions and 669 deletions

6
.gitignore vendored
View File

@@ -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

View File

@@ -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"
]
}
],

View File

@@ -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": {

View File

@@ -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<AdminConfig>;
public config: AdminConfig;
public serviceName: string;
public adminPullRequestPending$: Observable<PullRequestInfo>;
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<AdminConfig>;
config: AdminConfig;
serviceName: string;
adminPullRequestPending$: Observable<PullRequestInfo>;
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);
}
}

View File

@@ -21,7 +21,7 @@
<div @list cdkDropList id="store-list" [cdkDropListData]="allConfigs$ | async" [cdkDropListConnectedTo]="['deployment-list']" [cdkDropListEnterPredicate]="noReturnPredicate"
(cdkDropListDropped)="drop($event)" class="rule-list">
<re-config-tile *ngFor="let config of (filteredConfigs$ | async); index as i; trackBy: trackConfigByName"
cdkDrag [cdkDragData]="config" [config]="config" [hideAddDeployment]="i>=filteredDeployment.configs.length" (onEdit)="onEdit(i)" (onView)="onView(i)" (onClone)="onClone(i)" (onAddToDeployment)="addToDeployment(i)">
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)">
</re-config-tile>
</div>
</div>
@@ -44,7 +44,7 @@
</popper-content>
<mat-icon>history</mat-icon>
</div>
<span *ngIf="!(pullRequestPending$ | async).pull_request_pending && !(releaseSubmitInFlight$ | async); else prMessage">
<span *ngIf="(pullRequestPending$ | async).pull_request_pending === false && (releaseSubmitInFlight$ | async) === false; else prMessage">
<button class="button" mat-button color="accent" title="Deploy Configs" (click)="onDeploy()">DEPLOY</button>
</span>
<ng-template #prMessage>

View File

@@ -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<Config[]>;
public filteredConfigs$: Observable<Config[]>;
public deployment$: Observable<Deployment>;
public deployment: Deployment;
public configs: Config[];
public selectedConfig$: Observable<number>;
public selectedConfig: number;
public pullRequestPending$: Observable<PullRequestInfo>;
public releaseSubmitInFlight$: Observable<boolean>;
public searchTerm$: Observable<string>;
public filteredDeployment: Deployment;
public filteredDeployment$: Observable<Deployment>;
private filteredConfigs: Config[];
public filterMyConfigs$: Observable<boolean>;
public filterUndeployed$: Observable<boolean>;
public filterUpgradable$: Observable<boolean>;
public deploymentHistory$: Observable<FileHistory[]>;
public deploymentHistory;
@BlockUI() blockUI: NgBlockUI;
allConfigs$: Observable<Config[]>;
filteredConfigs$: Observable<Config[]>;
deployment$: Observable<Deployment>;
deployment: Deployment;
configs: Config[];
selectedConfig$: Observable<number>;
selectedConfig: number;
pullRequestPending$: Observable<PullRequestInfo>;
releaseSubmitInFlight$: Observable<boolean>;
searchTerm$: Observable<string>;
filteredDeployment: Deployment;
filteredDeployment$: Observable<Deployment>;
filterMyConfigs$: Observable<boolean>;
filterUndeployed$: Observable<boolean>;
filterUpgradable$: Observable<boolean>;
deploymentHistory$: Observable<FileHistory[]>;
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<Config[]>) {
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<Config[]>) {
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<Config>, deployment: CdkDropList<Config[]>) {
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<Config>, deployment: CdkDropList<Config[]>) {
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);
}
}

View File

@@ -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<EditorViewComponent>;
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);
}));
});

View File

@@ -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<Config>;
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]);
}
}

View File

@@ -1,95 +1,101 @@
<mat-card>
<mat-card-title *ngIf="!(editingTestCase$ | async)">
<h2>Test Suite</h2>
<div class="button-group-end">
<button mat-icon-button color="primary" title="Add New Test Case" (click)="onAddTestCase()">
<mat-icon>add</mat-icon>
</button>
<button mat-raised-button color="accent" title="Run Test Suite" (click)="onRunTestSuite()"
[disabled]="testCases?.length === 0">Run Test Suite</button>
</div>
</mat-card-title>
<mat-card-title *ngIf="(editingTestCase$ | async)">
<button mat-raised-button title="Test Suite" (click)="onCancelEditing()"><</button>
<h2>{{ testCase.testCase.test_case_name }} v{{ testCase.testCase.version }}</h2>
<div class="test-status-badge" *ngIf="testCase?.testCaseResult"
[ngClass]="getTestBadge(testCase.testCaseResult)"
[popper]="testCaseResult"
[popperTrigger]="'hover'"
[popperPlacement]="'left-start'"
[popperHideOnScroll]="true"
[popperDisableStyle]="true">
<popper-content #testCaseResult>
<re-test-results [testResult]="testCase.testCaseResult"></re-test-results>
</popper-content>
{{testCase.testCaseResult?.evaluationResult?.number_matched_assertions}}<mat-icon
class="test-icon">done</mat-icon>{{testCase.testCaseResult?.evaluationResult?.number_failed_assertions}}<mat-icon
class="test-icon">clear</mat-icon>
{{testCase.testCaseResult?.evaluationResult?.number_skipped_assertions}}<strong>?</strong>
</div>
</mat-card-title>
<div *ngIf="!(editingTestCase$ | async); else editTestCaseTemplate">
<div class="box" *ngFor="let testCase of testCases; index as i">
<div class="inline">
<div class="column-fixed"
[popper]="fileHistory"
[popperTrigger]="'hover'"
[popperPlacement]="'left-start'"
[popperHideOnScroll]="true"
[popperDisableStyle]="true">
<popper-content #fileHistory>
<re-change-history [history]="testCase.fileHistory"></re-change-history>
</popper-content>
<span class="chip-bag">
<span #versionChip class="chip">v{{testCase?.testCase?.version || '0' }}</span>
</span>
<h4 class="author">{{testCase?.testCase?.author}}</h4>
</div>
<mat-divider [vertical]="true"></mat-divider>
<div class="column">
<h3 class="rule-title">{{ testCase?.testCase?.test_case_name }}</h3>
<div class="subtitle">
{{ testCase?.testCase?.description }} Assertions: {{ testCase?.testCase?.assertions?.length }}
<block-ui>
<mat-card>
<mat-card-title *ngIf="(editingTestCase$ | async) === false">
<h2>Test Suite</h2>
<div class="button-group-end">
<button mat-icon-button color="primary" title="Add New Test Case" (click)="onAddTestCase()">
<mat-icon>add</mat-icon>
</button>
<button mat-raised-button color="accent" title="Run Test Suite" (click)="onRunTestSuite()"
[disabled]="testCases?.length === 0">Run Test Suite</button>
</div>
</mat-card-title>
<mat-card-title *ngIf="(editingTestCase$ | async)">
<button mat-raised-button title="Test Suite" (click)="onCancelEditing()"><</button>
<h2>{{ testCase.testCase.test_case_name }} v{{ testCase.testCase.version }}</h2>
<div class="test-status-badge" *ngIf="testCase?.testCaseResult"
[ngClass]="getTestBadge(testCase.testCaseResult)"
[popper]="testCaseResult"
[popperTrigger]="'hover'"
[popperPlacement]="'left-start'"
[popperHideOnScroll]="true"
[popperDisableStyle]="true">
<popper-content #testCaseResult>
<re-test-results [testResult]="testCase.testCaseResult"></re-test-results>
</popper-content>
{{testCase.testCaseResult?.evaluationResult?.number_matched_assertions}}<mat-icon
class="test-icon">done</mat-icon>{{testCase.testCaseResult?.evaluationResult?.number_failed_assertions}}<mat-icon
class="test-icon">clear</mat-icon>
{{testCase.testCaseResult?.evaluationResult?.number_skipped_assertions}}<strong>?</strong>
</div>
</mat-card-title>
<div *ngIf="(editingTestCase$ | async) === false; else editTestCaseTemplate">
<div class="box" *ngFor="let testCase of testCases; index as i">
<div class="inline">
<div class="column-fixed"
[popper]="fileHistory"
[popperTrigger]="'hover'"
[popperPlacement]="'left-start'"
[popperHideOnScroll]="true"
[popperDisableStyle]="true">
<popper-content #fileHistory>
<re-change-history [history]="testCase.fileHistory"></re-change-history>
</popper-content>
<span class="chip-bag">
<span #versionChip class="chip">v{{testCase?.testCase?.version || '0' }}</span>
</span>
<h4 class="author">{{testCase?.testCase?.author}}</h4>
</div>
</div>
<div class="right-block">
<div class="button-group-end">
<span class="buttons">
<a (click)="onEditTestCase(i)">
<mat-icon>edit</mat-icon>
</a>
<a (click)="onCloneTestCase(i)">
<mat-icon>content_copy</mat-icon>
</a>
</span>
<span>
<div *ngIf="testCase?.testCaseResult?.evaluationResult"
class="test-status-badge"
[ngClass]="getTestBadge(testCase.testCaseResult)"
[popper]="testCaseResult"
[popperTrigger]="'hover'"
[popperPlacement]="'left-start'"
[popperHideOnScroll]="true"
[popperDisableStyle]="true"
[popperAppendTo]="'body'"
[popperModifiers]="{preventOverflow: {boundariesElement: 'viewport'}}"
>
<popper-content #testCaseResult>
<re-test-results [testResult]="testCase.testCaseResult"></re-test-results>
</popper-content>
{{testCase?.testCaseResult?.evaluationResult?.number_matched_assertions}}<mat-icon
class="test-icon">done</mat-icon>
{{testCase?.testCaseResult?.evaluationResult?.number_failed_assertions}}<mat-icon
class="test-icon">clear</mat-icon>
{{testCase?.testCaseResult?.evaluationResult?.number_skipped_assertions}}<strong>?</strong>
</div>
</span>
<mat-divider [vertical]="true"></mat-divider>
<div class="column">
<h3 class="rule-title">{{ testCase?.testCase?.test_case_name }}</h3>
<div class="subtitle">
{{ testCase?.testCase?.description }} Assertions: {{ testCase?.testCase?.assertions?.length }}
</div>
</div>
<div class="right-block">
<div class="button-group-end">
<span class="buttons">
<a (click)="onEditTestCase(i)">
<mat-icon>edit</mat-icon>
</a>
<a (click)="onCloneTestCase(i)">
<mat-icon>content_copy</mat-icon>
</a>
<a (click)="onDeleteTestCase(i)">
<mat-icon>delete</mat-icon>
</a>
</span>
<span>
<div *ngIf="testCase?.testCaseResult?.evaluationResult"
class="test-status-badge"
[ngClass]="getTestBadge(testCase.testCaseResult)"
[popper]="testCaseResult"
[popperTrigger]="'hover'"
[popperPlacement]="'left-start'"
[popperHideOnScroll]="true"
[popperDisableStyle]="true"
[popperAppendTo]="'body'"
[popperModifiers]="{preventOverflow: {boundariesElement: 'viewport'}}"
>
<popper-content #testCaseResult>
<re-test-results [testResult]="testCase.testCaseResult"></re-test-results>
</popper-content>
{{testCase?.testCaseResult?.evaluationResult?.number_matched_assertions}}<mat-icon
class="test-icon">done</mat-icon>
{{testCase?.testCaseResult?.evaluationResult?.number_failed_assertions}}<mat-icon
class="test-icon">clear</mat-icon>
{{testCase?.testCaseResult?.evaluationResult?.number_skipped_assertions}}<strong>?</strong>
</div>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #editTestCaseTemplate>
<re-test-case-editor></re-test-case-editor>
</ng-template>
</mat-card>
<ng-template #editTestCaseTemplate>
<re-test-case-editor></re-test-case-editor>
</ng-template>
</mat-card>
</block-ui>

View File

@@ -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<TestCaseWrapper[]>;
editingTestCase$: Observable<boolean>;
editedTestCase$: Observable<TestCaseWrapper>;
public testCases$: Observable<TestCaseWrapper[]>;
public editingTestCase$: Observable<boolean>;
public editedTestCase$: Observable<TestCaseWrapper>;
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);
}
}

View File

@@ -40,7 +40,10 @@
<a (click)="cloneConfig()" [title]="'Clone Config'">
<mat-icon>content_copy</mat-icon>
</a>
<a *ngIf="hideAddDeployment" (click)="addToDeployment()" [title]="'Add to Deployment'">
<a *ngIf="notDeployed" (click)="deleteConfigFromStore()" [title]="'Delete Config From Store'">
<mat-icon>delete</mat-icon>
</a>
<a *ngIf="notDeployed" (click)="addConfigToDeployment()" [title]="'Add to Deployment'">
<mat-icon>arrow_forward</mat-icon>
</a>
</span>

View File

@@ -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<number>();
@Output() readonly view = new EventEmitter<number>();
@Output() readonly addToDeployment = new EventEmitter<number>();
@Output() readonly clone = new EventEmitter<number>();
@Output() readonly deleteFromStore = new EventEmitter<number>();
@Output() onEdit = new EventEmitter<number>();
@Output() onView = new EventEmitter<number>();
@Output() onAddToDeployment = new EventEmitter<number>();
@Output() onClone = new EventEmitter<number>();
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();
}
}

View File

@@ -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;
}
callbackPath: string;
expiresIntervalMinimum: number;
oidcSettings: UserSettings;
}

View File

@@ -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<any>;
submit: () => Observable<boolean>;
name: string;
type: string;
validate: () => Observable<any>;
submit: () => Observable<boolean>;
}
export interface GitFiles<T> {
files: T[];
files: T[];
}
export interface GitFilesDelete<T> {
configs_files: T[];
test_cases_files?: T[];
}
export interface AdminConfigGitFiles<T> extends GitFiles<T> {
config_version: number;
config_version: number;
}
export interface DeploymentGitFiles<T> extends GitFiles<T> {
rules_version: number;
rules_version: number;
}
export interface TestCaseEvaluation {
files: Content<TestCase>[];
test_result_raw_output: string;
files: Content<TestCase>[];
test_result_raw_output: string;
}
export interface GeneralRule {
file_name?: string;
file_name?: string;
}
export interface Content<T> 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<ConfigData>[],
test_specification: string,
files: Content<ConfigData>[];
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;
}

View File

@@ -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';

View File

@@ -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);

View File

@@ -26,13 +26,13 @@ export class AppConfigService {
this._authenticationService = new DefaultAuthenticationService();
}
public loadConfigAndMetadata(): Promise<any> {
loadConfigAndMetadata(): Promise<any> {
return this.loadConfig()
.then(() => this.loadUiMetadata())
.then(() => this.createAuthenticationService());
}
public loadBuildInfo(): Promise<any> {
loadBuildInfo(): Promise<any> {
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<any> {
return this.http
.get('config/ui-config.json')

View File

@@ -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', () => {

View File

@@ -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<Config[]> {
getConfigs(): Observable<Config[]> {
return this.http
.get<GitFiles<any>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/configs`)
.map(result => {
@@ -74,13 +76,13 @@ export class ConfigLoaderService {
});
}
public getTestSpecificationSchema(): Observable<JSONSchema7> {
getTestSpecificationSchema(): Observable<JSONSchema7> {
return this.http
.get<TestSchemaInfo>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/testschema`)
.pipe(map(x => x.test_schema));
}
public getSchema(): Observable<JSONSchema7> {
getSchema(): Observable<JSONSchema7> {
return this.http.get<SchemaInfo>(`${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<JSONSchema7> {
getAdminSchema(): Observable<JSONSchema7> {
return this.http
.get<AdminSchemaInfo>(`${this.config.serviceRoot}api/v1/${this.serviceName}/adminconfig/schema`)
.map(x => {
@@ -102,19 +104,19 @@ export class ConfigLoaderService {
});
}
public getPullRequestStatus(): Observable<PullRequestInfo> {
getPullRequestStatus(): Observable<PullRequestInfo> {
return this.http.get<PullRequestInfo>(
`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release/status`
);
}
public getAdminPullRequestStatus(): Observable<PullRequestInfo> {
getAdminPullRequestStatus(): Observable<PullRequestInfo> {
return this.http.get<PullRequestInfo>(
`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/adminconfig/status`
);
}
public getRelease(): Observable<DeploymentWrapper> {
getRelease(): Observable<DeploymentWrapper> {
return this.http
.get<DeploymentGitFiles<any>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release`)
.pipe(
@@ -160,7 +162,7 @@ export class ConfigLoaderService {
);
}
public getAdminConfig(): Observable<AdminConfig> {
getAdminConfig(): Observable<AdminConfig> {
return this.http
.get<AdminConfigGitFiles<any>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/adminconfig`)
.pipe(
@@ -185,13 +187,13 @@ export class ConfigLoaderService {
);
}
public getTestCases(): Observable<TestCaseMap> {
getTestCases(): Observable<TestCaseMap> {
return this.http
.get<GitFiles<Content<any>>>(`${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<any> {
validateConfig(config: Config): Observable<any> {
const json = JSON.stringify(config.configData, null, 2);
return this.http.post<any>(
@@ -200,27 +202,27 @@ export class ConfigLoaderService {
);
}
public validateRelease(deployment: Deployment): Observable<any> {
validateRelease(deployment: Deployment): Observable<any> {
const validationFormat = this.marshalDeploymentFormat(deployment);
const json = JSON.stringify(validationFormat, null, 2);
return this.http.post<any>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/validate`, json);
}
public validateAdminConfig(config: AdminConfig): Observable<any> {
validateAdminConfig(config: AdminConfig): Observable<any> {
const json = JSON.stringify(config.configData, null, 2);
return this.http.post<any>(`${this.config.serviceRoot}api/v1/${this.serviceName}/adminconfig/validate`, json);
}
public submitRelease(deployment: Deployment): Observable<any> {
submitRelease(deployment: Deployment): Observable<any> {
const releaseFormat = this.marshalDeploymentFormat(deployment);
const json = JSON.stringify(releaseFormat, null, 2);
return this.http.post<any>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release`, json);
}
public submitConfig(config: Config): Observable<Config[]> {
submitConfig(config: Config): Observable<Config[]> {
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<GitFiles<any>> {
submitConfigEdit(config: Config): Observable<GitFiles<any>> {
const json = JSON.stringify(config.configData, null, 2);
return this.http.put<GitFiles<any>>(
@@ -240,7 +242,7 @@ export class ConfigLoaderService {
);
}
public submitNewConfig(config: Config): Observable<GitFiles<any>> {
submitNewConfig(config: Config): Observable<GitFiles<any>> {
const json = JSON.stringify(config.configData, null, 2);
return this.http.post<GitFiles<any>>(
@@ -249,7 +251,7 @@ export class ConfigLoaderService {
);
}
public submitAdminConfig(config: AdminConfig): Observable<GitFiles<any>> {
submitAdminConfig(config: AdminConfig): Observable<GitFiles<any>> {
const json = JSON.stringify(config.configData, null, 2);
return this.http.post<GitFiles<any>>(
@@ -258,7 +260,7 @@ export class ConfigLoaderService {
);
}
public testDeploymentConfig(deployment: Deployment, testSpecification: any): Observable<ConfigTestResult> {
testDeploymentConfig(deployment: Deployment, testSpecification: any): Observable<ConfigTestResult> {
const testDto: ConfigTestDto = {
files: [
{
@@ -276,7 +278,7 @@ export class ConfigLoaderService {
);
}
public testSingleConfig(configData: any, testSpecification: any): Observable<ConfigTestResult> {
testSingleConfig(configData: any, testSpecification: any): Observable<ConfigTestResult> {
const testDto: ConfigTestDto = {
files: [
{
@@ -292,27 +294,27 @@ export class ConfigLoaderService {
);
}
public submitTestCase(testCase: TestCaseWrapper): Observable<TestCaseMap> {
submitTestCase(testCase: TestCaseWrapper): Observable<TestCaseMap> {
return isNewTestCase(testCase) ? this.submitNewTestCase(testCase) : this.submitTestCaseEdit(testCase);
}
public submitTestCaseEdit(testCase: TestCaseWrapper): Observable<TestCaseMap> {
submitTestCaseEdit(testCase: TestCaseWrapper): Observable<TestCaseMap> {
const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2);
return this.http
.put<GitFiles<TestCase>>(`${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<TestCaseMap> {
submitNewTestCase(testCase: TestCaseWrapper): Observable<TestCaseMap> {
const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2);
return this.http
.post<GitFiles<TestCase>>(`${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<any> {
validateTestCase(testcase: TestCase): Observable<any> {
const outObj = {
files: [
{
@@ -325,7 +327,7 @@ export class ConfigLoaderService {
return this.http.post<any>(`${this.config.serviceRoot}api/v1/testcases/validate`, json);
}
public evaluateTestCase(configData: any, testCaseWrapper: TestCaseWrapper): Observable<TestCaseResult> {
evaluateTestCase(configData: any, testCaseWrapper: TestCaseWrapper): Observable<TestCaseResult> {
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<TestCaseEvaluationResult> {
evaluateTestCaseFromResult(testcase: TestCase, testResult: any): Observable<TestCaseEvaluationResult> {
const outObj: TestCaseEvaluation = {
files: [
{
@@ -362,10 +364,40 @@ export class ConfigLoaderService {
.pipe(map(x => x.test_case_result));
}
private testCaseFilesToMap(result: GitFiles<any>): TestCaseMap {
deleteConfig(configName: string): Observable<ConfigAndTestCases> {
return this.http
.post<GitFilesDelete<any>>(
`${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<TestCaseMap> {
return this.http
.post<GitFiles<Content<any>>>(
`${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] = [];
}

View File

@@ -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<any> {
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<any> {
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)

View File

@@ -3,6 +3,8 @@
"serviceRoot": "https://config-editor/",
"uiBootstrapPath": "./ui-bootstrap.json",
"authType": "disabled",
"historyMaxSize": 5,
"blockingTimeout": "30000",
"homeLinks": [
{
"icon": "library_books",

View File

@@ -11,7 +11,8 @@ On the home page all services are listed alphabetically by name on the left side
</p>
### 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.