Analytics - Improve look and feel and user experience (#1585)

* #1583 Improve look and feel and user experience

* #1583
Add layoutType property to report list
Improve unit test
Improve docs

* #1583 Review changes
This commit is contained in:
Maurizio Vitale 2017-02-03 16:12:17 +00:00 committed by Mario Romano
parent 6431d7c04f
commit 30b4db8161
13 changed files with 318 additions and 61 deletions

View File

@ -112,7 +112,7 @@ Follow the 3 steps below:
The component shows the list of all the available reports The component shows the list of all the available reports
```html ```html
<analytics-report-list></analytics-report-list> <analytics-report-list [layoutType]="'LIST'"></analytics-report-list>
``` ```
Usage example of this component : Usage example of this component :
@ -132,7 +132,7 @@ import { AnalyticsModule } from 'ng2-activiti-analytics';
<div class="page-content"> <div class="page-content">
<div class="mdl-grid"> <div class="mdl-grid">
<div class="mdl-cell mdl-cell--8-col task-column mdl-shadow--2dp"> <div class="mdl-cell mdl-cell--8-col task-column mdl-shadow--2dp">
<analytics-report-list></analytics-report-list> <analytics-report-list [layoutType]="'LIST'"></analytics-report-list>
</div> </div>
</div> </div>
</div>` </div>`
@ -179,7 +179,9 @@ platformBrowserDynamic().bootstrapModule(AppModule);
#### Options #### Options
No options. | Name | Type | Required | Description |
| --- | --- | --- | --- |
| `layoutType` | {string} | required | Define the layout of the apps. There are two possible values: GRID or LIST. LIST is the default value|
## Basic usage example Activiti Analytics ## Basic usage example Activiti Analytics
@ -258,6 +260,29 @@ platformBrowserDynamic().bootstrapModule(AppModule);
|`reportId` | The report id | |`reportId` | The report id |
|`debug` | Flag to enable or disable the Form values in the console log | |`debug` | Flag to enable or disable the Form values in the console log |
## Basic usage example Analytics Generator
The component generate and show the charts
```html
<activiti-analytics-generator [reportId]="reportId" [reportParamQuery]="reportParamQuery"></activiti-analytics>
```
#### Events
| Name | Description |
| --- | --- |
|`onSuccess` | The event is emitted when the charts are loaded |
|`onError` | The event is emitted when an error occur during the loading |
#### Options
| Name | Description |
| --- | --- |
|`reportId` | The report id |
|`reportParamQuery` | The object contains all the parameters that the report needs |
## Build from sources ## Build from sources
Alternatively you can build component from sources with the following commands: Alternatively you can build component from sources with the following commands:

View File

@ -3,3 +3,8 @@
.analytics-row__entry { .analytics-row__entry {
cursor: pointer; cursor: pointer;
} }
.report-icons {
margin: 20px 20px 20px 20px;
float: right;
}

View File

@ -1,9 +1,18 @@
<div *ngIf="reports"> <div *ngIf="reports">
<div *ngFor="let report of reports"> <div class="report-icons">
<h4>{{report.title}}</h4> <button mdTooltip="{{report.title}}" (click)="selectCurrent(idx)"
[class.mdl-button--accent]="isCurrent(idx)"
class="mdl-button mdl-js-button"
*ngFor="let report of reports; let idx = index">
<i class="material-icons">{{report.icon}}</i>
</button>
</div>
<div style="clear: both"> </div>
<div *ngFor="let report of reports; let idx = index">
<div [ngSwitch]="report.type"> <div [ngSwitch]="report.type">
<div *ngSwitchCase="'pie'"> <div *ngSwitchCase="'pie'">
<div class="col-md-6"> <div class="col-md-6" *ngIf="isCurrent(idx)">
<h4>{{report.title}}</h4>
<div *ngIf="!report.hasData()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div> <div *ngIf="!report.hasData()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div>
<div *ngIf="report.hasData()"> <div *ngIf="report.hasData()">
<div *ngIf="report.hasZeroValues()">{{'ANALYTICS.MESSAGES.ZERO-DATA-FOUND' | translate}}</div> <div *ngIf="report.hasZeroValues()">{{'ANALYTICS.MESSAGES.ZERO-DATA-FOUND' | translate}}</div>
@ -14,44 +23,51 @@
</div> </div>
</div> </div>
</div> </div>
<div *ngSwitchCase="'table'"> <div *ngSwitchCase="'table'" >
<div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div> <div *ngIf="isCurrent(idx)">
<div [attr.id]="'chart-table-' + report.id" *ngIf="report.hasDatasets()"> <h4>{{report.title}}</h4>
<table class="table table-responsive table-condensed" style="width: 100%"> <div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div>
<tr> <div [attr.id]="'chart-table-' + report.id" *ngIf="report.hasDatasets()">
<th *ngFor="let label of report.labels">{{label | translate}}</th> <table class="table table-responsive table-condensed" style="width: 80%;margin-left: 20px">
</tr> <tr>
<tr *ngFor="let rows of report.datasets" style="text-align: center;"> <th *ngFor="let label of report.labels">{{label | translate}}</th>
<td *ngFor="let row of rows">{{row | translate }}</td> </tr>
</tr> <tr *ngFor="let rows of report.datasets">
</table> <td *ngFor="let row of rows">{{row | translate }}</td>
</tr>
</table>
</div>
</div> </div>
</div> </div>
<div *ngSwitchCase="'masterDetailTable'"> <div *ngSwitchCase="'masterDetailTable'" >
<div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div> <div *ngIf="isCurrent(idx)">
<div [attr.id]="'chart-master-detail-table-' + report.id" *ngIf="report.hasDatasets()"> <h4>{{report.title}}</h4>
<table class="table table-responsive table-condensed" style="width: 100%"> <div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div>
<tr> <div [attr.id]="'chart-master-detail-table-' + report.id" *ngIf="report.hasDatasets()">
<th *ngFor="let label of report.labels">{{label | translate}}</th> <table class="table table-responsive table-condensed" style="width: 100%">
</tr> <tr>
<tr *ngFor="let rows of report.datasets" class="analytics-row__entry" style="text-align: center;"> <th *ngFor="let label of report.labels">{{label | translate}}</th>
<td *ngFor="let row of rows" (click)="toggleDetailsTable()">{{row | translate }}</td> </tr>
</tr> <tr *ngFor="let rows of report.datasets" class="analytics-row__entry">
</table> <td *ngFor="let row of rows" (click)="toggleDetailsTable()">{{row | translate }}</td>
</div> </tr>
<div [attr.id]="'chart-master-detail-' + report.id" *ngIf="isShowDetails()"> </table>
<table class="table table-responsive table-condensed" style="width: 100%"> </div>
<tr> <div [attr.id]="'chart-master-detail-' + report.id" *ngIf="isShowDetails()">
<th *ngFor="let label of report.detailsTable.labels">{{label | translate}}</th> <table class="table table-responsive table-condensed" style="width: 100%">
</tr> <tr>
<tr *ngFor="let rows of report.detailsTable.datasets" style="text-align: center;"> <th *ngFor="let label of report.detailsTable.labels">{{label | translate}}</th>
<td *ngFor="let row of rows">{{row | translate }}</td> </tr>
</tr> <tr *ngFor="let rows of report.detailsTable.datasets">
</table> <td *ngFor="let row of rows">{{row | translate }}</td>
</tr>
</table>
</div>
</div> </div>
</div> </div>
<div *ngSwitchCase="'bar'"> <div *ngSwitchCase="'bar'">
<div class="col-md-6"> <div class="col-md-6" *ngIf="isCurrent(idx)">
<h4>{{report.title}}</h4>
<div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div> <div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div>
<base-chart *ngIf="report.hasDatasets()" class="chart" <base-chart *ngIf="report.hasDatasets()" class="chart"
[datasets]="report.datasets" [datasets]="report.datasets"
@ -61,7 +77,8 @@
</div> </div>
</div> </div>
<div *ngSwitchCase="'multiBar'"> <div *ngSwitchCase="'multiBar'">
<div class="col-md-6"> <div class="col-md-6" *ngIf="isCurrent(idx)">
<h4>{{report.title}}</h4>
<div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div> <div *ngIf="!report.hasDatasets()">{{'ANALYTICS.MESSAGES.NO-DATA-FOUND' | translate}}</div>
<div *ngIf="report.hasDatasets()"> <div *ngIf="report.hasDatasets()">
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" [attr.for]="'stacked-id'"> <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" [attr.for]="'stacked-id'">
@ -80,7 +97,10 @@
</div> </div>
</div> </div>
<div *ngSwitchCase="'HeatMap'"> <div *ngSwitchCase="'HeatMap'">
<analytics-report-heat-map [report]="report"></analytics-report-heat-map> <div *ngIf="isCurrent(idx)">
<h4>{{report.title}}</h4>
<analytics-report-heat-map [report]="report"></analytics-report-heat-map>
</div>
</div> </div>
<div *ngSwitchDefault> <div *ngSwitchDefault>
<span>{{'ANALYTICS.MESSAGES.UNKNOWN-WIDGET-TYPE' | translate}}: {{report.type}}</span> <span>{{'ANALYTICS.MESSAGES.UNKNOWN-WIDGET-TYPE' | translate}}: {{report.type}}</span>

View File

@ -135,6 +135,44 @@ describe('AnalyticsGeneratorComponent', () => {
}); });
}); });
it('Should render the Process definition overview report when onchanges is called ', (done) => {
component.onSuccess.subscribe((res) => {
expect(res).toBeDefined();
expect(res.length).toEqual(3);
expect(res[0]).toBeDefined();
expect(res[0].type).toEqual('table');
expect(res[0].datasets).toBeDefined();
expect(res[0].datasets.length).toEqual(4);
expect(res[0].datasets[0][0]).toEqual('__KEY_REPORTING.DEFAULT-REPORTS.PROCESS-DEFINITION-OVERVIEW.GENERAL-TABLE-TOTAL-PROCESS-DEFINITIONS');
expect(res[0].datasets[0][1]).toEqual('9');
expect(res[0].datasets[1][0]).toEqual('__KEY_REPORTING.DEFAULT-REPORTS.PROCESS-DEFINITION-OVERVIEW.GENERAL-TABLE-TOTAL-PROCESS-INSTANCES');
expect(res[0].datasets[1][1]).toEqual('41');
expect(res[0].datasets[2][0]).toEqual('__KEY_REPORTING.DEFAULT-REPORTS.PROCESS-DEFINITION-OVERVIEW.GENERAL-TABLE-ACTIVE-PROCESS-INSTANCES');
expect(res[0].datasets[2][1]).toEqual('3');
expect(res[0].datasets[3][0]).toEqual('__KEY_REPORTING.DEFAULT-REPORTS.PROCESS-DEFINITION-OVERVIEW.GENERAL-TABLE-COMPLETED-PROCESS-INSTANCES');
expect(res[0].datasets[3][1]).toEqual('38');
expect(res[1]).toBeDefined();
expect(res[1].type).toEqual('pie');
expect(res[2]).toBeDefined();
expect(res[2].type).toEqual('table');
done();
});
component.reportId = 1001;
component.reportParamQuery = new ReportQuery({status: 'All'});
component.ngOnChanges();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: analyticMock.chartProcessDefOverview
});
});
it('Should render the Task overview report ', (done) => { it('Should render the Task overview report ', (done) => {
component.onSuccess.subscribe((res) => { component.onSuccess.subscribe((res) => {
expect(res).toBeDefined(); expect(res).toBeDefined();

View File

@ -44,6 +44,7 @@ export class AnalyticsGeneratorComponent implements OnChanges {
reports: Chart[]; reports: Chart[];
showDetails: boolean = false; showDetails: boolean = false;
currentChartPosition: number;
public barChartOptions: any = { public barChartOptions: any = {
responsive: true, responsive: true,
@ -83,6 +84,9 @@ export class AnalyticsGeneratorComponent implements OnChanges {
this.analyticsService.getReportsByParams(reportId, reportParamQuery).subscribe( this.analyticsService.getReportsByParams(reportId, reportParamQuery).subscribe(
(res: Chart[]) => { (res: Chart[]) => {
this.reports = res; this.reports = res;
if (this.reports) {
this.selectFirstReport();
}
this.onSuccess.emit(res); this.onSuccess.emit(res);
}, },
(err: any) => { (err: any) => {
@ -116,4 +120,16 @@ export class AnalyticsGeneratorComponent implements OnChanges {
isShowDetails(): boolean { isShowDetails(): boolean {
return this.showDetails; return this.showDetails;
} }
isCurrent(position: number) {
return position === this.currentChartPosition ? true : false;
}
selectCurrent(position: number) {
this.currentChartPosition = position;
}
selectFirstReport() {
this.selectCurrent(0);
}
} }

View File

@ -16,4 +16,37 @@
.activiti-filters__entry.active .activiti-filters__entry-icon { .activiti-filters__entry.active .activiti-filters__entry-icon {
color: rgb(68,138,255); color: rgb(68,138,255);
} }
.application-title {
color: white;
z-index: 7;
}
.logo {
position: absolute;
right: 20px;
top: 35px;
z-index: 6;
}
.logo i{
font-size: 70px;
}
.theme-1 {
background-color: #269abc;
}
.theme-1 .logo i {
color: #168aac;
}
.theme-1 .mdl-card__actions i {
color: #168aac;
}
.theme-1 .mdl-card__actions i:hover {
color: #b7dfea;
}
.selectedIcon{
color: #e9f1f3!important;
}

View File

@ -1,5 +1,5 @@
<div class="menu-container"> <div class="menu-container">
<ul class='mdl-list'> <ul class='mdl-list' *ngIf="isList()">
<li class="mdl-list__item activiti-filters__entry" (click)="selectReport(report)" *ngFor="let report of reports; let idx = index" <li class="mdl-list__item activiti-filters__entry" (click)="selectReport(report)" *ngFor="let report of reports; let idx = index"
[class.active]="currentReport === report"> [class.active]="currentReport === report">
<span [attr.id]="'report-list-' + idx" class="mdl-list__item-primary-content"> <span [attr.id]="'report-list-' + idx" class="mdl-list__item-primary-content">
@ -8,4 +8,18 @@
</span> </span>
</li> </li>
</ul> </ul>
</div> <div class="mdl-grid" *ngIf="isGrid()">
<div (click)="selectReport(report)" [ngClass]="['mdl-card mdl-cell', 'theme-1']" *ngFor="let report of reports;">
<div class="logo"><i class="material-icons">equalizer</i></div>
<div class="mdl-card__title">
<h1 class="mdl-card__title-text application-title">{{report.name}}</h1>
</div>
<div class="mdl-card__supporting-text">
<p>{{report.description}}</p>
</div>
<div class="mdl-card__actions mdl-card--border">
<i class="material-icons selectedIcon" *ngIf="isSelected(report)">done</i>
</div>
</div>
</div>
</div>

View File

@ -21,6 +21,7 @@ import { Observable } from 'rxjs/Rx';
import { CoreModule, AlfrescoTranslationService } from 'ng2-alfresco-core'; import { CoreModule, AlfrescoTranslationService } from 'ng2-alfresco-core';
import { AnalyticsReportListComponent } from '../components/analytics-report-list.component'; import { AnalyticsReportListComponent } from '../components/analytics-report-list.component';
import { AnalyticsService } from '../services/analytics.service'; import { AnalyticsService } from '../services/analytics.service';
import { ReportParametersModel } from '../models/report.model';
declare let jasmine: any; declare let jasmine: any;
@ -166,6 +167,59 @@ describe('AnalyticsReportListComponent', () => {
component.selectReport(reportSelected); component.selectReport(reportSelected);
}); });
it('Should return true if the current report is selected', () => {
component.selectReport(reportSelected);
expect(component.isSelected(reportSelected)).toBe(true);
});
it('Should return false if the current report is different', () => {
component.selectReport(reportSelected);
let anotherReport = {'id': 111, 'name': 'Another Fake Test Process definition overview'};
expect(component.isSelected(anotherReport)).toBe(false);
});
it('Should reload the report list', (done) => {
component.initObserver();
let report = new ReportParametersModel({'id': 2002, 'name': 'Fake Test Process definition heat map'});
component.reports = [report];
expect(component.reports.length).toEqual(1);
component.reload();
component.onSuccess.subscribe(() => {
expect(component.reports.length).toEqual(5);
done();
});
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
contentType: 'json',
responseText: reportList
});
});
});
describe('layout', () => {
it('should display a list by default', () => {
fixture.detectChanges();
expect(component.isGrid()).toBe(false);
expect(component.isList()).toBe(true);
});
it('should display a grid when configured to', () => {
component.layoutType = AnalyticsReportListComponent.LAYOUT_GRID;
fixture.detectChanges();
expect(component.isGrid()).toBe(true);
expect(component.isList()).toBe(false);
});
it('should display a list when configured to', () => {
component.layoutType = AnalyticsReportListComponent.LAYOUT_LIST;
fixture.detectChanges();
expect(component.isGrid()).toBe(false);
expect(component.isList()).toBe(true);
});
}); });
}); });

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component, EventEmitter, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, OnInit, Output, Input } from '@angular/core';
import { Observer, Observable } from 'rxjs/Rx'; import { Observer, Observable } from 'rxjs/Rx';
import { LogService } from 'ng2-alfresco-core'; import { LogService } from 'ng2-alfresco-core';
import { AnalyticsService } from '../services/analytics.service'; import { AnalyticsService } from '../services/analytics.service';
@ -29,6 +29,12 @@ import { ReportParametersModel } from '../models/report.model';
}) })
export class AnalyticsReportListComponent implements OnInit { export class AnalyticsReportListComponent implements OnInit {
public static LAYOUT_LIST: string = 'LIST';
public static LAYOUT_GRID: string = 'GRID';
@Input()
layoutType: string = AnalyticsReportListComponent.LAYOUT_LIST;
@Output() @Output()
reportClick: EventEmitter<ReportParametersModel> = new EventEmitter<ReportParametersModel>(); reportClick: EventEmitter<ReportParametersModel> = new EventEmitter<ReportParametersModel>();
@ -51,11 +57,15 @@ export class AnalyticsReportListComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.initObserver();
this.getReportList();
}
initObserver() {
this.report$.subscribe((report: ReportParametersModel) => { this.report$.subscribe((report: ReportParametersModel) => {
this.reports.push(report); this.reports.push(report);
}); });
this.getReportList();
} }
/** /**
@ -131,4 +141,16 @@ export class AnalyticsReportListComponent implements OnInit {
this.currentReport = report; this.currentReport = report;
this.reportClick.emit(report); this.reportClick.emit(report);
} }
isSelected(report: any) {
return this.currentReport === report ? true : false;
}
isList() {
return this.layoutType === AnalyticsReportListComponent.LAYOUT_LIST;
}
isGrid() {
return this.layoutType === AnalyticsReportListComponent.LAYOUT_GRID;
}
} }

View File

@ -56,7 +56,7 @@
} }
.report-container { .report-container {
border: solid 1px rgb(212, 212, 212); border-bottom: solid 1px rgb(212, 212, 212);
padding: 10px 10px 10px 10px; padding: 10px 10px 10px 10px;
} }

View File

@ -1,11 +1,5 @@
<div class="report-container"> <div class="report-container">
<a class="mdl-navigation__link setting-button" data-automation-id="settings"> <div class="col-md-6">
<button (click)="toggleParameters()" class="mdl-button mdl-js-button mdl-button--fab mdl-button--colored">
<i class="material-icons">settings</i>
</button>
<span class="report-container-setting">{{'ANALYTICS.MESSAGES.SETTING-TITLE' | translate}}</span>
</a>
<div class="col-md-6" [class.is-hide]="isParametersHide()" >
<div *ngIf="reportParameters"> <div *ngIf="reportParameters">
<form [formGroup]="reportForm" novalidate> <form [formGroup]="reportForm" novalidate>
<div *ngIf="isEditable"> <div *ngIf="isEditable">
@ -21,12 +15,15 @@
/> />
</div> </div>
<div *ngIf="!isEditable"> <div *ngIf="!isEditable">
<span class="icon-small"> <button mdTooltip="{{'ANALYTICS.MESSAGES.SETTING-TITLE' | translate}}" (click)="toggleParameters()" class="mdl-button mdl-js-button" style="float: right">
<i class="material-icons">settings</i>
</button>
<div class="icon-small">
<i class="material-icons">mode_edit</i> <i class="material-icons">mode_edit</i>
<h4 (click)="editEnable()">{{reportParameters.name}}</h4> <h4 (click)="editEnable()">{{reportParameters.name}}</h4>
</span> </div>
</div><hr> </div>
<div *ngFor="let field of reportParameters.definition.parameters"> <div *ngFor="let field of reportParameters.definition.parameters" [class.is-hide]="isParametersHide()">
<div [ngSwitch]="field.type"> <div [ngSwitch]="field.type">
<div *ngSwitchCase="'integer'"> <div *ngSwitchCase="'integer'">
<br> <br>

View File

@ -6,7 +6,7 @@
"FILL-PARAMETER": "Fill in the parameters to generate your report", "FILL-PARAMETER": "Fill in the parameters to generate your report",
"NO-DATA-FOUND": "No data found", "NO-DATA-FOUND": "No data found",
"ZERO-DATA-FOUND": "There are only zero values", "ZERO-DATA-FOUND": "There are only zero values",
"SETTING-TITLE": "Change report setting" "SETTING-TITLE": "Settings"
} }
}, },
"__KEY_REPORTING": { "__KEY_REPORTING": {

View File

@ -20,11 +20,13 @@ import * as moment from 'moment';
export class Chart { export class Chart {
id: string; id: string;
type: string; type: string;
icon: string;
constructor(obj?: any) { constructor(obj?: any) {
this.id = obj && obj.id || null; this.id = obj && obj.id || null;
if (obj && obj.type) { if (obj && obj.type) {
this.type = this.convertType(obj.type); this.type = this.convertType(obj.type);
this.icon = this.getIconType(this.type);
} }
} }
@ -58,6 +60,37 @@ export class Chart {
} }
return chartType; return chartType;
} }
private getIconType(type: string): string {
let typeIcon: string = '';
switch (type) {
case 'pie':
typeIcon = 'pie_chart';
break;
case 'table':
typeIcon = 'web';
break;
case 'line':
typeIcon = 'show_chart';
break;
case 'bar':
typeIcon = 'equalizer';
break;
case 'multiBar':
typeIcon = 'poll';
break;
case 'HeatMap':
typeIcon = 'share';
break;
case 'masterDetailTable':
typeIcon = 'subtitles';
break;
default:
typeIcon = 'web';
break;
}
return typeIcon;
}
} }
export class LineChart extends Chart { export class LineChart extends Chart {