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
This commit is contained in:
Arnav Palnitkar 2021-09-22 12:50:59 -07:00 committed by GitHub
parent 4cca2e0303
commit 4f19fd1624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 177 additions and 19 deletions

3
changelog/12577.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: namespace search in client count views
```

View File

@ -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;
}
// 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 dataList = this.args.model.activity.byNamespace;
return dataList.map(d => {
return {
name: d['namespace_id'],
id: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
};
});
}
// 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;
// Filter out root data
dataset = dataset.filter(item => {
return item.namespace_id !== 'root';
});
// Show only top 10 namespaces
dataset = dataset.slice(0, this.max_namespaces);
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;
}
}
}

View File

@ -31,7 +31,7 @@
}
.header-right {
text-align: center;
text-align: right;
> button {
font-size: $size-8;

View File

@ -101,10 +101,10 @@
<div class="columns">
<div class="column" data-test-client-count-stats>
<StatText
@label="Total active Clients"
@label="Total active clients"
@value={{or @model.activity.clients @model.activity.total.clients}}
@size="l"
@subText="The sum of unique entities and active direct tokens; Vault's primary billing metric."
@subText="The sum of unique entities and non-entity tokens; Vault's primary billing metric."
/>
</div>
<div class="column">
@ -119,7 +119,7 @@
<div class="column">
<StatText
class="column"
@label="Active direct tokens"
@label="Non-entity tokens"
@value={{or @model.activity.non_entity_tokens @model.activity.total.non_entity_tokens}}
@size="l"
@subText="Tokens created via a method that is not associated with an entity."
@ -130,18 +130,54 @@
</div>
{{#if this.showGraphs}}
<div class="columns has-bottom-margin-m">
<div class="column is-two-thirds">
<div class="column is-two-thirds" data-test-client-count-graph>
<BarChart
@title="Top 10 Namespaces"
@description="Each namespace's client count includes clients in child namespaces."
@dataset={{this.barChartDataset}}
@onClick={{action this.selectNamespace}}
@mapLegend={{array
(hash key='non_entity_tokens' label='Active direct tokens')
(hash key='non_entity_tokens' label='Non-entity tokens')
(hash key='distinct_entities' label='Unique entities')
}}
/>
>
<DownloadCsv @label={{'Export all namespace data'}} @csvData={{this.getCsvData}} @fileName={{'client-count-by-namespaces.csv'}} />
</BarChart>
</div>
<div class="column">
<div class="card">
<div class="card-content">
<SearchSelect
@id="namespaces"
@labelClass="title is-5"
@disallowNewItems={{true}}
@onChange={{action this.selectNamespace}}
@label="Single namespace"
@options={{or this.searchDataset []}}
@searchField="namespace_path"
@selectLimit={{1}}
/>
{{#if this.selectedNamespace}}
<div class="columns">
<div class="column">
<StatText @label="Active clients" @value={{this.selectedNamespace.counts.clients}} @size="l" />
</div>
</div>
<div class="columns">
<div class="column">
<StatText @label="Unique entities" @value={{this.selectedNamespace.counts.distinct_entities}} @size="m" />
</div>
<div class="column">
<StatText @label="Non-entity tokens" @value={{this.selectedNamespace.counts.non_entity_tokens}} @size="m" />
</div>
</div>
{{else}}
<EmptyState @title="No namespace selected"
@message="Click on a namespace in the Top 10 chart or type its name in the box to view it's individual client counts." />
{{/if}}
</div>
</div>
</div>
<div class="column"></div>
</div>
{{/if}}
{{/unless}}

View File

@ -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.
`
);
});

View File

@ -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
* <DownloadCsv @label={{'Export all namespace data'}} @csvData={{"Namespace path,Active clients /n nsTest5/,2725"}} @fileName={{'client-count.csv'}} />
* ```
*
* @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);

View File

@ -0,0 +1,3 @@
<button type="button" class="link" {{on "click" this.downloadCsv}}>
{{or @label 'Download'}}
</button>

View File

@ -0,0 +1 @@
export { default } from 'core/components/download-csv';

View File

@ -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');
});
});