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:
parent
bef350c916
commit
33de0a0a49
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}}>
|
||||
|
|
|
@ -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}}
|
||||
/>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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;
|
||||
};
|
|
@ -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]);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"}}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue