Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit 8c87b22

Browse files
ghillertjvalkeal
authored andcommitted
gh-322 Refactor and port Analytics Counter tab
- Create Analytics component - Create Counter component - Add routing configuration - Migrate Counter Line Chart component to Typescript and D3v4 - squashed review commits - Fixes #322 - Polish - Remove unused import - Change to use property getter instead of direct access.
1 parent fb8b388 commit 8c87b22

18 files changed

+677
-30
lines changed

ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@angular/router": "^4.2.4",
2727
"bootstrap-sass": "^3.3.7",
2828
"core-js": "^2.4.1",
29+
"d3": "^4.10.0",
2930
"dagre": "^0.7.4",
3031
"moment": "^2.18.1",
3132
"ng2-stomp-service": "^1.2.2",
@@ -45,6 +46,7 @@
4546
"@angular/cli": "1.3.0",
4647
"@angular/compiler-cli": "^4.2.4",
4748
"@angular/language-service": "^4.2.4",
49+
"@types/d3": "^4.10.0",
4850
"@types/jasmine": "~2.5.53",
4951
"@types/jasminewd2": "~2.0.2",
5052
"@types/node": "~6.0.60",

ui/proxy.conf.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
"secure": false,
2525
"logLevel": "debug"
2626
},
27+
"/metrics" : {
28+
"target": "http://localhost:9393/",
29+
"secure": false,
30+
"logLevel": "debug"
31+
},
2732
"/security/info" : {
2833
"target": "http://localhost:9393/",
2934
"secure": false,

ui/src/app/analytics/analytics-routing.module.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { NgModule } from '@angular/core';
22
import { RouterModule } from '@angular/router';
33

44
import { AnalyticsComponent } from './analytics.component';
5+
import { CountersComponent } from './counters/counters.component';
6+
import { DashboardComponent } from './dashboard/dashboard.component';
57

68
@NgModule({
79
imports: [RouterModule.forChild([
@@ -12,7 +14,22 @@ import { AnalyticsComponent } from './analytics.component';
1214
authenticate: true,
1315
roles: ['ROLE_VIEW'],
1416
feature: 'analyticsEnabled'
15-
}
17+
},
18+
children: [
19+
{
20+
path: '',
21+
pathMatch: 'full',
22+
redirectTo: 'dashboard'
23+
},
24+
{
25+
path: 'dashboard',
26+
component: DashboardComponent,
27+
},
28+
{
29+
path: 'counters',
30+
component: CountersComponent,
31+
}
32+
]
1633
}
1734
])],
1835
exports: [RouterModule]
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
<h1>Analytics</h1>
2-
<p>
3-
This is in under development. We would love to hear feedback on the other available tabs/views.
2+
<p [hidden]="!router.isActive('/analytics/dashboard', true)">
3+
This section displays dashboard for the metrics data from various counter apps.
44
</p>
5+
<p [hidden]="!router.isActive('/analytics/counters', true)">
6+
This section displays counters and their values.
7+
</p>
8+
9+
<div class="tab-simple">
10+
<ul class="nav nav-tabs">
11+
<li role="presentation" routerLinkActive="active"><a routerLink="dashboard">Dashboard</a></li>
12+
<li role="presentation" routerLinkActive="active"><a routerLink="counters">Counters</a></li>
13+
</ul>
14+
<div class="tab-content">
15+
<div class="tab-pane in active"><router-outlet></router-outlet></div>
16+
</div>
17+
</div>

ui/src/app/analytics/analytics.component.spec.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
import {ActivatedRoute} from '@angular/router';
12
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3+
import {RouterTestingModule} from '@angular/router/testing';
24

5+
import { MockActivatedRoute } from '../tests/mocks/activated-route';
36
import { AnalyticsComponent } from './analytics.component';
47

