From 24cf4bd7bb4d7fb2da66de52b0fcb1a33cb4ff95 Mon Sep 17 00:00:00 2001 From: timkeiner Date: Mon, 15 May 2017 23:29:34 +0200 Subject: [PATCH] #44 closes 44 --- .../src/main/resources/static/package.json | 27 +- .../detail/sources/source.code.component.ts | 3 +- .../app/components/test/test-group-run.html | 55 +++- .../test/test.group.run.component.ts | 289 +++++++++++++----- .../src/app/components/test/test.module.ts | 11 +- .../src/app/components/test/test.state.ts | 62 +++- .../static/src/environments/environment.ts | 4 +- .../src/main/resources/static/tsconfig.json | 1 - 8 files changed, 342 insertions(+), 110 deletions(-) diff --git a/citrus-admin-client/src/main/resources/static/package.json b/citrus-admin-client/src/main/resources/static/package.json index cdab4ea..8b7e6da 100644 --- a/citrus-admin-client/src/main/resources/static/package.json +++ b/citrus-admin-client/src/main/resources/static/package.json @@ -1,17 +1,15 @@ { "private": true, "dependencies": { - "@angular/common": "4.0.0", - "@angular/compiler": "4.0.0", - "@angular/core": "4.0.0", - "@angular/forms": "4.0.0", - "@angular/http": "4.0.0", - "@angular/platform-browser": "4.0.0", - "@angular/platform-browser-dynamic": "4.0.0", - "@angular/router": "4.0.0", - "core-js": "2.4.1", - "rxjs": "5.1.0", - "zone.js": "^0.8.4", + "@angular/animations": "4.1.2", + "@angular/common": "4.1.2", + "@angular/compiler": "4.1.2", + "@angular/core": "4.1.2", + "@angular/forms": "4.1.2", + "@angular/http": "4.1.2", + "@angular/platform-browser": "4.1.2", + "@angular/platform-browser-dynamic": "4.1.2", + "@angular/router": "4.1.2", "@ngrx/core": "1.2.0", "@ngrx/effects": "2.0.3", "@ngrx/router-store": "^1.2.5", @@ -21,21 +19,24 @@ "angular-in-memory-web-api": "0.1.5", "bootstrap": "3.3.7", "brace": "0.9.1", + "core-js": "2.4.1", "font-awesome": "4.5.0", "jquery": "2.2.4", "jquery-ui-bundle": "1.11.4", "lodash": "4.17.4", "moment": "2.17.1", "reflect-metadata": "0.1.8", + "rxjs": "5.1.0", "sockjs-client": "1.1.2", "stompjs": "2.3.3", "systemjs": "0.19.39", "underscore": "1.8.3", - "url": "0.11.0" + "url": "0.11.0", + "zone.js": "^0.8.4" }, "devDependencies": { "@angular/cli": "1.0.2", - "@angular/compiler-cli": "4.0.0", + "@angular/compiler-cli": "4.1.2", "@types/ace": "0.0.32", "@types/jasmine": "2.5.38", "@types/jquery": "2.0.40", diff --git a/citrus-admin-client/src/main/resources/static/src/app/components/test/detail/sources/source.code.component.ts b/citrus-admin-client/src/main/resources/static/src/app/components/test/detail/sources/source.code.component.ts index b512e2d..747bc82 100644 --- a/citrus-admin-client/src/main/resources/static/src/app/components/test/detail/sources/source.code.component.ts +++ b/citrus-admin-client/src/main/resources/static/src/app/components/test/detail/sources/source.code.component.ts @@ -7,7 +7,8 @@ import * as ace from 'brace'; import 'brace/theme/chrome'; import 'brace/mode/java'; import 'brace/mode/xml'; -import Editor = AceAjax.Editor; +import Editor = ace.Editor; + @Component({ selector: "source-code", diff --git a/citrus-admin-client/src/main/resources/static/src/app/components/test/test-group-run.html b/citrus-admin-client/src/main/resources/static/src/app/components/test/test-group-run.html index dd1158e..47733c2 100644 --- a/citrus-admin-client/src/main/resources/static/src/app/components/test/test-group-run.html +++ b/citrus-admin-client/src/main/resources/static/src/app/components/test/test-group-run.html @@ -16,7 +16,7 @@

