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:
parent
4cca2e0303
commit
4f19fd1624
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
ui: namespace search in client count views
|
||||
```
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
}
|
||||
|
||||
.header-right {
|
||||
text-align: center;
|
||||
text-align: right;
|
||||
|
||||
> button {
|
||||
font-size: $size-8;
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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.
|
||||
`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
|||
<button type="button" class="link" {{on "click" this.downloadCsv}}>
|
||||
{{or @label 'Download'}}
|
||||
</button>
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/components/download-csv';
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue