CSV Export include monthly data (#15169)

* setup

* add new clients to attribution

* refactor serializers, move to util folder

* cleanup export csv generator

* fix isDateRange getter

* remove new chart from partial/current month

* fix export modal text

* update version history text

* update variable naming, remove new client data from current/partial month

* add filtering by namespace to month over month charts

* remove filtering for namespace by month, need to change serializer

* add checks

* update horizontal bar chart test

* update tests

* cleanup

* address comments

* fix flakey test

* add new counts to export

Co-authored-by: Claire Bontempo <cbontempo@hashicorp.com>
This commit is contained in:
Angel Garbarino 2022-05-02 19:37:09 -06:00 committed by GitHub
parent bef350c916
commit 33de0a0a49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 469 additions and 385 deletions

View File

@ -13,23 +13,25 @@ import { inject as service } from '@ember/service';
* @chartLegend={{this.chartLegend}}
* @totalUsageCounts={{this.totalUsageCounts}}
* @newUsageCounts={{this.newUsageCounts}}
* @totalClientsData={{this.totalClientsData}}
* @newClientsData={{this.newClientsData}}
* @totalClientAttribution={{this.totalClientAttribution}}
* @newClientAttribution={{this.newClientAttribution}}
* @selectedNamespace={{this.selectedNamespace}}
* @startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
* @isDateRange={{this.isDateRange}}
* @isCurrentMonth={{false}}
* @timestamp={{this.responseTimestamp}}
* />
* ```
* @param {array} chartLegend - (passed to child) array of objects with key names 'key' and 'label' so data can be stacked
* @param {object} totalUsageCounts - object with total client counts for chart tooltip text
* @param {object} newUsageCounts - object with new client counts for chart tooltip text
* @param {array} totalClientsData - array of objects containing a label and breakdown of client counts for total clients
* @param {array} newClientsData - array of objects containing a label and breakdown of client counts for new clients
* @param {array} totalClientAttribution - array of objects containing a label and breakdown of client counts for total clients
* @param {array} newClientAttribution - array of objects containing a label and breakdown of client counts for new clients
* @param {string} selectedNamespace - namespace selected from filter bar
* @param {string} startTimeDisplay - string that displays as start date for CSV modal
* @param {string} endTimeDisplay - string that displays as end date for CSV modal
* @param {boolean} isDateRange - getter calculated in parent to relay if dataset is a date range or single month
* @param {boolean} isDateRange - getter calculated in parent to relay if dataset is a date range or single month and display text accordingly
* @param {boolean} isCurrentMonth - boolean to determine if rendered in current month tab or not
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response
*/
@ -38,14 +40,15 @@ export default class Attribution extends Component {
@service downloadCsv;
get hasCsvData() {
return this.args.totalClientsData ? this.args.totalClientsData.length > 0 : false;
return this.args.totalClientAttribution ? this.args.totalClientAttribution.length > 0 : false;
}
get isDateRange() {
return this.args.isDateRange;
}
get isSingleNamespace() {
if (!this.args.totalClientsData) {
if (!this.args.totalClientAttribution) {
return 'no data';
}
// if a namespace is selected, then we're viewing top 10 auth methods (mounts)
@ -53,18 +56,17 @@ export default class Attribution extends Component {
}
// truncate data before sending to chart component
// move truncating to serializer when we add separate request to fetch and export ALL namespace data
get barChartTotalClients() {
return this.args.totalClientsData?.slice(0, 10);
return this.args.totalClientAttribution?.slice(0, 10);
}
get barChartNewClients() {
return this.args.newClientsData?.slice(0, 10);
return this.args.newClientAttribution?.slice(0, 10);
}
get topClientCounts() {
// get top namespace or auth method
return this.args.totalClientsData ? this.args.totalClientsData[0] : null;
return this.args.totalClientAttribution ? this.args.totalClientAttribution[0] : null;
}
get attributionBreakdown() {
@ -103,9 +105,27 @@ export default class Attribution extends Component {
}
}
get getCsvData() {
destructureCountsToArray(object) {
// destructure the namespace object {label: 'some-namespace', entity_clients: 171, non_entity_clients: 20, clients: 191}
// to get integers for CSV file
let { clients, entity_clients, non_entity_clients } = object;
return [clients, entity_clients, non_entity_clients];
}
constructCsvRow(namespaceColumn, mountColumn = null, totalColumns, newColumns = null) {
// if namespaceColumn is a string, then we're at mount level attribution, otherwise it is an object
// if constructing a namespace row, mountColumn=null so the column is blank, otherwise it is an object
let otherColumns = newColumns ? [...totalColumns, ...newColumns] : [...totalColumns];
return [
`${typeof namespaceColumn === 'string' ? namespaceColumn : namespaceColumn.label}`,
`${mountColumn ? mountColumn.label : ''}`,
...otherColumns,
];
}
generateCsvData() {
const totalAttribution = this.args.totalClientAttribution;
const newAttribution = this.barChartNewClients ? this.args.newClientAttribution : null;
let csvData = [],
graphData = this.args.totalClientsData,
csvHeader = [
'Namespace path',
'Authentication method',
@ -114,24 +134,41 @@ export default class Attribution extends Component {
'Non-entity clients',
];
// each array will be a row in the csv file
if (this.isSingleNamespace) {
graphData.forEach((mount) => {
csvData.push(['', mount.label, mount.clients, mount.entity_clients, mount.non_entity_clients]);
});
csvData.forEach((d) => (d[0] = this.args.selectedNamespace));
} else {
graphData.forEach((ns) => {
csvData.push([ns.label, '', ns.clients, ns.entity_clients, ns.non_entity_clients]);
if (ns.mounts) {
ns.mounts.forEach((m) => {
csvData.push([ns.label, m.label, m.clients, m.entity_clients, m.non_entity_clients]);
if (newAttribution) {
csvHeader = [...csvHeader, 'Total new clients, New entity clients, New non-entity clients'];
}
totalAttribution.forEach((totalClientsObject) => {
let namespace = this.isSingleNamespace ? this.args.selectedNamespace : totalClientsObject;
let mount = this.isSingleNamespace ? totalClientsObject : null;
// find new client data for namespace/mount object we're iterating over
let newClientsObject = newAttribution
? newAttribution.find((d) => d.label === totalClientsObject.label)
: null;
let totalClients = this.destructureCountsToArray(totalClientsObject);
let newClients = newClientsObject ? this.destructureCountsToArray(newClientsObject) : null;
csvData.push(this.constructCsvRow(namespace, mount, totalClients, newClients));
// constructCsvRow returns an array that corresponds to a row in the csv file:
// ['ns label', 'mount label', total client #, entity #, non-entity #, ...new client #'s]
// only iterate through mounts if NOT viewing a single namespace
if (!this.isSingleNamespace && namespace.mounts) {
namespace.mounts.forEach((mount) => {
let newMountData = newAttribution
? newClientsObject?.mounts.find((m) => m.label === mount.label)
: null;
let mountTotalClients = this.destructureCountsToArray(mount);
let mountNewClients = newMountData ? this.destructureCountsToArray(newMountData) : null;
csvData.push(this.constructCsvRow(namespace, mount, mountTotalClients, mountNewClients));
});
}
});
}
csvData.unshift(csvHeader);
// make each nested array a comma separated string, join each array in csvData with line break (\n)
// make each nested array a comma separated string, join each array "row" in csvData with line break (\n)
return csvData.map((d) => d.join()).join('\n');
}
@ -145,7 +182,8 @@ export default class Attribution extends Component {
// ACTIONS
@action
exportChartData(filename, contents) {
exportChartData(filename) {
let contents = this.generateCsvData();
this.downloadCsv.download(filename, contents);
this.showCSVDownloadModal = false;
}

View File

@ -9,7 +9,7 @@ export default class Current extends Component {
{ key: 'non_entity_clients', label: 'non-entity clients' },
];
@tracked selectedNamespace = null;
@tracked namespaceArray = this.byNamespaceTotalClients.map((namespace) => {
@tracked namespaceArray = this.byNamespace.map((namespace) => {
return { name: namespace['label'], id: namespace['label'] };
});
@ -29,37 +29,18 @@ export default class Current extends Component {
let findUpgrade = versionHistory.find((versionData) => versionData.id.match(version));
if (findUpgrade) relevantUpgrades.push(findUpgrade);
});
// if no history for 1.9 or 1.10, customer skipped these releases so get first stored upgrade
// TODO account for customer STARTING on 1.11
if (relevantUpgrades.length === 0) {
relevantUpgrades.push({
id: versionHistory[0].id,
previousVersion: versionHistory[0].previousVersion,
timestampInstalled: versionHistory[0].timestampInstalled,
});
}
// array of upgrade data objects for noteworthy upgrades
return relevantUpgrades;
}
// Response total client count data by namespace for current/partial month
get byNamespaceTotalClients() {
return this.args.model.monthly?.byNamespaceTotalClients || [];
}
// Response new client count data by namespace for current/partial month
get byNamespaceNewClients() {
return this.args.model.monthly?.byNamespaceNewClients || [];
// Response client count data by namespace for current/partial month
get byNamespace() {
return this.args.model.monthly?.byNamespace || [];
}
get isGatheringData() {
// return true if tracking IS enabled but no data collected yet
return (
this.args.model.config?.enabled === 'On' &&
this.byNamespaceTotalClients.length === 0 &&
this.byNamespaceNewClients.length === 0
);
return this.args.model.config?.enabled === 'On' && this.byNamespace.length === 0;
}
get hasAttributionData() {
@ -67,33 +48,19 @@ export default class Current extends Component {
if (this.selectedNamespace) {
return this.authMethodOptions.length > 0;
}
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData;
return this.totalUsageCounts.clients !== 0 && !!this.totalClientAttribution;
}
get filteredTotalData() {
get filteredCurrentData() {
const namespace = this.selectedNamespace;
const auth = this.selectedAuthMethod;
if (!namespace && !auth) {
return this.byNamespaceTotalClients;
return this.byNamespace;
}
if (!auth) {
return this.byNamespaceTotalClients.find((ns) => ns.label === namespace);
return this.byNamespace.find((ns) => ns.label === namespace);
}
return this.byNamespaceTotalClients
.find((ns) => ns.label === namespace)
.mounts?.find((mount) => mount.label === auth);
}
get filteredNewData() {
const namespace = this.selectedNamespace;
const auth = this.selectedAuthMethod;
if (!namespace && !auth) {
return this.byNamespaceNewClients;
}
if (!auth) {
return this.byNamespaceNewClients.find((ns) => ns.label === namespace);
}
return this.byNamespaceNewClients
return this.byNamespace
.find((ns) => ns.label === namespace)
.mounts?.find((mount) => mount.label === auth);
}
@ -134,37 +101,24 @@ export default class Current extends Component {
return ' How we count clients changed in 1.9, so keep that in mind when looking at the data below.';
}
if (version.match('1.10')) {
return ' We added new client breakdowns starting in 1.10, so keep that in mind when looking at the data below.';
return ' We added mount level attribution starting in 1.10, so keep that in mind when looking at the data below.';
}
}
// return combined explanation if spans multiple upgrades, or customer skipped 1.9 and 1.10
return ' How we count clients changed in 1.9 and we added new client breakdowns starting in 1.10. Keep this in mind when looking at the data below.';
// return combined explanation if spans multiple upgrades
return ' How we count clients changed in 1.9 and we added mount level attribution starting in 1.10. Keep this in mind when looking at the data below.';
}
// top level TOTAL client counts for current/partial month
get totalUsageCounts() {
return this.selectedNamespace ? this.filteredTotalData : this.args.model.monthly?.total;
return this.selectedNamespace ? this.filteredCurrentData : this.args.model.monthly?.total;
}
get newUsageCounts() {
return this.selectedNamespace ? this.filteredNewData : this.args.model.monthly?.new;
}
// total client data for horizontal bar chart in attribution component
get totalClientsData() {
// total client attribution data for horizontal bar chart in attribution component
get totalClientAttribution() {
if (this.selectedNamespace) {
return this.filteredTotalData?.mounts || null;
return this.filteredCurrentData?.mounts || null;
} else {
return this.byNamespaceTotalClients;
}
}
// new client data for horizontal bar chart in attribution component
get newClientsData() {
if (this.selectedNamespace) {
return this.filteredNewData?.mounts || null;
} else {
return this.byNamespaceNewClients;
return this.byNamespace;
}
}
@ -183,7 +137,7 @@ export default class Current extends Component {
this.selectedAuthMethod = null;
} else {
// Side effect: set auth namespaces
const mounts = this.filteredTotalData.mounts?.map((mount) => ({
const mounts = this.filteredCurrentData.mounts?.map((mount) => ({
id: mount.label,
name: mount.label,
}));

View File

@ -6,6 +6,7 @@ import { isSameMonth, isAfter, isBefore } from 'date-fns';
import getStorage from 'vault/lib/token-storage';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import { dateFormat } from 'core/helpers/date-format';
import { parseAPITimestamp } from 'core/utils/date-formatters';
const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
@ -85,8 +86,8 @@ export default class History extends Component {
get isDateRange() {
return !isSameMonth(
new Date(this.getActivityResponse.startTime),
new Date(this.getActivityResponse.endTime)
parseAPITimestamp(this.getActivityResponse.startTime),
parseAPITimestamp(this.getActivityResponse.endTime)
);
}
@ -104,15 +105,6 @@ export default class History extends Component {
if (findUpgrade) relevantUpgrades.push(findUpgrade);
});
// if no history for 1.9 or 1.10, customer skipped these releases so get first stored upgrade
// TODO account for customer STARTING on 1.11
if (relevantUpgrades.length === 0) {
relevantUpgrades.push({
id: versionHistory[0].id,
previousVersion: versionHistory[0].previousVersion,
timestampInstalled: versionHistory[0].timestampInstalled,
});
}
// array of upgrade data objects for noteworthy upgrades
return relevantUpgrades;
}
@ -161,11 +153,11 @@ export default class History extends Component {
return ' How we count clients changed in 1.9, so keep that in mind when looking at the data below.';
}
if (version.match('1.10')) {
return ' We added monthly breakdowns starting in 1.10, so keep that in mind when looking at the data below.';
return ' We added monthly breakdowns and mount level attribution starting in 1.10, so keep that in mind when looking at the data below.';
}
}
// return combined explanation if spans multiple upgrades, or customer skipped 1.9 and 1.10
return ' How we count clients changed in 1.9 and we added monthly breakdowns starting in 1.10. Keep this in mind when looking at the data below.';
// return combined explanation if spans multiple upgrades
return ' How we count clients changed in 1.9 and we added monthly breakdowns and mount level attribution starting in 1.10. Keep this in mind when looking at the data below.';
}
get startTimeDisplay() {
@ -193,12 +185,20 @@ export default class History extends Component {
return this.queriedActivityResponse || this.args.model.activity;
}
get byMonthTotalClients() {
return this.getActivityResponse?.byMonth;
}
get byMonthNewClients() {
return this.byMonthTotalClients.map((m) => m.new_clients);
}
get hasAttributionData() {
if (this.selectedAuthMethod) return false;
if (this.selectedNamespace) {
return this.authMethodOptions.length > 0;
}
return !!this.totalClientsData && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
return !!this.totalClientAttribution && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
}
// top level TOTAL client counts for given date range
@ -206,8 +206,14 @@ export default class History extends Component {
return this.selectedNamespace ? this.filteredActivity : this.getActivityResponse.total;
}
get newUsageCounts() {
return this.selectedNamespace
? this.filteredNewClientAttribution
: this.byMonthTotalClients[0]?.new_clients;
}
// total client data for horizontal bar chart in attribution component
get totalClientsData() {
get totalClientAttribution() {
if (this.selectedNamespace) {
return this.filteredActivity?.mounts || null;
} else {
@ -215,18 +221,24 @@ export default class History extends Component {
}
}
// new client data for horizontal bar chart
get newClientAttribution() {
// new client attribution only available in a single, historical month
if (this.isDateRange) {
return null;
}
// only a single month is returned from the api
if (this.selectedNamespace) {
return this.filteredNewClientAttribution?.mounts || null;
} else {
return this.byMonthTotalClients[0]?.new_clients.namespaces || null;
}
}
get responseTimestamp() {
return this.getActivityResponse.responseTimestamp;
}
get byMonthTotalClients() {
return this.getActivityResponse?.byMonth;
}
get byMonthNewClients() {
return this.byMonthTotalClients.map((m) => m.new_clients);
}
get filteredActivity() {
const namespace = this.selectedNamespace;
const auth = this.selectedAuthMethod;
@ -241,6 +253,21 @@ export default class History extends Component {
.mounts?.find((mount) => mount.label === auth);
}
get filteredNewClientAttribution() {
const namespace = this.selectedNamespace;
const auth = this.selectedAuthMethod;
// new client data is only available by month
const newClientsData = this.byMonthTotalClients[0]?.new_clients;
if (!newClientsData) return null;
if (this.isDateRange) return null;
if (!namespace && !auth) return newClientsData;
const foundNamespace = newClientsData.namespaces.find((ns) => ns.label === namespace);
if (!foundNamespace) return null;
if (!auth) return foundNamespace;
return foundNamespace.mounts?.find((mount) => mount.label === auth);
}
@action
async handleClientActivityQuery(month, year, dateType) {
this.isEditStartMonthOpen = false;

View File

@ -19,6 +19,9 @@ import { tracked } from '@glimmer/tracking';
* ```
* @param {array} dataset - dataset for the chart, must be an array of flattened objects
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
* @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
*/
// SIZING CONSTANTS
@ -36,12 +39,20 @@ export default class HorizontalBarChart extends Component {
return this.args.labelKey || 'label';
}
get xKey() {
return this.args.xKey || 'clients';
}
get chartLegend() {
return this.args.chartLegend;
}
get topNamespace() {
return this.args.dataset[maxIndex(this.args.dataset, (d) => d.clients)];
return this.args.dataset[maxIndex(this.args.dataset, (d) => d[this.xKey])];
}
get total() {
return this.args.totalCounts[this.xKey] || null;
}
@action removeTooltip() {
@ -49,17 +60,17 @@ export default class HorizontalBarChart extends Component {
}
@action
renderChart(element, args) {
renderChart(element, [chartData]) {
// chart legend tells stackFunction how to stack/organize data
// creates an array of data for each key name
// each array contains coordinates for each data bar
let stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
let dataset = args[0];
let dataset = chartData;
let stackedData = stackFunction(dataset);
let labelKey = this.labelKey;
let xKey = this.xKey;
let xScale = scaleLinear()
.domain([0, max(dataset.map((d) => d.clients))])
.domain([0, max(dataset.map((d) => d[xKey]))])
.range([0, 75]); // 25% reserved for margins
let yScale = scaleBand()
@ -162,13 +173,15 @@ export default class HorizontalBarChart extends Component {
// MOUSE EVENTS FOR DATA BARS
actionBars
.on('mouseover', (data) => {
let hoveredElement = actionBars.filter((bar) => bar.label === data.label).node();
let hoveredElement = actionBars.filter((bar) => bar[labelKey] === data[labelKey]).node();
this.tooltipTarget = hoveredElement;
this.isLabel = false;
this.tooltipText = `${Math.round((data.clients * 100) / this.args.totalUsageCounts.clients)}%
this.tooltipText = this.total
? `${Math.round((data[xKey] * 100) / this.total)}%
of total client counts:
${formatTooltipNumber(data.entity_clients)} entity clients,
${formatTooltipNumber(data.non_entity_clients)} non-entity clients.`;
${formatTooltipNumber(data.non_entity_clients)} non-entity clients.`
: '';
select(hoveredElement).style('opacity', 1);
@ -190,11 +203,11 @@ export default class HorizontalBarChart extends Component {
// MOUSE EVENTS FOR Y-AXIS LABELS
labelActionBar
.on('mouseover', (data) => {
if (data.label.length >= CHAR_LIMIT) {
let hoveredElement = labelActionBar.filter((bar) => bar.label === data.label).node();
if (data[labelKey].length >= CHAR_LIMIT) {
let hoveredElement = labelActionBar.filter((bar) => bar[labelKey] === data[labelKey]).node();
this.tooltipTarget = hoveredElement;
this.isLabel = true;
this.tooltipText = data.label;
this.tooltipText = data[labelKey];
} else {
this.tooltipTarget = null;
}
@ -234,13 +247,13 @@ export default class HorizontalBarChart extends Component {
.data(dataset)
.enter()
.append('text')
.text((d) => d.clients)
.text((d) => d[xKey])
.attr('fill', '#000')
.attr('class', 'total-value')
.style('font-size', '.8rem')
.attr('text-anchor', 'start')
.attr('alignment-baseline', 'middle')
.attr('x', (chartData) => `${xScale(chartData.clients)}%`)
.attr('y', (chartData) => yScale(chartData.label));
.attr('x', (chartData) => `${xScale(chartData[xKey])}%`)
.attr('y', (chartData) => yScale(chartData[labelKey]));
}
}

View File

@ -47,11 +47,11 @@ export default class LineChart extends Component {
}
@action
renderChart(element, args) {
const dataset = args[0];
renderChart(element, [chartData]) {
const dataset = chartData;
const upgradeData = [];
if (args[1]) {
args[1].forEach((versionData) =>
if (this.args.upgradeData) {
this.args.upgradeData.forEach((versionData) =>
upgradeData.push({ month: parseAPITimestamp(versionData.timestampInstalled, 'M/yy'), ...versionData })
);
}
@ -59,6 +59,9 @@ export default class LineChart extends Component {
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();
// DEFINE AXES SCALES
const yScale = scaleLinear()
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
@ -154,10 +157,11 @@ export default class LineChart extends Component {
// MOUSE EVENT FOR TOOLTIP
hoverCircles.on('mouseover', (data) => {
// TODO: how to genericize this?
// TODO: how to generalize this?
let { new_clients } = data || null;
this.tooltipMonth = formatChartDate(data[this.xKey]);
this.tooltipTotal = data[this.yKey] + ' total clients';
this.tooltipNew = data?.new_clients[this.yKey] + ' new clients';
this.tooltipNew = (new_clients ? new_clients[this.yKey] : '0') + ' new clients';
this.tooltipUpgradeText = '';
let upgradeInfo = findUpgradeData(data);
if (upgradeInfo) {

View File

@ -18,7 +18,7 @@ import { mean } from 'd3-array';
/>
* ```
* @param {array} lineChartData - array of objects
* @param {array} chartData - array of objects from /activity response
object example: {
month: '1/22',
entity_clients: 23,
@ -32,7 +32,6 @@ import { mean } from 'd3-array';
namespaces: [],
},
};
* @param {array} barChartData - array of objects, object example: { month: '1/22', entity_clients: 11, non_entity_clients: 36, total: 47, namespaces: [] };
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
* @param {object} runningTotals - top level totals from /activity response { clients: 3517, entity_clients: 1593, non_entity_clients: 1924 }
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response

View File

@ -48,8 +48,8 @@ export default class VerticalBarChart extends Component {
}
@action
registerListener(element, args) {
const dataset = args[0];
renderChart(element, [chartData]) {
const dataset = chartData;
const filteredData = dataset.filter((e) => Object.keys(e).includes('clients')); // months with data will contain a 'clients' key (otherwise only a timestamp)
const stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
const stackedData = stackFunction(filteredData);
@ -67,6 +67,9 @@ export default class VerticalBarChart extends Component {
.range([0, SVG_DIMENSIONS.width]) // set width to fix number of pixels
.paddingInner(0.85);
// clear out DOM before appending anything
chartSvg.selectAll('g').remove().exit().data(stackedData).enter();
const dataBars = chartSvg
.selectAll('g')
.data(stackedData)

View File

@ -2,7 +2,5 @@ import Model, { attr } from '@ember-data/model';
export default class MonthlyModel extends Model {
@attr('string') responseTimestamp;
@attr('object') total; // total clients during the current/partial month
@attr('object') new; // total NEW clients during the current/partial
@attr('array') byNamespaceTotalClients;
@attr('array') byNamespaceNewClients;
@attr('array') byNamespace;
}

View File

@ -1,90 +1,8 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
import { parseAPITimestamp, parseRFC3339 } from 'core/utils/date-formatters';
import { parseRFC3339 } from 'core/utils/date-formatters';
import { formatByMonths, formatByNamespace, homogenizeClientNaming } from 'core/utils/client-count-utils';
export default class ActivitySerializer extends ApplicationSerializer {
flattenDataset(object) {
let flattenedObject = {};
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
return this.homogenizeClientNaming(flattenedObject);
}
formatByNamespace(namespaceArray) {
return namespaceArray?.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'];
let flattenedNs = this.flattenDataset(ns);
// if no mounts, mounts will be an empty array
flattenedNs.mounts = [];
if (ns?.mounts && ns.mounts.length > 0) {
flattenedNs.mounts = ns.mounts.map((mount) => {
return {
label: mount['mount_path'],
...this.flattenDataset(mount),
};
});
}
return {
label,
...flattenedNs,
};
});
}
formatByMonths(monthsArray) {
const sortedPayload = [...monthsArray];
// months are always returned from the API: [mostRecent...oldestMonth]
sortedPayload.reverse();
return sortedPayload.map((m) => {
if (Object.keys(m).includes('counts')) {
let totalClients = this.flattenDataset(m);
let newClients = this.flattenDataset(m.new_clients);
return {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
...totalClients,
namespaces: this.formatByNamespace(m.namespaces),
new_clients: {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
...newClients,
namespaces: this.formatByNamespace(m.new_clients.namespaces),
},
};
}
// TODO CMB below is an assumption, need to test
// if no monthly data (no counts key), month object will just contain a timestamp
return {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
new_clients: {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
},
};
});
}
// In 1.10 'distinct_entities' changed to 'entity_clients' and
// 'non_entity_tokens' to 'non_entity_clients'
homogenizeClientNaming(object) {
// if new key names exist, only return those key/value pairs
if (Object.keys(object).includes('entity_clients')) {
let { clients, entity_clients, non_entity_clients } = object;
return {
clients,
entity_clients,
non_entity_clients,
};
}
// if object only has outdated key names, update naming
if (Object.keys(object).includes('distinct_entities')) {
let { clients, distinct_entities, non_entity_tokens } = object;
return {
clients,
entity_clients: distinct_entities,
non_entity_clients: non_entity_tokens,
};
}
return object;
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.id === 'no-data') {
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
@ -93,9 +11,9 @@ export default class ActivitySerializer extends ApplicationSerializer {
let transformedPayload = {
...payload,
response_timestamp,
by_namespace: this.formatByNamespace(payload.data.by_namespace),
by_month: this.formatByMonths(payload.data.months),
total: this.homogenizeClientNaming(payload.data.total),
by_namespace: formatByNamespace(payload.data.by_namespace),
by_month: formatByMonths(payload.data.months),
total: homogenizeClientNaming(payload.data.total),
formatted_end_time: parseRFC3339(payload.data.end_time),
formatted_start_time: parseRFC3339(payload.data.start_time),
};

View File

@ -1,60 +1,8 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
import { formatByNamespace, homogenizeClientNaming } from 'core/utils/client-count-utils';
export default class MonthlySerializer extends ApplicationSerializer {
flattenDataset(object) {
let flattenedObject = {};
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
return this.homogenizeClientNaming(flattenedObject);
}
formatByNamespace(namespaceArray) {
return namespaceArray?.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'];
let flattenedNs = this.flattenDataset(ns);
// if no mounts, mounts will be an empty array
flattenedNs.mounts = [];
if (ns?.mounts && ns.mounts.length > 0) {
flattenedNs.mounts = ns.mounts.map((mount) => {
return {
label: mount['mount_path'],
...this.flattenDataset(mount),
};
});
}
return {
label,
...flattenedNs,
};
});
}
// In 1.10 'distinct_entities' changed to 'entity_clients' and
// 'non_entity_tokens' to 'non_entity_clients'
homogenizeClientNaming(object) {
// if new key names exist, only return those key/value pairs
if (Object.keys(object).includes('entity_clients')) {
let { clients, entity_clients, non_entity_clients } = object;
return {
clients,
entity_clients,
non_entity_clients,
};
}
// if object only has outdated key names, update naming
if (Object.keys(object).includes('distinct_entities')) {
let { clients, distinct_entities, non_entity_tokens } = object;
return {
clients,
entity_clients: distinct_entities,
non_entity_clients: non_entity_tokens,
};
}
return object;
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.id === 'no-data') {
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
@ -62,23 +10,12 @@ export default class MonthlySerializer extends ApplicationSerializer {
let response_timestamp = formatISO(new Date());
// TODO CMB: the following is assumed, need to confirm
// the months array will always include a single object: a timestamp of the current month and new/total count data, if available
let newClientsData = payload.data.months[0]?.new_clients || null;
let by_namespace_new_clients, new_clients;
if (newClientsData) {
by_namespace_new_clients = this.formatByNamespace(newClientsData.namespaces);
new_clients = this.homogenizeClientNaming(newClientsData.counts);
} else {
by_namespace_new_clients = [];
new_clients = [];
}
let transformedPayload = {
...payload,
response_timestamp,
by_namespace_total_clients: this.formatByNamespace(payload.data.by_namespace),
by_namespace_new_clients,
by_namespace: formatByNamespace(payload.data.by_namespace),
// nest within 'total' object to mimic /activity response shape
total: this.homogenizeClientNaming(payload.data),
new: new_clients,
total: homogenizeClientNaming(payload.data),
};
delete payload.data.by_namespace;
delete payload.data.months;

View File

@ -1,6 +1,6 @@
{{! show single chart if data is from a range, show two charts if from a single month}}
{{! show single horizontal bar chart unless data is from a single, historical month (isDateRange = false) }}
<div
class={{concat "chart-wrapper" (if @isDateRange " single-chart-grid") (unless @isDateRange " dual-chart-grid")}}
class={{concat "chart-wrapper" (if (or @isCurrentMonth @isDateRange) " single-chart-grid" " dual-chart-grid")}}
data-test-clients-attribution
>
<div class="chart-header has-header-link has-bottom-margin-m">
@ -23,12 +23,12 @@
</div>
{{#if this.barChartTotalClients}}
{{#if (eq @isDateRange true)}}
{{#if (or @isDateRange @isCurrentMonth)}}
<div class="chart-container-wide" data-test-chart-container="total-clients">
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
@totalCounts={{@totalUsageCounts}}
/>
</div>
<div class="chart-subTitle">
@ -51,7 +51,7 @@
<Clients::HorizontalBarChart
@dataset={{this.barChartNewClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@newUsageCounts}}
@totalCounts={{@newUsageCounts}}
@noDataMessage={{"There are no new clients for this namespace during this time period."}}
/>
</div>
@ -62,7 +62,7 @@
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
@totalCounts={{@totalUsageCounts}}
/>
</div>
{{/if}}
@ -93,18 +93,15 @@
>
<section class="modal-card-body">
<p class="has-bottom-margin-s">
This export will include the namespace path, authentication method path, and the associated clients, unique entities,
and non-entity tokens for the below date range.
This export will include the namespace path, authentication method path, and the associated total, entity, and
non-entity clients for the below
{{if @isCurrentMonth "month" "date range"}}.
</p>
<p class="has-bottom-margin-s is-subtitle-gray">SELECTED DATE {{if @endTimeDisplay " RANGE"}}</p>
<p class="has-bottom-margin-s">{{@startTimeDisplay}} {{if @endTimeDisplay "-"}} {{@endTimeDisplay}}</p>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<button
type="button"
class="button is-primary"
{{on "click" (fn this.exportChartData this.getCsvFileName this.getCsvData)}}
>
<button type="button" class="button is-primary" {{on "click" (fn this.exportChartData this.getCsvFileName)}}>
Export
</button>
<button type="button" class="button is-secondary" onclick={{action (mut this.showCSVDownloadModal) false}}>

View File

@ -73,10 +73,10 @@
@chartLegend={{this.chartLegend}}
@totalUsageCounts={{this.totalUsageCounts}}
@newUsageCounts={{this.newUsageCounts}}
@totalClientsData={{this.totalClientsData}}
@newClientsData={{this.newClientsData}}
@totalClientAttribution={{this.totalClientAttribution}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
@isCurrentMonth={{true}}
@isDateRange={{false}}
@timestamp={{this.responseTimestamp}}
/>

View File

@ -133,6 +133,7 @@
{{#if this.byMonthTotalClients}}
<Clients::RunningTotal
@chartLegend={{this.chartLegend}}
@selectedNamespace={{this.selectedNamespace}}
@lineChartData={{this.byMonthTotalClients}}
@barChartData={{this.byMonthNewClients}}
@runningTotals={{this.totalUsageCounts}}
@ -143,8 +144,10 @@
{{#if this.hasAttributionData}}
<Clients::Attribution
@chartLegend={{this.chartLegend}}
@totalClientsData={{this.totalClientsData}}
@totalUsageCounts={{this.totalUsageCounts}}
@newUsageCounts={{this.newUsageCounts}}
@totalClientAttribution={{this.totalClientAttribution}}
@newClientAttribution={{this.newClientAttribution}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{this.startTimeDisplay}}
@endTimeDisplay={{this.endTimeDisplay}}

View File

@ -2,8 +2,8 @@
data-test-line-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset @upgradeData}}
{{did-update this.renderChart @dataset @upgradeData}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 988 B

View File

@ -2,7 +2,7 @@
data-test-vertical-bar-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.registerListener @dataset}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 857 B

View File

@ -0,0 +1,89 @@
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { compareAsc } from 'date-fns';
export const flattenDataset = (object) => {
if (Object.keys(object).includes('counts') && object.counts) {
let flattenedObject = {};
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
return homogenizeClientNaming(flattenedObject);
}
return object;
};
export const sortMonthsByTimestamp = (monthsArray) => {
// backend is working on a fix to sort months by date
// right now months are ordered in descending client count number
const sortedPayload = [...monthsArray];
return sortedPayload.sort((a, b) =>
compareAsc(parseAPITimestamp(a.timestamp), parseAPITimestamp(b.timestamp))
);
};
export const formatByMonths = (monthsArray) => {
if (!Array.isArray(monthsArray)) return monthsArray;
const sortedPayload = sortMonthsByTimestamp(monthsArray);
return sortedPayload.map((m) => {
if (Object.keys(m).includes('counts')) {
let totalClients = flattenDataset(m);
let newClients = m.new_clients ? flattenDataset(m.new_clients) : {};
return {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
...totalClients,
namespaces: formatByNamespace(m.namespaces),
new_clients: {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
...newClients,
namespaces: formatByNamespace(m.new_clients?.namespaces) || [],
},
};
}
});
};
export const formatByNamespace = (namespaceArray) => {
if (!Array.isArray(namespaceArray)) return namespaceArray;
return namespaceArray?.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'];
let flattenedNs = flattenDataset(ns);
// if no mounts, mounts will be an empty array
flattenedNs.mounts = [];
if (ns?.mounts && ns.mounts.length > 0) {
flattenedNs.mounts = ns.mounts.map((mount) => {
return {
label: mount['mount_path'],
...flattenDataset(mount),
};
});
}
return {
label,
...flattenedNs,
};
});
};
// In 1.10 'distinct_entities' changed to 'entity_clients' and
// 'non_entity_tokens' to 'non_entity_clients'
export const homogenizeClientNaming = (object) => {
// if new key names exist, only return those key/value pairs
if (Object.keys(object).includes('entity_clients')) {
let { clients, entity_clients, non_entity_clients } = object;
return {
clients,
entity_clients,
non_entity_clients,
};
}
// if object only has outdated key names, update naming
if (Object.keys(object).includes('distinct_entities')) {
let { clients, distinct_entities, non_entity_tokens } = object;
return {
clients,
entity_clients: distinct_entities,
non_entity_clients: non_entity_tokens,
};
}
return object;
};

View File

@ -15,7 +15,7 @@ export const ARRAY_OF_MONTHS = [
'December',
];
// convert RFC3339 timestamp ( '2021-03-21T00:00:00Z' ) to date object, optionally format
// convert API timestamp ( '2021-03-21T00:00:00Z' ) to date object, optionally format
export const parseAPITimestamp = (timestamp, style) => {
if (!timestamp) return;
let date = parseISO(timestamp.split('T')[0]);

View File

@ -1,15 +1,128 @@
import {
differenceInCalendarMonths,
formatRFC3339,
formatISO,
isAfter,
isBefore,
sub,
isSameMonth,
startOfMonth,
} from 'date-fns';
import { formatISO, isAfter, isBefore, sub, isSameMonth, startOfMonth } from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';
const MOCK_MONTHLY_DATA = [
{
timestamp: '2021-05-01T00:00:00Z',
counts: {
distinct_entities: 0,
entity_clients: 25,
non_entity_tokens: 0,
non_entity_clients: 20,
clients: 50,
},
namespaces: [
{
namespace_id: 'root',
namespace_path: '',
counts: {
distinct_entities: 0,
entity_clients: 13,
non_entity_tokens: 0,
non_entity_clients: 7,
clients: 20,
},
mounts: [
{
mount_path: 'auth/up2/',
counts: {
distinct_entities: 0,
entity_clients: 8,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 8,
},
},
{
mount_path: 'auth/up1/',
counts: {
distinct_entities: 0,
entity_clients: 0,
non_entity_tokens: 0,
non_entity_clients: 7,
clients: 7,
},
},
],
},
{
namespace_id: 's07UR',
namespace_path: 'ns1/',
counts: {
distinct_entities: 0,
entity_clients: 5,
non_entity_tokens: 0,
non_entity_clients: 5,
clients: 10,
},
mounts: [
{
mount_path: 'auth/up1/',
counts: {
distinct_entities: 0,
entity_clients: 0,
non_entity_tokens: 0,
non_entity_clients: 5,
clients: 5,
},
},
{
mount_path: 'auth/up2/',
counts: {
distinct_entities: 0,
entity_clients: 5,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 5,
},
},
],
},
],
new_clients: {
counts: {
distinct_entities: 0,
entity_clients: 3,
non_entity_tokens: 0,
non_entity_clients: 2,
clients: 5,
},
namespaces: [
{
namespace_id: 'root',
namespace_path: '',
counts: {
distinct_entities: 0,
entity_clients: 3,
non_entity_tokens: 0,
non_entity_clients: 2,
clients: 5,
},
mounts: [
{
mount_path: 'auth/up2/',
counts: {
distinct_entities: 0,
entity_clients: 3,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 3,
},
},
{
mount_path: 'auth/up1/',
counts: {
distinct_entities: 0,
entity_clients: 0,
non_entity_tokens: 0,
non_entity_clients: 2,
clients: 2,
},
},
],
},
],
},
},
{
timestamp: '2021-10-01T00:00:00Z',
counts: {
@ -661,14 +774,7 @@ const handleMockQuery = (queryStartTimestamp, queryEndTimestamp, monthlyData) =>
const endDateByMonth = parseAPITimestamp(monthlyData[0].timestamp);
let transformedMonthlyArray = [...monthlyData];
if (isBefore(queryStartDate, startDateByMonth)) {
// no data for months before (upgraded to 1.10 during billing period)
let i = 0;
do {
i++;
let timestamp = formatRFC3339(sub(startDateByMonth, { months: i }));
// TODO CMB update this when we confirm what combined data looks like
transformedMonthlyArray.push({ timestamp });
} while (i < differenceInCalendarMonths(startDateByMonth, queryStartDate));
return transformedMonthlyArray;
}
if (isAfter(queryStartDate, startDateByMonth)) {
let index = monthlyData.findIndex((e) => isSameMonth(queryStartDate, parseAPITimestamp(e.timestamp)));
@ -976,6 +1082,14 @@ export default function (server) {
non_entity_clients: 15,
},
},
{
mount_path: 'auth_userpass_3158c012',
counts: {
clients: 2,
entity_clients: 2,
non_entity_clients: 0,
},
},
],
},
],

View File

@ -89,21 +89,12 @@ module('Acceptance | clients current', function (hooks) {
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
assert
.dom('[data-test-chart-container="total-clients"] [data-test-horizontal-bar-chart]')
.exists('Shows totals attribution bar chart');
assert
// TODO CMB - this assertion should be updated so the response includes new client counts
// TODO then move somewhere to assert empty state shows when filtering a namespace with no new clients
.dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]')
.includesText(
'There are no new clients for this namespace during this time period.',
'Shows empty state if no new client counts'
);
// check chart displays correct elements and values
for (const key in CHART_ELEMENTS) {
let namespaceNumber = by_namespace.length < 10 ? by_namespace.length : 10;
@ -128,21 +119,16 @@ module('Acceptance | clients current', function (hooks) {
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
await waitUntil(() => find('[data-test-horizontal-bar-chart]'));
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
assert
.dom('[data-test-chart-container="total-clients"] [data-test-horizontal-bar-chart]')
.exists('Still shows totals attribution bar chart');
assert
.dom('[data-test-chart-container="total-clients"] .chart-description')
.includesText('The total clients used by the auth method for this month.');
assert
.dom('[data-test-chart-container="new-clients"] .chart-description')
.includesText('The new clients used by the auth method for this month.');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
// check chart displays correct elements and values
for (const key in CHART_ELEMENTS) {
@ -179,8 +165,8 @@ module('Acceptance | clients current', function (hooks) {
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
await settled();
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
assert.dom(SELECTORS.attributionBlock).exists('Still shows attribution block');
await clickTrigger();
await searchSelect.options.objectAt(0).click();
@ -193,11 +179,9 @@ module('Acceptance | clients current', function (hooks) {
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert
.dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]')
.includesText('There are no new clients', 'Shows empty state if no new client counts');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
assert.dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]').doesNotExist();
});
test('filters correctly on current with no auth mounts', async function (assert) {
@ -224,14 +208,12 @@ module('Acceptance | clients current', function (hooks) {
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
assert
.dom('[data-test-chart-container="total-clients"] [data-test-horizontal-bar-chart]')
.exists('Shows totals attribution bar chart');
assert
.dom('[data-test-chart-container="total-clients"] .chart-description')
.includesText('The total clients in the namespace for this month.');
assert.dom('[data-test-chart-container="total-clients"]').exists();
// Filter by namespace
await clickTrigger();
@ -250,8 +232,8 @@ module('Acceptance | clients current', function (hooks) {
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert.dom('[data-test-chart-container="new-clients"]').doesNotExist();
assert.dom('[data-test-chart-container="total-clients"]').exists();
});
test('shows correct empty state when config off but no read on config', async function (assert) {

View File

@ -144,6 +144,15 @@ module('Acceptance | clients history tab', function (hooks) {
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
// TODO CMB - add assertion so double charts show for single historical month
// TODO and check for empty state there
// assert
// .dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]')
// .includesText(
// 'There are no new clients for this namespace during this time period.',
// 'Shows empty state if no new client counts'
// );
// check chart displays correct elements and values
for (const key in CHART_ELEMENTS) {
let namespaceNumber = by_namespace.length < 10 ? by_namespace.length : 10;

View File

@ -17,7 +17,7 @@ module('Integration | Component | clients/attribution', function (hooks) {
{ label: 'non-entity clients', key: 'non_entity_clients' },
]);
this.set('totalUsageCounts', { clients: 15, entity_clients: 10, non_entity_clients: 5 });
this.set('totalClientsData', [
this.set('totalClientAttribution', [
{ label: 'second', clients: 10, entity_clients: 7, non_entity_clients: 3 },
{ label: 'first', clients: 5, entity_clients: 3, non_entity_clients: 2 },
]);
@ -46,7 +46,7 @@ module('Integration | Component | clients/attribution', function (hooks) {
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{totalClientsData}}
@totalClientAttribution={{totalClientAttribution}}
@totalUsageCounts={{totalUsageCounts}}
@timestamp={{timestamp}}
@selectedNamespace={{selectedNamespace}}
@ -77,7 +77,7 @@ module('Integration | Component | clients/attribution', function (hooks) {
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{totalClientsData}}
@totalClientAttribution={{totalClientAttribution}}
@totalUsageCounts={{totalUsageCounts}}
@timestamp={{timestamp}}
@selectedNamespace={{selectedNamespace}}
@ -130,7 +130,7 @@ module('Integration | Component | clients/attribution', function (hooks) {
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{namespaceMountsData}}
@totalClientAttribution={{namespaceMountsData}}
@totalUsageCounts={{totalUsageCounts}}
@timestamp={{timestamp}}
@selectedNamespace={{selectedNamespace}}
@ -160,7 +160,7 @@ module('Integration | Component | clients/attribution', function (hooks) {
<div id="modal-wormhole"></div>
<Clients::Attribution
@chartLegend={{chartLegend}}
@totalClientsData={{namespaceMountsData}}
@totalClientAttribution={{namespaceMountsData}}
@timestamp={{timestamp}}
@startTimeDisplay={{"January 2022"}}
@endTimeDisplay={{"February 2022"}}

View File

@ -18,14 +18,14 @@ module('Integration | Component | clients/horizontal-bar-chart', function (hooks
{ label: 'second', clients: 3, entity_clients: 1, non_entity_clients: 2 },
{ label: 'first', clients: 2, entity_clients: 1, non_entity_clients: 1 },
];
this.set('totalUsageCounts', totalObject);
this.set('totalClientsData', dataArray);
this.set('totalCounts', totalObject);
this.set('totalClientAttribution', dataArray);
await render(hbs`
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@dataset={{this.totalClientAttribution}}
@chartLegend={{chartLegend}}
@totalUsageCounts={{totalUsageCounts}}
@totalCounts={{totalCounts}}
/>`);
assert.dom('[data-test-horizontal-bar-chart]').exists();
@ -43,7 +43,6 @@ module('Integration | Component | clients/horizontal-bar-chart', function (hooks
textTotals.forEach((label, index) => {
assert.dom(label).hasText(`${dataArray[index].clients}`, 'total value renders correct number');
});
for (let [i, bar] of actionBars.entries()) {
let percent = Math.round((dataArray[i].clients / totalObject.clients) * 100);
await triggerEvent(bar, 'mouseover');
@ -58,14 +57,14 @@ module('Integration | Component | clients/horizontal-bar-chart', function (hooks
{ label: 'second', clients: 5929093, entity_clients: 1391896, non_entity_clients: 4537100 },
{ label: 'first', clients: 300, entity_clients: 101, non_entity_clients: 296 },
];
this.set('totalUsageCounts', totalObject);
this.set('totalClientsData', dataArray);
this.set('totalCounts', totalObject);
this.set('totalClientAttribution', dataArray);
await render(hbs`
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@dataset={{this.totalClientAttribution}}
@chartLegend={{chartLegend}}
@totalUsageCounts={{totalUsageCounts}}
@totalCounts={{totalCounts}}
/>`);
assert.dom('[data-test-horizontal-bar-chart]').exists();