Skip to content

Commit

Permalink
CSV Export include monthly data (#15169)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Monkeychip and hellobontempo committed May 3, 2022
1 parent ca6e593 commit b1864b6
Show file tree
Hide file tree
Showing 22 changed files with 469 additions and 385 deletions.
98 changes: 68 additions & 30 deletions ui/app/components/clients/attribution.js
Expand Up @@ -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
*/

Expand All @@ -38,33 +40,33 @@ 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)
return !!this.args.selectedNamespace;
}

// 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() {
Expand Down Expand Up @@ -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',
Expand All @@ -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');
}

Expand All @@ -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;
}
Expand Down
84 changes: 19 additions & 65 deletions ui/app/components/clients/current.js
Expand Up @@ -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'] };
});

Expand All @@ -29,71 +29,38 @@ 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() {
if (this.selectedAuthMethod) return false;
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);
}
Expand Down Expand Up @@ -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;
}

get newUsageCounts() {
return this.selectedNamespace ? this.filteredNewData : this.args.model.monthly?.new;
}

// total client data for horizontal bar chart in attribution component
get totalClientsData() {
if (this.selectedNamespace) {
return this.filteredTotalData?.mounts || null;
} else {
return this.byNamespaceTotalClients;
}
return this.selectedNamespace ? this.filteredCurrentData : this.args.model.monthly?.total;
}

// new client data for horizontal bar chart in attribution component
get newClientsData() {
// total client attribution data for horizontal bar chart in attribution component
get totalClientAttribution() {
if (this.selectedNamespace) {
return this.filteredNewData?.mounts || null;
return this.filteredCurrentData?.mounts || null;
} else {
return this.byNamespaceNewClients;
return this.byNamespace;
}
}

Expand All @@ -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,
}));
Expand Down

0 comments on commit b1864b6

Please sign in to comment.