mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
UI/1.11 client count component tests (#15748)
* add line chart test * add empty state option to line chart * add empty state test * add tooltip coverage * add test files * add monthly usage tests * finish tests * tidying * address comments, add average test * finish tests broken from calendar
This commit is contained in:
@@ -8,6 +8,7 @@ import { axisLeft } from 'd3-axis';
|
|||||||
import { max, maxIndex } from 'd3-array';
|
import { max, maxIndex } from 'd3-array';
|
||||||
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from 'vault/utils/chart-helpers';
|
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from 'vault/utils/chart-helpers';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { formatNumber } from 'core/helpers/format-number';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module HorizontalBarChart
|
* @module HorizontalBarChart
|
||||||
@@ -22,6 +23,7 @@ import { tracked } from '@glimmer/tracking';
|
|||||||
* @param {string} labelKey - string of key name for label value in chart data
|
* @param {string} labelKey - string of key name for label value in chart data
|
||||||
* @param {string} xKey - string of key name for x value in chart data
|
* @param {string} xKey - string of key name for x value in chart data
|
||||||
* @param {object} totalCounts - object to calculate percentage for tooltip
|
* @param {object} totalCounts - object to calculate percentage for tooltip
|
||||||
|
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// SIZING CONSTANTS
|
// SIZING CONSTANTS
|
||||||
@@ -247,7 +249,7 @@ export default class HorizontalBarChart extends Component {
|
|||||||
.data(dataset)
|
.data(dataset)
|
||||||
.enter()
|
.enter()
|
||||||
.append('text')
|
.append('text')
|
||||||
.text((d) => d[xKey])
|
.text((d) => formatNumber([d[xKey]]))
|
||||||
.attr('fill', '#000')
|
.attr('fill', '#000')
|
||||||
.attr('class', 'total-value')
|
.attr('class', 'total-value')
|
||||||
.style('font-size', '.8rem')
|
.style('font-size', '.8rem')
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
formatNumbers,
|
formatNumbers,
|
||||||
} from 'vault/utils/chart-helpers';
|
} from 'vault/utils/chart-helpers';
|
||||||
import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
|
import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
|
||||||
|
import { formatNumber } from 'core/helpers/format-number';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module LineChart
|
* @module LineChart
|
||||||
@@ -21,10 +22,12 @@ import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* <LineChart @dataset={dataset} />
|
* <LineChart @dataset={{dataset}} @upgradeData={{this.versionHistory}}/>
|
||||||
* ```
|
* ```
|
||||||
* @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset
|
* @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset
|
||||||
* @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset
|
* @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset
|
||||||
|
* @param {array} upgradeData - array of objects containing version history from the /version-history endpoint
|
||||||
|
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class LineChart extends Component {
|
export default class LineChart extends Component {
|
||||||
@@ -42,6 +45,27 @@ export default class LineChart extends Component {
|
|||||||
return this.args.xKey || 'month';
|
return this.args.xKey || 'month';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get upgradeData() {
|
||||||
|
const upgradeData = this.args.upgradeData;
|
||||||
|
if (!upgradeData) return null;
|
||||||
|
if (!Array.isArray(upgradeData)) {
|
||||||
|
console.debug('upgradeData must be an array of objects containing upgrade history');
|
||||||
|
return null;
|
||||||
|
} else if (!Object.keys(upgradeData[0]).includes('timestampInstalled')) {
|
||||||
|
console.debug(
|
||||||
|
`upgrade must be an object with the following key names: ['id', 'previousVersion', 'timestampInstalled']`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return upgradeData?.map((versionData) => {
|
||||||
|
return {
|
||||||
|
[this.xKey]: parseAPITimestamp(versionData.timestampInstalled, 'M/yy'),
|
||||||
|
...versionData,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action removeTooltip() {
|
@action removeTooltip() {
|
||||||
this.tooltipTarget = null;
|
this.tooltipTarget = null;
|
||||||
}
|
}
|
||||||
@@ -49,17 +73,10 @@ export default class LineChart extends Component {
|
|||||||
@action
|
@action
|
||||||
renderChart(element, [chartData]) {
|
renderChart(element, [chartData]) {
|
||||||
const dataset = chartData;
|
const dataset = chartData;
|
||||||
const upgradeData = [];
|
|
||||||
if (this.args.upgradeData) {
|
|
||||||
this.args.upgradeData.forEach((versionData) =>
|
|
||||||
upgradeData.push({ month: parseAPITimestamp(versionData.timestampInstalled, 'M/yy'), ...versionData })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const filteredData = dataset.filter((e) => Object.keys(e).includes(this.yKey)); // months with data will contain a 'clients' key (otherwise only a timestamp)
|
const filteredData = dataset.filter((e) => Object.keys(e).includes(this.yKey)); // months with data will contain a 'clients' key (otherwise only a timestamp)
|
||||||
const domainMax = max(filteredData.map((d) => d[this.yKey]));
|
const domainMax = max(filteredData.map((d) => d[this.yKey]));
|
||||||
const chartSvg = select(element);
|
const chartSvg = select(element);
|
||||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||||
|
|
||||||
// clear out DOM before appending anything
|
// clear out DOM before appending anything
|
||||||
chartSvg.selectAll('g').remove().exit().data(filteredData).enter();
|
chartSvg.selectAll('g').remove().exit().data(filteredData).enter();
|
||||||
|
|
||||||
@@ -93,7 +110,9 @@ export default class LineChart extends Component {
|
|||||||
chartSvg.selectAll('.domain').remove();
|
chartSvg.selectAll('.domain').remove();
|
||||||
|
|
||||||
const findUpgradeData = (datum) => {
|
const findUpgradeData = (datum) => {
|
||||||
return upgradeData.find((upgrade) => upgrade[this.xKey] === datum[this.xKey]);
|
return this.upgradeData
|
||||||
|
? this.upgradeData.find((upgrade) => upgrade[this.xKey] === datum[this.xKey])
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// VERSION UPGRADE INDICATOR
|
// VERSION UPGRADE INDICATOR
|
||||||
@@ -104,6 +123,7 @@ export default class LineChart extends Component {
|
|||||||
.enter()
|
.enter()
|
||||||
.append('circle')
|
.append('circle')
|
||||||
.attr('class', 'upgrade-circle')
|
.attr('class', 'upgrade-circle')
|
||||||
|
.attr('data-test-line-chart', (d) => `upgrade-${d[this.xKey]}`)
|
||||||
.attr('fill', UPGRADE_WARNING)
|
.attr('fill', UPGRADE_WARNING)
|
||||||
.style('opacity', (d) => (findUpgradeData(d) ? '1' : '0'))
|
.style('opacity', (d) => (findUpgradeData(d) ? '1' : '0'))
|
||||||
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
|
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
|
||||||
@@ -158,8 +178,8 @@ export default class LineChart extends Component {
|
|||||||
hoverCircles.on('mouseover', (data) => {
|
hoverCircles.on('mouseover', (data) => {
|
||||||
// TODO: how to generalize this?
|
// TODO: how to generalize this?
|
||||||
this.tooltipMonth = formatChartDate(data[this.xKey]);
|
this.tooltipMonth = formatChartDate(data[this.xKey]);
|
||||||
this.tooltipTotal = data[this.yKey] + ' total clients';
|
this.tooltipTotal = formatNumber([data[this.yKey]]) + ' total clients';
|
||||||
this.tooltipNew = (data?.new_clients[this.yKey] || '0') + ' new clients';
|
this.tooltipNew = (formatNumber([data?.new_clients[this.yKey]]) || '0') + ' new clients';
|
||||||
this.tooltipUpgradeText = '';
|
this.tooltipUpgradeText = '';
|
||||||
let upgradeInfo = findUpgradeData(data);
|
let upgradeInfo = findUpgradeData(data);
|
||||||
if (upgradeInfo) {
|
if (upgradeInfo) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { calculateAverageClients } from 'vault/utils/chart-helpers';
|
import { calculateAverage } from 'vault/utils/chart-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module MonthlyUsage
|
* @module MonthlyUsage
|
||||||
@@ -34,13 +34,13 @@ import { calculateAverageClients } from 'vault/utils/chart-helpers';
|
|||||||
*/
|
*/
|
||||||
export default class MonthlyUsage extends Component {
|
export default class MonthlyUsage extends Component {
|
||||||
get averageTotalClients() {
|
get averageTotalClients() {
|
||||||
return calculateAverageClients(this.args.verticalBarChartData, 'clients') || '0';
|
return calculateAverage(this.args.verticalBarChartData, 'clients') || '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
get averageNewClients() {
|
get averageNewClients() {
|
||||||
return (
|
return (
|
||||||
calculateAverageClients(
|
calculateAverage(
|
||||||
this.args.verticalBarChartData.map((d) => d.new_clients),
|
this.args.verticalBarChartData?.map((d) => d.new_clients),
|
||||||
'clients'
|
'clients'
|
||||||
) || '0'
|
) || '0'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { calculateAverageClients } from 'vault/utils/chart-helpers';
|
import { calculateAverage } from 'vault/utils/chart-helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module RunningTotal
|
* @module RunningTotal
|
||||||
@@ -45,14 +45,14 @@ export default class RunningTotal extends Component {
|
|||||||
get entityClientData() {
|
get entityClientData() {
|
||||||
return {
|
return {
|
||||||
runningTotal: this.args.runningTotals.entity_clients,
|
runningTotal: this.args.runningTotals.entity_clients,
|
||||||
averageNewClients: calculateAverageClients(this.args.barChartData, 'entity_clients') || '0',
|
averageNewClients: calculateAverage(this.args.barChartData, 'entity_clients') || '0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
get nonEntityClientData() {
|
get nonEntityClientData() {
|
||||||
return {
|
return {
|
||||||
runningTotal: this.args.runningTotals.non_entity_clients,
|
runningTotal: this.args.runningTotals.non_entity_clients,
|
||||||
averageNewClients: calculateAverageClients(this.args.barChartData, 'non_entity_clients') || '0',
|
averageNewClients: calculateAverage(this.args.barChartData, 'non_entity_clients') || '0',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +71,8 @@ export default class RunningTotal extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get showSingleMonth() {
|
get showSingleMonth() {
|
||||||
if (this.args.barChartData.length === 1) {
|
if (this.args.lineChartData?.length === 1) {
|
||||||
const monthData = this.args.lineChartData[0];
|
const monthData = this.args?.lineChartData[0];
|
||||||
return {
|
return {
|
||||||
total: {
|
total: {
|
||||||
total: monthData.clients,
|
total: monthData.clients,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TRANSLATE,
|
TRANSLATE,
|
||||||
formatNumbers,
|
formatNumbers,
|
||||||
} from 'vault/utils/chart-helpers';
|
} from 'vault/utils/chart-helpers';
|
||||||
|
import { formatNumber } from 'core/helpers/format-number';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module VerticalBarChart
|
* @module VerticalBarChart
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||||
* @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset
|
* @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset
|
||||||
* @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset
|
* @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset
|
||||||
|
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class VerticalBarChart extends Component {
|
export default class VerticalBarChart extends Component {
|
||||||
@@ -83,6 +85,7 @@ export default class VerticalBarChart extends Component {
|
|||||||
.append('rect')
|
.append('rect')
|
||||||
.attr('width', '7px')
|
.attr('width', '7px')
|
||||||
.attr('class', 'data-bar')
|
.attr('class', 'data-bar')
|
||||||
|
.attr('data-test-vertical-chart', 'data-bar')
|
||||||
.attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`)
|
.attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`)
|
||||||
.attr('x', ({ data }) => xScale(data[this.xKey])) // uses destructuring because was data.data.month
|
.attr('x', ({ data }) => xScale(data[this.xKey])) // uses destructuring because was data.data.month
|
||||||
.attr('y', (data) => `${100 - yScale(data[1])}%`); // subtract higher than 100% to give space for x axis ticks
|
.attr('y', (data) => `${100 - yScale(data[1])}%`); // subtract higher than 100% to give space for x axis ticks
|
||||||
@@ -101,8 +104,13 @@ export default class VerticalBarChart extends Component {
|
|||||||
|
|
||||||
const xAxis = axisBottom(xScale).tickSize(0);
|
const xAxis = axisBottom(xScale).tickSize(0);
|
||||||
|
|
||||||
yAxis(chartSvg.append('g'));
|
yAxis(chartSvg.append('g').attr('data-test-vertical-chart', 'y-axis-labels'));
|
||||||
xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`));
|
xAxis(
|
||||||
|
chartSvg
|
||||||
|
.append('g')
|
||||||
|
.attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`)
|
||||||
|
.attr('data-test-vertical-chart', 'x-axis-labels')
|
||||||
|
);
|
||||||
|
|
||||||
chartSvg.selectAll('.domain').remove(); // remove domain lines
|
chartSvg.selectAll('.domain').remove(); // remove domain lines
|
||||||
|
|
||||||
@@ -129,9 +137,9 @@ export default class VerticalBarChart extends Component {
|
|||||||
// MOUSE EVENT FOR TOOLTIP
|
// MOUSE EVENT FOR TOOLTIP
|
||||||
tooltipRect.on('mouseover', (data) => {
|
tooltipRect.on('mouseover', (data) => {
|
||||||
let hoveredMonth = data[this.xKey];
|
let hoveredMonth = data[this.xKey];
|
||||||
this.tooltipTotal = `${data[this.yKey]} ${data.new_clients ? 'total' : 'new'} clients`;
|
this.tooltipTotal = `${formatNumber([data[this.yKey]])} ${data.new_clients ? 'total' : 'new'} clients`;
|
||||||
this.entityClients = `${data.entity_clients} entity clients`;
|
this.entityClients = `${formatNumber([data.entity_clients])} entity clients`;
|
||||||
this.nonEntityClients = `${data.non_entity_clients} non-entity clients`;
|
this.nonEntityClients = `${formatNumber([data.non_entity_clients])} non-entity clients`;
|
||||||
let node = chartSvg
|
let node = chartSvg
|
||||||
.selectAll('rect.data-bar')
|
.selectAll('rect.data-bar')
|
||||||
// filter for the top data bar (so y-coord !== 0) with matching month
|
// filter for the top data bar (so y-coord !== 0) with matching month
|
||||||
|
|||||||
@@ -130,10 +130,17 @@
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
padding-right: 20px;
|
padding-right: 20px;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> div.empty-state {
|
||||||
|
white-space: nowrap;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-subTitle {
|
.chart-subTitle {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@
|
|||||||
|
|
||||||
{{#if this.barChartTotalClients}}
|
{{#if this.barChartTotalClients}}
|
||||||
{{#if (or @isDateRange @isCurrentMonth)}}
|
{{#if (or @isDateRange @isCurrentMonth)}}
|
||||||
<div class="chart-container-wide" data-test-chart-container="total-clients">
|
<div
|
||||||
|
class={{concat (unless this.barChartTotalClients "chart-empty-state ") "chart-container-wide"}}
|
||||||
|
data-test-chart-container="total-clients"
|
||||||
|
>
|
||||||
<Clients::HorizontalBarChart
|
<Clients::HorizontalBarChart
|
||||||
@dataset={{this.barChartTotalClients}}
|
@dataset={{this.barChartTotalClients}}
|
||||||
@chartLegend={{@chartLegend}}
|
@chartLegend={{@chartLegend}}
|
||||||
|
|||||||
@@ -204,6 +204,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button link"
|
class="button link"
|
||||||
|
data-test-date-modal-month={{month}}
|
||||||
disabled={{if (lt index this.allowedMonthMax) false true}}
|
disabled={{if (lt index this.allowedMonthMax) false true}}
|
||||||
{{on "click" (fn this.selectStartMonth month D.actions)}}
|
{{on "click" (fn this.selectStartMonth month D.actions)}}
|
||||||
>
|
>
|
||||||
@@ -230,6 +231,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button link"
|
class="button link"
|
||||||
|
data-test-date-modal-year={{year}}
|
||||||
disabled={{if (eq year this.disabledYear) true false}}
|
disabled={{if (eq year this.disabledYear) true false}}
|
||||||
{{on "click" (fn this.selectStartYear year D.actions)}}
|
{{on "click" (fn this.selectStartYear year D.actions)}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
>
|
>
|
||||||
</svg>
|
</svg>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="chart-empty-state">
|
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
|
||||||
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if this.tooltipTarget}}
|
{{#if this.tooltipTarget}}
|
||||||
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
|
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<svg
|
{{#if @dataset}}
|
||||||
data-test-line-chart
|
<svg
|
||||||
class="chart has-grid"
|
data-test-line-chart
|
||||||
{{on "mouseleave" this.removeTooltip}}
|
class="chart has-grid"
|
||||||
{{did-insert this.renderChart @dataset}}
|
{{on "mouseleave" this.removeTooltip}}
|
||||||
{{did-update this.renderChart @dataset}}
|
{{did-insert this.renderChart @dataset}}
|
||||||
>
|
{{did-update this.renderChart @dataset}}
|
||||||
</svg>
|
>
|
||||||
|
</svg>
|
||||||
|
{{else}}
|
||||||
|
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{! TOOLTIP }}
|
{{! TOOLTIP }}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 988 B After Width: | Height: | Size: 1.1 KiB |
@@ -6,7 +6,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container-wide">
|
<div class={{concat (unless @verticalBarChartData "chart-empty-state ") "chart-container-wide"}}>
|
||||||
<Clients::VerticalBarChart @dataset={{@verticalBarChartData}} @chartLegend={{@chartLegend}} />
|
<Clients::VerticalBarChart @dataset={{@verticalBarChartData}} @chartLegend={{@chartLegend}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -17,30 +17,31 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="data-details-top">
|
<div class="data-details-top" data-test-monthly-usage-average-total>
|
||||||
<h3 class="data-details">Average total clients per month</h3>
|
<h3 class="data-details">Average total clients per month</h3>
|
||||||
<p class="data-details">
|
<p class="data-details">
|
||||||
{{format-number this.averageTotalClients}}
|
{{format-number this.averageTotalClients}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="data-details-bottom">
|
<div class="data-details-bottom" data-test-monthly-usage-average-new>
|
||||||
<h3 class="data-details">Average new clients per month</h3>
|
<h3 class="data-details">Average new clients per month</h3>
|
||||||
<p class="data-details">
|
<p class="data-details">
|
||||||
{{format-number this.averageNewClients}}
|
{{format-number this.averageNewClients}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timestamp">
|
<div data-test-monthly-usage-timestamp class="timestamp">
|
||||||
{{#if @timestamp}}
|
{{#if @timestamp}}
|
||||||
Updated
|
Updated
|
||||||
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legend-right">
|
{{#if @verticalBarChartData}}
|
||||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
<div data-test-monthly-usage-legend class="legend-right">
|
||||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||||
</div>
|
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
The total client count number is an important consideration for Vault billing.
|
The total client count number is an important consideration for Vault billing.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-month-stats">
|
<div class="single-month-stats" data-test-new>
|
||||||
<div class="single-month-section-title">
|
<div class="single-month-section-title">
|
||||||
<StatText
|
<StatText
|
||||||
@label="New clients"
|
@label="New clients"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<StatText @label="Non-entity clients" @value={{this.showSingleMonth.new.nonEntityClients}} @size="m" />
|
<StatText @label="Non-entity clients" @value={{this.showSingleMonth.new.nonEntityClients}} @size="m" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="single-month-stats">
|
<div class="single-month-stats" data-test-total>
|
||||||
<div class="single-month-section-title">
|
<div class="single-month-section-title">
|
||||||
<StatText
|
<StatText
|
||||||
@label="Total monthly clients"
|
@label="Total monthly clients"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart-container-wide">
|
<div class={{concat (unless @lineChartData "chart-empty-state ") "chart-container-wide"}}>
|
||||||
<Clients::LineChart @dataset={{@lineChartData}} @upgradeData={{@upgradeData}} />
|
<Clients::LineChart @dataset={{@lineChartData}} @upgradeData={{@upgradeData}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,17 +109,19 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="timestamp">
|
<div class="timestamp" data-test-running-total-timestamp>
|
||||||
{{#if @timestamp}}
|
{{#if @timestamp}}
|
||||||
Updated
|
Updated
|
||||||
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="legend-right">
|
{{#if this.hasAverageNewClients}}
|
||||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
<div class="legend-right" data-test-running-total-legend>
|
||||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||||
</div>
|
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -8,9 +8,7 @@
|
|||||||
>
|
>
|
||||||
</svg>
|
</svg>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="chart-empty-state">
|
<EmptyState @title={{@noDataTitle}} @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
|
||||||
<EmptyState @title={{@noDataTitle}} @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{! TOOLTIP }}
|
{{! TOOLTIP }}
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ export function formatTooltipNumber(value) {
|
|||||||
return new Intl.NumberFormat().format(value);
|
return new Intl.NumberFormat().format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateAverageClients(dataset, objectKey) {
|
export function calculateAverage(dataset, objectKey) {
|
||||||
// dataset is an array of objects (consumed by the chart components)
|
if (!Array.isArray(dataset) || dataset?.length === 0) return null;
|
||||||
// objectKey is the key of the integer we want to calculate, ex: 'entity_clients', 'non_entity_clients', 'clients'
|
// if an array of objects, objectKey of the integer we want to calculate, ex: 'entity_clients'
|
||||||
let getIntegers = dataset.map((d) => (d[objectKey] ? d[objectKey] : 0)); // if undefined no data, so return 0
|
// if d[objectKey] is undefined there is no value, so return 0
|
||||||
return getIntegers.length !== 0 ? Math.round(mean(getIntegers)) : null;
|
const getIntegers = objectKey ? dataset?.map((d) => (d[objectKey] ? d[objectKey] : 0)) : dataset;
|
||||||
|
let checkIntegers = getIntegers.every((n) => Number.isInteger(n)); // decimals will be false
|
||||||
|
return checkIntegers ? Math.round(mean(getIntegers)) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div
|
<div
|
||||||
class={{concat "stat-text-container " @size (unless @subText "-no-subText")}}
|
class={{concat "stat-text-container " @size (unless @subText "-no-subText")}}
|
||||||
data-test-stat-text-container
|
data-test-stat-text-container={{(or @label "true")}}
|
||||||
...attributes
|
...attributes
|
||||||
>
|
>
|
||||||
<div class="stat-label has-bottom-margin-xs">{{@label}}</div>
|
<div class="stat-label has-bottom-margin-xs">{{@label}}</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const parseRFC3339 = (timestamp) => {
|
|||||||
return date ? [`${date.getFullYear()}`, date.getMonth()] : null;
|
return date ? [`${date.getFullYear()}`, date.getMonth()] : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert MM/yy (format of dates in charts) to 'Month yyyy' (format in tooltip)
|
// convert M/yy (format of dates in charts) to 'Month yyyy' (format in tooltip)
|
||||||
export function formatChartDate(date) {
|
export function formatChartDate(date) {
|
||||||
let array = date.split('/');
|
let array = date.split('/');
|
||||||
array.splice(1, 0, '01');
|
array.splice(1, 0, '01');
|
||||||
|
|||||||
@@ -1684,6 +1684,7 @@ const MOCK_MONTHLY_DATA = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
timestamp: formatISO(addMonths(UPGRADE_DATE, 3)),
|
||||||
counts: {
|
counts: {
|
||||||
distinct_entities: 0,
|
distinct_entities: 0,
|
||||||
entity_clients: 10873,
|
entity_clients: 10873,
|
||||||
@@ -2236,7 +2237,7 @@ const MOCK_MONTHLY_DATA = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: formatISO(addMonths(UPGRADE_DATE, 3)),
|
timestamp: formatISO(addMonths(UPGRADE_DATE, 4)),
|
||||||
counts: {
|
counts: {
|
||||||
distinct_entities: 0,
|
distinct_entities: 0,
|
||||||
entity_clients: 10342,
|
entity_clients: 10342,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { SELECTORS, overrideResponse } from '../helpers/clients';
|
|||||||
import { create } from 'ember-cli-page-object';
|
import { create } from 'ember-cli-page-object';
|
||||||
import ss from 'vault/tests/pages/components/search-select';
|
import ss from 'vault/tests/pages/components/search-select';
|
||||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||||
|
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||||
|
|
||||||
const searchSelect = create(ss);
|
const searchSelect = create(ss);
|
||||||
|
|
||||||
const NEW_DATE = new Date();
|
const NEW_DATE = new Date();
|
||||||
@@ -133,18 +135,47 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('updates correctly when querying date ranges', async function (assert) {
|
test('updates correctly when querying date ranges', async function (assert) {
|
||||||
assert.expect(17);
|
assert.expect(26);
|
||||||
// TODO CMB: wire up dynamically generated activity to mirage clients handler
|
// TODO CMB: wire up dynamically generated activity to mirage clients handler
|
||||||
// const activity = generateActivityResponse(5, LICENSE_START, LAST_MONTH);
|
// const activity = generateActivityResponse(5, LICENSE_START, LAST_MONTH);
|
||||||
await visit('/vault/clients/history');
|
await visit('/vault/clients/history');
|
||||||
assert.equal(currentURL(), '/vault/clients/history');
|
assert.equal(currentURL(), '/vault/clients/history');
|
||||||
|
|
||||||
// change billing start month
|
// query for single, historical month with no new counts
|
||||||
|
await click(SELECTORS.rangeDropdown);
|
||||||
|
await click('[data-test-show-calendar]');
|
||||||
|
if (parseInt(find('[data-test-display-year]').innerText) > LICENSE_START.getFullYear()) {
|
||||||
|
await click('[data-test-previous-year]');
|
||||||
|
}
|
||||||
|
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}]`));
|
||||||
|
|
||||||
|
assert.dom('[data-test-usage-stats]').exists('total usage stats show');
|
||||||
|
assert
|
||||||
|
.dom(SELECTORS.runningTotalMonthStats)
|
||||||
|
.doesNotExist('running total single month stat boxes do not show');
|
||||||
|
assert
|
||||||
|
.dom(SELECTORS.runningTotalMonthlyCharts)
|
||||||
|
.doesNotExist('running total month over month charts do not show');
|
||||||
|
assert.dom(SELECTORS.monthlyUsageBlock).doesNotExist('does not show monthly usage block');
|
||||||
|
assert.dom(SELECTORS.attributionBlock).exists('attribution area shows');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-chart-container="new-clients"] [data-test-component="empty-state"]')
|
||||||
|
.exists('new client attribution has empty state');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-empty-state-subtext]')
|
||||||
|
.hasText('There are no new clients for this namespace during this time period. ');
|
||||||
|
assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows');
|
||||||
|
|
||||||
|
// reset to billing period
|
||||||
|
await click('[data-test-popup-menu-trigger]');
|
||||||
|
await click('[data-test-current-billing-period]');
|
||||||
|
|
||||||
|
// change billing start to month/year of first upgrade
|
||||||
await click('[data-test-start-date-editor] button');
|
await click('[data-test-start-date-editor] button');
|
||||||
await click(SELECTORS.monthDropdown);
|
await click(SELECTORS.monthDropdown);
|
||||||
await click(find('.menu-list button:not([disabled])'));
|
await click(find(`[data-test-date-modal-month="${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}"]`));
|
||||||
await click(SELECTORS.yearDropdown);
|
await click(SELECTORS.yearDropdown);
|
||||||
await click(find('.menu-list button:not([disabled])'));
|
await click(find(`[data-test-date-modal-year="${UPGRADE_DATE.getFullYear()}`));
|
||||||
await click('[data-test-modal-save]');
|
await click('[data-test-modal-save]');
|
||||||
|
|
||||||
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
||||||
@@ -154,7 +185,7 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||||||
.exists('Shows running totals with monthly breakdown charts');
|
.exists('Shows running totals with monthly breakdown charts');
|
||||||
assert
|
assert
|
||||||
.dom(find('[data-test-line-chart="x-axis-labels"] g.tick text'))
|
.dom(find('[data-test-line-chart="x-axis-labels"] g.tick text'))
|
||||||
.hasText('1/22', 'x-axis labels start with updated billing start month');
|
.hasText(`${format(UPGRADE_DATE, 'M/yy')}`, 'x-axis labels start with updated billing start month');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
findAll('[data-test-line-chart="plot-point"]').length,
|
findAll('[data-test-line-chart="plot-point"]').length,
|
||||||
5,
|
5,
|
||||||
@@ -164,9 +195,10 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||||||
// query custom end month
|
// query custom end month
|
||||||
await click(SELECTORS.rangeDropdown);
|
await click(SELECTORS.rangeDropdown);
|
||||||
await click('[data-test-show-calendar]');
|
await click('[data-test-show-calendar]');
|
||||||
let readOnlyMonths = findAll('[data-test-calendar-month].is-readOnly');
|
if (parseInt(find('[data-test-display-year]').innerText) < NEW_DATE.getFullYear()) {
|
||||||
let clickableMonths = findAll('[data-test-calendar-month]').filter((m) => !readOnlyMonths.includes(m));
|
await click('[data-test-future-year]');
|
||||||
await click(clickableMonths[1]);
|
}
|
||||||
|
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LAST_MONTH.getMonth() - 2]}]`));
|
||||||
|
|
||||||
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
assert.dom(SELECTORS.attributionBlock).exists('Shows attribution area');
|
||||||
assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block');
|
assert.dom(SELECTORS.monthlyUsageBlock).exists('Shows monthly usage block');
|
||||||
@@ -175,23 +207,27 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||||||
.exists('Shows running totals with monthly breakdown charts');
|
.exists('Shows running totals with monthly breakdown charts');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
findAll('[data-test-line-chart="plot-point"]').length,
|
findAll('[data-test-line-chart="plot-point"]').length,
|
||||||
2,
|
3,
|
||||||
`line chart plots 2 points to match query`
|
`line chart plots 3 points to match query`
|
||||||
);
|
);
|
||||||
|
let xAxisLabels = findAll('[data-test-line-chart="x-axis-labels"] g.tick text');
|
||||||
assert
|
assert
|
||||||
.dom(findAll('[data-test-line-chart="x-axis-labels"] g.tick text')[1])
|
.dom(xAxisLabels[xAxisLabels.length - 1])
|
||||||
.hasText('2/22', 'x-axis labels start with updated billing start month');
|
.hasText(`${format(subMonths(LAST_MONTH, 2), 'M/yy')}`, 'x-axis labels end with queried end month');
|
||||||
|
|
||||||
// query for single, historical month
|
// query for single, historical month
|
||||||
await click(SELECTORS.rangeDropdown);
|
await click(SELECTORS.rangeDropdown);
|
||||||
await click('[data-test-show-calendar]');
|
await click('[data-test-show-calendar]');
|
||||||
readOnlyMonths = findAll('[data-test-calendar-month].is-readOnly');
|
if (parseInt(find('[data-test-display-year]').innerText) < NEW_DATE.getFullYear()) {
|
||||||
clickableMonths = findAll('[data-test-calendar-month]').filter((m) => !readOnlyMonths.includes(m));
|
await click('[data-test-future-year]');
|
||||||
await click(clickableMonths[0]);
|
}
|
||||||
|
await click(find(`[data-test-calendar-month=${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}]`));
|
||||||
|
|
||||||
assert.dom(SELECTORS.runningTotalMonthStats).exists('running total single month stat boxes show');
|
assert.dom(SELECTORS.runningTotalMonthStats).exists('running total single month stat boxes show');
|
||||||
assert
|
assert
|
||||||
.dom(SELECTORS.runningTotalMonthlyCharts)
|
.dom(SELECTORS.runningTotalMonthlyCharts)
|
||||||
.doesNotExist('running total month over month charts do not show');
|
.doesNotExist('running total month over month charts do not show');
|
||||||
|
assert.dom(SELECTORS.monthlyUsageBlock).doesNotExist('Does not show monthly usage block');
|
||||||
assert.dom(SELECTORS.attributionBlock).exists('attribution area shows');
|
assert.dom(SELECTORS.attributionBlock).exists('attribution area shows');
|
||||||
assert.dom('[data-test-chart-container="new-clients"]').exists('new client attribution chart shows');
|
assert.dom('[data-test-chart-container="new-clients"]').exists('new client attribution chart shows');
|
||||||
assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows');
|
assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows');
|
||||||
@@ -203,8 +239,7 @@ module('Acceptance | clients history tab', function (hooks) {
|
|||||||
// query month older than count start date
|
// query month older than count start date
|
||||||
await click('[data-test-start-date-editor] button');
|
await click('[data-test-start-date-editor] button');
|
||||||
await click(SELECTORS.yearDropdown);
|
await click(SELECTORS.yearDropdown);
|
||||||
let years = findAll('.menu-list button:not([disabled])');
|
await click(find(`[data-test-date-modal-year="${LICENSE_START.getFullYear() - 3}`));
|
||||||
await click(years[years.length - 1]);
|
|
||||||
await click('[data-test-modal-save]');
|
await click('[data-test-modal-save]');
|
||||||
|
|
||||||
assert
|
assert
|
||||||
|
|||||||
@@ -8,20 +8,14 @@ import { Response } from 'miragejs';
|
|||||||
Filtering (data with mounts)
|
Filtering (data with mounts)
|
||||||
Filtering (data without mounts)
|
Filtering (data without mounts)
|
||||||
Filtering (data without mounts)
|
Filtering (data without mounts)
|
||||||
|
* -- HISTORY ONLY --
|
||||||
* -- HISTORY ONLY --
|
Filtering different date ranges (hist only)
|
||||||
|
Upgrade warning
|
||||||
No permissions for license
|
No permissions for license
|
||||||
Version
|
Version
|
||||||
queries available
|
queries available
|
||||||
queries unavailable
|
queries unavailable
|
||||||
License start date this month
|
License start date this month
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
/*
|
|
||||||
Filtering different date ranges (hist only)
|
|
||||||
Upgrade warning
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const SELECTORS = {
|
export const SELECTORS = {
|
||||||
currentMonthActiveTab: '.active[data-test-current-month]',
|
currentMonthActiveTab: '.active[data-test-current-month]',
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { render } from '@ember/test-helpers';
|
import { find, render, findAll, triggerEvent } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
import { format, formatRFC3339, subMonths } from 'date-fns';
|
||||||
|
import { formatChartDate } from 'core/utils/date-formatters';
|
||||||
module('Integration | Component | clients/line-chart', function (hooks) {
|
module('Integration | Component | clients/line-chart', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
const CURRENT_DATE = new Date();
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
|
this.set('xKey', 'foo');
|
||||||
|
this.set('yKey', 'bar');
|
||||||
this.set('dataset', [
|
this.set('dataset', [
|
||||||
{
|
{
|
||||||
foo: 1,
|
foo: 1,
|
||||||
@@ -30,11 +33,179 @@ module('Integration | Component | clients/line-chart', function (hooks) {
|
|||||||
test('it renders', async function (assert) {
|
test('it renders', async function (assert) {
|
||||||
await render(hbs`
|
await render(hbs`
|
||||||
<div class="chart-container-wide">
|
<div class="chart-container-wide">
|
||||||
<Clients::LineChart @dataset={{dataset}} @xKey="foo" @yKey="bar" />
|
<Clients::LineChart @dataset={{dataset}} @xKey={{xKey}} @yKey={{yKey}} />
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
|
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
|
||||||
assert.dom('.hover-circle').exists({ count: 4 }, 'Renders dot for each data point');
|
assert
|
||||||
|
.dom('[data-test-line-chart="plot-point"]')
|
||||||
|
.exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`);
|
||||||
|
|
||||||
|
findAll('[data-test-line-chart="x-axis-labels"] text').forEach((e, i) => {
|
||||||
|
assert
|
||||||
|
.dom(e)
|
||||||
|
.hasText(`${this.dataset[i][this.xKey]}`, `renders x-axis label: ${this.dataset[i][this.xKey]}`);
|
||||||
|
});
|
||||||
|
assert.dom(find('[data-test-line-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders upgrade data', async function (assert) {
|
||||||
|
this.set('dataset', [
|
||||||
|
{
|
||||||
|
foo: format(subMonths(CURRENT_DATE, 4), 'M/yy'),
|
||||||
|
bar: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: format(subMonths(CURRENT_DATE, 3), 'M/yy'),
|
||||||
|
bar: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: format(subMonths(CURRENT_DATE, 2), 'M/yy'),
|
||||||
|
bar: 14,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foo: format(subMonths(CURRENT_DATE, 1), 'M/yy'),
|
||||||
|
bar: 10,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.set('upgradeData', [
|
||||||
|
{
|
||||||
|
id: '1.10.1',
|
||||||
|
previousVersion: '1.9.2',
|
||||||
|
timestampInstalled: formatRFC3339(subMonths(CURRENT_DATE, 2)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::LineChart
|
||||||
|
@dataset={{dataset}}
|
||||||
|
@upgradeData={{upgradeData}}
|
||||||
|
@xKey={{xKey}}
|
||||||
|
@yKey={{yKey}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-line-chart="plot-point"]')
|
||||||
|
.exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`);
|
||||||
|
assert
|
||||||
|
.dom(find(`[data-test-line-chart="upgrade-${this.dataset[2][this.xKey]}"]`))
|
||||||
|
.hasStyle({ opacity: '1' }, `upgrade data point ${this.dataset[2][this.xKey]} has yellow highlight`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders tooltip', async function (assert) {
|
||||||
|
const tooltipData = [
|
||||||
|
{
|
||||||
|
month: format(subMonths(CURRENT_DATE, 4), 'M/yy'),
|
||||||
|
clients: 4,
|
||||||
|
new_clients: {
|
||||||
|
clients: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
month: format(subMonths(CURRENT_DATE, 3), 'M/yy'),
|
||||||
|
clients: 8,
|
||||||
|
new_clients: {
|
||||||
|
clients: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
month: format(subMonths(CURRENT_DATE, 2), 'M/yy'),
|
||||||
|
clients: 14,
|
||||||
|
new_clients: {
|
||||||
|
clients: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
month: format(subMonths(CURRENT_DATE, 1), 'M/yy'),
|
||||||
|
clients: 20,
|
||||||
|
new_clients: {
|
||||||
|
clients: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
this.set('dataset', tooltipData);
|
||||||
|
this.set('upgradeData', [
|
||||||
|
{
|
||||||
|
id: '1.10.1',
|
||||||
|
previousVersion: '1.9.2',
|
||||||
|
timestampInstalled: formatRFC3339(subMonths(CURRENT_DATE, 2)),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::LineChart
|
||||||
|
@dataset={{dataset}}
|
||||||
|
@upgradeData={{upgradeData}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const tooltipHoverCircles = findAll('[data-test-line-chart] circle.hover-circle');
|
||||||
|
for (let [i, bar] of tooltipHoverCircles.entries()) {
|
||||||
|
await triggerEvent(bar, 'mouseover');
|
||||||
|
let tooltip = document.querySelector('.ember-modal-dialog');
|
||||||
|
let { month, clients, new_clients } = tooltipData[i];
|
||||||
|
assert
|
||||||
|
.dom(tooltip)
|
||||||
|
.includesText(
|
||||||
|
`${formatChartDate(month)} ${clients} total clients ${new_clients.clients} new clients`,
|
||||||
|
`tooltip text is correct for ${month}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it fails gracefully when upgradeData is an object', async function (assert) {
|
||||||
|
this.set('upgradeData', { some: 'object' });
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::LineChart
|
||||||
|
@dataset={{dataset}}
|
||||||
|
@upgradeData={{upgradeData}}
|
||||||
|
@xKey={{xKey}}
|
||||||
|
@yKey={{yKey}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('[data-test-line-chart="plot-point"]')
|
||||||
|
.exists({ count: this.dataset.length }, 'chart still renders when upgradeData is not an array');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it fails gracefully when upgradeData has incorrect key names', async function (assert) {
|
||||||
|
this.set('upgradeData', [{ incorrect: 'key names' }]);
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::LineChart
|
||||||
|
@dataset={{dataset}}
|
||||||
|
@upgradeData={{upgradeData}}
|
||||||
|
@xKey={{xKey}}
|
||||||
|
@yKey={{yKey}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('[data-test-line-chart="plot-point"]')
|
||||||
|
.exists({ count: this.dataset.length }, 'chart still renders when upgradeData has incorrect keys');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders empty state when no dataset', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::LineChart @noDataMessage="this is a custom message to explain why you're not seeing a line chart"/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-empty-state-subtext]')
|
||||||
|
.hasText(
|
||||||
|
`this is a custom message to explain why you're not seeing a line chart`,
|
||||||
|
'custom message renders'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
1474
ui/tests/integration/components/clients/monthly-usage-test.js
Normal file
1474
ui/tests/integration/components/clients/monthly-usage-test.js
Normal file
File diff suppressed because it is too large
Load Diff
1585
ui/tests/integration/components/clients/running-total-test.js
Normal file
1585
ui/tests/integration/components/clients/running-total-test.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render, findAll, find, triggerEvent } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
module('Integration | Component | clients/vertical-bar-chart', function (hooks) {
|
module('Integration | Component | clients/vertical-bar-chart', function (hooks) {
|
||||||
@@ -12,19 +12,96 @@ module('Integration | Component | clients/vertical-bar-chart', function (hooks)
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it renders', async function (assert) {
|
test('it renders chart and tooltip for total clients', async function (assert) {
|
||||||
const barChartData = [
|
const barChartData = [
|
||||||
{ month: 'january', clients: 200, entity_clients: 91, non_entity_clients: 50, new_clients: 5 },
|
{ month: 'january', clients: 141, entity_clients: 91, non_entity_clients: 50, new_clients: 5 },
|
||||||
{ month: 'february', clients: 300, entity_clients: 101, non_entity_clients: 150, new_clients: 5 },
|
{ month: 'february', clients: 251, entity_clients: 101, non_entity_clients: 150, new_clients: 5 },
|
||||||
];
|
];
|
||||||
this.set('barChartData', barChartData);
|
this.set('barChartData', barChartData);
|
||||||
|
|
||||||
await render(hbs`
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
<Clients::VerticalBarChart
|
<Clients::VerticalBarChart
|
||||||
@dataset={{barChartData}}
|
@dataset={{barChartData}}
|
||||||
@chartLegend={{chartLegend}}
|
@chartLegend={{chartLegend}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
`);
|
`);
|
||||||
assert.dom('[data-test-vertical-bar-chart]').exists();
|
const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect');
|
||||||
|
assert.dom('[data-test-vertical-bar-chart]').exists('renders chart');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-vertical-chart="data-bar"]')
|
||||||
|
.exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked
|
||||||
|
|
||||||
|
assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
|
||||||
|
findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => {
|
||||||
|
assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let [i, bar] of tooltipHoverBars.entries()) {
|
||||||
|
await triggerEvent(bar, 'mouseover');
|
||||||
|
let tooltip = document.querySelector('.ember-modal-dialog');
|
||||||
|
assert
|
||||||
|
.dom(tooltip)
|
||||||
|
.includesText(
|
||||||
|
`${barChartData[i].clients} total clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`,
|
||||||
|
'tooltip text is correct'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders chart and tooltip for new clients', async function (assert) {
|
||||||
|
const barChartData = [
|
||||||
|
{ month: 'january', entity_clients: 91, non_entity_clients: 50, clients: 0 },
|
||||||
|
{ month: 'february', entity_clients: 101, non_entity_clients: 150, clients: 110 },
|
||||||
|
];
|
||||||
|
this.set('barChartData', barChartData);
|
||||||
|
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::VerticalBarChart
|
||||||
|
@dataset={{barChartData}}
|
||||||
|
@chartLegend={{chartLegend}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect');
|
||||||
|
assert.dom('[data-test-vertical-bar-chart]').exists('renders chart');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-vertical-chart="data-bar"]')
|
||||||
|
.exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked
|
||||||
|
|
||||||
|
assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
|
||||||
|
findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => {
|
||||||
|
assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let [i, bar] of tooltipHoverBars.entries()) {
|
||||||
|
await triggerEvent(bar, 'mouseover');
|
||||||
|
let tooltip = document.querySelector('.ember-modal-dialog');
|
||||||
|
assert
|
||||||
|
.dom(tooltip)
|
||||||
|
.includesText(
|
||||||
|
`${barChartData[i].clients} new clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`,
|
||||||
|
'tooltip text is correct'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it renders empty state when no dataset', async function (assert) {
|
||||||
|
await render(hbs`
|
||||||
|
<div class="chart-container-wide">
|
||||||
|
<Clients::VerticalBarChart @noDataMessage="this is a custom message to explain why you're not seeing a vertical bar chart"/>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-empty-state-subtext]')
|
||||||
|
.hasText(
|
||||||
|
`this is a custom message to explain why you're not seeing a vertical bar chart`,
|
||||||
|
'custom message renders'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { formatNumbers, formatTooltipNumber } from 'vault/utils/chart-helpers';
|
import { formatNumbers, formatTooltipNumber, calculateAverage } from 'vault/utils/chart-helpers';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
const SMALL_NUMBERS = [0, 7, 27, 103, 999];
|
const SMALL_NUMBERS = [0, 7, 27, 103, 999];
|
||||||
@@ -28,4 +28,35 @@ module('Unit | Utility | chart-helpers', function () {
|
|||||||
const formatted = formatTooltipNumber(120300200100);
|
const formatted = formatTooltipNumber(120300200100);
|
||||||
assert.equal(formatted.length, 15, 'adds punctuation at proper place for large numbers');
|
assert.equal(formatted.length, 15, 'adds punctuation at proper place for large numbers');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('calculateAverage is accurate', function (assert) {
|
||||||
|
const testArray1 = [
|
||||||
|
{ label: 'foo', value: 10 },
|
||||||
|
{ label: 'bar', value: 22 },
|
||||||
|
];
|
||||||
|
const testArray2 = [
|
||||||
|
{ label: 'foo', value: undefined },
|
||||||
|
{ label: 'bar', value: 22 },
|
||||||
|
];
|
||||||
|
const getAverage = (array) => array.reduce((a, b) => a + b, 0) / array.length;
|
||||||
|
assert.equal(calculateAverage(null), null, 'returns null if dataset it null');
|
||||||
|
assert.equal(calculateAverage([]), null, 'returns null if dataset it empty array');
|
||||||
|
assert.equal(calculateAverage([0]), getAverage([0]), `returns ${getAverage([0])} if array is just 0 0`);
|
||||||
|
assert.equal(calculateAverage([1]), getAverage([1]), `returns ${getAverage([1])} if array is just 1`);
|
||||||
|
assert.equal(
|
||||||
|
calculateAverage([5, 1, 41, 5]),
|
||||||
|
getAverage([5, 1, 41, 5]),
|
||||||
|
`returns correct average for array of integers`
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
calculateAverage(testArray1, 'value'),
|
||||||
|
getAverage([10, 22]),
|
||||||
|
`returns correct average for array of objects`
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
calculateAverage(testArray2, 'value'),
|
||||||
|
getAverage([0, 22]),
|
||||||
|
`returns correct average for array of objects containing undefined values`
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user