5-
xdescribe('AnalyticsComponent', () => {
8+
describe('AnalyticsComponent', () => {
69
let component: AnalyticsComponent;
710
let fixture: ComponentFixture<AnalyticsComponent>;
11+
let activeRoute: MockActivatedRoute;
812

913
beforeEach(async(() => {
14+
activeRoute = new MockActivatedRoute();
1015
TestBed.configureTestingModule({
11-
declarations: [ AnalyticsComponent ]
16+
declarations: [ AnalyticsComponent ],
17+
imports: [
18+
RouterTestingModule.withRoutes([])
19+
],
20+
providers: [
21+
{ provide: ActivatedRoute, useValue: activeRoute }
22+
]
1223
})
1324
.compileComponents();
1425
}));

ui/src/app/analytics/analytics.component.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { Component, OnInit } from '@angular/core';
2+
import {Router} from '@angular/router';
23

4+
/**
5+
* Component of the Analytics module that handles
6+
* the Analytics tabs.
7+
*
8+
* @author Gunnar Hillert
9+
*/
310
@Component({
4-
selector: 'app-analytics',
511
templateUrl: './analytics.component.html',
612
})
713
export class AnalyticsComponent implements OnInit {
814

9-
constructor() { }
15+
constructor(public router: Router) { }
1016

1117
ngOnInit() {
1218
}

ui/src/app/analytics/analytics.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import { NgModule } from '@angular/core';
22
import { SharedModule } from '../shared/shared.module';
33

44
import { AnalyticsComponent } from './analytics.component';
5+
import { CountersComponent } from './counters/counters.component';
6+
import { DashboardComponent } from './dashboard/dashboard.component';
7+
58
import { AnalyticsService } from './analytics.service';
69
import { AnalyticsRoutingModule } from './analytics-routing.module';
10+
import { GraphChartComponent } from './charts/graph-chart/graph-chart.component';
711

812
@NgModule({
913
imports: [ AnalyticsRoutingModule, SharedModule ],
10-
declarations: [ AnalyticsComponent ],
14+
declarations: [ AnalyticsComponent, CountersComponent, DashboardComponent, GraphChartComponent ],
1115
providers: [ AnalyticsService ]
1216
})
1317
export class AnalyticsModule { }
Lines changed: 124 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,141 @@
1-
import { Injectable } from '@angular/core';
2-
import { Http, Response } from '@angular/http';
1+
import { Injectable, OnDestroy } from '@angular/core';
2+
import { Http, Response, RequestOptionsArgs } from '@angular/http';
3+
34
import { Observable } from 'rxjs/Observable';
5+
import { Subscription } from 'rxjs/Subscription';
46
import 'rxjs/add/operator/catch';
57
import 'rxjs/add/operator/map';
68

9+
import { ErrorHandler, Page } from '../shared/model';
10+
import { Counter } from './counters/model/counter.model';
11+
import { HttpUtils } from '../shared/support/http.utils';
12+
import { ToastyService } from 'ng2-toasty';
13+
/**
14+
* @author Gunnar Hillert
15+
*/
716
@Injectable()
817
export class AnalyticsService {
918

10-
private appstUrl = '/apps';
19+
private metricsCountersUrl = '/metrics/counters';
20+
21+
public counters: Page<Counter>;
22+
public _counterInterval = 2;
23+
public counterPoller: Subscription;
24+
25+
constructor(
26+
private http: Http,
27+
private errorHandler: ErrorHandler,
28+
private toastyService: ToastyService) {
29+
}
30+
31+
public set counterInterval(rate: number) {
32+
if (rate && !isNaN(rate)) {
33+
if (rate < 0.01) {
34+
rate = 0;
35+
this.stopPollingForCounters();
36+
this.toastyService.success(`Polling stopped.`);
37+
} else {
38+
console.log('Setting interval to ' + rate);
39+
this._counterInterval = rate;
40+
if (this.counterPoller && !this.counterPoller.closed) {
41+
this.stopPollingForCounters();
42+
this.startPollingForCounters();
43+
this.toastyService.success(`Polling interval changed to ${rate}s.`);
44+
} else {
45+
this.startPollingForCounters();
46+
this.toastyService.success(`Polling started with interval of ${rate}s.`);
47+
}
48+
}
49+
}
50+
}
1151

12-
constructor(private http: Http) { }
52+
public get counterInterval(): number {
53+
return this._counterInterval;
54+
}
1355

14-
getApps(): Observable<any[]> {
15-
return this.http.get(this.appstUrl)
16-
.map(this.extractData)
17-
.catch(this.handleError);
56+
public totalCacheSize() {
57+
return Math.max(Math.ceil(60 / this.counterInterval), 20);
1858
}
1959

20-
private extractData(res: Response) {
21-
const body = res.json();
22-
return body._embedded;
60+
/**
61+
* Starts the polling process for counters. Method
62+
* will check if the poller is already running and will
63+
* start the poller only if the poller is undefined or
64+
* stopped.
65+
*/
66+
public startPollingForCounters() {
67+
if (!this.counterPoller || this.counterPoller.closed) {
68+
this.counterPoller = Observable.interval(this.counterInterval * 1000)
69+
.switchMap(() => this.getAllCounters(true)).subscribe(
70+
result => {},
71+
error => {
72+
this.toastyService.error(error);
73+
});
74+
}
2375
}
2476

25-
private handleError (error: Response | any) {
26-
// In a real world app, you might use a remote logging infrastructure
27-
let errMsg: string;
28-
if (error instanceof Response) {
29-
const body = error.json() || '';
30-
const err = body.error || JSON.stringify(body);
31-
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
32-
} else {
33-
errMsg = error.message ? error.message : error.toString();
77+
/**
78+
* Stops the polling process for counters if the poller
79+
* is running and is defined.
80+
*/
81+
public stopPollingForCounters() {
82+
if (this.counterPoller && !this.counterPoller.closed) {
83+
this.counterPoller.unsubscribe();
3484
}
35-
console.error(errMsg);
36-
return Observable.throw(errMsg);
3785
}
3886

87+
/**
88+
* Retrieves all counters. Will take pagination into account.
89+
*
90+
* @param detailed If true will request additional counter values from the REST endpoint
91+
*/
92+
public getAllCounters(detailed = false): Observable<Page<Counter>> {
93+
94+
if (!this.counters) {
95+
this.counters = new Page<Counter>();
96+
}
97+
98+
const params = HttpUtils.getPaginationParams(this.counters.pageNumber, this.counters.pageSize);
99+
const requestOptionsArgs: RequestOptionsArgs = HttpUtils.getDefaultRequestOptions();
100+
101+
if (detailed) {
102+
params.append('detailed', detailed.toString());
103+
}
104+
105+
requestOptionsArgs.search = params;
106+
return this.http.get(this.metricsCountersUrl, requestOptionsArgs)
107+
.map(this.extractData.bind(this))
108+
.catch(this.errorHandler.handleError);
109+
}
110+
111+
private extractData(response: Response): Page<Counter> {
112+
const body = response.json();
113+
const items: Counter[] = [];
114+
const cache: Counter[] = [];
115+
for (const oldCounter of this.counters.items) {
116+
cache[oldCounter.name] = oldCounter;
117+
}
118+
if (body._embedded && body._embedded.counterResourceList) {
119+
for (const counterResourceListItems of body._embedded.counterResourceList) {
120+
const counter = new Counter().deserialize(counterResourceListItems);
121+
122+
if (cache[counter.name]) {
123+
const cached = cache[counter.name];
124+
counter.rates = cached.rates;
125+
counter.rates.push((counter.value - cached.value) / this.counterInterval);
126+
counter.rates.splice(0, counter.rates.length - this.totalCacheSize());
127+
}
128+
items.push(counter);
129+
}
130+
}
131+
132+
const page = new Page<Counter>();
133+
page.items = items;
134+
page.totalElements = body.page.totalElements;
135+
page.pageNumber = body.page.number;
136+
page.pageSize = body.page.size;
137+
page.totalPages = body.page.totalPages;
138+
this.counters.update(page);
139+
return page;
140+
}
39141
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="d3-chart" #chart></div>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.d3-chart {
2+
3+
.axis {
4+
5+
path, line {
6+
stroke: #999;
7+
}
8+
9+
text {
10+
fill: #999;
11+
}
12+
}
13+
}
14+
15+
.line {
16+
fill: none;
17+
stroke: steelblue;
18+
stroke-width: 2px;
19+
}
20+

0 commit comments

Comments
 (0)