From 36bcf8e141c2047b48f39d1d3278360de2521ef3 Mon Sep 17 00:00:00 2001 From: Arnav Palnitkar Date: Wed, 22 Sep 2021 12:50:59 -0700 Subject: [PATCH] Added namespace search to client count (#12577) * Added namespace search to client count - Used existing search select component for namespace search * Added changelog * Added download csv component - generate namespaces data in csv format - Show root in top 10 namespaces - Changed active direct tokens to non-entity tokens * Added test for checking graph render * Added documentation for the download csv component --- changelog/12577.txt | 3 + ui/app/components/clients/history.js | 82 ++++++++++++++++--- ui/app/styles/components/bar-chart.scss | 2 +- .../templates/components/clients/history.hbs | 50 +++++++++-- ui/lib/core/addon/components/bar-chart.js | 2 +- ui/lib/core/addon/components/download-csv.js | 31 +++++++ .../templates/components/download-csv.hbs | 3 + ui/lib/core/app/components/download-csv.js | 1 + .../components/clients-history-test.js | 22 +++++ 9 files changed, 177 insertions(+), 19 deletions(-) create mode 100644 changelog/12577.txt create mode 100644 ui/lib/core/addon/components/download-csv.js create mode 100644 ui/lib/core/addon/templates/components/download-csv.hbs create mode 100644 ui/lib/core/app/components/download-csv.js diff --git a/changelog/12577.txt b/changelog/12577.txt new file mode 100644 index 0000000000000..7f69476931bef --- /dev/null +++ b/changelog/12577.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: namespace search in client count views +``` \ No newline at end of file diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js index cb4ba01fb32f9..56e63507d578d 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -1,8 +1,14 @@ import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; export default class HistoryComponent extends Component { max_namespaces = 10; + @tracked selectedNamespace = null; + + // Determine if we have client count data based on the current tab, + // since model is slightly different for current month vs history api get hasClientData() { if (this.args.tab === 'current') { return this.args.model.activity && this.args.model.activity.clients; @@ -10,20 +16,38 @@ export default class HistoryComponent extends Component { return this.args.model.activity && this.args.model.activity.total; } - get barChartDataset() { + // Show namespace graph only if we have more than 1 + get showGraphs() { + return ( + this.args.model.activity && + this.args.model.activity.byNamespace && + this.args.model.activity.byNamespace.length > 1 + ); + } + + // Construct the namespace model for the search select component + get searchDataset() { if (!this.args.model.activity || !this.args.model.activity.byNamespace) { return null; } - let dataset = this.args.model.activity.byNamespace; - // Filter out root data - dataset = dataset.filter(item => { - return item.namespace_id !== 'root'; + let dataList = this.args.model.activity.byNamespace; + return dataList.map(d => { + return { + name: d['namespace_id'], + id: d['namespace_path'] === '' ? 'root' : d['namespace_path'], + }; }); - // Show only top 10 namespaces - dataset = dataset.slice(0, this.max_namespaces); + } + + // Construct the namespace model for the bar chart component + get barChartDataset() { + if (!this.args.model.activity || !this.args.model.activity.byNamespace) { + return null; + } + let dataset = this.args.model.activity.byNamespace.slice(0, this.max_namespaces); return dataset.map(d => { return { - label: d['namespace_path'], + label: d['namespace_path'] === '' ? 'root' : d['namespace_path'], non_entity_tokens: d['counts']['non_entity_tokens'], distinct_entities: d['counts']['distinct_entities'], total: d['counts']['clients'], @@ -31,10 +55,48 @@ export default class HistoryComponent extends Component { }); } - get showGraphs() { + // Create namespaces data for csv format + get getCsvData() { if (!this.args.model.activity || !this.args.model.activity.byNamespace) { return null; } - return this.args.model.activity.byNamespace.length > 1; + let results = '', + namespaces = this.args.model.activity.byNamespace, + fields = ['Namespace path', 'Active clients', 'Unique entities', 'Non-entity tokens']; + + results = fields.join(',') + '\n'; + + namespaces.forEach(function(item) { + let path = item.namespace_path !== '' ? item.namespace_path : 'root', + total = item.counts.clients, + unique = item.counts.distinct_entities, + non_entity = item.counts.non_entity_tokens; + + results += path + ',' + total + ',' + unique + ',' + non_entity + '\n'; + }); + return results; + } + + // Get the namespace by matching the path from the namespace list + getNamespace(path) { + return this.args.model.activity.byNamespace.find(ns => { + if (path === 'root') { + return ns.namespace_path === ''; + } + return ns.namespace_path === path; + }); + } + + @action + selectNamespace(value) { + // In case of search select component, value returned is an array + if (Array.isArray(value)) { + this.selectedNamespace = this.getNamespace(value[0]); + } else if (typeof value === 'object') { + // While D3 bar selection returns an object + this.selectedNamespace = this.getNamespace(value.label); + } else { + this.selectedNamespace = null; + } } } diff --git a/ui/app/styles/components/bar-chart.scss b/ui/app/styles/components/bar-chart.scss index 44458d04c58cc..1b9484ee23d1d 100644 --- a/ui/app/styles/components/bar-chart.scss +++ b/ui/app/styles/components/bar-chart.scss @@ -31,7 +31,7 @@ } .header-right { - text-align: center; + text-align: right; > button { font-size: $size-8; diff --git a/ui/app/templates/components/clients/history.hbs b/ui/app/templates/components/clients/history.hbs index 6936c11cd8b92..c22f23d7326a3 100644 --- a/ui/app/templates/components/clients/history.hbs +++ b/ui/app/templates/components/clients/history.hbs @@ -101,10 +101,10 @@
@@ -119,7 +119,7 @@
{{#if this.showGraphs}}
-
+
+ > + + +
+
+
+
+ + {{#if this.selectedNamespace}} +
+
+ +
+
+
+
+ +
+
+ +
+
+ {{else}} + + {{/if}} +
+
-
{{/if}} {{/unless}} diff --git a/ui/lib/core/addon/components/bar-chart.js b/ui/lib/core/addon/components/bar-chart.js index d4da82844f5d0..230adbeb27526 100644 --- a/ui/lib/core/addon/components/bar-chart.js +++ b/ui/lib/core/addon/components/bar-chart.js @@ -218,7 +218,7 @@ class BarChartComponent extends Component { .style('top', `${event.pageY - 155}px`) .text( `${Math.round((chartData.total * 100) / totalCount)}% of total client counts: - ${chartData.non_entity_tokens} active tokens, ${chartData.distinct_entities} unique entities. + ${chartData.non_entity_tokens} non-entity tokens, ${chartData.distinct_entities} unique entities. ` ); }); diff --git a/ui/lib/core/addon/components/download-csv.js b/ui/lib/core/addon/components/download-csv.js new file mode 100644 index 0000000000000..76b58641f83f6 --- /dev/null +++ b/ui/lib/core/addon/components/download-csv.js @@ -0,0 +1,31 @@ +import Component from '@glimmer/component'; +import layout from '../templates/components/download-csv'; +import { setComponentTemplate } from '@ember/component'; +import { action } from '@ember/object'; + +/** + * @module DownloadCsv + * Download csv component is used to display a link which initiates a csv file download of the data provided by it's parent component. + * + * @example + * ```js + * + * ``` + * + * @param {string} label - Label for the download link button + * @param {string} csvData - Data in csv format + * @param {string} fileName - Custom name for the downloaded file + * + */ +class DownloadCsvComponent extends Component { + @action + downloadCsv() { + let hiddenElement = document.createElement('a'); + hiddenElement.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURI(this.args.csvData)); + hiddenElement.setAttribute('target', '_blank'); + hiddenElement.setAttribute('download', this.args.fileName || 'vault-data.csv'); + hiddenElement.click(); + } +} + +export default setComponentTemplate(layout, DownloadCsvComponent); diff --git a/ui/lib/core/addon/templates/components/download-csv.hbs b/ui/lib/core/addon/templates/components/download-csv.hbs new file mode 100644 index 0000000000000..2e4809e4ce881 --- /dev/null +++ b/ui/lib/core/addon/templates/components/download-csv.hbs @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/ui/lib/core/app/components/download-csv.js b/ui/lib/core/app/components/download-csv.js new file mode 100644 index 0000000000000..23ea50f576495 --- /dev/null +++ b/ui/lib/core/app/components/download-csv.js @@ -0,0 +1 @@ +export { default } from 'core/components/download-csv'; diff --git a/ui/tests/integration/components/clients-history-test.js b/ui/tests/integration/components/clients-history-test.js index dc4b8ce4dd163..0523ad76020e3 100644 --- a/ui/tests/integration/components/clients-history-test.js +++ b/ui/tests/integration/components/clients-history-test.js @@ -52,6 +52,26 @@ module('Integration | Component | client count history', function(hooks) { test('it shows data when available from query', async function(assert) { Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } }); Object.assign(this.model.activity, { + byNamespace: [ + { + counts: { + clients: 2725, + distinct_entities: 1137, + non_entity_tokens: 1588, + }, + namespace_id: '8VIZc', + namespace_path: 'nsTest5/', + }, + { + counts: { + clients: 200, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespace_id: 'sd3Zc', + namespace_path: 'nsTest1/', + }, + ], total: { clients: 1234, distinct_entities: 234, @@ -63,5 +83,7 @@ module('Integration | Component | client count history', function(hooks) { assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component exists'); assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists'); assert.dom('[data-test-client-count-stats]').exists('Client count data exists'); + assert.dom('[data-test-client-count-graph]').exists('Top 10 namespaces chart exists'); + assert.dom('[data-test-empty-state-title]').hasText('No namespace selected'); }); });