From 725544e6fbfc40f2a90d5962f16491321d0cd2f6 Mon Sep 17 00:00:00 2001 From: Ludek Novy <13610612+ludeknovy@users.noreply.github.com> Date: Mon, 27 Dec 2021 11:11:09 +0100 Subject: [PATCH] V4 (#185) --- .github/workflows/docker-hub-v4.yml | 34 +++++ package.json | 2 +- src/app/_interceptors/error-interceptor.ts | 14 ++- src/app/_services/authentication.service.ts | 5 +- src/app/_services/init.service.spec.ts | 15 +++ src/app/_services/init.service.ts | 15 +++ .../projects/administration.component.html | 15 ++- .../delete-project.component.html | 2 +- .../delete-project.component.ts | 2 +- .../edit-project/edit-project.component.css | 3 - .../edit-project/edit-project.component.html | 34 ----- .../edit-project/edit-project.component.ts | 67 ---------- src/app/app.module.ts | 15 ++- src/app/graphs/monitoring.ts | 11 +- src/app/graphs/status-codes.ts | 43 +++++++ src/app/init-user/init-user.component.css | 3 + src/app/init-user/init-user.component.html | 44 +++++++ src/app/init-user/init-user.component.spec.ts | 29 +++++ src/app/init-user/init-user.component.ts | 52 ++++++++ .../attachements/attachements.component.css | 7 -- .../attachements/attachements.component.html | 17 --- .../attachements.component.spec.ts | 27 ---- .../attachements/attachements.component.ts | 43 ------- .../delete-item/delete-item.component.ts | 3 + .../item-detail/item-detail.component.html | 66 +++++----- .../item-detail/item-detail.component.scss | 9 ++ .../item-detail/item-detail.component.spec.ts | 3 - src/app/item-detail/item-detail.component.ts | 99 +++++++++------ .../monitoring-stats.component.ts | 18 ++- .../performance-analysis.component.ts | 1 - .../label-health/label-health.component.css | 12 ++ .../label-health/label-health.component.html | 62 +++++++++ .../label-health.component.spec.ts | 27 ++++ .../label-health/label-health.component.ts | 61 +++++++++ .../request-stats-compare.component.css | 0 .../request-stats-compare.component.html | 100 +++++++++++---- .../request-stats-compare.component.spec.ts | 65 +++++++++- .../request-stats-compare.component.ts | 66 +++++++++- ...zero-error-tolerance-warning.component.css | 3 + ...ero-error-tolerance-warning.component.html | 13 ++ ...-error-tolerance-warning.component.spec.ts | 25 ++++ .../zero-error-tolerance-warning.component.ts | 15 +++ src/app/items.service.model.ts | 66 +++++++--- src/app/login/login.component.html | 2 +- src/app/login/login.component.ts | 13 +- src/app/notification/notification-messages.ts | 6 +- src/app/project-api.service.ts | 10 +- .../graph/scenarios-graph.component.ts | 7 -- .../project-settings.component.css | 0 .../project-settings.component.html | 89 +++++++++++++ .../project-settings.component.spec.ts | 28 +++++ .../project-settings.component.ts | 119 ++++++++++++++++++ src/app/project/project.component.html | 8 ++ src/app/scenario.service.model.ts | 1 + .../add-new-item/add-new-item.component.html | 9 +- .../add-new-item/add-new-item.component.ts | 3 - .../scenario-settings.component.html | 37 ++++-- .../scenario-settings.component.ts | 16 ++- .../scenario-trends.component.ts | 2 +- src/app/scenario/scenario.component.html | 17 ++- src/app/scenario/scenario.component.scss | 2 +- src/app/scenario/scenario.component.ts | 42 ++++--- src/app/top-panel/top-panel.component.html | 2 +- src/app/top-panel/top-panel.component.scss | 12 +- src/app/utils/showZeroErrorTolerance.ts | 11 ++ tsconfig.json | 1 + 66 files changed, 1248 insertions(+), 402 deletions(-) create mode 100644 .github/workflows/docker-hub-v4.yml create mode 100644 src/app/_services/init.service.spec.ts create mode 100644 src/app/_services/init.service.ts delete mode 100644 src/app/administration/projects/edit-project/edit-project.component.css delete mode 100644 src/app/administration/projects/edit-project/edit-project.component.html delete mode 100644 src/app/administration/projects/edit-project/edit-project.component.ts create mode 100644 src/app/graphs/status-codes.ts create mode 100644 src/app/init-user/init-user.component.css create mode 100644 src/app/init-user/init-user.component.html create mode 100644 src/app/init-user/init-user.component.spec.ts create mode 100644 src/app/init-user/init-user.component.ts delete mode 100644 src/app/item-detail/attachements/attachements.component.css delete mode 100644 src/app/item-detail/attachements/attachements.component.html delete mode 100644 src/app/item-detail/attachements/attachements.component.spec.ts delete mode 100644 src/app/item-detail/attachements/attachements.component.ts create mode 100644 src/app/item-detail/request-stats/label-health/label-health.component.css create mode 100644 src/app/item-detail/request-stats/label-health/label-health.component.html create mode 100644 src/app/item-detail/request-stats/label-health/label-health.component.spec.ts create mode 100644 src/app/item-detail/request-stats/label-health/label-health.component.ts rename src/app/item-detail/{request-stats-compare => request-stats}/request-stats-compare.component.css (100%) rename src/app/item-detail/{request-stats-compare => request-stats}/request-stats-compare.component.html (54%) rename src/app/item-detail/{request-stats-compare => request-stats}/request-stats-compare.component.spec.ts (56%) rename src/app/item-detail/{request-stats-compare => request-stats}/request-stats-compare.component.ts (79%) create mode 100644 src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.css create mode 100644 src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.html create mode 100644 src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.spec.ts create mode 100644 src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.ts create mode 100644 src/app/project/project-settings/project-settings.component.css create mode 100644 src/app/project/project-settings/project-settings.component.html create mode 100644 src/app/project/project-settings/project-settings.component.spec.ts create mode 100644 src/app/project/project-settings/project-settings.component.ts create mode 100644 src/app/utils/showZeroErrorTolerance.ts diff --git a/.github/workflows/docker-hub-v4.yml b/.github/workflows/docker-hub-v4.yml new file mode 100644 index 00000000..9a6f03a5 --- /dev/null +++ b/.github/workflows/docker-hub-v4.yml @@ -0,0 +1,34 @@ +name: Publish Docker image on v4 push + +on: + push: + branches: + - 'v4' + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 + with: + images: novyl/jtl-reporter-fe + + - name: Build and push Docker image + uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc + with: + context: . + push: true + labels: novyl/jtl-reporter-fe:v4 + tags: novyl/jtl-reporter-fe:v4 diff --git a/package.json b/package.json index 51ef6210..6797a5a3 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "moment": "^2.24.0", "ngx-spinner": "^8.1.0", "ngx-toastr": "^10.0.2", - "node-sass": "^4.13.1", + "node-sass": "^4.14.1", "rxjs": "^6.0.0", "time-ago-pipe": "^1.3.2", "zone.js": "~0.9.1" diff --git a/src/app/_interceptors/error-interceptor.ts b/src/app/_interceptors/error-interceptor.ts index 8564e701..1c002d9d 100644 --- a/src/app/_interceptors/error-interceptor.ts +++ b/src/app/_interceptors/error-interceptor.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@angular/core'; -import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; -import { Observable, throwError } from 'rxjs'; -import { catchError } from 'rxjs/operators'; -import { AuthenticationService } from '../_services/authentication.service'; +import {Injectable} from '@angular/core'; +import {HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@angular/common/http'; +import {Observable, throwError, of} from 'rxjs'; +import {catchError} from 'rxjs/operators'; +import {AuthenticationService} from '../_services/authentication.service'; import {Router} from '@angular/router'; @@ -10,7 +10,8 @@ import {Router} from '@angular/router'; export class ErrorInterceptor implements HttpInterceptor { constructor( private authenticationService: AuthenticationService, - private router: Router) { } + private router: Router) { + } intercept(request: HttpRequest, next: HttpHandler): Observable> { return next.handle(request).pipe(catchError(err => { @@ -20,6 +21,7 @@ export class ErrorInterceptor implements HttpInterceptor { this.router.navigate(['login']); } + const error = err.error.message || err.statusText; return throwError(error); })); diff --git a/src/app/_services/authentication.service.ts b/src/app/_services/authentication.service.ts index 0541c3e3..4d2511a1 100644 --- a/src/app/_services/authentication.service.ts +++ b/src/app/_services/authentication.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { map } from 'rxjs/operators'; import { BehaviorSubject, Observable } from 'rxjs'; -import { ApiKey } from './api-token.model'; @Injectable({ @@ -36,6 +35,10 @@ export class AuthenticationService { return this.http.post('auth/change-password', body, { observe: 'response'}); } + initUser(body) { + return this.http.post('auth/initialize-user', body); + } + setLogin (value) { this.loggedIn.next(value); } diff --git a/src/app/_services/init.service.spec.ts b/src/app/_services/init.service.spec.ts new file mode 100644 index 00000000..ae16ccad --- /dev/null +++ b/src/app/_services/init.service.spec.ts @@ -0,0 +1,15 @@ +import { HttpClientModule } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; + +import { InitService } from './init.service'; + +describe('InitService', () => { + beforeEach(() => TestBed.configureTestingModule({ + imports: [ HttpClientModule ] + })); + + it('should be created', () => { + const service: InitService = TestBed.get(InitService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/_services/init.service.ts b/src/app/_services/init.service.ts new file mode 100644 index 00000000..520cb923 --- /dev/null +++ b/src/app/_services/init.service.ts @@ -0,0 +1,15 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class InitService { + + constructor(private http: HttpClient) { } + + fetchInfo(): Observable<{ initialized: boolean }> { + return this.http.get<{ initialized: boolean }>('info'); + } +} diff --git a/src/app/administration/projects/administration.component.html b/src/app/administration/projects/administration.component.html index 2d1b0e81..67aa8de8 100644 --- a/src/app/administration/projects/administration.component.html +++ b/src/app/administration/projects/administration.component.html @@ -47,8 +47,19 @@ {{_.latestRun | timeAgo}} - - +
+
+ + + +
+
diff --git a/src/app/administration/projects/delete-project/delete-project.component.html b/src/app/administration/projects/delete-project/delete-project.component.html index b44beac7..9f1c25f6 100644 --- a/src/app/administration/projects/delete-project/delete-project.component.html +++ b/src/app/administration/projects/delete-project/delete-project.component.html @@ -27,6 +27,6 @@ - diff --git a/src/app/administration/projects/delete-project/delete-project.component.ts b/src/app/administration/projects/delete-project/delete-project.component.ts index a366009a..927f50ce 100644 --- a/src/app/administration/projects/delete-project/delete-project.component.ts +++ b/src/app/administration/projects/delete-project/delete-project.component.ts @@ -10,7 +10,7 @@ import { ProjectService } from 'src/app/project.service'; @Component({ selector: 'app-delete-project', templateUrl: './delete-project.component.html', - styleUrls: ['./delete-project.component.css', '../../administration.css'] + styleUrls: ['./delete-project.component.css'] }) export class DeleteProjectComponent implements OnInit { diff --git a/src/app/administration/projects/edit-project/edit-project.component.css b/src/app/administration/projects/edit-project/edit-project.component.css deleted file mode 100644 index c230688a..00000000 --- a/src/app/administration/projects/edit-project/edit-project.component.css +++ /dev/null @@ -1,3 +0,0 @@ -.edit-project { - font-size: 14px; -} \ No newline at end of file diff --git a/src/app/administration/projects/edit-project/edit-project.component.html b/src/app/administration/projects/edit-project/edit-project.component.html deleted file mode 100644 index bc974995..00000000 --- a/src/app/administration/projects/edit-project/edit-project.component.html +++ /dev/null @@ -1,34 +0,0 @@ - - -
- - -
- -
- - - - diff --git a/src/app/administration/projects/edit-project/edit-project.component.ts b/src/app/administration/projects/edit-project/edit-project.component.ts deleted file mode 100644 index dddca12e..00000000 --- a/src/app/administration/projects/edit-project/edit-project.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { ProjectApiService } from 'src/app/project-api.service'; -import { NotificationMessage } from 'src/app/notification/notification-messages'; -import { ProjectService } from 'src/app/project.service'; -import { catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; - -@Component({ - selector: 'app-edit-project', - templateUrl: './edit-project.component.html', - styleUrls: ['./edit-project.component.css'] -}) -export class EditProjectComponent implements OnInit { - - myform: FormGroup; - projectName; - - @Input() projectDataInput: any; - - constructor( - private modalService: NgbModal, - private projectApiService: ProjectApiService, - private notification: NotificationMessage, - private projectService: ProjectService - ) { - - } - ngOnInit(): void { - this.createFormControls(); - this.createForm(); - } - - createFormControls() { - this.projectName = new FormControl(this.projectDataInput.projectName, [ - Validators.required, - Validators.maxLength(100) - ]); - } - - createForm() { - this.myform = new FormGroup({ - projectName: this.projectName, - }); - } - - open(content) { - this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }); - } - - onSubmit() { - if (this.myform.valid) { - const { projectName } = this.myform.value; - this.projectApiService.updateProject(this.projectDataInput.projectName, { projectName }) - .pipe(catchError(r => of(r))) - .subscribe(_ => { - const message = this.notification.projectUpdate(_); - this.projectService.loadProjects(); - return this.projectApiService.setData(message); - }); - this.modalService.dismissAll(); - } - } - - -} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0663153c..36a81887 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -17,7 +17,6 @@ import { ProjectsAdministrationComponent } from './administration/projects/admin import { TimeAgoPipe } from 'time-ago-pipe'; import { DataTableModule } from '@rushvora/ng-datatable'; import { AddNewProjectComponent } from './administration/projects/add-project/add-project-modal.component'; -import { EditProjectComponent } from './administration/projects/edit-project/edit-project.component'; import { DeleteProjectComponent } from './administration/projects/delete-project/delete-project.component'; import { DeleteItemComponent } from './item-detail/delete-item/delete-item.component'; import { ProjectComponent } from './project/project.component'; @@ -28,7 +27,6 @@ import { SettingsScenarioComponent } from './scenario/scenario-settings/scenario import { DeleteScenarioComponent } from './scenario/delete-scenario/delete-scenario.component'; import { NgxSpinnerModule } from 'ngx-spinner'; import { StatsCompareComponent } from './item-detail/stats-compare/stats-compare.component'; -import { AttachementsComponent } from './item-detail/attachements/attachements.component'; import { ControlPanelComponent } from './control-panel/control-panel.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ToastrModule } from 'ngx-toastr'; @@ -64,7 +62,11 @@ import { LabelChartComponent } from './item-detail/label-chart/label-chart.compo import { AnalyzeChartsComponent } from './item-detail/analyze-charts/analyze-charts.component'; import { AddMetricComponent } from './item-detail/analyze-charts/add-metric/add-metric.component'; import { ScenarioTrendsComponent } from './scenario/scenario-trends/scenario-trends.component'; -import { RequestStatsCompareComponent } from './item-detail/request-stats-compare/request-stats-compare.component'; +import { ProjectSettingsComponent } from './project/project-settings/project-settings.component'; +import { RequestStatsCompareComponent } from './item-detail/request-stats/request-stats-compare.component'; +import { InitUserComponent } from './init-user/init-user.component'; +import { LabelHealthComponent } from './item-detail/request-stats/label-health/label-health.component'; +import { ZeroErrorToleranceWarningComponent } from './item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component'; const appRoutes: Routes = [ { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, @@ -94,6 +96,7 @@ const appRoutes: Routes = [ runGuardsAndResolvers: 'always', canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent }, + { path: 'init', component: InitUserComponent } ]; @@ -112,7 +115,6 @@ const appRoutes: Routes = [ EditItemComponent, ProjectsAdministrationComponent, TimeAgoPipe, - EditProjectComponent, DeleteProjectComponent, DeleteItemComponent, ProjectComponent, @@ -121,7 +123,6 @@ const appRoutes: Routes = [ SettingsScenarioComponent, DeleteScenarioComponent, StatsCompareComponent, - AttachementsComponent, ControlPanelComponent, BreadcrumbComponent, LabelTrendComponent, @@ -149,7 +150,11 @@ const appRoutes: Routes = [ AnalyzeChartsComponent, AddMetricComponent, ScenarioTrendsComponent, + ProjectSettingsComponent, RequestStatsCompareComponent, + InitUserComponent, + LabelHealthComponent, + ZeroErrorToleranceWarningComponent, ], imports: [ RouterModule.forRoot( diff --git a/src/app/graphs/monitoring.ts b/src/app/graphs/monitoring.ts index 712d0b59..14e65a73 100644 --- a/src/app/graphs/monitoring.ts +++ b/src/app/graphs/monitoring.ts @@ -2,8 +2,13 @@ export const monitoringGraphSettings: any = () => { return { chart: { type: 'line', - spacingRight: -7, - spacingLeft: -7 + }, + time: { + getTimezoneOffset: function (timestamp) { + const d = new Date(); + const timezoneOffset = d.getTimezoneOffset(); + return timezoneOffset; + } }, title: { text: '' @@ -42,7 +47,7 @@ export const monitoringGraphSettings: any = () => { lineWidth: 0, opposite: true, title: { - text: 'threads' + text: '' } }], }; diff --git a/src/app/graphs/status-codes.ts b/src/app/graphs/status-codes.ts new file mode 100644 index 00000000..f5391c5c --- /dev/null +++ b/src/app/graphs/status-codes.ts @@ -0,0 +1,43 @@ +import { StatusCodes } from '../item-detail/request-stats/label-health/label-health.component'; +import { colors } from './colors'; + +export const statusCodesChart = (data: StatusCodes[]) => { + return { + exporting: { + enabled: false, + }, + colorAxis: { + minColor: '#FFFFFF', + maxColor: '#FFF' + }, + series: [{ + type: 'treemap', + layoutAlgorithm: 'squarified', + allowDrillToNode: false, + animationLimit: 1000, + dataLabels: { + enabled: false + }, + levelIsConstant: true, + levels: [{ + level: 1, + dataLabels: { + enabled: true + }, + borderWidth: 3 + }], + data: data.map((_, index) => ({ + name: _.statusCode.toString(), + value: _.count, + color: colors[index] + })) + }], + subtitle: { + text: '' + }, + title: { + text: '' + } + }; +}; + diff --git a/src/app/init-user/init-user.component.css b/src/app/init-user/init-user.component.css new file mode 100644 index 00000000..bc87b3e6 --- /dev/null +++ b/src/app/init-user/init-user.component.css @@ -0,0 +1,3 @@ +.card-subtitle { + padding-bottom: 20px; +} diff --git a/src/app/init-user/init-user.component.html b/src/app/init-user/init-user.component.html new file mode 100644 index 00000000..8c5acc7a --- /dev/null +++ b/src/app/init-user/init-user.component.html @@ -0,0 +1,44 @@ +
+ +
+
+
+
Welcome to JtlReporter! 🎉
+ +
+
To proceed, please, create new credetials.
+ +
+
+ + +
+ +
+ + + +
+ +
+
+ + + + +
+ +
{{error}}
+
+
+
+
+
+
diff --git a/src/app/init-user/init-user.component.spec.ts b/src/app/init-user/init-user.component.spec.ts new file mode 100644 index 00000000..239f5ac3 --- /dev/null +++ b/src/app/init-user/init-user.component.spec.ts @@ -0,0 +1,29 @@ +import { HttpClientModule } from '@angular/common/http'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { InitUserComponent } from './init-user.component'; + +describe('InitUserComponent', () => { + let component: InitUserComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, HttpClientModule, RouterTestingModule], + declarations: [InitUserComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(InitUserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/init-user/init-user.component.ts b/src/app/init-user/init-user.component.ts new file mode 100644 index 00000000..64f0f959 --- /dev/null +++ b/src/app/init-user/init-user.component.ts @@ -0,0 +1,52 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { first } from 'rxjs/operators'; +import { AuthenticationService } from '../_services/authentication.service'; + +@Component({ + selector: 'app-init-user', + templateUrl: './init-user.component.html', + styleUrls: ['./init-user.component.css'] +}) +export class InitUserComponent implements OnInit { + initUserForm: FormGroup; + submitted = false; + error; + + + constructor( + private formBuilder: FormBuilder, + private authenticationService: AuthenticationService, + private router: Router, + ) { } + + ngOnInit() { + this.initUserForm = this.formBuilder.group({ + username: ['', Validators.required], + password: ['', [Validators.required, Validators.minLength(8)]], + }); + } + + onSubmit() { + this.submitted = true; + // stop here if form is invalid + if (this.initUserForm.invalid) { + return; + } + + this.authenticationService.initUser({ + username: this.initUserForm.controls.username.value, + password: this.initUserForm.controls.password.value + }) + .pipe(first()) + .subscribe( + data => { + this.router.navigate(['/']); + }, + error => { + this.error = error; + }); + } + +} diff --git a/src/app/item-detail/attachements/attachements.component.css b/src/app/item-detail/attachements/attachements.component.css deleted file mode 100644 index 460a7be5..00000000 --- a/src/app/item-detail/attachements/attachements.component.css +++ /dev/null @@ -1,7 +0,0 @@ -.btn { - margin-right: 15px; -} - -a:hover { - text-decoration: underline !important; -} \ No newline at end of file diff --git a/src/app/item-detail/attachements/attachements.component.html b/src/app/item-detail/attachements/attachements.component.html deleted file mode 100644 index c2b56713..00000000 --- a/src/app/item-detail/attachements/attachements.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - diff --git a/src/app/item-detail/attachements/attachements.component.spec.ts b/src/app/item-detail/attachements/attachements.component.spec.ts deleted file mode 100644 index 51e97b36..00000000 --- a/src/app/item-detail/attachements/attachements.component.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AttachementsComponent } from './attachements.component'; -import { HttpClientModule } from '@angular/common/http'; - -describe('AttachementsComponent', () => { - let component: AttachementsComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [AttachementsComponent], - imports: [HttpClientModule] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AttachementsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - xit('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/item-detail/attachements/attachements.component.ts b/src/app/item-detail/attachements/attachements.component.ts deleted file mode 100644 index d5f02c01..00000000 --- a/src/app/item-detail/attachements/attachements.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { ItemsApiService } from 'src/app/items-api.service'; - -@Component({ - selector: 'app-attachements', - templateUrl: './attachements.component.html', - styleUrls: ['./attachements.component.css'] -}) -export class AttachementsComponent implements OnInit { - - @Input() params: any; - @Input() attachements: []; - - constructor( - private modalService: NgbModal, - private itemApiService: ItemsApiService - ) { } - - ngOnInit() { - } - - open(content) { - this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }); - } - - getErrors() { - this.itemApiService.downloadTestErrors(this.params).subscribe(res => { - const url = window.URL.createObjectURL(res); - const a = document.createElement('a'); - document.body.appendChild(a); - a.setAttribute('style', 'display: none'); - a.href = url; - a.download = 'errors.json'; - a.click(); - window.URL.revokeObjectURL(url); - a.remove(); // remove the element - }, error => { - }, () => { - }); - } - -} diff --git a/src/app/item-detail/delete-item/delete-item.component.ts b/src/app/item-detail/delete-item/delete-item.component.ts index 64db04ae..b690bb94 100644 --- a/src/app/item-detail/delete-item/delete-item.component.ts +++ b/src/app/item-detail/delete-item/delete-item.component.ts @@ -70,6 +70,9 @@ export class DeleteItemComponent implements OnInit { } this.itemApiService.setData(message); this.redirect(); + } else { + const message = this.notification.itemDeleted(_); + this.itemApiService.setData(message); } }); this.myform.reset(); diff --git a/src/app/item-detail/item-detail.component.html b/src/app/item-detail/item-detail.component.html index 65f99e20..edaa241a 100644 --- a/src/app/item-detail/item-detail.component.html +++ b/src/app/item-detail/item-detail.component.html @@ -15,7 +15,6 @@ -
@@ -41,7 +40,7 @@
@@ -61,22 +60,26 @@ +
+
+ +
+
-
-
-
- +
+
Overview requests: {{totalRequests | number}}
+
-
+

{{itemData.overview.maxVu}}

@@ -86,7 +89,7 @@

{{itemData.overview.maxVu}}

-
+

{{itemData.overview.throughput > 1000 ? @@ -102,11 +105,11 @@

{{itemData.overview.throughput > 1000 ?

-
+

{{ - Math.round((itemData.overview.percentil / 1000) * 100) / 100}} s + Math.round((itemData.overview.percentil / 1000) * 100) / 100}} s

{{ itemData.overview.percentil}} ms @@ -116,17 +119,27 @@

{{

-
+
- -

- n/a +

{{ + Math.round((itemData.overview.avgResponseTime / 1000) * 100) / 100}} s +

+

{{ + itemData.overview.avgResponseTime}} ms

+
+ +
+
+ +
+
+

{{ Math.round((itemData.overview.avgLatency / 1000) * 100) / 100}} s

-

{{ itemData.overview.avgLatency}} ms

@@ -138,17 +151,13 @@

+
- -

- n/a -

{{ Math.round((itemData.overview.avgConnect / 1000) * 100) / 100}} s

-

{{ itemData.overview.avgConnect}} ms

@@ -160,8 +169,7 @@

+

{{convertBytesToMbps(itemData.overview.bytesPerSecond + @@ -172,7 +180,7 @@

{{convertBytesToMbps(itemData.overview.bytesPerSecond +

-
+

{{itemData.overview.errorRate}} {{itemData.overview.errorRate}} +
max cpu usage: - {{itemData.monitoringData.maxCpu}}% -
-
- max memory usage: - {{itemData.monitoringData.maxMem}}% + {{itemData.monitoring.cpu.max}}%
- +
diff --git a/src/app/item-detail/item-detail.component.scss b/src/app/item-detail/item-detail.component.scss index 3ba97630..055d36e3 100644 --- a/src/app/item-detail/item-detail.component.scss +++ b/src/app/item-detail/item-detail.component.scss @@ -269,6 +269,10 @@ thead .hd { border-left: 4px solid #28a745; } +.performance-analysis-warning{ + border-left: 4px solid #ffc107; +} + .btn-link-custom { font-size: 0.775rem; padding: 0; @@ -309,3 +313,8 @@ thead .hd { margin-right: -10px; margin-left: -10px; } + +.total-samples { + font-size: 13px; + font-weight: normal; +} diff --git a/src/app/item-detail/item-detail.component.spec.ts b/src/app/item-detail/item-detail.component.spec.ts index 82555a1c..64663ae6 100644 --- a/src/app/item-detail/item-detail.component.spec.ts +++ b/src/app/item-detail/item-detail.component.spec.ts @@ -3,11 +3,9 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ItemDetailComponent } from './item-detail.component'; import { BreadcrumbComponent } from '../breadcrumb/breadcrumb.component'; import { EditItemComponent } from './edit-item/edit-item.component'; -import { AttachementsComponent } from './attachements/attachements.component'; import { DeleteItemComponent } from './delete-item/delete-item.component'; import { ControlPanelComponent } from '../control-panel/control-panel.component'; import { StatsCompareComponent } from './stats-compare/stats-compare.component'; -import { LabelTrendComponent } from './label-trend/label-trend.component'; import { HighchartsChartModule } from 'highcharts-angular'; import { RouterTestingModule } from '@angular/router/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -27,7 +25,6 @@ describe('ItemDetailComponent', () => { ItemDetailComponent, BreadcrumbComponent, EditItemComponent, - AttachementsComponent, DeleteItemComponent, ControlPanelComponent, StatsCompareComponent, diff --git a/src/app/item-detail/item-detail.component.ts b/src/app/item-detail/item-detail.component.ts index 414115ff..9fe5cdf9 100644 --- a/src/app/item-detail/item-detail.component.ts +++ b/src/app/item-detail/item-detail.component.ts @@ -1,9 +1,9 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { ItemsApiService } from '../items-api.service'; -import { ItemDetail } from '../items.service.model'; -import { NgxSpinnerService } from 'ngx-spinner'; -import { DecimalPipe } from '@angular/common'; +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {ItemsApiService} from '../items-api.service'; +import {ItemDetail} from '../items.service.model'; +import {NgxSpinnerService} from 'ngx-spinner'; +import {DecimalPipe} from '@angular/common'; import * as Highcharts from 'highcharts'; import exporting from 'highcharts/modules/exporting'; @@ -16,15 +16,16 @@ import { networkLineSettings, commonGraphSettings, } from '../graphs/item-detail'; -import { catchError, withLatestFrom } from 'rxjs/operators'; -import { of } from 'rxjs'; -import { SharedMainBarService } from '../shared-main-bar.service'; -import { ToastrService } from 'ngx-toastr'; -import { bytesToMbps, roundNumberTwoDecimals } from './calculations'; -import { logScaleButton } from '../graphs/log-scale-button'; -import { ItemStatusValue } from './item-detail.model'; +import {catchError, withLatestFrom} from 'rxjs/operators'; +import {of} from 'rxjs'; +import {SharedMainBarService} from '../shared-main-bar.service'; +import {ToastrService} from 'ngx-toastr'; +import {bytesToMbps} from './calculations'; +import {logScaleButton} from '../graphs/log-scale-button'; +import {ItemStatusValue} from './item-detail.model'; import {Metrics} from './metrics'; import {AnalyzeChartService} from '../analyze-chart.service'; +import {showZeroErrorWarning} from '../utils/showZeroErrorTolerance'; @Component({ selector: 'app-item-detail', @@ -32,7 +33,7 @@ import {AnalyzeChartService} from '../analyze-chart.service'; styleUrls: ['./item-detail.component.scss', '../shared-styles.css'], providers: [DecimalPipe] }) -export class ItemDetailComponent implements OnInit { +export class ItemDetailComponent implements OnInit, OnDestroy { Highcharts: typeof Highcharts = Highcharts; itemData: ItemDetail = { overview: null, @@ -44,9 +45,14 @@ export class ItemDetailComponent implements OnInit { hostname: null, statistics: [], testName: null, - attachements: [], - monitoringData: { mem: [], maxCpu: 0, maxMem: 0, cpu: [] }, + monitoring: { + cpu: { + max: 0, data: [] + } + }, analysisEnabled: null, + zeroErrorToleranceEnabled: null, + topMetricsSettings: null }; overallChartOptions; updateChartFlag = false; @@ -65,6 +71,7 @@ export class ItemDetailComponent implements OnInit { activeId = 1; performanceAnalysisLines = null; externalSearchTerm = null; + totalRequests = null; constructor( private route: ActivatedRoute, @@ -96,7 +103,7 @@ export class ItemDetailComponent implements OnInit { this.itemParams.projectName, this.itemParams.scenarioName, this.itemParams.id, - { token: this.token } + {token: this.token} ) .pipe(catchError(r => { this.spinner.hide(); @@ -104,9 +111,9 @@ export class ItemDetailComponent implements OnInit { })) .subscribe((results) => { this.itemData = results; - this.hasErrorsAttachment = this.itemData.attachements.find((_) => _ === 'error'); this.monitoringAlerts(); this.generateCharts(); + this.calculateTotalRequests(); this.spinner.hide(); }); this.analyzeChartService.currentData.subscribe(data => { @@ -116,22 +123,33 @@ export class ItemDetailComponent implements OnInit { }); } + ngOnDestroy() { + this.toastr.clear(); + } + + private calculateTotalRequests() { + this.totalRequests = this.itemData.statistics.reduce((accumulator, currentValue) => { + return accumulator + currentValue.samples; + }, 0); + } + private getChartLines() { - const { threads, overallTimeResponse, + const { + threads, overallTimeResponse, overallThroughput, overAllFailRate, overAllNetworkV2, responseTime, throughput, networkV2, minResponseTime, maxResponseTime, percentile90, percentile95, percentile99, } = this.itemData.plot; - const threadLine = { ...threadLineSettings, name: 'virtual users', data: threads }; - const errorLine = { ...errorLineSettings, ...overAllFailRate }; - const throughputLine = { ...throughputLineSettings, ...overallThroughput }; + const threadLine = {...threadLineSettings, name: 'virtual users', data: threads}; + const errorLine = {...errorLineSettings, ...overAllFailRate}; + const throughputLine = {...throughputLineSettings, ...overallThroughput}; if (overAllNetworkV2) { const networkMbps = overAllNetworkV2.data.map((_) => { return [_[0], bytesToMbps(_[1])]; }); - const networkLine = { ...networkLineSettings, data: networkMbps }; + const networkLine = {...networkLineSettings, data: networkMbps}; this.chartLines.overall.set(Metrics.Network, networkLine); } @@ -155,33 +173,31 @@ export class ItemDetailComponent implements OnInit { if (minResponseTime) { this.chartLines.labels.set(Metrics.ResponseTimeMin, minResponseTime); - this.labelCharts.set(Metrics.ResponseTimeMin, { ...commonGraphSettings('ms'), series: [...minResponseTime, ...threadLine]}); + this.labelCharts.set(Metrics.ResponseTimeMin, {...commonGraphSettings('ms'), series: [...minResponseTime, ...threadLine]}); } if (maxResponseTime) { this.chartLines.labels.set(Metrics.ResponseTimeMax, maxResponseTime); - this.labelCharts.set(Metrics.ResponseTimeMax, { ...commonGraphSettings('ms'), series: [...maxResponseTime, ...threadLine]}); + this.labelCharts.set(Metrics.ResponseTimeMax, {...commonGraphSettings('ms'), series: [...maxResponseTime, ...threadLine]}); } if (percentile90) { this.chartLines.labels.set(Metrics.ResponseTimeP90, percentile90); - this.labelCharts.set(Metrics.ResponseTimeP90, { ...commonGraphSettings('ms'), series: [...percentile90, ...threadLine]}); + this.labelCharts.set(Metrics.ResponseTimeP90, {...commonGraphSettings('ms'), series: [...percentile90, ...threadLine]}); } if (percentile95) { this.chartLines.labels.set(Metrics.ResponseTimeP95, percentile95); - this.labelCharts.set(Metrics.ResponseTimeP95, { ...commonGraphSettings('ms'), series: [...percentile95, ...threadLine]}); + this.labelCharts.set(Metrics.ResponseTimeP95, {...commonGraphSettings('ms'), series: [...percentile95, ...threadLine]}); } if (percentile99) { this.chartLines.labels.set(Metrics.ResponseTimeP99, percentile99); - this.labelCharts.set(Metrics.ResponseTimeP99, { ...commonGraphSettings('ms'), series: [...percentile99, ...threadLine]}); + this.labelCharts.set(Metrics.ResponseTimeP99, {...commonGraphSettings('ms'), series: [...percentile99, ...threadLine]}); } this.chartLines.labels.set(Metrics.ResponseTimeAvg, responseTime); - this.labelCharts.set(Metrics.ResponseTimeAvg, { ...commonGraphSettings('ms'), series: [...responseTime, ...threadLine]}); + this.labelCharts.set(Metrics.ResponseTimeAvg, {...commonGraphSettings('ms'), series: [...responseTime, ...threadLine]}); this.chartLines.labels.set(Metrics.Throughput, throughput); - this.labelCharts.set(Metrics.Throughput, { ...commonGraphSettings('hits/s'), series: [...throughput, ...threadLine]}); - - + this.labelCharts.set(Metrics.Throughput, {...commonGraphSettings('hits/s'), series: [...throughput, ...threadLine]}); } private generateCharts() { @@ -193,7 +209,7 @@ export class ItemDetailComponent implements OnInit { }; } - itemDetailChanged({ note, environment, hostname }) { + itemDetailChanged({note, environment, hostname}) { this.itemData.note = note; this.itemData.environment = environment; this.itemData.hostname = hostname; @@ -201,13 +217,10 @@ export class ItemDetailComponent implements OnInit { monitoringAlerts() { const alertMessages = []; - const { maxCpu, maxMem } = this.itemData.monitoringData; + const {max: maxCpu} = this.itemData.monitoring.cpu; if (maxCpu > 90) { alertMessages.push(`High CPU usage`); } - if (maxMem > 90) { - alertMessages.push(`High memory usage`); - } if (alertMessages.length > 0) { this.toastr.warning(alertMessages.join('
'), 'Monitoring Alert!', @@ -227,7 +240,7 @@ export class ItemDetailComponent implements OnInit { } } - toggleThroughputBand({ element, perfAnalysis }) { + toggleThroughputBand({element, perfAnalysis}) { this.overallChartOptions.series.forEach(serie => { if (['response time', 'errors'].includes(serie.name)) { serie.visible = this.toggleThroughputBandFlag; @@ -264,7 +277,15 @@ export class ItemDetailComponent implements OnInit { return bytesToMbps(bytes); } - focusOnLabel($event: { label: string, metrics: Metrics[]}) { + showZeroErrorToleranceWarning(): boolean | string { + if (this.itemData.zeroErrorToleranceEnabled) { + return showZeroErrorWarning(this.itemData.overview.errorRate, + this.itemData.overview.errorCount); + } + return false; + } + + focusOnLabel($event: { label: string, metrics: Metrics[] }) { this.activeId = 2; this.performanceAnalysisLines = $event; this.externalSearchTerm = $event.label; diff --git a/src/app/item-detail/monitoring-stats/monitoring-stats.component.ts b/src/app/item-detail/monitoring-stats/monitoring-stats.component.ts index 5ac0cc02..df39abfb 100644 --- a/src/app/item-detail/monitoring-stats/monitoring-stats.component.ts +++ b/src/app/item-detail/monitoring-stats/monitoring-stats.component.ts @@ -26,21 +26,31 @@ export class MonitoringStatsComponent implements OnInit { }; } - @Input() data: any; + @Input() data: [{ name: string, timestamp: Date, avgCpu: number, avgMem: number }]; ngOnInit() { } open(content) { - // @ts-ignore this.modalService.open(content, { size: 'xl' }).result .then((_) => { this.monitoringChartOptions = null; }, () => { this.monitoringChartOptions = null; }); + const workers = Array.from(new Set(this.data.map(data => data.name))); + const series = workers.map((worker) => this.data + .filter(data => data.name === worker) + .reduce((acc, current) => { + acc.data.cpu.push([current.timestamp, current.avgCpu]); + acc.data.mem.push([current.timestamp, current.avgMem]); + acc.name = current.name; + return acc; + }, { data: { cpu: [], mem: [] }, name: null })) + .map((worker) => [{ data: worker.data.cpu, name: worker.name + ' - cpu' }, { data: worker.data.mem, name: worker.name + ' - mem' }]) + .flat(); + from(new Promise(resolve => setTimeout(resolve, 50))).subscribe((val: any) => { this.monitoringChartOptions = { - ...monitoringGraphSettings(), series: [ - { data: this.data.cpu, name: 'cpu' }, { data: this.data.mem, name: 'mem' }] + ...monitoringGraphSettings(), series: series }; this.updateFlag = true; }); diff --git a/src/app/item-detail/performance-analysis/performance-analysis.component.ts b/src/app/item-detail/performance-analysis/performance-analysis.component.ts index db1717f3..df896dda 100644 --- a/src/app/item-detail/performance-analysis/performance-analysis.component.ts +++ b/src/app/item-detail/performance-analysis/performance-analysis.component.ts @@ -2,7 +2,6 @@ import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/c import {animate, state, style, transition, trigger} from '@angular/animations'; import {Metrics} from '../metrics'; import {AnalyzeChartService} from '../../analyze-chart.service'; -import {DOCUMENT} from '@angular/common'; @Component({ selector: 'app-performance-analysis', diff --git a/src/app/item-detail/request-stats/label-health/label-health.component.css b/src/app/item-detail/request-stats/label-health/label-health.component.css new file mode 100644 index 00000000..45ee1110 --- /dev/null +++ b/src/app/item-detail/request-stats/label-health/label-health.component.css @@ -0,0 +1,12 @@ +.response-failures { + margin-top: 25px; +} + +thead .hd { + font-size: 12px; + border: none; +} + +.warn-icon { + margin-left: 5px; +} diff --git a/src/app/item-detail/request-stats/label-health/label-health.component.html b/src/app/item-detail/request-stats/label-health/label-health.component.html new file mode 100644 index 00000000..ca67d653 --- /dev/null +++ b/src/app/item-detail/request-stats/label-health/label-health.component.html @@ -0,0 +1,62 @@ + + + + + + + + diff --git a/src/app/item-detail/request-stats/label-health/label-health.component.spec.ts b/src/app/item-detail/request-stats/label-health/label-health.component.spec.ts new file mode 100644 index 00000000..dc6f8cc6 --- /dev/null +++ b/src/app/item-detail/request-stats/label-health/label-health.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DataTableModule } from '@rushvora/ng-datatable'; +import { HighchartsChartModule } from 'highcharts-angular'; +import { LabelHealthComponent } from './label-health.component'; + +describe('LabelHealthComponent', () => { + let component: LabelHealthComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LabelHealthComponent ], + imports: [HighchartsChartModule, DataTableModule] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LabelHealthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/item-detail/request-stats/label-health/label-health.component.ts b/src/app/item-detail/request-stats/label-health/label-health.component.ts new file mode 100644 index 00000000..9d683590 --- /dev/null +++ b/src/app/item-detail/request-stats/label-health/label-health.component.ts @@ -0,0 +1,61 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import * as Highcharts from 'highcharts'; +import { statusCodesChart } from 'src/app/graphs/status-codes'; +import Tree from 'highcharts/modules/treemap'; +Tree(Highcharts); + +@Component({ + selector: 'app-label-health', + templateUrl: './label-health.component.html', + styleUrls: ['./label-health.component.css'] +}) +export class LabelHealthComponent implements OnInit { + + @Input() statusCodes: StatusCodes[]; + @Input() responseFailures: ResponseMessageFailures[]; + @Input() labelName: string; + @Input() errorRate: number; + Highcharts: typeof Highcharts = Highcharts; + labelChartOption; + updateFlag = false; + chartConstructor = 'chart'; + showBelowPrecisionWarning = false; + chartCallback: Highcharts.ChartCallbackFunction = function (chart): void { + setTimeout(() => { + chart.reflow(); + }, 0); + }; + + + constructor( + private modalService: NgbModal, + ) { } + + ngOnInit() { + this.showBelowPrecisionWarning = this.isBellowPrecisionError(); + } + + open(content) { + // @ts-ignore + this.modalService.open(content, { size: 'xl' }); + + this.labelChartOption = statusCodesChart(this.statusCodes); + this.updateFlag = true; + } + + private isBellowPrecisionError() { + return this.errorRate === 0 && this.responseFailures.length > 0; + } + +} + +export interface StatusCodes { + statusCode: number; + count: number; +} + +export interface ResponseMessageFailures { + responseMessage: number; + count: number; +} diff --git a/src/app/item-detail/request-stats-compare/request-stats-compare.component.css b/src/app/item-detail/request-stats/request-stats-compare.component.css similarity index 100% rename from src/app/item-detail/request-stats-compare/request-stats-compare.component.css rename to src/app/item-detail/request-stats/request-stats-compare.component.css diff --git a/src/app/item-detail/request-stats-compare/request-stats-compare.component.html b/src/app/item-detail/request-stats/request-stats-compare.component.html similarity index 54% rename from src/app/item-detail/request-stats-compare/request-stats-compare.component.html rename to src/app/item-detail/request-stats/request-stats-compare.component.html index f1f86119..be36d500 100644 --- a/src/app/item-detail/request-stats-compare/request-stats-compare.component.html +++ b/src/app/item-detail/request-stats/request-stats-compare.component.html @@ -2,11 +2,14 @@
Request Statistics - The values shown are in %. - Comparing to test: {{comparedMetadata.id}} with - {{comparedMetadata.maxVu}} VU -
-
+
@@ -36,8 +39,12 @@
Request Statistics
- +
@@ -87,35 +94,82 @@
Request Statistics {{_.label}} {{_.samples}} - {{_.avgResponseTime}} - {{_.minResponseTime}} - {{_.maxResponseTime}} - {{_.n0}} - {{_.n5}} - {{_.n9}} + {{_.avgResponseTime}} + + {{_.minResponseTime}} + + {{_.maxResponseTime}} + + {{_.n0}} + + {{_.n5}} + + {{_.n9}} + - {{_.throughput}} - + {{_.throughput}} + - {{convertBytesToMbps(_.bytesPerSecond + _.bytesSentPerSecond) || 0 }} + {{convertBytesToMbps(_.bytesPerSecond + + _.bytesSentPerSecond) || 0 }} - {{_.errorRate}} + {{_.errorRate}} - + + - +
+ + + + + +
+

diff --git a/src/app/item-detail/request-stats-compare/request-stats-compare.component.spec.ts b/src/app/item-detail/request-stats/request-stats-compare.component.spec.ts similarity index 56% rename from src/app/item-detail/request-stats-compare/request-stats-compare.component.spec.ts rename to src/app/item-detail/request-stats/request-stats-compare.component.spec.ts index dfeef435..d1682eef 100644 --- a/src/app/item-detail/request-stats-compare/request-stats-compare.component.spec.ts +++ b/src/app/item-detail/request-stats/request-stats-compare.component.spec.ts @@ -8,6 +8,7 @@ import { ToastrModule } from 'ngx-toastr'; import { LabelErrorComponent } from '../label-error/label-error.component'; import { LabelTrendComponent } from '../label-trend/label-trend.component'; import { StatsCompareComponent } from '../stats-compare/stats-compare.component'; +import { LabelHealthComponent } from './label-health/label-health.component'; import { RequestStatsCompareComponent } from './request-stats-compare.component'; @@ -22,6 +23,7 @@ describe('RequestStatsCompareComponent', () => { StatsCompareComponent, LabelErrorComponent, LabelTrendComponent, + LabelHealthComponent, ], imports: [ DataTableModule, @@ -55,7 +57,50 @@ describe('RequestStatsCompareComponent', () => { n9: 37, samples: 200, throughput: 0.17, - }] + responseMessageFailures: [ + { responseMessage: 'error', count: 1 } + ], + statusCodes: [{ + statusCode: 200, count: 1 + }], + }, { + avgResponseTime: 10, + bytes: 758, + errorRate: 0, + label: '02 - Click_Log_In-21', + maxResponseTime: 38, + minResponseTime: 8, + n0: 33, + n5: 34, + n9: 37, + samples: 200, + throughput: 0.17, + responseMessageFailures: [ + { responseMessage: 'error', count: 1 } + ], + statusCodes: [{ + statusCode: 200, count: 1 + }], + }, + { + avgResponseTime: 10, + bytes: 758, + errorRate: 0, + label: '03 - Click_Log_In-20', + maxResponseTime: 38, + minResponseTime: 8, + n0: 33, + n5: 34, + n9: 37, + samples: 200, + throughput: 0.17, + responseMessageFailures: [ + { responseMessage: 'error', count: 1 } + ], + statusCodes: [{ + statusCode: 200, count: 1 + }], + }, ] }; fixture.detectChanges(); }); @@ -84,4 +129,22 @@ describe('RequestStatsCompareComponent', () => { }); expect(spy).toHaveBeenCalled(); }); + describe('label search', () => { + it('should search fulltext', () => { + component.search('02'); + expect(component.labelsData.length).toBe(2); + }); + it('should correctly filter out labels if NOT keyword used', () => { + component.search('not "02 - Click_Log_In-22"'); + expect(component.labelsData.map(x => x.label)) + .toEqual(['02 - Click_Log_In-21', '03 - Click_Log_In-20']); + }); + it('should correctly filter out if OR keyword used', () => { + component.search('"02 - Click_Log_In-22" or "03 - Click_Log_In-20"'); + expect(component.labelsData.length).toBe(2); + expect(component.labelsData.map(x => x.label)) + .toEqual(['02 - Click_Log_In-22', '03 - Click_Log_In-20']); + }); + }); + }); diff --git a/src/app/item-detail/request-stats-compare/request-stats-compare.component.ts b/src/app/item-detail/request-stats/request-stats-compare.component.ts similarity index 79% rename from src/app/item-detail/request-stats-compare/request-stats-compare.component.ts rename to src/app/item-detail/request-stats/request-stats-compare.component.ts index 6cc9d344..29a6e54d 100644 --- a/src/app/item-detail/request-stats-compare/request-stats-compare.component.ts +++ b/src/app/item-detail/request-stats/request-stats-compare.component.ts @@ -4,6 +4,9 @@ import { bytesToMbps, roundNumberTwoDecimals } from '../calculations'; import { AnalyzeChartService } from '../../analyze-chart.service'; import { ItemsApiService } from 'src/app/items-api.service'; import { ToastrService } from 'ngx-toastr'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + + @Component({ selector: 'app-request-stats-compare', @@ -28,7 +31,8 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { constructor( private itemsService: ItemsApiService, private toastr: ToastrService, - private analyzeChartService: AnalyzeChartService + private analyzeChartService: AnalyzeChartService, + private modalService: NgbModal, ) { } @@ -52,12 +56,48 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { this.comparisonMs = true; } - search(term: string) { + search(query: string) { const dataToFilter = this.comparedData || this.itemData.statistics; - if (term) { - this.labelsData = dataToFilter.filter(x => - x.label.trim().toLowerCase().includes(term.trim().toLowerCase()) - ); + const terms = query.match(/(?:[^\s"]+|"[^"]*")+/g); + + let notTerm = null; + let orTerms = []; + + if (terms && terms.length > 0) { + if (terms[0] === 'not' && terms.length > 1) { + notTerm = terms[1]; + terms.splice(0, 1); + } + + if (terms.includes('or')) { + orTerms = terms.map((term, index, arr) => { + if (term.toLowerCase() === 'or') { + return this.trimTerm(arr[index + 1]); + } else if (index === 0) { + return this.trimTerm(term); + } + }); + } + + // search with operators + if (notTerm || orTerms.length > 0) { + this.labelsData = dataToFilter + .filter(x => { + if (notTerm) { + return this.trimTerm(x.label) !== this.trimTerm(notTerm); + } + return true; + }) + .filter(x => { + if (orTerms.length > 0) { + return orTerms.includes(this.trimTerm(x.label)); + } + return true; + }); + return; + } + // fulltext search + this.labelsData = dataToFilter.filter(x => this.trimTerm(x.label).includes(this.trimTerm(terms[0]))); } else { this.labelsData = dataToFilter; } @@ -197,6 +237,16 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { return roundNumberTwoDecimals(percDiff); } + private trimTerm(term: string) { + if (!term) { + return; + } + return term + .trim() + .replace(/^"+|"+$/g, '') + .toLowerCase(); + } + getUnit() { if (this.comparisonMs) { return 'ms'; @@ -207,4 +257,8 @@ export class RequestStatsCompareComponent implements OnInit, OnDestroy { focusLabel(label: string) { this.analyzeChartService.changeMessage({ label }); } + + openSearchHelp(content) { + this.modalService.open(content, { ariaLabelledBy: 'modal-basic-title' }); + } } diff --git a/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.css b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.css new file mode 100644 index 00000000..6b07c383 --- /dev/null +++ b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.css @@ -0,0 +1,3 @@ +.error-tolerance-issue { + border-left: 4px solid #dc3545 !important; +} diff --git a/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.html b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.html new file mode 100644 index 00000000..7b0368a4 --- /dev/null +++ b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.html @@ -0,0 +1,13 @@ +
+
+
Test failure
+
+
+ Error(s) detected
+
+ The scenario has zero error tolerance check enabled. +
+
+
+
diff --git a/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.spec.ts b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.spec.ts new file mode 100644 index 00000000..73fbdf07 --- /dev/null +++ b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ZeroErrorToleranceWarningComponent } from './zero-error-tolerance-warning.component'; + +describe('ZeroErrorToleranceWarningComponent', () => { + let component: ZeroErrorToleranceWarningComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ZeroErrorToleranceWarningComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ZeroErrorToleranceWarningComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.ts b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.ts new file mode 100644 index 00000000..7f7770ed --- /dev/null +++ b/src/app/item-detail/zero-error-tolerance-warning/zero-error-tolerance-warning.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-zero-error-tolerance-warning', + templateUrl: './zero-error-tolerance-warning.component.html', + styleUrls: ['./zero-error-tolerance-warning.component.css', '../item-detail.component.scss'] +}) +export class ZeroErrorToleranceWarningComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/items.service.model.ts b/src/app/items.service.model.ts index 9a647855..262f7954 100644 --- a/src/app/items.service.model.ts +++ b/src/app/items.service.model.ts @@ -5,6 +5,7 @@ export interface ItemsListing { environment: string; startTime: string; status: string; + overview: ItemOverview; } export interface Items { @@ -20,20 +21,13 @@ export enum ReportStatus { } export interface ItemDetail { - overview: { - avgLatency: number - avgResponseTime: number - duration: number - endDate: string - errorRate: number - maxVu: number - percentil: number - startDate: string - throughput: number - }; + overview: ItemOverview; analysisEnabled: boolean; + zeroErrorToleranceEnabled: boolean; reportStatus: ReportStatus; - monitoringData: { cpu: [], mem: [], maxCpu?: number, maxMem?: number }; + monitoring: { + cpu: { data: { name: string, cpu: number, timestamp: number }[], max?: number } + }; baseId: string; testName: string; note: string; @@ -41,7 +35,6 @@ export interface ItemDetail { environment: string; plot: ItemDataPlot; statistics: ItemStatistics[]; - attachements: []; thresholds?: { passed: boolean, diff: { @@ -50,6 +43,45 @@ export interface ItemDetail { throughputRateDiff: number } }; + topMetricsSettings: TopMetricsSettings; +} + +interface TopMetricsSettings { + errorRate: boolean; + virtualUsers: boolean; + throughput: boolean; + network: boolean; + avgResponseTime: boolean; + avgConnectionTime: boolean; + avgLatency: boolean; + percentile: boolean; +} + +interface ItemOverview { + avgLatency: number; + avgConnect: number; + avgResponseTime: number; + duration: number; + endDate: string; + errorRate: number; + maxVu: number; + percentil: number; + startDate: string; + throughput: number; + errorCount?: number; +} + +interface ItemOverview { + avgLatency: number; + avgResponseTime: number; + duration: number; + endDate: string; + errorRate: number; + maxVu: number; + percentil: number; + startDate: string; + throughput: number; + errorCount?: number; } export interface ItemDataPlot { @@ -59,7 +91,7 @@ export interface ItemDataPlot { throughput: LabelSeries[]; networkV2: LabelSeries[]; networkUp: LabelSeries[]; - networkDown: LabelSeries[]; + networkDown: LabelSeries[]; percentile90?: LabelSeries[]; percentile95?: LabelSeries[]; percentile99?: LabelSeries[]; @@ -89,6 +121,12 @@ export interface ItemStatistics { n9: number; samples: number; throughput: number; + responseMessageFailures?: ResponseMessageFailure[]; +} + +interface ResponseMessageFailure { + count: number; + responseMessage: string; } interface MonitoringData { diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index d5e2327f..dbde1fb4 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -1,6 +1,6 @@
-
+
Login
diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index fa740b3a..978b0cef 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -3,6 +3,7 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { AuthenticationService } from '../_services/authentication.service'; import { first } from 'rxjs/operators'; +import { InitService } from '../_services/init.service'; @Component({ selector: 'app-login', @@ -12,6 +13,7 @@ import { first } from 'rxjs/operators'; export class LoginComponent implements OnInit { loginForm: FormGroup; loading = false; + initLoaded = false; submitted = false; returnUrl: string; error = null; @@ -20,7 +22,9 @@ export class LoginComponent implements OnInit { private formBuilder: FormBuilder, private route: ActivatedRoute, private router: Router, - private authenticationService: AuthenticationService) { } + private authenticationService: AuthenticationService, + private initService: InitService, + ) { } ngOnInit() { // reset login status @@ -28,6 +32,13 @@ export class LoginComponent implements OnInit { // slow down to give top panel time to disappear new Promise(resolve => setTimeout(resolve, 0)).then(); + this.initService.fetchInfo().subscribe((res) => { + if (res.initialized === false) { + this.router.navigate(['init']); + } + this.initLoaded = true; + }); + this.loginForm = this.formBuilder.group({ username: ['', Validators.required], diff --git a/src/app/notification/notification-messages.ts b/src/app/notification/notification-messages.ts index e51e66d1..067cd253 100644 --- a/src/app/notification/notification-messages.ts +++ b/src/app/notification/notification-messages.ts @@ -82,13 +82,13 @@ export class NotificationMessage { return this.statusCodeMessage(response, 'Thresholds were updated'); } - private statusCodeMessage(response, succesMessgae) { + private statusCodeMessage(response, successMessage) { let message = { success: false, message: `Something went wrong` }; if (response.status >= 200 && response.status < 300) { - message = { success: true, message: succesMessgae }; + message = { success: true, message: successMessage }; } else if (response.status === 400) { try { - message = { success: false, message: response.error.message }; + message = { success: false, message: response.message }; } catch (e) { } } diff --git a/src/app/project-api.service.ts b/src/app/project-api.service.ts index 93b99441..7ae6b9c8 100644 --- a/src/app/project-api.service.ts +++ b/src/app/project-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; +import {HttpClient, HttpResponse} from '@angular/common/http'; import { Observable, BehaviorSubject } from 'rxjs'; import { ProjectsListing, NewProjectBody } from './project-api.service.model'; import { ItemsListing, IScenarios, ProjectsOverallStats } from './items.service.model'; @@ -28,12 +28,16 @@ export class ProjectApiService { return this.http.get('projects/latest-items'); } + getProject(projectName): Observable { + return this.http.get(`projects/${projectName}`, { observe: 'response' }); + } + deleteProject(projectName): Observable { return this.http.delete(`projects/${projectName}`, { observe: 'response' }); } - updateProject(projectName, body): Observable { - return this.http.put(`projects/${projectName}`, body, { observe: 'response' }); + updateProject(projectName, body): Observable> { + return this.http.put(`projects/${projectName}`, body, { observe: 'response' }); } fetchOverallStats(): Observable { diff --git a/src/app/project/graph/scenarios-graph.component.ts b/src/app/project/graph/scenarios-graph.component.ts index c97e24c1..c65328a4 100644 --- a/src/app/project/graph/scenarios-graph.component.ts +++ b/src/app/project/graph/scenarios-graph.component.ts @@ -48,14 +48,7 @@ export class ScenariosGraphComponent implements AfterViewInit, OnDestroy { }, // after the update .. afterUpdate: function(chart) { - const data = chart.config.data; if (length === -1) { return; } - // prevents new charts to be drawn - // for (let i = length; i < data.maxBarNumber; i++) { - // data.datasets[0]._meta[0].data[i].draw = function() { - // return; - // }; - // } }, afterDraw: function(chart) { diff --git a/src/app/project/project-settings/project-settings.component.css b/src/app/project/project-settings/project-settings.component.css new file mode 100644 index 00000000..e69de29b diff --git a/src/app/project/project-settings/project-settings.component.html b/src/app/project/project-settings/project-settings.component.html new file mode 100644 index 00000000..b17dc7d9 --- /dev/null +++ b/src/app/project/project-settings/project-settings.component.html @@ -0,0 +1,89 @@ + + +
+ + + + + +
+ + +
+ + diff --git a/src/app/project/project-settings/project-settings.component.spec.ts b/src/app/project/project-settings/project-settings.component.spec.ts new file mode 100644 index 00000000..683f1081 --- /dev/null +++ b/src/app/project/project-settings/project-settings.component.spec.ts @@ -0,0 +1,28 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProjectSettingsComponent } from './project-settings.component'; +import { ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; + +describe('ProjectSettingsComponent', () => { + let component: ProjectSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, HttpClientModule], + declarations: [ ProjectSettingsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/project/project-settings/project-settings.component.ts b/src/app/project/project-settings/project-settings.component.ts new file mode 100644 index 00000000..ef8e74be --- /dev/null +++ b/src/app/project/project-settings/project-settings.component.ts @@ -0,0 +1,119 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {FormControl, FormGroup, Validators} from '@angular/forms'; +import {catchError} from 'rxjs/operators'; +import {of} from 'rxjs'; +import {ProjectApiService} from '../../project-api.service'; +import {NotificationMessage} from '../../notification/notification-messages'; +import {ProjectService} from '../../project.service'; + +@Component({ + selector: 'app-project-settings', + templateUrl: './project-settings.component.html', + styleUrls: ['./project-settings.component.css'] +}) +export class ProjectSettingsComponent implements OnInit { + + projectSettingsForm: FormGroup; + metricsEditable; + formControls = { + virtualUsers: null, + throughput: null, + percentile: null, + avgResponseTime: null, + avgConnectionTime: null, + avgLatency: null, + errorRate: null, + network: null, + projectName: null, + }; + @Input() projectName: string; + + constructor( + private modalService: NgbModal, + private projectApiService: ProjectApiService, + private notification: NotificationMessage, + private projectService: ProjectService, + ) { + } + + ngOnInit() { + this.projectApiService.getProject(this.projectName).subscribe((r) => { + this.createFormControls(r.body); + this.createForm(); + this.isEditable(); + }); + } + + createFormControls(settings) { + this.formControls.virtualUsers = new FormControl(settings.topMetricsSettings.virtualUsers, []); + this.formControls.percentile = new FormControl(settings.topMetricsSettings.percentile, []); + this.formControls.throughput = new FormControl(settings.topMetricsSettings.throughput, []); + this.formControls.errorRate = new FormControl(settings.topMetricsSettings.errorRate, []); + this.formControls.network = new FormControl(settings.topMetricsSettings.network, []); + this.formControls.avgLatency = new FormControl(settings.topMetricsSettings.avgLatency, []); + this.formControls.avgConnectionTime = new FormControl(settings.topMetricsSettings.avgConnectionTime, []); + this.formControls.avgResponseTime = new FormControl(settings.topMetricsSettings.avgResponseTime, []); + this.formControls.projectName = new FormControl(settings.projectName, [ + Validators.required, + Validators.maxLength(100), + Validators.minLength(3), + ]); + + } + + createForm() { + this.projectSettingsForm = new FormGroup({ + virtualUsers: this.formControls.virtualUsers, + errorRate: this.formControls.errorRate, + percentile: this.formControls.percentile, + throughput: this.formControls.throughput, + network: this.formControls.network, + avgLatency: this.formControls.avgLatency, + avgConnectionTime: this.formControls.avgConnectionTime, + avgResponseTime: this.formControls.avgResponseTime, + projectName: this.formControls.projectName, + }); + } + + open(content) { + this.modalService.open(content, {ariaLabelledBy: 'modal-basic-title', size: 'lg'}); + } + + onSubmit() { + const payload = { + projectName: this.formControls.projectName.value, + topMetricsSettings: { + virtualUsers: this.formControls.virtualUsers.value, + errorRate: this.formControls.errorRate.value, + percentile: this.formControls.percentile.value, + throughput: this.formControls.throughput.value, + network: this.formControls.network.value, + avgLatency: this.formControls.avgLatency.value, + avgResponseTime: this.formControls.avgResponseTime.value, + avgConnectionTime: this.formControls.avgConnectionTime.value + } + }; + if (this.projectSettingsForm.valid) { + this.projectApiService.updateProject(this.projectName, payload) + .pipe(catchError(r => of(r))) + .subscribe(_ => { + const message = this.notification.projectUpdate(_); + this.projectService.loadProjects(); + return this.projectApiService.setData(message); + }); + this.modalService.dismissAll(); + } + } + + isEditable() { + const enabledMetrics = Object.values(this.formControls).map(control => control.value).filter(value => value === true); + this.metricsEditable = enabledMetrics.length > 5; + console.log(this.metricsEditable); + } + + onCheckboxChange() { + this.isEditable(); + } + +} diff --git a/src/app/project/project.component.html b/src/app/project/project.component.html index 79af305c..ef3f2290 100644 --- a/src/app/project/project.component.html +++ b/src/app/project/project.component.html @@ -26,6 +26,14 @@
+
+ + +
diff --git a/src/app/scenario.service.model.ts b/src/app/scenario.service.model.ts index 02b08578..c6933300 100644 --- a/src/app/scenario.service.model.ts +++ b/src/app/scenario.service.model.ts @@ -1,5 +1,6 @@ export interface Scenario { analysisEnabled: boolean; + zeroErrorToleranceEnabled: boolean; name: string; thresholds: { enabled: boolean; diff --git a/src/app/scenario/add-new-item/add-new-item.component.html b/src/app/scenario/add-new-item/add-new-item.component.html index 8e163f5e..bd73724e 100644 --- a/src/app/scenario/add-new-item/add-new-item.component.html +++ b/src/app/scenario/add-new-item/add-new-item.component.html @@ -9,18 +9,11 @@