diff --git a/ui/app/components/clients/horizontal-bar-chart.js b/ui/app/components/clients/horizontal-bar-chart.js index e59f0d4288..f64ea332d6 100644 --- a/ui/app/components/clients/horizontal-bar-chart.js +++ b/ui/app/components/clients/horizontal-bar-chart.js @@ -8,6 +8,7 @@ import { axisLeft } from 'd3-axis'; import { max, maxIndex } from 'd3-array'; import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from 'vault/utils/chart-helpers'; import { tracked } from '@glimmer/tracking'; +import { formatNumber } from 'core/helpers/format-number'; /** * @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} xKey - string of key name for x value in chart data * @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 @@ -247,7 +249,7 @@ export default class HorizontalBarChart extends Component { .data(dataset) .enter() .append('text') - .text((d) => d[xKey]) + .text((d) => formatNumber([d[xKey]])) .attr('fill', '#000') .attr('class', 'total-value') .style('font-size', '.8rem') diff --git a/ui/app/components/clients/line-chart.js b/ui/app/components/clients/line-chart.js index f9b67f4a52..399e7f09ec 100644 --- a/ui/app/components/clients/line-chart.js +++ b/ui/app/components/clients/line-chart.js @@ -14,6 +14,7 @@ import { formatNumbers, } from 'vault/utils/chart-helpers'; import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters'; +import { formatNumber } from 'core/helpers/format-number'; /** * @module LineChart @@ -21,10 +22,12 @@ import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters'; * * @example * ```js - * + * * ``` * @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 {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 { @@ -42,6 +45,27 @@ export default class LineChart extends Component { 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() { this.tooltipTarget = null; } @@ -49,17 +73,10 @@ export default class LineChart extends Component { @action renderChart(element, [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 domainMax = max(filteredData.map((d) => d[this.yKey])); const chartSvg = select(element); chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions - // clear out DOM before appending anything chartSvg.selectAll('g').remove().exit().data(filteredData).enter(); @@ -93,7 +110,9 @@ export default class LineChart extends Component { chartSvg.selectAll('.domain').remove(); 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 @@ -104,6 +123,7 @@ export default class LineChart extends Component { .enter() .append('circle') .attr('class', 'upgrade-circle') + .attr('data-test-line-chart', (d) => `upgrade-${d[this.xKey]}`) .attr('fill', UPGRADE_WARNING) .style('opacity', (d) => (findUpgradeData(d) ? '1' : '0')) .attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`) @@ -158,8 +178,8 @@ export default class LineChart extends Component { hoverCircles.on('mouseover', (data) => { // TODO: how to generalize this? this.tooltipMonth = formatChartDate(data[this.xKey]); - this.tooltipTotal = data[this.yKey] + ' total clients'; - this.tooltipNew = (data?.new_clients[this.yKey] || '0') + ' new clients'; + this.tooltipTotal = formatNumber([data[this.yKey]]) + ' total clients'; + this.tooltipNew = (formatNumber([data?.new_clients[this.yKey]]) || '0') + ' new clients'; this.tooltipUpgradeText = ''; let upgradeInfo = findUpgradeData(data); if (upgradeInfo) { diff --git a/ui/app/components/clients/monthly-usage.js b/ui/app/components/clients/monthly-usage.js index 0072806c5e..ea803875ce 100644 --- a/ui/app/components/clients/monthly-usage.js +++ b/ui/app/components/clients/monthly-usage.js @@ -1,5 +1,5 @@ import Component from '@glimmer/component'; -import { calculateAverageClients } from 'vault/utils/chart-helpers'; +import { calculateAverage } from 'vault/utils/chart-helpers'; /** * @module MonthlyUsage @@ -34,13 +34,13 @@ import { calculateAverageClients } from 'vault/utils/chart-helpers'; */ export default class MonthlyUsage extends Component { get averageTotalClients() { - return calculateAverageClients(this.args.verticalBarChartData, 'clients') || '0'; + return calculateAverage(this.args.verticalBarChartData, 'clients') || '0'; } get averageNewClients() { return ( - calculateAverageClients( - this.args.verticalBarChartData.map((d) => d.new_clients), + calculateAverage( + this.args.verticalBarChartData?.map((d) => d.new_clients), 'clients' ) || '0' ); diff --git a/ui/app/components/clients/running-total.js b/ui/app/components/clients/running-total.js index 5d46ada5a5..6c3b09951b 100644 --- a/ui/app/components/clients/running-total.js +++ b/ui/app/components/clients/running-total.js @@ -1,5 +1,5 @@ import Component from '@glimmer/component'; -import { calculateAverageClients } from 'vault/utils/chart-helpers'; +import { calculateAverage } from 'vault/utils/chart-helpers'; /** * @module RunningTotal @@ -45,14 +45,14 @@ export default class RunningTotal extends Component { get entityClientData() { return { runningTotal: this.args.runningTotals.entity_clients, - averageNewClients: calculateAverageClients(this.args.barChartData, 'entity_clients') || '0', + averageNewClients: calculateAverage(this.args.barChartData, 'entity_clients') || '0', }; } get nonEntityClientData() { return { 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() { - if (this.args.barChartData.length === 1) { - const monthData = this.args.lineChartData[0]; + if (this.args.lineChartData?.length === 1) { + const monthData = this.args?.lineChartData[0]; return { total: { total: monthData.clients, diff --git a/ui/app/components/clients/vertical-bar-chart.js b/ui/app/components/clients/vertical-bar-chart.js index d3b8cae035..dc49339e95 100644 --- a/ui/app/components/clients/vertical-bar-chart.js +++ b/ui/app/components/clients/vertical-bar-chart.js @@ -14,6 +14,7 @@ import { TRANSLATE, formatNumbers, } from 'vault/utils/chart-helpers'; +import { formatNumber } from 'core/helpers/format-number'; /** * @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 {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} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart */ export default class VerticalBarChart extends Component { @@ -83,6 +85,7 @@ export default class VerticalBarChart extends Component { .append('rect') .attr('width', '7px') .attr('class', 'data-bar') + .attr('data-test-vertical-chart', 'data-bar') .attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`) .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 @@ -101,8 +104,13 @@ export default class VerticalBarChart extends Component { const xAxis = axisBottom(xScale).tickSize(0); - yAxis(chartSvg.append('g')); - xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`)); + yAxis(chartSvg.append('g').attr('data-test-vertical-chart', 'y-axis-labels')); + 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 @@ -129,9 +137,9 @@ export default class VerticalBarChart extends Component { // MOUSE EVENT FOR TOOLTIP tooltipRect.on('mouseover', (data) => { let hoveredMonth = data[this.xKey]; - this.tooltipTotal = `${data[this.yKey]} ${data.new_clients ? 'total' : 'new'} clients`; - this.entityClients = `${data.entity_clients} entity clients`; - this.nonEntityClients = `${data.non_entity_clients} non-entity clients`; + this.tooltipTotal = `${formatNumber([data[this.yKey]])} ${data.new_clients ? 'total' : 'new'} clients`; + this.entityClients = `${formatNumber([data.entity_clients])} entity clients`; + this.nonEntityClients = `${formatNumber([data.non_entity_clients])} non-entity clients`; let node = chartSvg .selectAll('rect.data-bar') // filter for the top data bar (so y-coord !== 0) with matching month diff --git a/ui/app/styles/core/charts.scss b/ui/app/styles/core/charts.scss index dbc2204d0c..be73b2d0db 100644 --- a/ui/app/styles/core/charts.scss +++ b/ui/app/styles/core/charts.scss @@ -130,10 +130,17 @@ max-width: none; padding-right: 20px; padding-left: 20px; + display: flex; > div { box-shadow: none !important; } + + > div.empty-state { + white-space: nowrap; + align-self: stretch; + width: 100%; + } } .chart-subTitle { diff --git a/ui/app/templates/components/clients/attribution.hbs b/ui/app/templates/components/clients/attribution.hbs index 98475bc0e2..a71ce44e1a 100644 --- a/ui/app/templates/components/clients/attribution.hbs +++ b/ui/app/templates/components/clients/attribution.hbs @@ -24,7 +24,10 @@ {{#if this.barChartTotalClients}} {{#if (or @isDateRange @isCurrentMonth)}} -
+
@@ -230,6 +231,7 @@
-
+
@@ -17,30 +17,31 @@

-
+

Average total clients per month

{{format-number this.averageTotalClients}}

-
+

Average new clients per month

{{format-number this.averageNewClients}}

-
+
{{#if @timestamp}} Updated {{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}} {{/if}}
-
- {{capitalize @chartLegend.0.label}} - {{capitalize @chartLegend.1.label}} -
- + {{#if @verticalBarChartData}} +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+ {{/if}}
\ No newline at end of file diff --git a/ui/app/templates/components/clients/running-total.hbs b/ui/app/templates/components/clients/running-total.hbs index 9266f308a9..d20289d41d 100644 --- a/ui/app/templates/components/clients/running-total.hbs +++ b/ui/app/templates/components/clients/running-total.hbs @@ -7,7 +7,7 @@ The total client count number is an important consideration for Vault billing.

-
+
-
+
-
+
@@ -109,17 +109,19 @@

-
+
{{#if @timestamp}} Updated {{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}} {{/if}}
-
- {{capitalize @chartLegend.0.label}} - {{capitalize @chartLegend.1.label}} -
+ {{#if this.hasAverageNewClients}} +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+ {{/if}}
{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/clients/vertical-bar-chart.hbs b/ui/app/templates/components/clients/vertical-bar-chart.hbs index 4cb369f113..ffe03d75c2 100644 --- a/ui/app/templates/components/clients/vertical-bar-chart.hbs +++ b/ui/app/templates/components/clients/vertical-bar-chart.hbs @@ -8,9 +8,7 @@ > {{else}} -
- -
+ {{/if}} {{! TOOLTIP }} diff --git a/ui/app/utils/chart-helpers.js b/ui/app/utils/chart-helpers.js index c35d591888..25dc57460e 100644 --- a/ui/app/utils/chart-helpers.js +++ b/ui/app/utils/chart-helpers.js @@ -27,9 +27,11 @@ export function formatTooltipNumber(value) { return new Intl.NumberFormat().format(value); } -export function calculateAverageClients(dataset, objectKey) { - // dataset is an array of objects (consumed by the chart components) - // objectKey is the key of the integer we want to calculate, ex: 'entity_clients', 'non_entity_clients', 'clients' - let getIntegers = dataset.map((d) => (d[objectKey] ? d[objectKey] : 0)); // if undefined no data, so return 0 - return getIntegers.length !== 0 ? Math.round(mean(getIntegers)) : null; +export function calculateAverage(dataset, objectKey) { + if (!Array.isArray(dataset) || dataset?.length === 0) return null; + // if an array of objects, objectKey of the integer we want to calculate, ex: 'entity_clients' + // if d[objectKey] is undefined there is no value, so return 0 + 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; } diff --git a/ui/lib/core/addon/templates/components/stat-text.hbs b/ui/lib/core/addon/templates/components/stat-text.hbs index 805bafc747..66eb70db9e 100644 --- a/ui/lib/core/addon/templates/components/stat-text.hbs +++ b/ui/lib/core/addon/templates/components/stat-text.hbs @@ -1,6 +1,6 @@
{{@label}}
diff --git a/ui/lib/core/addon/utils/date-formatters.js b/ui/lib/core/addon/utils/date-formatters.js index a768949f06..25736d3f4f 100644 --- a/ui/lib/core/addon/utils/date-formatters.js +++ b/ui/lib/core/addon/utils/date-formatters.js @@ -34,7 +34,7 @@ export const parseRFC3339 = (timestamp) => { 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) { let array = date.split('/'); array.splice(1, 0, '01'); diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js index 68249c5f44..ebb6009c55 100644 --- a/ui/mirage/handlers/clients.js +++ b/ui/mirage/handlers/clients.js @@ -1684,6 +1684,7 @@ const MOCK_MONTHLY_DATA = [ }, }, { + timestamp: formatISO(addMonths(UPGRADE_DATE, 3)), counts: { distinct_entities: 0, entity_clients: 10873, @@ -2236,7 +2237,7 @@ const MOCK_MONTHLY_DATA = [ }, }, { - timestamp: formatISO(addMonths(UPGRADE_DATE, 3)), + timestamp: formatISO(addMonths(UPGRADE_DATE, 4)), counts: { distinct_entities: 0, entity_clients: 10342, diff --git a/ui/tests/acceptance/client-history-test.js b/ui/tests/acceptance/client-history-test.js index b73c0a4de1..148f9fbb79 100644 --- a/ui/tests/acceptance/client-history-test.js +++ b/ui/tests/acceptance/client-history-test.js @@ -9,6 +9,8 @@ import { SELECTORS, overrideResponse } from '../helpers/clients'; import { create } from 'ember-cli-page-object'; import ss from 'vault/tests/pages/components/search-select'; import { clickTrigger } from 'ember-power-select/test-support/helpers'; +import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters'; + const searchSelect = create(ss); 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) { - assert.expect(17); + assert.expect(26); // TODO CMB: wire up dynamically generated activity to mirage clients handler // const activity = generateActivityResponse(5, LICENSE_START, LAST_MONTH); await visit('/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(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(find('.menu-list button:not([disabled])')); + await click(find(`[data-test-date-modal-year="${UPGRADE_DATE.getFullYear()}`)); await click('[data-test-modal-save]'); 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'); assert .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( findAll('[data-test-line-chart="plot-point"]').length, 5, @@ -164,9 +195,10 @@ module('Acceptance | clients history tab', function (hooks) { // query custom end month await click(SELECTORS.rangeDropdown); await click('[data-test-show-calendar]'); - let readOnlyMonths = findAll('[data-test-calendar-month].is-readOnly'); - let clickableMonths = findAll('[data-test-calendar-month]').filter((m) => !readOnlyMonths.includes(m)); - await click(clickableMonths[1]); + if (parseInt(find('[data-test-display-year]').innerText) < NEW_DATE.getFullYear()) { + await click('[data-test-future-year]'); + } + 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.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'); assert.equal( findAll('[data-test-line-chart="plot-point"]').length, - 2, - `line chart plots 2 points to match query` + 3, + `line chart plots 3 points to match query` ); + let xAxisLabels = findAll('[data-test-line-chart="x-axis-labels"] g.tick text'); assert - .dom(findAll('[data-test-line-chart="x-axis-labels"] g.tick text')[1]) - .hasText('2/22', 'x-axis labels start with updated billing start month'); + .dom(xAxisLabels[xAxisLabels.length - 1]) + .hasText(`${format(subMonths(LAST_MONTH, 2), 'M/yy')}`, 'x-axis labels end with queried end month'); // query for single, historical month await click(SELECTORS.rangeDropdown); await click('[data-test-show-calendar]'); - readOnlyMonths = findAll('[data-test-calendar-month].is-readOnly'); - clickableMonths = findAll('[data-test-calendar-month]').filter((m) => !readOnlyMonths.includes(m)); - await click(clickableMonths[0]); + if (parseInt(find('[data-test-display-year]').innerText) < NEW_DATE.getFullYear()) { + await click('[data-test-future-year]'); + } + 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.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"]').exists('new 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 await click('[data-test-start-date-editor] button'); await click(SELECTORS.yearDropdown); - let years = findAll('.menu-list button:not([disabled])'); - await click(years[years.length - 1]); + await click(find(`[data-test-date-modal-year="${LICENSE_START.getFullYear() - 3}`)); await click('[data-test-modal-save]'); assert diff --git a/ui/tests/helpers/clients.js b/ui/tests/helpers/clients.js index 4e9acbca66..f2f9dbcaa1 100644 --- a/ui/tests/helpers/clients.js +++ b/ui/tests/helpers/clients.js @@ -8,20 +8,14 @@ import { Response } from 'miragejs'; Filtering (data with 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 Version queries available queries unavailable License start date this month - */ - -// TODO -/* -Filtering different date ranges (hist only) -Upgrade warning - */ export const SELECTORS = { currentMonthActiveTab: '.active[data-test-current-month]', diff --git a/ui/tests/integration/components/clients/line-chart-test.js b/ui/tests/integration/components/clients/line-chart-test.js index 1129a7f431..013e763367 100644 --- a/ui/tests/integration/components/clients/line-chart-test.js +++ b/ui/tests/integration/components/clients/line-chart-test.js @@ -1,12 +1,15 @@ import { module, test } from '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 { format, formatRFC3339, subMonths } from 'date-fns'; +import { formatChartDate } from 'core/utils/date-formatters'; module('Integration | Component | clients/line-chart', function (hooks) { setupRenderingTest(hooks); - + const CURRENT_DATE = new Date(); hooks.beforeEach(function () { + this.set('xKey', 'foo'); + this.set('yKey', 'bar'); this.set('dataset', [ { foo: 1, @@ -30,11 +33,179 @@ module('Integration | Component | clients/line-chart', function (hooks) { test('it renders', async function (assert) { await render(hbs`
- +
`); 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` +
+ +
+ `); + 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` +
+ +
+ `); + + 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` +
+ +
+ `); + + 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` +
+ +
+ `); + + 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` +
+ +
+ `); + + 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' + ); }); }); diff --git a/ui/tests/integration/components/clients/monthly-usage-test.js b/ui/tests/integration/components/clients/monthly-usage-test.js new file mode 100644 index 0000000000..e14d0183b8 --- /dev/null +++ b/ui/tests/integration/components/clients/monthly-usage-test.js @@ -0,0 +1,1474 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { formatRFC3339 } from 'date-fns'; +import { findAll } from '@ember/test-helpers'; +import { calculateAverage } from 'vault/utils/chart-helpers'; +import { formatNumber } from 'core/helpers/format-number'; + +module('Integration | Component | clients/monthly-usage', function (hooks) { + setupRenderingTest(hooks); + const DATASET = [ + { + month: '8/21', + timestamp: '2021-08-01T00:00:00Z', + counts: null, + namespaces: [], + new_clients: { + month: '8/21', + namespaces: [], + }, + namespaces_by_key: {}, + }, + { + month: '9/21', + clients: 19251, + entity_clients: 10713, + non_entity_clients: 8538, + namespaces: [ + { + label: 'root', + clients: 4852, + entity_clients: 3108, + non_entity_clients: 1744, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1598, + entity_clients: 687, + non_entity_clients: 911, + }, + { + label: 'path-1', + clients: 1429, + entity_clients: 981, + non_entity_clients: 448, + }, + { + label: 'path-4-with-over-18-characters', + clients: 965, + entity_clients: 720, + non_entity_clients: 245, + }, + { + label: 'path-2', + clients: 860, + entity_clients: 720, + non_entity_clients: 140, + }, + ], + }, + { + label: 'test-ns-2/', + clients: 4702, + entity_clients: 3057, + non_entity_clients: 1645, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1686, + entity_clients: 926, + non_entity_clients: 760, + }, + { + label: 'path-4-with-over-18-characters', + clients: 1525, + entity_clients: 789, + non_entity_clients: 736, + }, + { + label: 'path-2', + clients: 905, + entity_clients: 849, + non_entity_clients: 56, + }, + { + label: 'path-1', + clients: 586, + entity_clients: 493, + non_entity_clients: 93, + }, + ], + }, + { + label: 'test-ns-1/', + clients: 4569, + entity_clients: 1871, + non_entity_clients: 2698, + mounts: [ + { + label: 'path-4-with-over-18-characters', + clients: 1534, + entity_clients: 619, + non_entity_clients: 915, + }, + { + label: 'path-3-with-over-18-characters', + clients: 1528, + entity_clients: 589, + non_entity_clients: 939, + }, + { + label: 'path-1', + clients: 828, + entity_clients: 612, + non_entity_clients: 216, + }, + { + label: 'path-2', + clients: 679, + entity_clients: 51, + non_entity_clients: 628, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 3771, + entity_clients: 2029, + non_entity_clients: 1742, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1249, + entity_clients: 793, + non_entity_clients: 456, + }, + { + label: 'path-1', + clients: 1046, + entity_clients: 444, + non_entity_clients: 602, + }, + { + label: 'path-2', + clients: 930, + entity_clients: 277, + non_entity_clients: 653, + }, + { + label: 'path-4-with-over-18-characters', + clients: 546, + entity_clients: 515, + non_entity_clients: 31, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 1357, + entity_clients: 648, + non_entity_clients: 709, + mounts: [ + { + label: 'path-1', + clients: 613, + entity_clients: 23, + non_entity_clients: 590, + }, + { + label: 'path-3-with-over-18-characters', + clients: 543, + entity_clients: 465, + non_entity_clients: 78, + }, + { + label: 'path-2', + clients: 146, + entity_clients: 141, + non_entity_clients: 5, + }, + { + label: 'path-4-with-over-18-characters', + clients: 55, + entity_clients: 19, + non_entity_clients: 36, + }, + ], + }, + ], + namespaces_by_key: { + root: { + month: '9/21', + clients: 4852, + entity_clients: 3108, + non_entity_clients: 1744, + new_clients: { + month: '9/21', + label: 'root', + clients: 2525, + entity_clients: 1315, + non_entity_clients: 1210, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1598, + entity_clients: 687, + non_entity_clients: 911, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1055, + entity_clients: 257, + non_entity_clients: 798, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 1429, + entity_clients: 981, + non_entity_clients: 448, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 543, + entity_clients: 340, + non_entity_clients: 203, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 965, + entity_clients: 720, + non_entity_clients: 245, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 136, + entity_clients: 7, + non_entity_clients: 129, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 860, + entity_clients: 720, + non_entity_clients: 140, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 791, + entity_clients: 711, + non_entity_clients: 80, + }, + }, + }, + }, + 'test-ns-2/': { + month: '9/21', + clients: 4702, + entity_clients: 3057, + non_entity_clients: 1645, + new_clients: { + month: '9/21', + label: 'test-ns-2/', + clients: 1537, + entity_clients: 662, + non_entity_clients: 875, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1686, + entity_clients: 926, + non_entity_clients: 760, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 520, + entity_clients: 13, + non_entity_clients: 507, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 1525, + entity_clients: 789, + non_entity_clients: 736, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 499, + entity_clients: 197, + non_entity_clients: 302, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 905, + entity_clients: 849, + non_entity_clients: 56, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 398, + entity_clients: 370, + non_entity_clients: 28, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 586, + entity_clients: 493, + non_entity_clients: 93, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 120, + entity_clients: 82, + non_entity_clients: 38, + }, + }, + }, + }, + 'test-ns-1/': { + month: '9/21', + clients: 4569, + entity_clients: 1871, + non_entity_clients: 2698, + new_clients: { + month: '9/21', + label: 'test-ns-1/', + clients: 2712, + entity_clients: 879, + non_entity_clients: 1833, + }, + mounts_by_key: { + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 1534, + entity_clients: 619, + non_entity_clients: 915, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 740, + entity_clients: 39, + non_entity_clients: 701, + }, + }, + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1528, + entity_clients: 589, + non_entity_clients: 939, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1250, + entity_clients: 536, + non_entity_clients: 714, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 828, + entity_clients: 612, + non_entity_clients: 216, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 463, + entity_clients: 283, + non_entity_clients: 180, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 679, + entity_clients: 51, + non_entity_clients: 628, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 259, + entity_clients: 21, + non_entity_clients: 238, + }, + }, + }, + }, + 'test-ns-2-with-namespace-length-over-18-characters/': { + month: '9/21', + clients: 3771, + entity_clients: 2029, + non_entity_clients: 1742, + new_clients: { + month: '9/21', + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 2087, + entity_clients: 902, + non_entity_clients: 1185, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1249, + entity_clients: 793, + non_entity_clients: 456, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 472, + entity_clients: 260, + non_entity_clients: 212, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 1046, + entity_clients: 444, + non_entity_clients: 602, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 775, + entity_clients: 349, + non_entity_clients: 426, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 930, + entity_clients: 277, + non_entity_clients: 653, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 632, + entity_clients: 90, + non_entity_clients: 542, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 546, + entity_clients: 515, + non_entity_clients: 31, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 208, + entity_clients: 203, + non_entity_clients: 5, + }, + }, + }, + }, + 'test-ns-1-with-namespace-length-over-18-characters/': { + month: '9/21', + clients: 1357, + entity_clients: 648, + non_entity_clients: 709, + new_clients: { + month: '9/21', + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 560, + entity_clients: 189, + non_entity_clients: 371, + }, + mounts_by_key: { + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 613, + entity_clients: 23, + non_entity_clients: 590, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 318, + entity_clients: 12, + non_entity_clients: 306, + }, + }, + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 543, + entity_clients: 465, + non_entity_clients: 78, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 126, + entity_clients: 89, + non_entity_clients: 37, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 146, + entity_clients: 141, + non_entity_clients: 5, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 76, + entity_clients: 75, + non_entity_clients: 1, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 55, + entity_clients: 19, + non_entity_clients: 36, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 40, + entity_clients: 13, + non_entity_clients: 27, + }, + }, + }, + }, + }, + new_clients: { + month: '9/21', + clients: 9421, + entity_clients: 3947, + non_entity_clients: 5474, + namespaces: [ + { + label: 'test-ns-1/', + clients: 2712, + entity_clients: 879, + non_entity_clients: 1833, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1250, + entity_clients: 536, + non_entity_clients: 714, + }, + { + label: 'path-4-with-over-18-characters', + clients: 740, + entity_clients: 39, + non_entity_clients: 701, + }, + { + label: 'path-1', + clients: 463, + entity_clients: 283, + non_entity_clients: 180, + }, + { + label: 'path-2', + clients: 259, + entity_clients: 21, + non_entity_clients: 238, + }, + ], + }, + { + label: 'root', + clients: 2525, + entity_clients: 1315, + non_entity_clients: 1210, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1055, + entity_clients: 257, + non_entity_clients: 798, + }, + { + label: 'path-2', + clients: 791, + entity_clients: 711, + non_entity_clients: 80, + }, + { + label: 'path-1', + clients: 543, + entity_clients: 340, + non_entity_clients: 203, + }, + { + label: 'path-4-with-over-18-characters', + clients: 136, + entity_clients: 7, + non_entity_clients: 129, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 2087, + entity_clients: 902, + non_entity_clients: 1185, + mounts: [ + { + label: 'path-1', + clients: 775, + entity_clients: 349, + non_entity_clients: 426, + }, + { + label: 'path-2', + clients: 632, + entity_clients: 90, + non_entity_clients: 542, + }, + { + label: 'path-3-with-over-18-characters', + clients: 472, + entity_clients: 260, + non_entity_clients: 212, + }, + { + label: 'path-4-with-over-18-characters', + clients: 208, + entity_clients: 203, + non_entity_clients: 5, + }, + ], + }, + { + label: 'test-ns-2/', + clients: 1537, + entity_clients: 662, + non_entity_clients: 875, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 520, + entity_clients: 13, + non_entity_clients: 507, + }, + { + label: 'path-4-with-over-18-characters', + clients: 499, + entity_clients: 197, + non_entity_clients: 302, + }, + { + label: 'path-2', + clients: 398, + entity_clients: 370, + non_entity_clients: 28, + }, + { + label: 'path-1', + clients: 120, + entity_clients: 82, + non_entity_clients: 38, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 560, + entity_clients: 189, + non_entity_clients: 371, + mounts: [ + { + label: 'path-1', + clients: 318, + entity_clients: 12, + non_entity_clients: 306, + }, + { + label: 'path-3-with-over-18-characters', + clients: 126, + entity_clients: 89, + non_entity_clients: 37, + }, + { + label: 'path-2', + clients: 76, + entity_clients: 75, + non_entity_clients: 1, + }, + { + label: 'path-4-with-over-18-characters', + clients: 40, + entity_clients: 13, + non_entity_clients: 27, + }, + ], + }, + ], + }, + }, + { + month: '10/21', + clients: 19417, + entity_clients: 10105, + non_entity_clients: 9312, + namespaces: [ + { + label: 'root', + clients: 4835, + entity_clients: 2364, + non_entity_clients: 2471, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1797, + entity_clients: 883, + non_entity_clients: 914, + }, + { + label: 'path-1', + clients: 1501, + entity_clients: 663, + non_entity_clients: 838, + }, + { + label: 'path-2', + clients: 1461, + entity_clients: 800, + non_entity_clients: 661, + }, + { + label: 'path-4-with-over-18-characters', + clients: 76, + entity_clients: 18, + non_entity_clients: 58, + }, + ], + }, + { + label: 'test-ns-2/', + clients: 4027, + entity_clients: 1692, + non_entity_clients: 2335, + mounts: [ + { + label: 'path-4-with-over-18-characters', + clients: 1223, + entity_clients: 820, + non_entity_clients: 403, + }, + { + label: 'path-3-with-over-18-characters', + clients: 1110, + entity_clients: 111, + non_entity_clients: 999, + }, + { + label: 'path-1', + clients: 1034, + entity_clients: 462, + non_entity_clients: 572, + }, + { + label: 'path-2', + clients: 660, + entity_clients: 299, + non_entity_clients: 361, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 3924, + entity_clients: 2132, + non_entity_clients: 1792, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1411, + entity_clients: 765, + non_entity_clients: 646, + }, + { + label: 'path-2', + clients: 1205, + entity_clients: 382, + non_entity_clients: 823, + }, + { + label: 'path-1', + clients: 884, + entity_clients: 850, + non_entity_clients: 34, + }, + { + label: 'path-4-with-over-18-characters', + clients: 424, + entity_clients: 135, + non_entity_clients: 289, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 3639, + entity_clients: 2314, + non_entity_clients: 1325, + mounts: [ + { + label: 'path-1', + clients: 1062, + entity_clients: 781, + non_entity_clients: 281, + }, + { + label: 'path-4-with-over-18-characters', + clients: 1021, + entity_clients: 609, + non_entity_clients: 412, + }, + { + label: 'path-2', + clients: 849, + entity_clients: 426, + non_entity_clients: 423, + }, + { + label: 'path-3-with-over-18-characters', + clients: 707, + entity_clients: 498, + non_entity_clients: 209, + }, + ], + }, + { + label: 'test-ns-1/', + clients: 2992, + entity_clients: 1603, + non_entity_clients: 1389, + mounts: [ + { + label: 'path-1', + clients: 1140, + entity_clients: 480, + non_entity_clients: 660, + }, + { + label: 'path-4-with-over-18-characters', + clients: 1058, + entity_clients: 651, + non_entity_clients: 407, + }, + { + label: 'path-2', + clients: 575, + entity_clients: 416, + non_entity_clients: 159, + }, + { + label: 'path-3-with-over-18-characters', + clients: 219, + entity_clients: 56, + non_entity_clients: 163, + }, + ], + }, + ], + namespaces_by_key: { + root: { + month: '10/21', + clients: 4835, + entity_clients: 2364, + non_entity_clients: 2471, + new_clients: { + month: '10/21', + label: 'root', + clients: 1732, + entity_clients: 586, + non_entity_clients: 1146, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 1797, + entity_clients: 883, + non_entity_clients: 914, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 907, + entity_clients: 192, + non_entity_clients: 715, + }, + }, + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1501, + entity_clients: 663, + non_entity_clients: 838, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 276, + entity_clients: 202, + non_entity_clients: 74, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 1461, + entity_clients: 800, + non_entity_clients: 661, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 502, + entity_clients: 189, + non_entity_clients: 313, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 76, + entity_clients: 18, + non_entity_clients: 58, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 47, + entity_clients: 3, + non_entity_clients: 44, + }, + }, + }, + }, + 'test-ns-2/': { + month: '10/21', + clients: 4027, + entity_clients: 1692, + non_entity_clients: 2335, + new_clients: { + month: '10/21', + label: 'test-ns-2/', + clients: 2301, + entity_clients: 678, + non_entity_clients: 1623, + }, + mounts_by_key: { + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 1223, + entity_clients: 820, + non_entity_clients: 403, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 602, + entity_clients: 212, + non_entity_clients: 390, + }, + }, + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 1110, + entity_clients: 111, + non_entity_clients: 999, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 440, + entity_clients: 7, + non_entity_clients: 433, + }, + }, + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1034, + entity_clients: 462, + non_entity_clients: 572, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 980, + entity_clients: 454, + non_entity_clients: 526, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 660, + entity_clients: 299, + non_entity_clients: 361, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 279, + entity_clients: 5, + non_entity_clients: 274, + }, + }, + }, + }, + 'test-ns-2-with-namespace-length-over-18-characters/': { + month: '10/21', + clients: 3924, + entity_clients: 2132, + non_entity_clients: 1792, + new_clients: { + month: '10/21', + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 1561, + entity_clients: 1225, + non_entity_clients: 336, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 1411, + entity_clients: 765, + non_entity_clients: 646, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 948, + entity_clients: 660, + non_entity_clients: 288, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 1205, + entity_clients: 382, + non_entity_clients: 823, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 305, + entity_clients: 289, + non_entity_clients: 16, + }, + }, + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 884, + entity_clients: 850, + non_entity_clients: 34, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 230, + entity_clients: 207, + non_entity_clients: 23, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 424, + entity_clients: 135, + non_entity_clients: 289, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 78, + entity_clients: 69, + non_entity_clients: 9, + }, + }, + }, + }, + 'test-ns-1-with-namespace-length-over-18-characters/': { + month: '10/21', + clients: 3639, + entity_clients: 2314, + non_entity_clients: 1325, + new_clients: { + month: '10/21', + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 1245, + entity_clients: 710, + non_entity_clients: 535, + }, + mounts_by_key: { + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1062, + entity_clients: 781, + non_entity_clients: 281, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 288, + entity_clients: 63, + non_entity_clients: 225, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 1021, + entity_clients: 609, + non_entity_clients: 412, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 440, + entity_clients: 323, + non_entity_clients: 117, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 849, + entity_clients: 426, + non_entity_clients: 423, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 339, + entity_clients: 308, + non_entity_clients: 31, + }, + }, + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 707, + entity_clients: 498, + non_entity_clients: 209, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 178, + entity_clients: 16, + non_entity_clients: 162, + }, + }, + }, + }, + 'test-ns-1/': { + month: '10/21', + clients: 2992, + entity_clients: 1603, + non_entity_clients: 1389, + new_clients: { + month: '10/21', + label: 'test-ns-1/', + clients: 820, + entity_clients: 356, + non_entity_clients: 464, + }, + mounts_by_key: { + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1140, + entity_clients: 480, + non_entity_clients: 660, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 239, + entity_clients: 30, + non_entity_clients: 209, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 1058, + entity_clients: 651, + non_entity_clients: 407, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 256, + entity_clients: 63, + non_entity_clients: 193, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 575, + entity_clients: 416, + non_entity_clients: 159, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 259, + entity_clients: 245, + non_entity_clients: 14, + }, + }, + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 219, + entity_clients: 56, + non_entity_clients: 163, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 66, + entity_clients: 18, + non_entity_clients: 48, + }, + }, + }, + }, + }, + new_clients: { + month: '10/21', + clients: 7659, + entity_clients: 3555, + non_entity_clients: 4104, + namespaces: [ + { + label: 'test-ns-2/', + clients: 2301, + entity_clients: 678, + non_entity_clients: 1623, + mounts: [ + { + label: 'path-1', + clients: 980, + entity_clients: 454, + non_entity_clients: 526, + }, + { + label: 'path-4-with-over-18-characters', + clients: 602, + entity_clients: 212, + non_entity_clients: 390, + }, + { + label: 'path-3-with-over-18-characters', + clients: 440, + entity_clients: 7, + non_entity_clients: 433, + }, + { + label: 'path-2', + clients: 279, + entity_clients: 5, + non_entity_clients: 274, + }, + ], + }, + { + label: 'root', + clients: 1732, + entity_clients: 586, + non_entity_clients: 1146, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 907, + entity_clients: 192, + non_entity_clients: 715, + }, + { + label: 'path-2', + clients: 502, + entity_clients: 189, + non_entity_clients: 313, + }, + { + label: 'path-1', + clients: 276, + entity_clients: 202, + non_entity_clients: 74, + }, + { + label: 'path-4-with-over-18-characters', + clients: 47, + entity_clients: 3, + non_entity_clients: 44, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 1561, + entity_clients: 1225, + non_entity_clients: 336, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 948, + entity_clients: 660, + non_entity_clients: 288, + }, + { + label: 'path-2', + clients: 305, + entity_clients: 289, + non_entity_clients: 16, + }, + { + label: 'path-1', + clients: 230, + entity_clients: 207, + non_entity_clients: 23, + }, + { + label: 'path-4-with-over-18-characters', + clients: 78, + entity_clients: 69, + non_entity_clients: 9, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 1245, + entity_clients: 710, + non_entity_clients: 535, + mounts: [ + { + label: 'path-4-with-over-18-characters', + clients: 440, + entity_clients: 323, + non_entity_clients: 117, + }, + { + label: 'path-2', + clients: 339, + entity_clients: 308, + non_entity_clients: 31, + }, + { + label: 'path-1', + clients: 288, + entity_clients: 63, + non_entity_clients: 225, + }, + { + label: 'path-3-with-over-18-characters', + clients: 178, + entity_clients: 16, + non_entity_clients: 162, + }, + ], + }, + { + label: 'test-ns-1/', + clients: 820, + entity_clients: 356, + non_entity_clients: 464, + mounts: [ + { + label: 'path-2', + clients: 259, + entity_clients: 245, + non_entity_clients: 14, + }, + { + label: 'path-4-with-over-18-characters', + clients: 256, + entity_clients: 63, + non_entity_clients: 193, + }, + { + label: 'path-1', + clients: 239, + entity_clients: 30, + non_entity_clients: 209, + }, + { + label: 'path-3-with-over-18-characters', + clients: 66, + entity_clients: 18, + non_entity_clients: 48, + }, + ], + }, + ], + }, + }, + ]; + hooks.beforeEach(function () { + this.set('timestamp', formatRFC3339(new Date())); + this.set('isDateRange', true); + this.set('chartLegend', [ + { label: 'entity clients', key: 'entity_clients' }, + { label: 'non-entity clients', key: 'non_entity_clients' }, + ]); + this.set('byMonthActivityData', DATASET); + }); + + test('it renders empty state with no data', async function (assert) { + await render(hbs` + + + `); + assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders'); + assert.dom('[data-test-component="empty-state"]').exists(); + assert.dom('[data-test-empty-state-subtext]').hasText('No data to display'); + assert.dom('[data-test-monthly-usage-average-total] p.data-details').hasText('0', 'average total is 0'); + assert.dom('[data-test-monthly-usage-average-new] p.data-details').hasText('0', 'average new is 0'); + assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render'); + assert.dom('[data-test-monthly-usage-legend]').doesNotExist('legend does not exist'); + assert.dom('[data-test-monthly-usage-timestamp]').exists('renders timestamp'); + }); + + test('it renders with month over month activity data', async function (assert) { + const expectedTotal = formatNumber([calculateAverage(DATASET, 'clients')]); + const expectedNew = formatNumber([ + calculateAverage( + DATASET?.map((d) => d.new_clients), + 'clients' + ), + ]); + await render(hbs` + + + `); + assert.dom('[data-test-monthly-usage]').exists('monthly usage component renders'); + assert.dom('[data-test-component="empty-state"]').doesNotExist(); + assert.dom('[data-test-vertical-bar-chart]').exists('vertical bar chart displays'); + assert.dom('[data-test-monthly-usage-legend]').exists('renders vertical bar chart legend'); + assert.dom('[data-test-monthly-usage-timestamp]').exists('renders timestamp'); + + findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => { + assert.dom(e).hasText(`${DATASET[i].month}`, `renders x-axis label: ${DATASET[i].month}`); + }); + assert + .dom('[data-test-vertical-chart="data-bar"]') + .exists( + { count: DATASET.filter((m) => m.counts !== null).length * 2 }, + 'renders correct number of data bars' + ); + assert + .dom('[data-test-monthly-usage-average-total] p.data-details') + .hasText(`${expectedTotal}`, `renders correct total average ${expectedTotal}`); + assert + .dom('[data-test-monthly-usage-average-new] p.data-details') + .hasText(`${expectedNew}`, `renders correct new average ${expectedNew}`); + }); +}); diff --git a/ui/tests/integration/components/clients/running-total-test.js b/ui/tests/integration/components/clients/running-total-test.js new file mode 100644 index 0000000000..82fa0adb04 --- /dev/null +++ b/ui/tests/integration/components/clients/running-total-test.js @@ -0,0 +1,1585 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { formatRFC3339 } from 'date-fns'; +import { findAll } from '@ember/test-helpers'; +import { calculateAverage } from 'vault/utils/chart-helpers'; +import { formatNumber } from 'core/helpers/format-number'; + +module('Integration | Component | clients/running-total', function (hooks) { + setupRenderingTest(hooks); + const MONTHLY_ACTIVITY = [ + { + month: '8/21', + timestamp: '2021-08-01T00:00:00Z', + counts: null, + namespaces: [], + new_clients: { + month: '8/21', + namespaces: [], + }, + namespaces_by_key: {}, + }, + { + month: '9/21', + clients: 19251, + entity_clients: 10713, + non_entity_clients: 8538, + namespaces: [ + { + label: 'root', + clients: 4852, + entity_clients: 3108, + non_entity_clients: 1744, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1598, + entity_clients: 687, + non_entity_clients: 911, + }, + { + label: 'path-1', + clients: 1429, + entity_clients: 981, + non_entity_clients: 448, + }, + { + label: 'path-4-with-over-18-characters', + clients: 965, + entity_clients: 720, + non_entity_clients: 245, + }, + { + label: 'path-2', + clients: 860, + entity_clients: 720, + non_entity_clients: 140, + }, + ], + }, + { + label: 'test-ns-2/', + clients: 4702, + entity_clients: 3057, + non_entity_clients: 1645, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1686, + entity_clients: 926, + non_entity_clients: 760, + }, + { + label: 'path-4-with-over-18-characters', + clients: 1525, + entity_clients: 789, + non_entity_clients: 736, + }, + { + label: 'path-2', + clients: 905, + entity_clients: 849, + non_entity_clients: 56, + }, + { + label: 'path-1', + clients: 586, + entity_clients: 493, + non_entity_clients: 93, + }, + ], + }, + { + label: 'test-ns-1/', + clients: 4569, + entity_clients: 1871, + non_entity_clients: 2698, + mounts: [ + { + label: 'path-4-with-over-18-characters', + clients: 1534, + entity_clients: 619, + non_entity_clients: 915, + }, + { + label: 'path-3-with-over-18-characters', + clients: 1528, + entity_clients: 589, + non_entity_clients: 939, + }, + { + label: 'path-1', + clients: 828, + entity_clients: 612, + non_entity_clients: 216, + }, + { + label: 'path-2', + clients: 679, + entity_clients: 51, + non_entity_clients: 628, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 3771, + entity_clients: 2029, + non_entity_clients: 1742, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1249, + entity_clients: 793, + non_entity_clients: 456, + }, + { + label: 'path-1', + clients: 1046, + entity_clients: 444, + non_entity_clients: 602, + }, + { + label: 'path-2', + clients: 930, + entity_clients: 277, + non_entity_clients: 653, + }, + { + label: 'path-4-with-over-18-characters', + clients: 546, + entity_clients: 515, + non_entity_clients: 31, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 1357, + entity_clients: 648, + non_entity_clients: 709, + mounts: [ + { + label: 'path-1', + clients: 613, + entity_clients: 23, + non_entity_clients: 590, + }, + { + label: 'path-3-with-over-18-characters', + clients: 543, + entity_clients: 465, + non_entity_clients: 78, + }, + { + label: 'path-2', + clients: 146, + entity_clients: 141, + non_entity_clients: 5, + }, + { + label: 'path-4-with-over-18-characters', + clients: 55, + entity_clients: 19, + non_entity_clients: 36, + }, + ], + }, + ], + namespaces_by_key: { + root: { + month: '9/21', + clients: 4852, + entity_clients: 3108, + non_entity_clients: 1744, + new_clients: { + month: '9/21', + label: 'root', + clients: 2525, + entity_clients: 1315, + non_entity_clients: 1210, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1598, + entity_clients: 687, + non_entity_clients: 911, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1055, + entity_clients: 257, + non_entity_clients: 798, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 1429, + entity_clients: 981, + non_entity_clients: 448, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 543, + entity_clients: 340, + non_entity_clients: 203, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 965, + entity_clients: 720, + non_entity_clients: 245, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 136, + entity_clients: 7, + non_entity_clients: 129, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 860, + entity_clients: 720, + non_entity_clients: 140, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 791, + entity_clients: 711, + non_entity_clients: 80, + }, + }, + }, + }, + 'test-ns-2/': { + month: '9/21', + clients: 4702, + entity_clients: 3057, + non_entity_clients: 1645, + new_clients: { + month: '9/21', + label: 'test-ns-2/', + clients: 1537, + entity_clients: 662, + non_entity_clients: 875, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1686, + entity_clients: 926, + non_entity_clients: 760, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 520, + entity_clients: 13, + non_entity_clients: 507, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 1525, + entity_clients: 789, + non_entity_clients: 736, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 499, + entity_clients: 197, + non_entity_clients: 302, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 905, + entity_clients: 849, + non_entity_clients: 56, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 398, + entity_clients: 370, + non_entity_clients: 28, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 586, + entity_clients: 493, + non_entity_clients: 93, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 120, + entity_clients: 82, + non_entity_clients: 38, + }, + }, + }, + }, + 'test-ns-1/': { + month: '9/21', + clients: 4569, + entity_clients: 1871, + non_entity_clients: 2698, + new_clients: { + month: '9/21', + label: 'test-ns-1/', + clients: 2712, + entity_clients: 879, + non_entity_clients: 1833, + }, + mounts_by_key: { + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 1534, + entity_clients: 619, + non_entity_clients: 915, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 740, + entity_clients: 39, + non_entity_clients: 701, + }, + }, + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1528, + entity_clients: 589, + non_entity_clients: 939, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1250, + entity_clients: 536, + non_entity_clients: 714, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 828, + entity_clients: 612, + non_entity_clients: 216, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 463, + entity_clients: 283, + non_entity_clients: 180, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 679, + entity_clients: 51, + non_entity_clients: 628, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 259, + entity_clients: 21, + non_entity_clients: 238, + }, + }, + }, + }, + 'test-ns-2-with-namespace-length-over-18-characters/': { + month: '9/21', + clients: 3771, + entity_clients: 2029, + non_entity_clients: 1742, + new_clients: { + month: '9/21', + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 2087, + entity_clients: 902, + non_entity_clients: 1185, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 1249, + entity_clients: 793, + non_entity_clients: 456, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 472, + entity_clients: 260, + non_entity_clients: 212, + }, + }, + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 1046, + entity_clients: 444, + non_entity_clients: 602, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 775, + entity_clients: 349, + non_entity_clients: 426, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 930, + entity_clients: 277, + non_entity_clients: 653, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 632, + entity_clients: 90, + non_entity_clients: 542, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 546, + entity_clients: 515, + non_entity_clients: 31, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 208, + entity_clients: 203, + non_entity_clients: 5, + }, + }, + }, + }, + 'test-ns-1-with-namespace-length-over-18-characters/': { + month: '9/21', + clients: 1357, + entity_clients: 648, + non_entity_clients: 709, + new_clients: { + month: '9/21', + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 560, + entity_clients: 189, + non_entity_clients: 371, + }, + mounts_by_key: { + 'path-1': { + month: '9/21', + label: 'path-1', + clients: 613, + entity_clients: 23, + non_entity_clients: 590, + new_clients: { + month: '9/21', + label: 'path-1', + clients: 318, + entity_clients: 12, + non_entity_clients: 306, + }, + }, + 'path-3-with-over-18-characters': { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 543, + entity_clients: 465, + non_entity_clients: 78, + new_clients: { + month: '9/21', + label: 'path-3-with-over-18-characters', + clients: 126, + entity_clients: 89, + non_entity_clients: 37, + }, + }, + 'path-2': { + month: '9/21', + label: 'path-2', + clients: 146, + entity_clients: 141, + non_entity_clients: 5, + new_clients: { + month: '9/21', + label: 'path-2', + clients: 76, + entity_clients: 75, + non_entity_clients: 1, + }, + }, + 'path-4-with-over-18-characters': { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 55, + entity_clients: 19, + non_entity_clients: 36, + new_clients: { + month: '9/21', + label: 'path-4-with-over-18-characters', + clients: 40, + entity_clients: 13, + non_entity_clients: 27, + }, + }, + }, + }, + }, + new_clients: { + month: '9/21', + clients: 9421, + entity_clients: 3947, + non_entity_clients: 5474, + namespaces: [ + { + label: 'test-ns-1/', + clients: 2712, + entity_clients: 879, + non_entity_clients: 1833, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1250, + entity_clients: 536, + non_entity_clients: 714, + }, + { + label: 'path-4-with-over-18-characters', + clients: 740, + entity_clients: 39, + non_entity_clients: 701, + }, + { + label: 'path-1', + clients: 463, + entity_clients: 283, + non_entity_clients: 180, + }, + { + label: 'path-2', + clients: 259, + entity_clients: 21, + non_entity_clients: 238, + }, + ], + }, + { + label: 'root', + clients: 2525, + entity_clients: 1315, + non_entity_clients: 1210, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1055, + entity_clients: 257, + non_entity_clients: 798, + }, + { + label: 'path-2', + clients: 791, + entity_clients: 711, + non_entity_clients: 80, + }, + { + label: 'path-1', + clients: 543, + entity_clients: 340, + non_entity_clients: 203, + }, + { + label: 'path-4-with-over-18-characters', + clients: 136, + entity_clients: 7, + non_entity_clients: 129, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 2087, + entity_clients: 902, + non_entity_clients: 1185, + mounts: [ + { + label: 'path-1', + clients: 775, + entity_clients: 349, + non_entity_clients: 426, + }, + { + label: 'path-2', + clients: 632, + entity_clients: 90, + non_entity_clients: 542, + }, + { + label: 'path-3-with-over-18-characters', + clients: 472, + entity_clients: 260, + non_entity_clients: 212, + }, + { + label: 'path-4-with-over-18-characters', + clients: 208, + entity_clients: 203, + non_entity_clients: 5, + }, + ], + }, + { + label: 'test-ns-2/', + clients: 1537, + entity_clients: 662, + non_entity_clients: 875, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 520, + entity_clients: 13, + non_entity_clients: 507, + }, + { + label: 'path-4-with-over-18-characters', + clients: 499, + entity_clients: 197, + non_entity_clients: 302, + }, + { + label: 'path-2', + clients: 398, + entity_clients: 370, + non_entity_clients: 28, + }, + { + label: 'path-1', + clients: 120, + entity_clients: 82, + non_entity_clients: 38, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 560, + entity_clients: 189, + non_entity_clients: 371, + mounts: [ + { + label: 'path-1', + clients: 318, + entity_clients: 12, + non_entity_clients: 306, + }, + { + label: 'path-3-with-over-18-characters', + clients: 126, + entity_clients: 89, + non_entity_clients: 37, + }, + { + label: 'path-2', + clients: 76, + entity_clients: 75, + non_entity_clients: 1, + }, + { + label: 'path-4-with-over-18-characters', + clients: 40, + entity_clients: 13, + non_entity_clients: 27, + }, + ], + }, + ], + }, + }, + { + month: '10/21', + clients: 19417, + entity_clients: 10105, + non_entity_clients: 9312, + namespaces: [ + { + label: 'root', + clients: 4835, + entity_clients: 2364, + non_entity_clients: 2471, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1797, + entity_clients: 883, + non_entity_clients: 914, + }, + { + label: 'path-1', + clients: 1501, + entity_clients: 663, + non_entity_clients: 838, + }, + { + label: 'path-2', + clients: 1461, + entity_clients: 800, + non_entity_clients: 661, + }, + { + label: 'path-4-with-over-18-characters', + clients: 76, + entity_clients: 18, + non_entity_clients: 58, + }, + ], + }, + { + label: 'test-ns-2/', + clients: 4027, + entity_clients: 1692, + non_entity_clients: 2335, + mounts: [ + { + label: 'path-4-with-over-18-characters', + clients: 1223, + entity_clients: 820, + non_entity_clients: 403, + }, + { + label: 'path-3-with-over-18-characters', + clients: 1110, + entity_clients: 111, + non_entity_clients: 999, + }, + { + label: 'path-1', + clients: 1034, + entity_clients: 462, + non_entity_clients: 572, + }, + { + label: 'path-2', + clients: 660, + entity_clients: 299, + non_entity_clients: 361, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 3924, + entity_clients: 2132, + non_entity_clients: 1792, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 1411, + entity_clients: 765, + non_entity_clients: 646, + }, + { + label: 'path-2', + clients: 1205, + entity_clients: 382, + non_entity_clients: 823, + }, + { + label: 'path-1', + clients: 884, + entity_clients: 850, + non_entity_clients: 34, + }, + { + label: 'path-4-with-over-18-characters', + clients: 424, + entity_clients: 135, + non_entity_clients: 289, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 3639, + entity_clients: 2314, + non_entity_clients: 1325, + mounts: [ + { + label: 'path-1', + clients: 1062, + entity_clients: 781, + non_entity_clients: 281, + }, + { + label: 'path-4-with-over-18-characters', + clients: 1021, + entity_clients: 609, + non_entity_clients: 412, + }, + { + label: 'path-2', + clients: 849, + entity_clients: 426, + non_entity_clients: 423, + }, + { + label: 'path-3-with-over-18-characters', + clients: 707, + entity_clients: 498, + non_entity_clients: 209, + }, + ], + }, + { + label: 'test-ns-1/', + clients: 2992, + entity_clients: 1603, + non_entity_clients: 1389, + mounts: [ + { + label: 'path-1', + clients: 1140, + entity_clients: 480, + non_entity_clients: 660, + }, + { + label: 'path-4-with-over-18-characters', + clients: 1058, + entity_clients: 651, + non_entity_clients: 407, + }, + { + label: 'path-2', + clients: 575, + entity_clients: 416, + non_entity_clients: 159, + }, + { + label: 'path-3-with-over-18-characters', + clients: 219, + entity_clients: 56, + non_entity_clients: 163, + }, + ], + }, + ], + namespaces_by_key: { + root: { + month: '10/21', + clients: 4835, + entity_clients: 2364, + non_entity_clients: 2471, + new_clients: { + month: '10/21', + label: 'root', + clients: 1732, + entity_clients: 586, + non_entity_clients: 1146, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 1797, + entity_clients: 883, + non_entity_clients: 914, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 907, + entity_clients: 192, + non_entity_clients: 715, + }, + }, + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1501, + entity_clients: 663, + non_entity_clients: 838, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 276, + entity_clients: 202, + non_entity_clients: 74, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 1461, + entity_clients: 800, + non_entity_clients: 661, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 502, + entity_clients: 189, + non_entity_clients: 313, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 76, + entity_clients: 18, + non_entity_clients: 58, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 47, + entity_clients: 3, + non_entity_clients: 44, + }, + }, + }, + }, + 'test-ns-2/': { + month: '10/21', + clients: 4027, + entity_clients: 1692, + non_entity_clients: 2335, + new_clients: { + month: '10/21', + label: 'test-ns-2/', + clients: 2301, + entity_clients: 678, + non_entity_clients: 1623, + }, + mounts_by_key: { + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 1223, + entity_clients: 820, + non_entity_clients: 403, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 602, + entity_clients: 212, + non_entity_clients: 390, + }, + }, + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 1110, + entity_clients: 111, + non_entity_clients: 999, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 440, + entity_clients: 7, + non_entity_clients: 433, + }, + }, + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1034, + entity_clients: 462, + non_entity_clients: 572, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 980, + entity_clients: 454, + non_entity_clients: 526, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 660, + entity_clients: 299, + non_entity_clients: 361, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 279, + entity_clients: 5, + non_entity_clients: 274, + }, + }, + }, + }, + 'test-ns-2-with-namespace-length-over-18-characters/': { + month: '10/21', + clients: 3924, + entity_clients: 2132, + non_entity_clients: 1792, + new_clients: { + month: '10/21', + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 1561, + entity_clients: 1225, + non_entity_clients: 336, + }, + mounts_by_key: { + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 1411, + entity_clients: 765, + non_entity_clients: 646, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 948, + entity_clients: 660, + non_entity_clients: 288, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 1205, + entity_clients: 382, + non_entity_clients: 823, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 305, + entity_clients: 289, + non_entity_clients: 16, + }, + }, + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 884, + entity_clients: 850, + non_entity_clients: 34, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 230, + entity_clients: 207, + non_entity_clients: 23, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 424, + entity_clients: 135, + non_entity_clients: 289, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 78, + entity_clients: 69, + non_entity_clients: 9, + }, + }, + }, + }, + 'test-ns-1-with-namespace-length-over-18-characters/': { + month: '10/21', + clients: 3639, + entity_clients: 2314, + non_entity_clients: 1325, + new_clients: { + month: '10/21', + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 1245, + entity_clients: 710, + non_entity_clients: 535, + }, + mounts_by_key: { + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1062, + entity_clients: 781, + non_entity_clients: 281, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 288, + entity_clients: 63, + non_entity_clients: 225, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 1021, + entity_clients: 609, + non_entity_clients: 412, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 440, + entity_clients: 323, + non_entity_clients: 117, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 849, + entity_clients: 426, + non_entity_clients: 423, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 339, + entity_clients: 308, + non_entity_clients: 31, + }, + }, + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 707, + entity_clients: 498, + non_entity_clients: 209, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 178, + entity_clients: 16, + non_entity_clients: 162, + }, + }, + }, + }, + 'test-ns-1/': { + month: '10/21', + clients: 2992, + entity_clients: 1603, + non_entity_clients: 1389, + new_clients: { + month: '10/21', + label: 'test-ns-1/', + clients: 820, + entity_clients: 356, + non_entity_clients: 464, + }, + mounts_by_key: { + 'path-1': { + month: '10/21', + label: 'path-1', + clients: 1140, + entity_clients: 480, + non_entity_clients: 660, + new_clients: { + month: '10/21', + label: 'path-1', + clients: 239, + entity_clients: 30, + non_entity_clients: 209, + }, + }, + 'path-4-with-over-18-characters': { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 1058, + entity_clients: 651, + non_entity_clients: 407, + new_clients: { + month: '10/21', + label: 'path-4-with-over-18-characters', + clients: 256, + entity_clients: 63, + non_entity_clients: 193, + }, + }, + 'path-2': { + month: '10/21', + label: 'path-2', + clients: 575, + entity_clients: 416, + non_entity_clients: 159, + new_clients: { + month: '10/21', + label: 'path-2', + clients: 259, + entity_clients: 245, + non_entity_clients: 14, + }, + }, + 'path-3-with-over-18-characters': { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 219, + entity_clients: 56, + non_entity_clients: 163, + new_clients: { + month: '10/21', + label: 'path-3-with-over-18-characters', + clients: 66, + entity_clients: 18, + non_entity_clients: 48, + }, + }, + }, + }, + }, + new_clients: { + month: '10/21', + clients: 7659, + entity_clients: 3555, + non_entity_clients: 4104, + namespaces: [ + { + label: 'test-ns-2/', + clients: 2301, + entity_clients: 678, + non_entity_clients: 1623, + mounts: [ + { + label: 'path-1', + clients: 980, + entity_clients: 454, + non_entity_clients: 526, + }, + { + label: 'path-4-with-over-18-characters', + clients: 602, + entity_clients: 212, + non_entity_clients: 390, + }, + { + label: 'path-3-with-over-18-characters', + clients: 440, + entity_clients: 7, + non_entity_clients: 433, + }, + { + label: 'path-2', + clients: 279, + entity_clients: 5, + non_entity_clients: 274, + }, + ], + }, + { + label: 'root', + clients: 1732, + entity_clients: 586, + non_entity_clients: 1146, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 907, + entity_clients: 192, + non_entity_clients: 715, + }, + { + label: 'path-2', + clients: 502, + entity_clients: 189, + non_entity_clients: 313, + }, + { + label: 'path-1', + clients: 276, + entity_clients: 202, + non_entity_clients: 74, + }, + { + label: 'path-4-with-over-18-characters', + clients: 47, + entity_clients: 3, + non_entity_clients: 44, + }, + ], + }, + { + label: 'test-ns-2-with-namespace-length-over-18-characters/', + clients: 1561, + entity_clients: 1225, + non_entity_clients: 336, + mounts: [ + { + label: 'path-3-with-over-18-characters', + clients: 948, + entity_clients: 660, + non_entity_clients: 288, + }, + { + label: 'path-2', + clients: 305, + entity_clients: 289, + non_entity_clients: 16, + }, + { + label: 'path-1', + clients: 230, + entity_clients: 207, + non_entity_clients: 23, + }, + { + label: 'path-4-with-over-18-characters', + clients: 78, + entity_clients: 69, + non_entity_clients: 9, + }, + ], + }, + { + label: 'test-ns-1-with-namespace-length-over-18-characters/', + clients: 1245, + entity_clients: 710, + non_entity_clients: 535, + mounts: [ + { + label: 'path-4-with-over-18-characters', + clients: 440, + entity_clients: 323, + non_entity_clients: 117, + }, + { + label: 'path-2', + clients: 339, + entity_clients: 308, + non_entity_clients: 31, + }, + { + label: 'path-1', + clients: 288, + entity_clients: 63, + non_entity_clients: 225, + }, + { + label: 'path-3-with-over-18-characters', + clients: 178, + entity_clients: 16, + non_entity_clients: 162, + }, + ], + }, + { + label: 'test-ns-1/', + clients: 820, + entity_clients: 356, + non_entity_clients: 464, + mounts: [ + { + label: 'path-2', + clients: 259, + entity_clients: 245, + non_entity_clients: 14, + }, + { + label: 'path-4-with-over-18-characters', + clients: 256, + entity_clients: 63, + non_entity_clients: 193, + }, + { + label: 'path-1', + clients: 239, + entity_clients: 30, + non_entity_clients: 209, + }, + { + label: 'path-3-with-over-18-characters', + clients: 66, + entity_clients: 18, + non_entity_clients: 48, + }, + ], + }, + ], + }, + }, + ]; + const NEW_ACTIVITY = MONTHLY_ACTIVITY.map((d) => d.new_clients); + const TOTAL_USAGE_COUNTS = { + clients: 38668, + entity_clients: 20818, + non_entity_clients: 17850, + }; + hooks.beforeEach(function () { + this.set('timestamp', formatRFC3339(new Date())); + this.set('chartLegend', [ + { label: 'entity clients', key: 'entity_clients' }, + { label: 'non-entity clients', key: 'non_entity_clients' }, + ]); + }); + + test('it renders with full monthly activity data', async function (assert) { + this.set('byMonthActivityData', MONTHLY_ACTIVITY); + this.set('byMonthNewClients', NEW_ACTIVITY); + this.set('totalUsageCounts', TOTAL_USAGE_COUNTS); + const expectedTotalEntity = formatNumber([TOTAL_USAGE_COUNTS.entity_clients]); + const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]); + const expectedNewEntity = formatNumber([calculateAverage(NEW_ACTIVITY, 'entity_clients')]); + const expectedNewNonEntity = formatNumber([calculateAverage(NEW_ACTIVITY, 'non_entity_clients')]); + + await render(hbs` + + + `); + + assert.dom('[data-test-running-total]').exists('running total component renders'); + assert.dom('[data-test-line-chart]').exists('line chart renders'); + assert.dom('[data-test-vertical-bar-chart]').exists('vertical bar chart renders'); + assert.dom('[data-test-running-total-legend]').exists('legend renders'); + assert.dom('[data-test-running-total-timestamp]').exists('renders timestamp'); + assert + .dom('[data-test-running-total-entity] p.data-details') + .hasText(`${expectedTotalEntity}`, `renders correct total average ${expectedTotalEntity}`); + assert + .dom('[data-test-running-total-nonentity] p.data-details') + .hasText(`${expectedTotalNonEntity}`, `renders correct new average ${expectedTotalNonEntity}`); + assert + .dom('[data-test-running-new-entity] p.data-details') + .hasText(`${expectedNewEntity}`, `renders correct total average ${expectedNewEntity}`); + assert + .dom('[data-test-running-new-nonentity] p.data-details') + .hasText(`${expectedNewNonEntity}`, `renders correct new average ${expectedNewNonEntity}`); + + // assert line chart is correct + findAll('[data-test-line-chart="x-axis-labels"] text').forEach((e, i) => { + assert + .dom(e) + .hasText( + `${MONTHLY_ACTIVITY[i].month}`, + `renders x-axis labels for line chart: ${MONTHLY_ACTIVITY[i].month}` + ); + }); + assert + .dom('[data-test-line-chart="plot-point"]') + .exists( + { count: MONTHLY_ACTIVITY.filter((m) => m.counts !== null).length }, + 'renders correct number of plot points' + ); + + // assert bar chart is correct + findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => { + assert + .dom(e) + .hasText( + `${MONTHLY_ACTIVITY[i].month}`, + `renders x-axis labels for bar chart: ${MONTHLY_ACTIVITY[i].month}` + ); + }); + assert + .dom('[data-test-vertical-chart="data-bar"]') + .exists( + { count: MONTHLY_ACTIVITY.filter((m) => m.counts !== null).length * 2 }, + 'renders correct number of data bars' + ); + }); + + test('it renders with no new monthly data', async function (assert) { + this.set('byMonthActivityData', MONTHLY_ACTIVITY); + this.set('byMonthNewClients', NEW_ACTIVITY); + this.set('totalUsageCounts', TOTAL_USAGE_COUNTS); + const expectedTotalEntity = formatNumber([TOTAL_USAGE_COUNTS.entity_clients]); + const expectedTotalNonEntity = formatNumber([TOTAL_USAGE_COUNTS.non_entity_clients]); + + await render(hbs` + + + `); + + assert.dom('[data-test-running-total]').exists('running total component renders'); + assert.dom('[data-test-line-chart]').exists('line chart renders'); + assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render'); + assert.dom('[data-test-running-total-legend]').doesNotExist('legend does not render'); + assert.dom('[data-test-component="empty-state"]').exists('renders empty state'); + assert.dom('[data-test-empty-state-title]').hasText('No new clients'); + assert.dom('[data-test-running-total-timestamp]').exists('renders timestamp'); + assert + .dom('[data-test-running-total-entity] p.data-details') + .hasText(`${expectedTotalEntity}`, `renders correct total average ${expectedTotalEntity}`); + assert + .dom('[data-test-running-total-nonentity] p.data-details') + .hasText(`${expectedTotalNonEntity}`, `renders correct new average ${expectedTotalNonEntity}`); + assert + .dom('[data-test-running-new-entity] p.data-details') + .hasText('0', 'renders 0 average new entity clients'); + assert + .dom('[data-test-running-new-nonentity] p.data-details') + .hasText('0', 'renders 0 average entity clients'); + }); + + test('it renders with single historical month data', async function (assert) { + const singleMonth = MONTHLY_ACTIVITY[MONTHLY_ACTIVITY.length - 1]; + const singleMonthNew = NEW_ACTIVITY[NEW_ACTIVITY.length - 1]; + this.set('singleMonth', [singleMonth]); + this.set('singleMonthNew', [singleMonthNew]); + const expectedTotalClients = formatNumber([singleMonth.clients]); + const expectedTotalEntity = formatNumber([singleMonth.entity_clients]); + const expectedTotalNonEntity = formatNumber([singleMonth.non_entity_clients]); + const expectedNewClients = formatNumber([singleMonthNew.clients]); + const expectedNewEntity = formatNumber([singleMonthNew.entity_clients]); + const expectedNewNonEntity = formatNumber([singleMonthNew.non_entity_clients]); + + await render(hbs` + + + `); + assert.dom('[data-test-running-total]').exists('running total component renders'); + assert.dom('[data-test-line-chart]').doesNotExist('line chart does not render'); + assert.dom('[data-test-vertical-bar-chart]').doesNotExist('vertical bar chart does not render'); + assert.dom('[data-test-running-total-legend]').doesNotExist('legend does not render'); + assert.dom('[data-test-running-total-timestamp]').doesNotExist('renders timestamp'); + assert.dom('[data-test-stat-text-container]').exists({ count: 6 }, 'renders stat text containers'); + assert + .dom('[data-test-new] [data-test-stat-text-container="New clients"] div.stat-value') + .hasText(`${expectedNewClients}`, `renders correct total new clients: ${expectedNewClients}`); + assert + .dom('[data-test-new] [data-test-stat-text-container="Entity clients"] div.stat-value') + .hasText(`${expectedNewEntity}`, `renders correct total new entity: ${expectedNewEntity}`); + assert + .dom('[data-test-new] [data-test-stat-text-container="Non-entity clients"] div.stat-value') + .hasText(`${expectedNewNonEntity}`, `renders correct total new non-entity: ${expectedNewNonEntity}`); + assert + .dom('[data-test-total] [data-test-stat-text-container="Total monthly clients"] div.stat-value') + .hasText(`${expectedTotalClients}`, `renders correct total clients: ${expectedTotalClients}`); + assert + .dom('[data-test-total] [data-test-stat-text-container="Entity clients"] div.stat-value') + .hasText(`${expectedTotalEntity}`, `renders correct total entity: ${expectedTotalEntity}`); + assert + .dom('[data-test-total] [data-test-stat-text-container="Non-entity clients"] div.stat-value') + .hasText(`${expectedTotalNonEntity}`, `renders correct total non-entity: ${expectedTotalNonEntity}`); + }); +}); diff --git a/ui/tests/integration/components/clients/vertical-bar-chart-test.js b/ui/tests/integration/components/clients/vertical-bar-chart-test.js index a17c922bb5..0d2d323e4a 100644 --- a/ui/tests/integration/components/clients/vertical-bar-chart-test.js +++ b/ui/tests/integration/components/clients/vertical-bar-chart-test.js @@ -1,6 +1,6 @@ import { module, test } from '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'; 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 = [ - { month: 'january', clients: 200, 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: 'january', clients: 141, entity_clients: 91, non_entity_clients: 50, new_clients: 5 }, + { month: 'february', clients: 251, entity_clients: 101, non_entity_clients: 150, new_clients: 5 }, ]; this.set('barChartData', barChartData); await render(hbs` +
+
`); - 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` +
+ +
+ `); + + 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` +
+ +
+ `); + + 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' + ); }); }); diff --git a/ui/tests/unit/utils/chart-helpers-test.js b/ui/tests/unit/utils/chart-helpers-test.js index 7171f43eca..6903818dba 100644 --- a/ui/tests/unit/utils/chart-helpers-test.js +++ b/ui/tests/unit/utils/chart-helpers-test.js @@ -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'; const SMALL_NUMBERS = [0, 7, 27, 103, 999]; @@ -28,4 +28,35 @@ module('Unit | Utility | chart-helpers', function () { const formatted = formatTooltipNumber(120300200100); 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` + ); + }); });