Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added namespace search to client count #12577

Merged
merged 5 commits into from Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/12577.txt
@@ -0,0 +1,3 @@
```release-note:improvement
ui: namespace search in client count views
```
82 changes: 72 additions & 10 deletions ui/app/components/clients/history.js
@@ -1,40 +1,102 @@
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;
}
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice call on adding these checks first

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

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;
}
}
}
2 changes: 1 addition & 1 deletion ui/app/styles/components/bar-chart.scss
Expand Up @@ -31,7 +31,7 @@
}

.header-right {
text-align: center;
text-align: right;

> button {
font-size: $size-8;
Expand Down
50 changes: 43 additions & 7 deletions ui/app/templates/components/clients/history.hbs
Expand Up @@ -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">
Expand All @@ -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."
Expand All @@ -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}}
Expand Down
2 changes: 1 addition & 1 deletion ui/lib/core/addon/components/bar-chart.js
Expand Up @@ -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.
`
);
});
Expand Down
17 changes: 17 additions & 0 deletions ui/lib/core/addon/components/download-csv.js
@@ -0,0 +1,17 @@
import Component from '@glimmer/component';
import layout from '../templates/components/download-csv';
import { setComponentTemplate } from '@ember/component';
import { action } from '@ember/object';

class DownloadCsvComponent extends Component {
arnav28 marked this conversation as resolved.
Show resolved Hide resolved
@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);
hiddenElement.click();
}
}

export default setComponentTemplate(layout, DownloadCsvComponent);
3 changes: 3 additions & 0 deletions ui/lib/core/addon/templates/components/download-csv.hbs
@@ -0,0 +1,3 @@
<button type="button" class="link" {{on "click" this.downloadCsv}}>
{{@label}}
</button>
1 change: 1 addition & 0 deletions ui/lib/core/app/components/download-csv.js
@@ -0,0 +1 @@
export { default } from 'core/components/download-csv';
22 changes: 22 additions & 0 deletions ui/tests/integration/components/clients-history-test.js
Expand Up @@ -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,
Expand All @@ -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');
});
});