diff --git a/apps/red-ui/src/app/app.module.ts b/apps/red-ui/src/app/app.module.ts index 907a54f0d..65081d387 100644 --- a/apps/red-ui/src/app/app.module.ts +++ b/apps/red-ui/src/app/app.module.ts @@ -112,6 +112,8 @@ import { DownloadsListScreenComponent } from './screens/downloads-list-screen/do import { DigitalSignatureScreenComponent } from './screens/admin/digital-signature-screen/digital-signature-screen.component'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { RemoveAnnotationsDialogComponent } from './dialogs/remove-annotations-dialog/remove-annotations-dialog.component'; +import { NgxChartsModule } from '@swimlane/ngx-charts'; +import { ComboChartComponent, ComboSeriesVerticalComponent } from './screens/admin/license-information-screen/combo-chart'; export function HttpLoaderFactory(httpClient: HttpClient) { return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json'); @@ -371,7 +373,9 @@ const matImports = [ EditColorDialogComponent, DownloadsListScreenComponent, DigitalSignatureScreenComponent, - RemoveAnnotationsDialogComponent + RemoveAnnotationsDialogComponent, + ComboChartComponent, + ComboSeriesVerticalComponent ], imports: [ BrowserModule, @@ -382,6 +386,7 @@ const matImports = [ AuthModule, IconsModule, ApiModule, + NgxChartsModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.html b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.html new file mode 100644 index 000000000..5cffc2f32 --- /dev/null +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.html @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.scss b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.scss new file mode 100644 index 000000000..708586e9e --- /dev/null +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.scss @@ -0,0 +1,88 @@ +.ngx-charts { + float: left; + overflow: visible; + + .circle, + .bar, + .arc { + cursor: pointer; + } + + .bar, + .cell, + .arc, + .card { + &.active, + &:hover { + opacity: 0.8; + transition: opacity 100ms ease-in-out; + } + + &:focus { + outline: none; + } + &.hidden { + display: none; + } + } + + g { + &:focus { + outline: none; + } + } + + .line-series, + .line-series-range, + .area-series { + &.inactive { + transition: opacity 100ms ease-in-out; + opacity: 0.2; + } + } + + .line-highlight { + display: none; + + &.active { + display: block; + } + } + + .area { + opacity: 0.6; + } + + .circle { + &:hover { + cursor: pointer; + } + } + + .label { + font-size: 12px; + font-weight: normal; + } + + .tooltip-anchor { + fill: rgb(0, 0, 0); + } + + .gridline-path { + stroke: #ddd; + stroke-width: 1; + fill: none; + } + + .grid-panel { + rect { + fill: none; + } + + &.odd { + rect { + fill: rgba(0, 0, 0, 0.05); + } + } + } +} diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.ts b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.ts new file mode 100644 index 000000000..47e88c180 --- /dev/null +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-chart.component.ts @@ -0,0 +1,388 @@ +import { Component, Input, ViewEncapsulation, Output, EventEmitter, ViewChild, HostListener, ContentChild, TemplateRef } from '@angular/core'; + +import { curveLinear } from 'd3-shape'; +import { scaleBand, scaleLinear, scalePoint, scaleTime } from 'd3-scale'; +import { BaseChartComponent, LineSeriesComponent, ViewDimensions, ColorHelper, calculateViewDimensions } from '@swimlane/ngx-charts'; + +@Component({ + // tslint:disable-next-line: component-selector + selector: 'combo-chart-component', + templateUrl: './combo-chart.component.html', + styleUrls: ['./combo-chart.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ComboChartComponent extends BaseChartComponent { + @Input() curve: any = curveLinear; + @Input() legend = false; + @Input() legendTitle = 'Legend'; + @Input() legendPosition = 'right'; + @Input() xAxis; + @Input() yAxis; + @Input() showXAxisLabel; + @Input() showYAxisLabel; + @Input() showRightYAxisLabel; + @Input() xAxisLabel; + @Input() yAxisLabel; + @Input() yAxisLabelRight; + @Input() tooltipDisabled = false; + @Input() gradient: boolean; + @Input() showGridLines = true; + @Input() activeEntries: any[] = []; + @Input() schemeType: string; + @Input() xAxisTickFormatting: any; + @Input() yAxisTickFormatting: any; + @Input() yRightAxisTickFormatting: any; + @Input() roundDomains = false; + @Input() colorSchemeLine: any; + @Input() autoScale; + @Input() lineChart: any; + @Input() yLeftAxisScaleFactor: any; + @Input() yRightAxisScaleFactor: any; + @Input() rangeFillOpacity: number; + @Input() animations = true; + @Input() noBarWhenZero = true; + + @Output() activate: EventEmitter = new EventEmitter(); + @Output() deactivate: EventEmitter = new EventEmitter(); + + @ContentChild('tooltipTemplate') tooltipTemplate: TemplateRef; + @ContentChild('seriesTooltipTemplate') seriesTooltipTemplate: TemplateRef; + + @ViewChild(LineSeriesComponent) lineSeriesComponent: LineSeriesComponent; + + dims: ViewDimensions; + xScale: any; + yScale: any; + xDomain: any; + yDomain: any; + transform: string; + colors: ColorHelper; + colorsLine: ColorHelper; + margin: any[] = [10, 20, 10, 20]; + xAxisHeight = 0; + yAxisWidth = 0; + legendOptions: any; + scaleType = 'linear'; + xScaleLine; + yScaleLine; + xDomainLine; + yDomainLine; + seriesDomain; + scaledAxis; + combinedSeries; + xSet; + filteredDomain; + hoveredVertical; + yOrientLeft = 'left'; + yOrientRight = 'right'; + legendSpacing = 0; + bandwidth; + barPadding = 8; + + trackBy(index, item): string { + return item.name; + } + + update(): void { + super.update(); + this.dims = calculateViewDimensions({ + width: this.width, + height: this.height, + margins: this.margin, + showXAxis: this.xAxis, + showYAxis: this.yAxis, + xAxisHeight: this.xAxisHeight, + yAxisWidth: this.yAxisWidth, + showXLabel: this.showXAxisLabel, + showYLabel: this.showYAxisLabel, + showLegend: this.legend, + legendType: this.schemeType, + legendPosition: this.legendPosition + }); + + if (!this.yAxis) { + this.legendSpacing = 0; + } else if (this.showYAxisLabel && this.yAxis) { + this.legendSpacing = 100; + } else { + this.legendSpacing = 40; + } + this.xScale = this.getXScale(); + this.yScale = this.getYScale(); + + // line chart + this.xDomainLine = this.getXDomainLine(); + if (this.filteredDomain) { + this.xDomainLine = this.filteredDomain; + } + + this.yDomainLine = this.getYDomainLine(); + this.seriesDomain = this.getSeriesDomain(); + + this.scaleLines(); + + this.setColors(); + this.legendOptions = this.getLegendOptions(); + + this.transform = `translate(${this.dims.xOffset} , ${this.margin[0]})`; + } + + deactivateAll() { + this.activeEntries = [...this.activeEntries]; + for (const entry of this.activeEntries) { + this.deactivate.emit({ value: entry, entries: [] }); + } + this.activeEntries = []; + } + + @HostListener('mouseleave') + hideCircles(): void { + this.hoveredVertical = null; + this.deactivateAll(); + } + + updateHoveredVertical(item): void { + this.hoveredVertical = item.value; + this.deactivateAll(); + } + + updateDomain(domain): void { + this.filteredDomain = domain; + this.xDomainLine = this.filteredDomain; + this.xScaleLine = this.getXScaleLine(this.xDomainLine, this.dims.width); + } + + scaleLines() { + this.xScaleLine = this.getXScaleLine(this.xDomainLine, this.dims.width); + this.yScaleLine = this.getYScaleLine(this.yDomainLine, this.dims.height); + } + + getSeriesDomain(): any[] { + this.combinedSeries = this.lineChart.slice(0); + this.combinedSeries.push({ + name: this.yAxisLabel, + series: this.results + }); + return this.combinedSeries.map((d) => d.name); + } + + isDate(value): boolean { + if (value instanceof Date) { + return true; + } + + return false; + } + + getScaleType(values): string { + let date = true; + let num = true; + + for (const value of values) { + if (!this.isDate(value)) { + date = false; + } + + if (typeof value !== 'number') { + num = false; + } + } + + if (date) return 'time'; + if (num) return 'linear'; + return 'ordinal'; + } + + getXDomainLine(): any[] { + let values = []; + + for (const results of this.lineChart) { + for (const d of results.series) { + if (!values.includes(d.name)) { + values.push(d.name); + } + } + } + + this.scaleType = this.getScaleType(values); + let domain = []; + + if (this.scaleType === 'time') { + const min = Math.min(...values); + const max = Math.max(...values); + domain = [min, max]; + } else if (this.scaleType === 'linear') { + values = values.map((v) => Number(v)); + const min = Math.min(...values); + const max = Math.max(...values); + domain = [min, max]; + } else { + domain = values; + } + + this.xSet = values; + return domain; + } + + getYDomainLine(): any[] { + const domain = []; + + for (const results of this.lineChart) { + for (const d of results.series) { + if (domain.indexOf(d.value) < 0) { + domain.push(d.value); + } + if (d.min !== undefined) { + if (domain.indexOf(d.min) < 0) { + domain.push(d.min); + } + } + if (d.max !== undefined) { + if (domain.indexOf(d.max) < 0) { + domain.push(d.max); + } + } + } + } + + let min = Math.min(...domain); + const max = Math.max(...domain); + if (this.yRightAxisScaleFactor) { + const minMax = this.yRightAxisScaleFactor(min, max); + return [Math.min(0, minMax.min), minMax.max]; + } else { + min = Math.min(0, min); + return [min, max]; + } + } + + getXScaleLine(domain, width): any { + let scale; + if (this.bandwidth === undefined) { + this.bandwidth = width - this.barPadding; + } + const offset = Math.floor((width + this.barPadding - (this.bandwidth + this.barPadding) * domain.length) / 2); + + if (this.scaleType === 'time') { + scale = scaleTime().range([0, width]).domain(domain); + } else if (this.scaleType === 'linear') { + scale = scaleLinear().range([0, width]).domain(domain); + + if (this.roundDomains) { + scale = scale.nice(); + } + } else if (this.scaleType === 'ordinal') { + scale = scalePoint() + .range([offset + this.bandwidth / 2, width - offset - this.bandwidth / 2]) + .domain(domain); + } + + return scale; + } + + getYScaleLine(domain, height): any { + const scale = scaleLinear().range([height, 0]).domain(domain); + + return this.roundDomains ? scale.nice() : scale; + } + + getXScale(): any { + this.xDomain = this.getXDomain(); + const spacing = this.xDomain.length / (this.dims.width / this.barPadding + 1); + return scaleBand().range([0, this.dims.width]).paddingInner(spacing).domain(this.xDomain); + } + + getYScale(): any { + this.yDomain = this.getYDomain(); + const scale = scaleLinear().range([this.dims.height, 0]).domain(this.yDomain); + return this.roundDomains ? scale.nice() : scale; + } + + getXDomain(): any[] { + return this.results.map((d) => d.name); + } + + getYDomain() { + const values = this.results.map((d) => d.value); + const min = Math.min(0, ...values); + const max = Math.max(...values); + if (this.yLeftAxisScaleFactor) { + const minMax = this.yLeftAxisScaleFactor(min, max); + return [Math.min(0, minMax.min), minMax.max]; + } else { + return [min, max]; + } + } + + onClick(data) { + this.select.emit(data); + } + + setColors(): void { + let domain; + if (this.schemeType === 'ordinal') { + domain = this.xDomain; + } else { + domain = this.yDomain; + } + this.colors = new ColorHelper(this.scheme, this.schemeType, domain, this.customColors); + this.colorsLine = new ColorHelper(this.colorSchemeLine, this.schemeType, domain, this.customColors); + } + + getLegendOptions() { + const opts = { + scaleType: this.schemeType, + colors: undefined, + domain: [], + title: undefined, + position: this.legendPosition + }; + if (opts.scaleType === 'ordinal') { + opts.domain = this.seriesDomain; + opts.colors = this.colorsLine; + opts.title = this.legendTitle; + } else { + opts.domain = this.seriesDomain; + opts.colors = this.colors.scale; + } + return opts; + } + + updateLineWidth(width): void { + this.bandwidth = width; + this.scaleLines(); + } + + updateYAxisWidth({ width }): void { + this.yAxisWidth = width + 20; + this.update(); + } + + updateXAxisHeight({ height }): void { + this.xAxisHeight = height; + this.update(); + } + + onActivate(item) { + const idx = this.activeEntries.findIndex((d) => { + return d.name === item.name && d.value === item.value && d.series === item.series; + }); + if (idx > -1) { + return; + } + + this.activeEntries = [item, ...this.activeEntries]; + this.activate.emit({ value: item, entries: this.activeEntries }); + } + + onDeactivate(item) { + const idx = this.activeEntries.findIndex((d) => { + return d.name === item.name && d.value === item.value && d.series === item.series; + }); + + this.activeEntries.splice(idx, 1); + this.activeEntries = [...this.activeEntries]; + + this.deactivate.emit({ value: item, entries: this.activeEntries }); + } +} diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-series-vertical.component.ts b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-series-vertical.component.ts new file mode 100644 index 000000000..0646b3eab --- /dev/null +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/combo-series-vertical.component.ts @@ -0,0 +1,189 @@ +import { Component, Input, Output, EventEmitter, OnChanges, ChangeDetectionStrategy } from '@angular/core'; +import { trigger, style, animate, transition } from '@angular/animations'; +import { formatLabel } from '@swimlane/ngx-charts'; + +@Component({ + // tslint:disable-next-line: component-selector + selector: 'g[ngx-combo-charts-series-vertical]', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [ + trigger('animationState', [ + transition('* => void', [ + style({ + opacity: 1, + transform: '*' + }), + animate(500, style({ opacity: 0, transform: 'scale(0)' })) + ]) + ]) + ] +}) +export class ComboSeriesVerticalComponent implements OnChanges { + @Input() dims; + @Input() type = 'standard'; + @Input() series; + @Input() seriesLine; + @Input() xScale; + @Input() yScale; + @Input() colors; + @Input() tooltipDisabled = false; + @Input() gradient: boolean; + @Input() activeEntries: any[]; + @Input() seriesName: string; + @Input() animations = true; + @Input() noBarWhenZero = true; + + @Output() activate = new EventEmitter(); + @Output() deactivate = new EventEmitter(); + @Output() bandwidth = new EventEmitter(); + + bars: any; + x: any; + y: any; + + ngOnChanges(changes): void { + this.update(); + } + + update(): void { + let width; + if (this.series.length) { + width = this.xScale.bandwidth(); + this.bandwidth.emit(width); + } + + let d0 = 0; + let total; + if (this.type === 'normalized') { + total = this.series.map((d) => d.value).reduce((sum, d) => sum + d, 0); + } + + this.bars = this.series.map((d, index) => { + let value = d.value; + const label = d.name; + const formattedLabel = formatLabel(label); + const roundEdges = this.type === 'standard'; + + const bar: any = { + value, + label, + roundEdges, + data: d, + width, + formattedLabel, + height: 0, + x: 0, + y: 0 + }; + + if (this.type === 'standard') { + bar.height = Math.abs(this.yScale(value) - this.yScale(0)); + bar.x = this.xScale(label); + + if (value < 0) { + bar.y = this.yScale(0); + } else { + bar.y = this.yScale(value); + } + } else if (this.type === 'stacked') { + const offset0 = d0; + const offset1 = offset0 + value; + d0 += value; + + bar.height = this.yScale(offset0) - this.yScale(offset1); + bar.x = 0; + bar.y = this.yScale(offset1); + bar.offset0 = offset0; + bar.offset1 = offset1; + } else if (this.type === 'normalized') { + let offset0 = d0; + let offset1 = offset0 + value; + d0 += value; + + if (total > 0) { + offset0 = (offset0 * 100) / total; + offset1 = (offset1 * 100) / total; + } else { + offset0 = 0; + offset1 = 0; + } + + bar.height = this.yScale(offset0) - this.yScale(offset1); + bar.x = 0; + bar.y = this.yScale(offset1); + bar.offset0 = offset0; + bar.offset1 = offset1; + value = (offset1 - offset0).toFixed(2) + '%'; + } + + if (this.colors.scaleType === 'ordinal') { + bar.color = this.colors.getColor(label); + } else { + if (this.type === 'standard') { + bar.color = this.colors.getColor(value); + bar.gradientStops = this.colors.getLinearGradientStops(value); + } else { + bar.color = this.colors.getColor(bar.offset1); + bar.gradientStops = this.colors.getLinearGradientStops(bar.offset1, bar.offset0); + } + } + + let tooltipLabel = formattedLabel; + if (this.seriesName) { + tooltipLabel = `${this.seriesName} • ${formattedLabel}`; + } + + this.getSeriesTooltips(this.seriesLine, index); + const lineValue = this.seriesLine[0].series[index].value; + bar.tooltipText = ` + ${tooltipLabel} + Y1 - ${value.toLocaleString()} • Y2 - ${lineValue.toLocaleString()}% + `; + + return bar; + }); + } + getSeriesTooltips(seriesLine, index) { + return seriesLine.map((d) => { + return d.series[index]; + }); + } + isActive(entry): boolean { + if (!this.activeEntries) return false; + const item = this.activeEntries.find((d) => { + return entry.name === d.name && entry.series === d.series; + }); + return item !== undefined; + } + + trackBy(index, bar): string { + return bar.label; + } +} diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/index.ts b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/index.ts new file mode 100644 index 000000000..2a0ab4f22 --- /dev/null +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/combo-chart/index.ts @@ -0,0 +1,2 @@ +export * from './combo-chart.component'; +export * from './combo-series-vertical.component'; diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.html b/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.html index f9a399fd3..d1122b62e 100644 --- a/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.html +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.html @@ -94,6 +94,26 @@ + + + diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.scss b/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.scss index 31c51d932..4e11ec7da 100644 --- a/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.scss +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.scss @@ -4,35 +4,46 @@ .left-container { width: 100vw; @include inset-shadow; -} + overflow: auto; + @include scroll-bar; -.grid-container { - display: grid; - grid-template-columns: 1fr 2fr; - margin: 20px; + display: flex; + flex-direction: column; + align-items: center; - .row { - display: contents; + .grid-container { + width: calc(100vw - 40px); + display: grid; + grid-template-columns: 1fr 2fr; + margin: 20px 20px 50px 20px; - > div { - padding: 8px 20px; + .row { + display: contents; - &:first-of-type { - font-weight: 600; - } - } - - &:hover { > div { - background-color: $grey-2; + padding: 8px 20px; + + &:first-of-type { + font-weight: 600; + } } + + &:hover { + > div { + background-color: $grey-2; + } + } + } + + .section-title { + grid-column: span 2; + padding: 20px 20px 8px; + margin-bottom: 8px; + border-bottom: 1px solid $separator; } } - .section-title { - grid-column: span 2; - padding: 20px 20px 8px; - margin-bottom: 8px; - border-bottom: 1px solid $separator; + combo-chart-component { + margin-bottom: 50px; } } diff --git a/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.ts b/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.ts index 90998b290..263d57718 100644 --- a/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.ts +++ b/apps/red-ui/src/app/screens/admin/license-information-screen/license-information-screen.component.ts @@ -1,16 +1,27 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { PermissionsService } from '../../../common/service/permissions.service'; import { LicenseReport, LicenseReportControllerService } from '@redaction/red-ui-http'; import { AppConfigService } from '../../../app-config/app-config.service'; import * as moment from 'moment'; import { TranslateService } from '@ngx-translate/core'; +import { Moment } from 'moment'; @Component({ selector: 'redaction-license-information-screen', templateUrl: './license-information-screen.component.html', styleUrls: ['./license-information-screen.component.scss'] }) -export class LicenseInformationScreenComponent { +export class LicenseInformationScreenComponent implements OnInit { + constructor( + public readonly permissionsService: PermissionsService, + private readonly _licenseReportController: LicenseReportControllerService, + public readonly appConfigService: AppConfigService, + private readonly _translateService: TranslateService + ) {} + + get currentYear(): number { + return new Date().getFullYear(); + } public currentInfo: LicenseReport = {}; public totalInfo: LicenseReport = {}; public unlicensedInfo: LicenseReport = {}; @@ -18,15 +29,31 @@ export class LicenseInformationScreenComponent { public analysisPercentageOfLicense = 100; public viewReady = false; - constructor( - public readonly permissionsService: PermissionsService, - private readonly _licenseReportController: LicenseReportControllerService, - public readonly appConfigService: AppConfigService, - private readonly _translateService: TranslateService - ) { + barChart: any[] = []; + lineChartSeries: any[] = []; + + yAxisLabel = this._translateService.instant('license-info-screen.chart.pages-per-month'); + yAxisLabelRight = this._translateService.instant('license-info-screen.chart.total-pages'); + + lineChartScheme = { + selectable: true, + group: 'Ordinal', + domain: ['#dd4d50', '#5ce594', '#0389ec'] + }; + + comboBarScheme = { + selectable: true, + group: 'Ordinal', + domain: ['#0389ec'] + }; + + public async ngOnInit() { this.totalLicensedNumberOfPages = this.appConfigService.getConfig('LICENSE_PAGE_COUNT', 0); const startDate = moment(this.appConfigService.getConfig('LICENSE_START'), 'DD-MM-YYYY'); const endDate = moment(this.appConfigService.getConfig('LICENSE_END'), 'DD-MM-YYYY'); + + await this._setMonthlyStats(startDate, endDate); + const currentConfig = { startDate: startDate.toDate(), endDate: endDate.toDate() @@ -48,8 +75,75 @@ export class LicenseInformationScreenComponent { }); } - get currentYear(): number { - return new Date().getFullYear(); + private async _setMonthlyStats(startDate: Moment, endDate: Moment) { + const [startMonth, startYear] = [startDate.month(), startDate.year()]; + const [endMonth, endYear] = [endDate.month(), endDate.year()]; + moment.locale(this._translateService.currentLang); + + let m = startMonth; + let y = startYear; + const totalLicensedSeries = []; + const cumulativePagesSeries = []; + const promises = []; + + while (m <= endMonth && y <= endYear) { + totalLicensedSeries.push({ + name: `${moment.localeData().monthsShort(moment([y, m]))} ${y}`, + value: this.totalLicensedNumberOfPages + }); + + let nm = m + 1; + let ny = y; + if (nm === 12) { + nm = 0; + ny++; + } + + promises.push( + this._licenseReportController + .licenseReport({ + startDate: moment(`01-${m + 1}-${y}`, 'DD-MM-YYYY').toDate(), + endDate: moment(`01-${nm + 1}-${ny}`, 'DD-MM-YYYY').toDate() + }) + .toPromise() + ); + + y = ny; + m = nm; + } + + const reports = await Promise.all(promises); + + m = startMonth; + y = startYear; + let cumulativePages = 0; + for (const report of reports) { + cumulativePages += report.numberOfAnalyzedPages; + this.barChart.push({ + name: `${moment.localeData().monthsShort(moment([y, m]))} ${y}`, + value: report.numberOfAnalyzedPages + }); + cumulativePagesSeries.push({ + name: `${moment.localeData().monthsShort(moment([y, m]))} ${y}`, + value: cumulativePages + }); + m++; + if (m === 12) { + m = 0; + y++; + } + } + + this.lineChartSeries = [ + { + name: this._translateService.instant('license-info-screen.chart.licensed-total'), + series: totalLicensedSeries + }, + { + name: this._translateService.instant('license-info-screen.chart.cumulative'), + series: cumulativePagesSeries + } + ]; } sendMail(): void { diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 0711b5924..8ce911a7d 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -775,6 +775,13 @@ "analyzed": "Total Analyzed Pages in current license period: {{pages}}.", "licensed": "Licensed Pages: {{pages}}." } + }, + "chart": { + "licensed-total": "Licensed Total", + "cumulative": "Cumulative Pages", + "pages-per-month": "Pages per Month", + "total-pages": "Total Pages", + "legend": "Legend" } }, "default-colors": "Default Colors", diff --git a/package.json b/package.json index 79999da15..8c745497b 100644 --- a/package.json +++ b/package.json @@ -1,107 +1,108 @@ { - "name": "redaction", - "version": "1.0.15", - "private": true, - "license": "MIT", - "scripts": { - "affected": "nx affected", - "affected:apps": "nx affected:apps", - "affected:build": "nx affected:build", - "affected:dep-graph": "nx affected:dep-graph", - "affected:e2e": "nx affected:e2e", - "affected:libs": "nx affected:libs", - "affected:lint": "nx affected:lint", - "affected:test": "nx affected:test", - "build": "nx build", - "build-lint-all": "ng lint --project=red-ui-http --fix && ng build --project=red-ui-http && ng lint --project=red-ui --fix && ng build --project=red-ui --prod", - "dep-graph": "nx dep-graph", - "e2e": "nx e2e", - "format": "nx format:write", - "format:check": "nx format:check", - "format:write": "nx format:write", - "help": "nx help", - "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", - "lint": "nx workspace-lint && nx lint", - "nx": "nx", - "start": "nx serve", - "test": "nx test", - "update": "nx migrate latest", - "workspace-schematic": "nx workspace-schematic" - }, - "husky": { - "hooks": { - "pre-commit": "pretty-quick --staged && ng lint --project=red-ui-http && ng lint --project=red-ui --fix" + "name": "redaction", + "version": "1.0.15", + "private": true, + "license": "MIT", + "scripts": { + "affected": "nx affected", + "affected:apps": "nx affected:apps", + "affected:build": "nx affected:build", + "affected:dep-graph": "nx affected:dep-graph", + "affected:e2e": "nx affected:e2e", + "affected:libs": "nx affected:libs", + "affected:lint": "nx affected:lint", + "affected:test": "nx affected:test", + "build": "nx build", + "build-lint-all": "ng lint --project=red-ui-http --fix && ng build --project=red-ui-http && ng lint --project=red-ui --fix && ng build --project=red-ui --prod", + "dep-graph": "nx dep-graph", + "e2e": "nx e2e", + "format": "nx format:write", + "format:check": "nx format:check", + "format:write": "nx format:write", + "help": "nx help", + "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", + "lint": "nx workspace-lint && nx lint", + "nx": "nx", + "start": "nx serve", + "test": "nx test", + "update": "nx migrate latest", + "workspace-schematic": "nx workspace-schematic" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged && ng lint --project=red-ui-http && ng lint --project=red-ui --fix" + } + }, + "dependencies": { + "@angular/animations": "~11.0.1", + "@angular/cdk": "~11.0.1", + "@angular/common": "~11.0.1", + "@angular/compiler": "~11.0.1", + "@angular/core": "~11.0.1", + "@angular/forms": "~11.0.1", + "@angular/material": "~11.0.1", + "@angular/material-moment-adapter": "^11.0.2", + "@angular/platform-browser": "~11.0.1", + "@angular/platform-browser-dynamic": "~11.0.1", + "@angular/router": "~11.0.1", + "@angular/service-worker": "~11.0.1", + "@ngx-translate/core": "^13.0.0", + "@ngx-translate/http-loader": "^6.0.0", + "@nrwl/angular": "^10.2.0", + "@pdftron/webviewer": "^7.2.0", + "@swimlane/ngx-charts": "^17.0.0", + "file-saver": "^2.0.2", + "jwt-decode": "^3.0.0", + "keycloak-angular": "^8.0.1", + "keycloak-js": "10.0.2", + "lint-staged": "^10.5.0", + "ng2-ace-editor": "^0.3.9", + "ngp-sort-pipe": "^0.0.4", + "ngx-color-picker": "^10.1.0", + "ngx-dropzone": "^2.2.2", + "ngx-toastr": "^13.0.0", + "rxjs": "~6.6.0", + "scroll-into-view-if-needed": "^2.2.26", + "streamsaver": "^2.0.5", + "tslib": "^2.0.0", + "zone.js": "~0.10.2" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~0.1100.2", + "@angular-devkit/build-ng-packagr": "~0.1002.0", + "@angular/cli": "~11.0.2", + "@angular/compiler": "~11.0.1", + "@angular/compiler-cli": "~11.0.1", + "@angular/language-service": "~11.0.2", + "@nrwl/cypress": "10.2.0", + "@nrwl/jest": "10.2.0", + "@nrwl/workspace": "10.2.0", + "@types/cypress": "^1.1.3", + "@types/jasmine": "~3.6.0", + "@types/jest": "26.0.8", + "@types/node": "^12.11.1", + "codelyzer": "^6.0.0", + "cypress": "^5.6.0", + "cypress-file-upload": "^4.1.1", + "cypress-keycloak": "^1.5.0", + "cypress-keycloak-commands": "^1.2.0", + "cypress-localstorage-commands": "^1.2.4", + "dotenv": "6.2.0", + "eslint": "6.8.0", + "google-translate-api-browser": "^1.1.71", + "husky": "^4.3.0", + "jest": "26.2.2", + "jest-preset-angular": "8.2.1", + "lodash": "^4.17.20", + "moment": "^2.29.1", + "ng-packagr": "^10.1.2", + "prettier": "2.0.4", + "pretty-quick": "^3.1.0", + "superagent": "^6.1.0", + "superagent-promise": "^1.1.0", + "ts-jest": "26.1.4", + "ts-node": "~8.3.0", + "tslint": "~6.1.0", + "typescript": "~4.0.2" } - }, - "dependencies": { - "@angular/animations": "~11.0.1", - "@angular/cdk": "~11.0.1", - "@angular/common": "~11.0.1", - "@angular/compiler": "~11.0.1", - "@angular/core": "~11.0.1", - "@angular/forms": "~11.0.1", - "@angular/material": "~11.0.1", - "@angular/material-moment-adapter": "^11.0.2", - "@angular/platform-browser": "~11.0.1", - "@angular/platform-browser-dynamic": "~11.0.1", - "@angular/router": "~11.0.1", - "@angular/service-worker": "~11.0.1", - "@ngx-translate/core": "^13.0.0", - "@ngx-translate/http-loader": "^6.0.0", - "@nrwl/angular": "^10.2.0", - "@pdftron/webviewer": "^7.2.0", - "file-saver": "^2.0.2", - "jwt-decode": "^3.0.0", - "keycloak-angular": "^8.0.1", - "keycloak-js": "10.0.2", - "lint-staged": "^10.5.0", - "ng2-ace-editor": "^0.3.9", - "ngp-sort-pipe": "^0.0.4", - "ngx-color-picker": "^10.1.0", - "ngx-dropzone": "^2.2.2", - "ngx-toastr": "^13.0.0", - "rxjs": "~6.6.0", - "scroll-into-view-if-needed": "^2.2.26", - "tslib": "^2.0.0", - "zone.js": "~0.10.2", - "streamsaver": "^2.0.5" - }, - "devDependencies": { - "@angular-devkit/build-angular": "~0.1100.2", - "@angular-devkit/build-ng-packagr": "~0.1002.0", - "@angular/cli": "~11.0.2", - "@angular/compiler": "~11.0.1", - "@angular/compiler-cli": "~11.0.1", - "@angular/language-service": "~11.0.2", - "@nrwl/cypress": "10.2.0", - "@nrwl/jest": "10.2.0", - "@nrwl/workspace": "10.2.0", - "@types/cypress": "^1.1.3", - "@types/jasmine": "~3.6.0", - "@types/jest": "26.0.8", - "@types/node": "^12.11.1", - "codelyzer": "^6.0.0", - "cypress": "^5.6.0", - "cypress-file-upload": "^4.1.1", - "cypress-keycloak": "^1.5.0", - "cypress-keycloak-commands": "^1.2.0", - "cypress-localstorage-commands": "^1.2.4", - "dotenv": "6.2.0", - "eslint": "6.8.0", - "google-translate-api-browser": "^1.1.71", - "husky": "^4.3.0", - "jest": "26.2.2", - "jest-preset-angular": "8.2.1", - "lodash": "^4.17.20", - "moment": "^2.29.1", - "ng-packagr": "^10.1.2", - "prettier": "2.0.4", - "pretty-quick": "^3.1.0", - "superagent": "^6.1.0", - "superagent-promise": "^1.1.0", - "ts-jest": "26.1.4", - "ts-node": "~8.3.0", - "tslint": "~6.1.0", - "typescript": "~4.0.2" - } } diff --git a/yarn.lock b/yarn.lock index 773e0ae04..6ae583b27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2343,6 +2343,24 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@swimlane/ngx-charts@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@swimlane/ngx-charts/-/ngx-charts-17.0.0.tgz#77b0b4277f7b75f9f26bbd7f464b5a6099fe944a" + integrity sha512-NsjDBWeizvWrDq6W/ZImBGL9/RERsW+2qLR30xJg/oNbIrSqtLty7Scmah3b9lgyCH00c0pK1DMYUak1QoMBJQ== + dependencies: + d3-array "^2.9.1" + d3-brush "^2.1.0" + d3-color "^2.0.0" + d3-format "^2.0.0" + d3-hierarchy "^2.0.0" + d3-interpolate "^2.0.1" + d3-scale "^3.2.3" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-time-format "^3.0.0" + d3-transition "^2.0.0" + tslib "^2.0.0" + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.9" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" @@ -4636,6 +4654,120 @@ cypress@*, cypress@^5.6.0: url "^0.11.0" yauzl "^2.10.0" +d3-array@^2.3.0, d3-array@^2.9.1: + version "2.11.0" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.11.0.tgz#5ed6a2869bc7d471aec8df9ff6ed9fef798facc4" + integrity sha512-26clcwmHQEdsLv34oNKq5Ia9tQ26Y/4HqS3dQzF42QBUqymZJ+9PORcN1G52bt37NsL2ABoX4lvyYZc+A9Y0zw== + dependencies: + internmap "^1.0.0" + +d3-brush@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-2.1.0.tgz#adadfbb104e8937af142e9a6e2028326f0471065" + integrity sha512-cHLLAFatBATyIKqZOkk/mDHUbzne2B3ZwxkzMHvFTCZCmLaXDpZRihQSn8UNXTkGD/3lb/W2sQz0etAftmHMJQ== + dependencies: + d3-dispatch "1 - 2" + d3-drag "2" + d3-interpolate "1 - 2" + d3-selection "2" + d3-transition "2" + +"d3-color@1 - 2", d3-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + +"d3-dispatch@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" + integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA== + +d3-drag@2: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-2.0.0.tgz#9eaf046ce9ed1c25c88661911c1d5a4d8eb7ea6d" + integrity sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w== + dependencies: + d3-dispatch "1 - 2" + d3-selection "2" + +"d3-ease@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-2.0.0.tgz#fd1762bfca00dae4bacea504b1d628ff290ac563" + integrity sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ== + +"d3-format@1 - 2", d3-format@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + +d3-hierarchy@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#dab88a58ca3e7a1bc6cab390e89667fcc6d20218" + integrity sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw== + +"d3-interpolate@1 - 2", "d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + +d3-scale@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.3.tgz#be380f57f1f61d4ff2e6cbb65a40593a51649cfd" + integrity sha512-8E37oWEmEzj57bHcnjPVOBS3n4jqakOeuv1EDdQSiSrYnMCBdMd3nc4HtKk7uia8DUHcY/CGuJ42xxgtEYrX0g== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "1 - 2" + d3-time-format "2 - 3" + +d3-selection@2, d3-selection@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" + integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== + +d3-shape@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.0.0.tgz#2331b62fa784a2a1daac47a7233cfd69301381fd" + integrity sha512-djpGlA779ua+rImicYyyjnOjeubyhql1Jyn1HK0bTyawuH76UQRWXd+pftr67H6Fa8hSwetkgb/0id3agKWykw== + dependencies: + d3-path "1 - 2" + +"d3-time-format@2 - 3", d3-time-format@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + +"d3-time@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.0.0.tgz#ad7c127d17c67bd57a4c61f3eaecb81108b1e0ab" + integrity sha512-2mvhstTFcMvwStWd9Tj3e6CEqtOivtD8AUiHT8ido/xmzrI9ijrUUihZ6nHuf/vsScRBonagOdj0Vv+SEL5G3Q== + +"d3-timer@1 - 2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" + integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA== + +d3-transition@2, d3-transition@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-2.0.0.tgz#366ef70c22ef88d1e34105f507516991a291c94c" + integrity sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog== + dependencies: + d3-color "1 - 2" + d3-dispatch "1 - 2" + d3-ease "1 - 2" + d3-interpolate "1 - 2" + d3-timer "1 - 2" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -6602,6 +6734,11 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" +internmap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.0.tgz#3c6bf0944b0eae457698000412108752bbfddb56" + integrity sha512-SdoDWwNOTE2n4JWUsLn4KXZGuZPjPF9yyOGc8bnfWnBQh7BD/l80rzSznKc/r4Y0aQ7z3RTk9X+tV4tHBpu+dA== + invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"