- + @@ -40,29 +40,57 @@

- - + + + + + All tests - + - -   + - - {{result.test.type | lowercase}} + + + + + + + + + {{package.name}} + {{package.tests.length}} + + + + + + + {{result.test.type | lowercase}} + {{result.test.name}} {{result.test.className}} -   - - + @@ -74,7 +102,6 @@

-
dialog-size="lg">

Log output:

-

+  

 
-
+ \ No newline at end of file diff --git a/citrus-admin-client/src/main/resources/static/src/app/components/test/test.group.run.component.ts b/citrus-admin-client/src/main/resources/static/src/app/components/test/test.group.run.component.ts index 473e896..86c1f31 100644 --- a/citrus-admin-client/src/main/resources/static/src/app/components/test/test.group.run.component.ts +++ b/citrus-admin-client/src/main/resources/static/src/app/components/test/test.group.run.component.ts @@ -1,5 +1,9 @@ -import {Component, OnInit} from '@angular/core'; -import {TestGroup, TestResult, Test} from "../../model/tests"; +import { + Component, EventEmitter, Input, OnDestroy, OnInit, Output +} from '@angular/core'; +import { style, transition, + trigger, animate } from '@angular/animations'; +import {TestGroup, TestResult, Test, TestDetail} from "../../model/tests"; import {Alert} from "../../model/alert"; import {AlertService} from "../../service/alert.service"; import {Observable} from "rxjs"; @@ -7,7 +11,7 @@ import {TestService} from "../../service/test.service"; import * as Stomp from 'stompjs'; import * as SockJS from 'sockjs-client'; import {Frame} from "stompjs"; -import {TestStateService} from "./test.state"; +import {TestExecutionInfo, TestStateActions, TestStateService} from "./test.state"; import {SocketEvent} from "../../model/socket.event"; import {Router} from "@angular/router"; import {AppState} from "../../state.module"; @@ -15,121 +19,264 @@ import {Store} from "@ngrx/store"; import {go} from "@ngrx/router-store"; import {parseBody, StompConnectionService} from "../../service/stomp-connection.service"; import {LoggingService} from "../../service/logging.service"; +import {Subscription} from "rxjs/Subscription"; +import {IdMap, log, notNull, toArray} from "../../util/redux.util"; +import {BehaviorSubject} from "rxjs/BehaviorSubject"; +import {Tupel} from "../../util/type.util"; +import * as _ from 'lodash'; + +const AllTests = new TestDetail('all-tests'); @Component({ templateUrl: 'test-group-run.html' }) -export class TestGroupRunComponent implements OnInit { +export class TestGroupRunComponent implements OnInit, OnDestroy { constructor(private testService: TestService, private testState: TestStateService, private alertService: AlertService, private loggingService: LoggingService, + private testActions: TestStateActions, private store: Store) { } + AllTests = AllTests + + private subscription = new Subscription(); + packages: Observable; + packagesOpen = new BehaviorSubject(true); + testsOpen: BehaviorSubject> = new BehaviorSubject({}); + running = false; - stompClient: any; - results: TestResult[] = []; + results: Observable; + + selected: BehaviorSubject = new BehaviorSubject(null); + + togglePackage() { + this.packagesOpen.next(!this.packagesOpen.getValue()); + } + + toggleTests(_package: TestGroup) { + const open = this.testsOpen.getValue(); + this.testsOpen.next({ + ...open, + [_package.name]: open[_package.name] ? false : true + }) + } + + private resultFromTest(test: Test) { + const result = new TestResult(); + result.test = test; + return result; + } + + get currentOutput() { + return this.selected.switchMap(s => this.testState.getExecutionInfo(s ? new TestDetail(s.name) : AllTests)).map(ei => ei.processOutput) + } + + get packageList() { + return this.packagesOpen.switchMap(open => open ? this.packages : Observable.of([])); + } - processOutput = ""; - currentOutput = ""; + isOpen(_package: TestGroup) { + return this.testsOpen.map(open => open[_package.name]) + } - selected: TestGroup; + resultListFor(_package: TestGroup) { + return this.packages + .map(ps => ps.find(p => p === _package)) + .filter(notNull()) + .withLatestFrom( + this.testState.results, + (p, r) => p.tests.map(t => r[t.name] || this.resultFromTest(t)) + ) + .withLatestFrom( + this.testsOpen, + (res, open) => open[_package.name] ? res : [] + ) + } + + getExecutionInfoForPackage(_package: TestGroup) { + return this.testState.getExecutionInfo(new Test(_package.name)); + } + + getExecutionInfoForTest(test: Test) { + return this.testState.getExecutionInfo(test); + } ngOnInit() { + this.results = + Observable.combineLatest( + this.selected.map(tg => (tg) ? tg.tests : [AllTests]), + this.testState.results + ) + .map(([tests, results]: Tupel>) => tests.map(t => results[t.name] || this.resultFromTest(t))) this.packages = this.testState.packages; + [ + this.loggingService.logOutput + .combineLatest(this.selected) + .subscribe(([event, selected]) => { + this.testActions.handleSocketEvent(selected ? new TestDetail(selected.name) : AllTests, event); + }), + this.loggingService.results + .subscribe((result: TestResult) => { + this.testActions.resultSuccess(result); + }) + ].forEach(s => this.subscription.add(s)); + } - this.loggingService.logOutput - .subscribe((event: SocketEvent) => { - this.processOutput += event.msg; - this.currentOutput = event.msg; - this.handle(event); - }); - this.loggingService.results - .subscribe((result: TestResult) => { - this.handleResult(result); - }); + ngOnDestroy() { + this.subscription.unsubscribe(); } execute() { - this.results.forEach(r => r.success = undefined); - if (this.selected) { - this.testService.executeGroup(this.selected) - .subscribe( - result => { - this.processOutput = ""; - this.currentOutput = ""; - this.running = true; - }, - error => this.notifyError(error)); - } else { - this.testService.executeAll() - .subscribe( - result => { - this.processOutput = ""; - this.currentOutput = ""; - this.running = true; - }, - error => this.notifyError(error)); - } + this.selected.take(1) + .subscribe(selected => { + if (selected) { + this.testActions.resetResults(new TestDetail(selected.name)); + this.testActions.executeTestGroup(selected) + } else { + this.testActions.resetResults(AllTests); + this.testActions.executeAll(); + } + }) } - select(group: TestGroup) { - this.selected = group; + getPackageSuccess(_package: TestGroup) { + return this.testState.results + .map(rMap => _ + .filter(rMap, r => _package.name === 'all-tests' || r.test.packageName === _package.name) + .reduce((success, r) => success && r.success, true) + ) + } - if (this.selected) { - this.results = this.selected.tests.map(t => { - let result = new TestResult(); - result.test = t; - return result; - }); - } else { - this.results = []; - } + resetAll() { + return this.packages.take(1).do(pcks => { + [AllTests, ...pcks].forEach(pck => this.testActions.resetResults(new TestDetail(pck.name))) + }); + } + + executeAll() { + this.resetAll().subscribe(() => this.testActions.executeAll()) + } + + executeGroup(group: TestGroup) { + this.resetAll().subscribe(() => this.testActions.executeTestGroup(group)) + } + + select(group ?: TestGroup) { + this.selected.next(group); } open(test: Test) { - this.store.dispatch(go(['/tests', 'detail', test.name])); + this.store.dispatch(go(['/tests', 'detail', test.name, 'run'])); } - openConsole() { + openConsole(_package?: TestGroup) { + this.selected.next(_package); (jQuery('#dialog-console') as any).modal(); } + notifyError(error: any) { + this.alertService.add(new Alert("danger", error, false)); + } +} - handleResult(result: TestResult) { - let found: TestResult = this.results.find(r => r.test.name == result.test.name); +@Component({ + selector: 'execution-status-package', + animations: [ + trigger('fade', [ + transition(':enter', [ + style({opacity: 0}), + animate('350ms ease-in', style({opacity: 1})) + ]), + transition(':leave', [ + style({opacity: 1}), + animate('350ms ease-in', style({opacity: 0})) + ]) + ] + ) + ], + styles: [` + .badge { + display: flex; + } - if (found) { - found.success = result.success; - found.processId = result.processId; - } else { - this.results.unshift(result); + .badge .badge--message { + align-self: center; + margin-right: 3px; } - } - handle(event: SocketEvent) { - if ("PROCESS_FAILED" == event.type || "PROCESS_SUCCESS" == event.type) { - this.running = false; - this.currentOutput = this.processOutput; - jQuery('pre.logger').scrollTop(jQuery('pre.logger')[0].scrollHeight); + .clickable { + cursor: pointer; } - if ("PROCESS_FAILED" == event.type) { - this.alertService.add(new Alert("warning", "Test run failed '" + event.processId + "': " + event.msg, false)); + .info-container { + transition: background .35s ease-in; } + `], + template: ` + + + + {{message}} + + + + + + ` +}) +export class ExecutionStatusPackageComponent { + @Input() executionInfo: TestExecutionInfo; + @Input() success: boolean; + @Output() logs = new EventEmitter(); + + onLogs() { + this.logs.next() + } - if ("PROCESS_SUCCESS" == event.type) { - this.alertService.add(new Alert("success", "Test run success '" + event.processId + "'", true)); + get message() { + if (this.executionInfo.running) { + return '' + } else { + return this.success ? 'SUCCESS' : 'FAILED' } } - notifyError(error: any) { - this.alertService.add(new Alert("danger", error, false)); + get cssClass() { + if (this.executionInfo.running) { + return '' + } else { + return `badge ${this.success ? 'badge-success' : 'badge-danger'}` + } } +} + +@Component({ + selector: 'execution-status-test', + template: ` + + + ` +}) +export class ExecutionStatusTestComponent { + @Input() result: TestResult } \ No newline at end of file diff --git a/citrus-admin-client/src/main/resources/static/src/app/components/test/test.module.ts b/citrus-admin-client/src/main/resources/static/src/app/components/test/test.module.ts index 7240480..e8155d5 100644 --- a/citrus-admin-client/src/main/resources/static/src/app/components/test/test.module.ts +++ b/citrus-admin-client/src/main/resources/static/src/app/components/test/test.module.ts @@ -7,7 +7,10 @@ import {TestListComponent} from "./test.list.component"; import {SourceCodeComponent} from "./detail/sources/source.code.component"; import {TestDetailComponent} from "./detail/test.detail.component"; import {TestRunComponent, TestRunOutlet} from "./detail/run/test.run.component"; -import {TestGroupRunComponent} from "./test.group.run.component"; +import { + ExecutionStatusPackageComponent, ExecutionStatusTestComponent, + TestGroupRunComponent +} from "./test.group.run.component"; import {TestListGroupComponent} from "./test.listgroup.component"; import {TestMessageComponent} from "./detail/run/test.message.component"; import {TestProgressComponent} from "./detail/run/test.progress.component"; @@ -25,6 +28,7 @@ import {TestStateActions, TestStateEffects, TestStateService} from "./test.state import {EffectsModule} from "@ngrx/effects"; import {SourcesOutletComponent} from "./detail/sources/sources-outlet.component"; import {TestResultComponent, TestResultOutletComponent} from "./detail/results/test.result.component"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; const components = [ TestsComponent, @@ -47,7 +51,9 @@ const components = [ InfoOutletComponent, TestRunOutlet, TestResultComponent, - TestResultOutletComponent + TestResultOutletComponent, + ExecutionStatusPackageComponent, + ExecutionStatusTestComponent ]; @NgModule({ @@ -58,6 +64,7 @@ const components = [ CommonModule, ServiceModule, TestRoutingModule, + BrowserAnimationsModule, EffectsModule.run(TestStateEffects) ], providers: [ diff --git a/citrus-admin-client/src/main/resources/static/src/app/components/test/test.state.ts b/citrus-admin-client/src/main/resources/static/src/app/components/test/test.state.ts index 5242216..26233f4 100644 --- a/citrus-admin-client/src/main/resources/static/src/app/components/test/test.state.ts +++ b/citrus-admin-client/src/main/resources/static/src/app/components/test/test.state.ts @@ -16,6 +16,7 @@ import {Message} from "../../model/message"; import * as moment from 'moment'; import {Tupel} from "../../util/type.util"; import {ReportService} from "../../service/report.service"; +import {TestReport} from "../../model/test.report"; export type TestMap = IdMap; export type TestGroupMap = IdMap; @@ -29,6 +30,8 @@ export class TestExecutionInfo { completed = 0; finishedActions = 0; messages: Message[] = []; + lastUpdate:number; + touched = false; } export interface TestState { @@ -144,6 +147,7 @@ export class TestStateService { this.executionInfos) .switchMap(([t]) => this.getExecutionInfo(t)) } + } @Injectable() @@ -155,6 +159,11 @@ export class TestStateEffects { private actions$: Actions) { } + socketEventFinally = this.actions$ + .ofType(TestStateActions.SOCKET_EVENT) + .filter((a: GenericAction>) => a.payload[1].type === SocketEvent.Types.PROCESS_SUCCESS || a.payload[1].type === SocketEvent.Types.PROCESS_FAILED) + .map((a: GenericAction>) => a.payload[0]) + @Effect() package = this.actions .handleEffect(TestStateActions.PACKAGES, () => this.testService.getTestPackages()); @@ -164,14 +173,36 @@ export class TestStateEffects { @Effect() execute = this.actions .handleEffect(TestStateActions.EXECUTE, ({payload}) => this.testService.execute(payload)); + @Effect() executeGroup = this.actions + .handleEffect(TestStateActions.EXECUTE_GROUP, ({payload}) => this.testService.executeGroup(payload)); + + @Effect() executeAll = this.actions + .handleEffect(TestStateActions.EXECUTE_ALL, () => this.testService.executeAll()); + + @Effect() latestResultsAfterExecution = this.actions$ + .ofType( + TestStateActions.EXECUTE_ALL.SUCCESS, TestStateActions.EXECUTE_GROUP.SUCCESS + ) + .switchMap(r => this.socketEventFinally) + .map(() => ({type:TestStateActions.REPORT_LATEST.FETCH})); + + @Effect() report = this.actions .handleEffect(TestStateActions.REPORT, ({payload}) => this.reportService.getTestResult(payload)); - @Effect() resultsAfterExecute = this.actions$ - .ofType(TestStateActions.SOCKET_EVENT) - .filter((a: GenericAction>) => a.payload[1].type === SocketEvent.Types.PROCESS_SUCCESS || a.payload[1].type === SocketEvent.Types.PROCESS_FAILED) - .map((a: GenericAction>) => a.payload[0]) - .map((payload) => ({type: TestStateActions.REPORT.FETCH, payload})); + @Effect() reportLatest = this.actions + .handleEffect(TestStateActions.REPORT_LATEST, () => this.reportService.getLatest()); + + @Effect() ditributeLatestResults = this.actions$.ofType(TestStateActions.REPORT_LATEST.SUCCESS) + .switchMap(({payload:r}:GenericAction) => { + return Observable.from(r.results.map(payload => ({type:TestStateActions.REPORT.SUCCESS, payload}))) + }) + + + @Effect() resultsAfterExecute = this.socketEventFinally + .withLatestFrom(this.testState.testNames) + .filter(([td, names]) => _.includes(names, td.name)) + .map(([payload]) => ({type: TestStateActions.REPORT.FETCH, payload})); @Effect() routingToLatestView = this.actions$.ofType(routerActions.GO, routerActions.UPDATE_LOCATION) .map(({payload: {path}}) => path) @@ -206,6 +237,9 @@ export class TestStateActions { static SOCKET_EVENT = 'TEST.SOCKET_EVENT'; static TEST_MESSAGE = 'TEST.TEST_MESSAGE'; static REPORT = AsyncActionType('TEST.REPORT'); + static EXECUTE_GROUP = AsyncActionType('TEST.EXECUTE_GROUP'); + static EXECUTE_ALL = AsyncActionType('TEST.EXECUTE_ALL'); + static REPORT_LATEST = AsyncActionType('Test.REPORT_LATEST'); constructor(private store: Store) { } @@ -234,6 +268,14 @@ export class TestStateActions { this.store.dispatch({type: TestStateActions.EXECUTE.FETCH, payload}); } + executeTestGroup(payload: TestGroup) { + this.store.dispatch({type: TestStateActions.EXECUTE_GROUP.FETCH, payload}); + } + + executeAll() { + this.store.dispatch({type: TestStateActions.EXECUTE_ALL.FETCH}); + } + resetResults(payload: TestDetail) { this.store.dispatch({type: TestStateActions.RESET_EXECUTION, payload}) } @@ -250,6 +292,10 @@ export class TestStateActions { fetchResults(payload: TestDetail) { this.store.dispatch({type: TestStateActions.REPORT.FETCH, payload}) } + + resultSuccess(payload: TestResult) { + this.store.dispatch(({type: TestStateActions.REPORT.SUCCESS, payload})) + } } export function reduce(state: TestState = TestStateInit, action: Action) { @@ -317,7 +363,7 @@ export function reduce(state: TestState = TestStateInit, action: Action) { } case TestStateActions.SOCKET_EVENT: { const {payload: [detail, event]} = action as GenericAction>; - const info = {...state.executionInfo[event.processId]}; + const info = addTimestamp(state.executionInfo[event.processId], event); return ({ ...state, executionInfo: { @@ -346,6 +392,10 @@ export function reduce(state: TestState = TestStateInit, action: Action) { return state; } +export function addTimestamp(info:TestExecutionInfo, event:SocketEvent):TestExecutionInfo { + return ({...info, lastUpdate:event.timestamp, touched:true}); +} + export function reduceSocketEvent(info: TestExecutionInfo, detail: TestDetail, event: SocketEvent): TestExecutionInfo { const {Types} = SocketEvent; switch (event.type) { diff --git a/citrus-admin-client/src/main/resources/static/src/environments/environment.ts b/citrus-admin-client/src/main/resources/static/src/environments/environment.ts index bbc63b6..da82658 100644 --- a/citrus-admin-client/src/main/resources/static/src/environments/environment.ts +++ b/citrus-admin-client/src/main/resources/static/src/environments/environment.ts @@ -2,7 +2,7 @@ const LOCAL_STORAGE_ENV_KEY = '$CITRUS_ADMIN'; let userEnv = {}; if(localStorage) { - const localStorageContent = localStorage.getItem(LOCAL_STORAGE_ENV_KEY); + const localStorageContent = localStorage.getItem(LOCAL_STORAGE_ENV_KEY) + ''; try { userEnv = JSON.parse(localStorageContent) || {}; } catch(e) { @@ -21,7 +21,7 @@ export const environment:Environment = { production: false, traceRouting: false, reduxTools: true, - stompDebug:false, + stompDebug:true, ...userEnv }; diff --git a/citrus-admin-client/src/main/resources/static/tsconfig.json b/citrus-admin-client/src/main/resources/static/tsconfig.json index 9e777ea..a35a8ee 100644 --- a/citrus-admin-client/src/main/resources/static/tsconfig.json +++ b/citrus-admin-client/src/main/resources/static/tsconfig.json @@ -9,7 +9,6 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es5", - "strict":true, "typeRoots": [ "node_modules/@types" ],