mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 11:08:10 +00:00
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:
3
changelog/12577.txt
Normal file
3
changelog/12577.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: namespace search in client count views
|
||||||
|
```
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
export default class HistoryComponent extends Component {
|
export default class HistoryComponent extends Component {
|
||||||
max_namespaces = 10;
|
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() {
|
get hasClientData() {
|
||||||
if (this.args.tab === 'current') {
|
if (this.args.tab === 'current') {
|
||||||
return this.args.model.activity && this.args.model.activity.clients;
|
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;
|
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() {
|
get barChartDataset() {
|
||||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
let dataset = this.args.model.activity.byNamespace;
|
let dataset = this.args.model.activity.byNamespace.slice(0, this.max_namespaces);
|
||||||
// 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);
|
|
||||||
return dataset.map(d => {
|
return dataset.map(d => {
|
||||||
return {
|
return {
|
||||||
label: d['namespace_path'],
|
label: d['namespace_path'] === '' ? 'root' : d['namespace_path'],
|
||||||
non_entity_tokens: d['counts']['non_entity_tokens'],
|
non_entity_tokens: d['counts']['non_entity_tokens'],
|
||||||
distinct_entities: d['counts']['distinct_entities'],
|
distinct_entities: d['counts']['distinct_entities'],
|
||||||
total: d['counts']['clients'],
|
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) {
|
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||||
return null;
|
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 {
|
.header-right {
|
||||||
text-align: center;
|
text-align: right;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
font-size: $size-8;
|
font-size: $size-8;
|
||||||
|
|||||||
@@ -101,10 +101,10 @@
|
|||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column" data-test-client-count-stats>
|
<div class="column" data-test-client-count-stats>
|
||||||
<StatText
|
<StatText
|
||||||
@label="Total active Clients"
|
@label="Total active clients"
|
||||||
@value={{or @model.activity.clients @model.activity.total.clients}}
|
@value={{or @model.activity.clients @model.activity.total.clients}}
|
||||||
@size="l"
|
@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>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<div class="column">
|
<div class="column">
|
||||||
<StatText
|
<StatText
|
||||||
class="column"
|
class="column"
|
||||||
@label="Active direct tokens"
|
@label="Non-entity tokens"
|
||||||
@value={{or @model.activity.non_entity_tokens @model.activity.total.non_entity_tokens}}
|
@value={{or @model.activity.non_entity_tokens @model.activity.total.non_entity_tokens}}
|
||||||
@size="l"
|
@size="l"
|
||||||
@subText="Tokens created via a method that is not associated with an entity."
|
@subText="Tokens created via a method that is not associated with an entity."
|
||||||
@@ -130,18 +130,54 @@
|
|||||||
</div>
|
</div>
|
||||||
{{#if this.showGraphs}}
|
{{#if this.showGraphs}}
|
||||||
<div class="columns has-bottom-margin-m">
|
<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
|
<BarChart
|
||||||
@title="Top 10 Namespaces"
|
@title="Top 10 Namespaces"
|
||||||
@description="Each namespace's client count includes clients in child namespaces."
|
@description="Each namespace's client count includes clients in child namespaces."
|
||||||
@dataset={{this.barChartDataset}}
|
@dataset={{this.barChartDataset}}
|
||||||
|
@onClick={{action this.selectNamespace}}
|
||||||
@mapLegend={{array
|
@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')
|
(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>
|
||||||
<div class="column"></div>
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ class BarChartComponent extends Component {
|
|||||||
.style('top', `${event.pageY - 155}px`)
|
.style('top', `${event.pageY - 155}px`)
|
||||||
.text(
|
.text(
|
||||||
`${Math.round((chartData.total * 100) / totalCount)}% of total client counts:
|
`${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.
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
31
ui/lib/core/addon/components/download-csv.js
Normal file
31
ui/lib/core/addon/components/download-csv.js
Normal 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);
|
||||||
3
ui/lib/core/addon/templates/components/download-csv.hbs
Normal file
3
ui/lib/core/addon/templates/components/download-csv.hbs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<button type="button" class="link" {{on "click" this.downloadCsv}}>
|
||||||
|
{{or @label 'Download'}}
|
||||||
|
</button>
|
||||||
1
ui/lib/core/app/components/download-csv.js
Normal file
1
ui/lib/core/app/components/download-csv.js
Normal file
@@ -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) {
|
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.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||||
Object.assign(this.model.activity, {
|
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: {
|
total: {
|
||||||
clients: 1234,
|
clients: 1234,
|
||||||
distinct_entities: 234,
|
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-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-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-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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user