UI/Add CSV export, update history and current tabs (#13812)

* add timestamp to attribution

* create usage stat component

* updates stat text boxes

* remove flex-header css

* remove comment

* add empty state if no data

* update monthly serializer

* remove empty state - unnecessary

* change tab to 'history'

* add usage stats to history view

* change css styling for upcased grey subtitle

* correctly exports namespace and auth data

* close modal on download

* test making a service?

* fix monthly attrs

* update csv content format

* remove component and make downloadCsv a service

* update function name

* wip//add warning labels, fixing up current and history tabs

* wip//clean up serializer fix with real data

* fix link styling:

* add conditionals for no data, add warning for 1.9 counting changes

* naming comment

* fix tooltip formatting

* fix number format and consolidate actions

* remove outdated test

* add revokeObjectURL and rename variable

* fix errors and empty state views when no activity data at all

* fix end time error

* fix comment

* return truncating to serializer

* PR review cleanup

* return new object
This commit is contained in:
claire bontempo 2022-02-02 11:46:59 -08:00 committed by GitHub
parent a562beaba8
commit 34630f6557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 583 additions and 489 deletions

View File

@ -41,7 +41,6 @@ export default Application.extend({
if (queryParams) {
return this.ajax(url, 'GET', { data: queryParams }).then((resp) => {
let response = resp || {};
// if the response is a 204 it has no request id (ARG TODO test that it returns a 204)
response.id = response.request_id || 'no-data';
return response;
});

View File

@ -1,6 +1,7 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
/**
* @module Attribution
* Attribution components display the top 10 total client counts for namespaces or auth methods (mounts) during a billing period.
@ -10,8 +11,8 @@ import { tracked } from '@glimmer/tracking';
* ```js
* <Clients::Attribution
* @chartLegend={{this.chartLegend}}
* @topTenNamespaces={{this.topTenNamespaces}}
* @runningTotals={{this.runningTotals}}
* @totalClientsData={{this.topTenChartData}}
* @totalUsageCounts={{this.totalUsageCounts}}
* @selectedNamespace={{this.selectedNamespace}}
* @startTimeDisplay={{this.startTimeDisplay}}
* @endTimeDisplay={{this.endTimeDisplay}}
@ -20,8 +21,8 @@ import { tracked } from '@glimmer/tracking';
* />
* ```
* @param {array} chartLegend - (passed to child) array of objects with key names 'key' and 'label' so data can be stacked
* @param {array} topTenNamespaces - (passed to child chart) array of top 10 namespace objects
* @param {object} runningTotals - object with total client counts for chart tooltip text
* @param {array} totalClientsData - (passed to child chart) array of top 10 namespace objects
* @param {object} totalUsageCounts - object with total client counts for chart tooltip text
* @param {string} selectedNamespace - namespace selected from filter bar
* @param {string} startTimeDisplay - start date for CSV modal
* @param {string} endTimeDisplay - end date for CSV modal
@ -31,6 +32,7 @@ import { tracked } from '@glimmer/tracking';
export default class Attribution extends Component {
@tracked showCSVDownloadModal = false;
@service downloadCsv;
get isDateRange() {
return this.args.isDateRange;
@ -42,10 +44,7 @@ export default class Attribution extends Component {
}
get totalClientsData() {
// get dataset for bar chart displaying top 10 namespaces/mounts with highest # of total clients
return this.isSingleNamespace
? this.filterByNamespace(this.args.selectedNamespace)
: this.args.topTenNamespaces;
return this.args.totalClientsData;
}
get topClientCounts() {
@ -54,8 +53,8 @@ export default class Attribution extends Component {
}
get attributionBreakdown() {
// display 'Auth method' or 'Namespace' respectively in CSV file
return this.isSingleNamespace ? 'Auth method' : 'Namespace';
// display text for hbs
return this.isSingleNamespace ? 'auth method' : 'namespace';
}
get chartText() {
@ -86,37 +85,50 @@ export default class Attribution extends Component {
}
}
// TODO CMB update with proper data format when we have
get getCsvData() {
let results = '',
data,
fields;
let csvData = [],
graphData = this.totalClientsData,
csvHeader = [
`Namespace path`,
'Authentication method',
'Total clients',
'Entity clients',
'Non-entity clients',
];
// TODO CMB will CSV for namespaces include mounts?
fields = [`${this.attributionBreakdown}`, 'Active clients', 'Unique entities', 'Non-entity tokens'];
results = fields.join(',') + '\n';
data.forEach(function (item) {
let path = item.label !== '' ? item.label : 'root',
total = item.total,
unique = item.entity_clients,
non_entity = item.non_entity_clients;
results += path + ',' + total + ',' + unique + ',' + non_entity + '\n';
});
return results;
// each array will be a row in the csv file
if (this.isSingleNamespace) {
graphData.forEach((mount) => {
csvData.push(['', mount.label, mount.clients, mount.entity_clients, mount.non_entity_clients]);
});
csvData.forEach((d) => (d[0] = this.args.selectedNamespace));
} else {
graphData.forEach((ns) => {
csvData.push([ns.label, '', ns.clients, ns.entity_clients, ns.non_entity_clients]);
if (ns.mounts) {
ns.mounts.forEach((m) => {
csvData.push([ns.label, m.label, m.clients, m.entity_clients, m.non_entity_clients]);
});
}
});
}
csvData.unshift(csvHeader);
// make each nested array a comma separated string, join each array in csvData with line break (\n)
return csvData.map((d) => d.join()).join('\n');
}
// TODO CMB - confirm with design file name structure
get getCsvFileName() {
let activityDateRange = `${this.args.startTimeDisplay} - ${this.args.endTimeDisplay}`;
return activityDateRange
? `clients-by-${this.attributionBreakdown}-${activityDateRange}`
: `clients-by-${this.attributionBreakdown}-${new Date()}`;
let endRange = this.isDateRange ? `-${this.args.endTimeDisplay}` : '';
let csvDateRange = this.args.startTimeDisplay + endRange;
return this.isSingleNamespace
? `clients_by_auth_method_${csvDateRange}`
: `clients_by_namespace_${csvDateRange}`;
}
// HELPERS
filterByNamespace(namespace) {
// return top 10 mounts for a namespace
return this.args.topTenNamespaces.find((ns) => ns.label === namespace).mounts.slice(0, 10);
// ACTIONS
@action
exportChartData(filename, contents) {
this.downloadCsv.download(filename, contents);
this.showCSVDownloadModal = false;
}
}

View File

@ -1,22 +1,51 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class Current extends Component {
chartLegend = [
{ key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' },
];
@tracked selectedNamespace = null;
// TODO CMB get from model
get upgradeDate() {
return this.args.upgradeDate || null;
}
get licenseStartDate() {
return this.args.licenseStartDate || null;
}
// by namespace client count data for partial month
get byNamespaceCurrent() {
return this.args.model.monthly?.byNamespace || null;
}
// data for horizontal bar chart in attribution component
get topTenNamespaces() {
return this.args.model.monthly?.byNamespace;
get topTenChartData() {
if (this.selectedNamespace) {
let filteredNamespace = this.filterByNamespace(this.selectedNamespace);
return filteredNamespace.mounts
? this.filterByNamespace(this.selectedNamespace).mounts.slice(0, 10)
: null;
} else {
return this.byNamespaceCurrent;
}
}
// top level TOTAL client counts from response for given month
get runningTotals() {
return this.args.model.monthly?.total;
get totalUsageCounts() {
return this.selectedNamespace
? this.filterByNamespace(this.selectedNamespace)
: this.args.model.monthly?.total;
}
get responseTimestamp() {
return this.args.model.monthly?.responseTimestamp;
}
// HELPERS
filterByNamespace(namespace) {
return this.byNamespaceCurrent.find((ns) => ns.label === namespace);
}
}

View File

@ -40,11 +40,12 @@ export default class Dashboard extends Component {
@tracked responseRangeDiffMessage = null;
@tracked startTimeRequested = null;
@tracked startTimeFromResponse = this.args.model.startTimeFromLicense; // ex: ['2021', 3] is April 2021 (0 indexed)
@tracked endTimeFromResponse = this.args.model.endTimeFromLicense;
@tracked endTimeFromResponse = this.args.model.endTimeFromResponse;
@tracked startMonth = null;
@tracked startYear = null;
@tracked selectedNamespace = null;
// @tracked selectedNamespace = 'namespacelonglonglong4/'; // for testing namespace selection view
@tracked noActivityDate = '';
// @tracked selectedNamespace = 'namespace18anotherlong/'; // for testing namespace selection view with mirage
get startTimeDisplay() {
if (!this.startTimeFromResponse) {
@ -73,36 +74,32 @@ export default class Dashboard extends Component {
);
}
// Determine if we have client count data based on the current tab
get hasClientData() {
if (this.args.tab === 'current') {
// Show the current numbers as long as config is on
return this.args.model.config?.enabled !== 'Off';
}
return this.args.model.activity && this.args.model.activity.total;
}
// top level TOTAL client counts from response for given date range
get runningTotals() {
if (!this.args.model.activity || !this.args.model.activity.total) {
return null;
}
return this.args.model.activity.total;
get totalUsageCounts() {
return this.selectedNamespace
? this.filterByNamespace(this.selectedNamespace)
: this.args.model.activity?.total;
}
// for horizontal bar chart in Attribution component
get topTenNamespaces() {
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
return null;
// by namespace client count data for date range
get byNamespaceActivity() {
return this.args.model.activity?.byNamespace || null;
}
// for horizontal bar chart in attribution component
get topTenChartData() {
if (this.selectedNamespace) {
let filteredNamespace = this.filterByNamespace(this.selectedNamespace);
return filteredNamespace.mounts
? this.filterByNamespace(this.selectedNamespace).mounts.slice(0, 10)
: null;
} else {
return this.byNamespaceActivity;
}
return this.args.model.activity.byNamespace;
}
get responseTimestamp() {
if (!this.args.model.activity || !this.args.model.activity.responseTimestamp) {
return null;
}
return this.args.model.activity.responseTimestamp;
return this.args.model.activity?.responseTimestamp;
}
// HELPERS
areArraysTheSame(a1, a2) {
@ -150,13 +147,15 @@ export default class Dashboard extends Component {
start_time: this.startTimeRequested,
end_time: this.endTimeRequested,
});
if (!response) {
// this.endTime will be null and use this to show EmptyState message on the template.
return;
if (response.id === 'no-data') {
// empty response is the only time we want to update the displayed date with the requested time
this.startTimeFromResponse = this.startTimeRequested;
this.noActivityDate = this.startTimeDisplay;
} else {
// note: this.startTimeDisplay (getter) is updated by this.startTimeFromResponse
this.startTimeFromResponse = response.formattedStartTime;
this.endTimeFromResponse = response.formattedEndTime;
}
// note: this.startTimeDisplay (at getter) is updated by this.startTimeFromResponse
this.startTimeFromResponse = response.formattedStartTime;
this.endTimeFromResponse = response.formattedEndTime;
// compare if the response and what you requested are the same. If they are not throw a warning.
// this only gets triggered if the data was returned, which does not happen if the user selects a startTime after for which we have data. That's an adapter error and is captured differently.
if (!this.areArraysTheSame(this.startTimeFromResponse, this.startTimeRequested)) {
@ -197,4 +196,9 @@ export default class Dashboard extends Component {
selectStartYear(year) {
this.startYear = year;
}
// HELPERS
filterByNamespace(namespace) {
return this.byNamespaceActivity.find((ns) => ns.label === namespace);
}
}

View File

@ -6,7 +6,7 @@ import { select, event, selectAll } from 'd3-selection';
import { scaleLinear, scaleBand } from 'd3-scale';
import { axisLeft } from 'd3-axis';
import { max, maxIndex } from 'd3-array';
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE } from '../../utils/chart-helpers';
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from '../../utils/chart-helpers';
import { tracked } from '@glimmer/tracking';
/**
@ -32,6 +32,7 @@ const LINE_HEIGHT = 24; // each bar w/ padding is 24 pixels thick
export default class HorizontalBarChart extends Component {
@tracked tooltipTarget = '';
@tracked tooltipText = '';
@tracked isLabel = null;
get labelKey() {
return this.args.labelKey || 'label';
@ -150,9 +151,11 @@ export default class HorizontalBarChart extends Component {
.on('mouseover', (data) => {
let hoveredElement = actionBars.filter((bar) => bar.label === data.label).node();
this.tooltipTarget = hoveredElement;
this.tooltipText = `${Math.round((data.clients * 100) / this.args.clientTotals.clients)}%
this.isLabel = false;
this.tooltipText = `${Math.round((data.clients * 100) / this.args.totalUsageCounts.clients)}%
of total client counts:
${data.entity_clients} entity clients, ${data.non_entity_clients} non-entity clients.`;
${formatTooltipNumber(data.entity_clients)} entity clients,
${formatTooltipNumber(data.non_entity_clients)} non-entity clients.`;
select(hoveredElement).style('opacity', 1);
@ -177,6 +180,7 @@ export default class HorizontalBarChart extends Component {
if (data.label.length >= CHAR_LIMIT) {
let hoveredElement = yLegendBars.filter((bar) => bar.label === data.label).node();
this.tooltipTarget = hoveredElement;
this.isLabel = true;
this.tooltipText = data.label;
} else {
this.tooltipTarget = null;

View File

@ -2,9 +2,9 @@ import Model, { attr } from '@ember-data/model';
export default class Activity extends Model {
@attr('string') responseTimestamp;
@attr('array') byNamespace;
@attr('string') endTime;
@attr('array') formattedEndTime;
@attr('array') formattedStartTime;
@attr('string') startTime;
@attr('string') endTime;
@attr('object') total;
}

View File

@ -43,8 +43,10 @@ export default Route.extend(ClusterRoute, {
},
rfc33395ToMonthYear(timestamp) {
// return [2021, 04 (e.g. 2021 March, make 0-indexed)
return [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1];
// return ['2021', 2] (e.g. 2021 March, make 0-indexed)
return timestamp
? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1]
: null;
},
async model() {
@ -57,7 +59,7 @@ export default Route.extend(ClusterRoute, {
let license = await this.getLicense(); // get default start_time
let activity = await this.getActivity(license.startTime); // returns client counts using license start_time.
let monthly = await this.getMonthly(); // returns the partial month endpoint
let endTimeFromLicense = this.rfc33395ToMonthYear(activity.endTime);
let endTimeFromResponse = activity ? this.rfc33395ToMonthYear(activity.endTime) : null;
let startTimeFromLicense = this.rfc33395ToMonthYear(license.startTime);
return hash({
@ -65,7 +67,7 @@ export default Route.extend(ClusterRoute, {
activity,
monthly,
config,
endTimeFromLicense,
endTimeFromResponse,
startTimeFromLicense,
});
},

View File

@ -1,5 +1,75 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
export default class ActivitySerializer extends ApplicationSerializer {
flattenDataset(byNamespaceArray) {
let topTen = byNamespaceArray ? byNamespaceArray.slice(0, 10) : [];
return topTen.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'];
let flattenedNs = {};
// we don't want client counts nested within the 'counts' object for stacked charts
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
flattenedNs = this.homogenizeClientNaming(flattenedNs);
// if mounts attribution unavailable, mounts will be undefined
flattenedNs.mounts = ns.mounts?.map((mount) => {
let flattenedMount = {};
flattenedMount.label = mount['path'];
Object.keys(mount['counts']).forEach((key) => (flattenedMount[key] = mount['counts'][key]));
return flattenedMount;
});
return {
label,
...flattenedNs,
};
});
}
// For 1.10 release naming changed from 'distinct_entities' to 'entity_clients' and
// 'non_entity_tokens' to 'non_entity_clients'
// accounting for deprecated API keys here and updating to latest nomenclature
homogenizeClientNaming(object) {
// TODO CMB check with API payload, latest draft includes both new and old key names
// Add else to delete old key names IF correct ones exist?
if (Object.keys(object).includes('distinct_entities', 'non_entity_tokens')) {
let entity_clients = object.distinct_entities;
let non_entity_clients = object.non_entity_tokens;
let { clients } = object;
return {
clients,
entity_clients,
non_entity_clients,
};
}
}
rfc33395ToMonthYear(timestamp) {
// return ['2021', 2] (e.g. 2021 March, make 0-indexed)
return timestamp
? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1]
: null;
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.id === 'no-data') {
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
}
let response_timestamp = formatISO(new Date());
let transformedPayload = {
...payload,
response_timestamp,
by_namespace: this.flattenDataset(payload.data.by_namespace),
total: this.homogenizeClientNaming(payload.data.total),
formatted_end_time: this.rfc33395ToMonthYear(payload.data.end_time),
formatted_start_time: this.rfc33395ToMonthYear(payload.data.start_time),
};
delete payload.data.by_namespace;
delete payload.data.total;
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);
}
}
/*
SAMPLE PAYLOAD BEFORE/AFTER:
@ -45,85 +115,3 @@ transformedPayload.by_namespace = [
},
]
*/
export default class ActivitySerializer extends ApplicationSerializer {
flattenDataset(payload) {
let topTen = payload.slice(0, 10);
return topTen.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'] || ns['namespace_id']; // TODO CMB will namespace_path ever be empty?
let flattenedNs = {};
// we don't want client counts nested within the 'counts' object for stacked charts
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
// homogenize client naming for all namespaces
if (Object.keys(flattenedNs).includes('distinct_entities', 'non_entity_tokens')) {
flattenedNs.entity_clients = flattenedNs.distinct_entities;
flattenedNs.non_entity_clients = flattenedNs.non_entity_tokens;
delete flattenedNs.distinct_entities;
delete flattenedNs.non_entity_tokens;
}
// if mounts attribution unavailable, mounts will be undefined
flattenedNs.mounts = ns.mounts?.map((mount) => {
let flattenedMount = {};
flattenedMount.label = mount['path'];
Object.keys(mount['counts']).forEach((key) => (flattenedMount[key] = mount['counts'][key]));
return flattenedMount;
});
return {
label,
...flattenedNs,
};
});
}
// TODO CMB remove and use abstracted function above
// prior to 1.10, client count key names are "distinct_entities" and "non_entity_tokens" so mapping below wouldn't work
flattenByNamespace(payload) {
// keys in the object created here must match the legend keys in dashboard.js ('entity_clients')
let topTen = payload.slice(0, 10);
return topTen.map((ns) => {
if (ns['namespace_path'] === '') ns['namespace_path'] = 'root';
// this may need to change when we have real data
// right now under months, namespaces have key value of "path" or "id", not "namespace_path"
let label = ns['namespace_path'] || ns['id'];
let namespaceMounts = ns.mounts.map((m) => {
return {
label: m['path'],
entity_clients: m['counts']['entity_clients'],
non_entity_clients: m['counts']['non_entity_clients'],
total: m['counts']['clients'],
};
});
return {
label,
entity_clients: ns['counts']['entity_clients'],
non_entity_clients: ns['counts']['non_entity_clients'],
total: ns['counts']['clients'],
mounts: namespaceMounts,
};
});
}
rfc33395ToMonthYear(timestamp) {
// return ['2021,' 04 (e.g. 2021 March, make 0-indexed)
return [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1];
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
let response_timestamp = formatISO(new Date());
let transformedPayload = {
...payload,
response_timestamp,
by_namespace: this.flattenDataset(payload.data.by_namespace),
formatted_end_time: this.rfc33395ToMonthYear(payload.data.end_time),
formatted_start_time: this.rfc33395ToMonthYear(payload.data.start_time),
};
delete payload.data.by_namespace;
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);
}
}

View File

@ -1,9 +1,8 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
export default class MonthlySerializer extends ApplicationSerializer {
flattenDataset(payload) {
let topTen = payload ? payload.slice(0, 10) : [];
flattenDataset(byNamespaceArray) {
let topTen = byNamespaceArray ? byNamespaceArray.slice(0, 10) : [];
return topTen.map((ns) => {
// 'namespace_path' is an empty string for root
@ -13,13 +12,7 @@ export default class MonthlySerializer extends ApplicationSerializer {
// we don't want client counts nested within the 'counts' object for stacked charts
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
// homogenize client naming for all namespaces
if (Object.keys(flattenedNs).includes('distinct_entities', 'non_entity_tokens')) {
flattenedNs.entity_clients = flattenedNs.distinct_entities;
flattenedNs.non_entity_clients = flattenedNs.non_entity_tokens;
delete flattenedNs.distinct_entities;
delete flattenedNs.non_entity_tokens;
}
flattenedNs = this.homogenizeClientNaming(flattenedNs);
// if mounts attribution unavailable, mounts will be undefined
flattenedNs.mounts = ns.mounts?.map((mount) => {
@ -36,20 +29,32 @@ export default class MonthlySerializer extends ApplicationSerializer {
});
}
// For 1.10 release naming changed from 'distinct_entities' to 'entity_clients' and
// 'non_entity_tokens' to 'non_entity_clients'
// accounting for deprecated API keys here and updating to latest nomenclature
homogenizeClientNaming(object) {
// TODO CMB check with API payload, latest draft includes both new and old key names
// Add else to delete old key names IF correct ones exist?
if (Object.keys(object).includes('distinct_entities', 'non_entity_tokens')) {
let entity_clients = object.distinct_entities;
let non_entity_clients = object.non_entity_tokens;
let { clients } = object;
return {
clients,
entity_clients,
non_entity_clients,
};
}
}
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
let { data } = payload;
let { clients, distinct_entities, non_entity_tokens } = data;
let response_timestamp = formatISO(new Date());
let transformedPayload = {
...payload,
response_timestamp,
by_namespace: this.flattenDataset(data.by_namespace),
by_namespace: this.flattenDataset(payload.data.by_namespace),
// nest within 'total' object to mimic /activity response shape
total: {
clients,
entityClients: distinct_entities,
nonEntityClients: non_entity_tokens,
},
total: this.homogenizeClientNaming(payload.data),
};
delete payload.data.by_namespace;
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);

View File

@ -0,0 +1,25 @@
import Service from '@ember/service';
// SAMPLE CSV FORMAT ('content' argument)
// Must be a string with each row \n separated and each column comma separated
// 'Namespace path,Authentication method,Total clients,Entity clients,Non-entity clients\n
// namespacelonglonglong4/,,191,171,20\n
// namespacelonglonglong4/,auth/method/uMGBU,35,20,15\n'
export default class DownloadCsvService extends Service {
download(filename, content) {
let formattedFilename = filename?.replace(/\s+/g, '-') || 'vault-data.csv';
let { document, URL } = window;
let downloadElement = document.createElement('a');
downloadElement.download = formattedFilename;
downloadElement.href = URL.createObjectURL(
new Blob([content], {
type: 'text/csv',
})
);
document.body.appendChild(downloadElement);
downloadElement.click();
URL.revokeObjectURL(downloadElement.href);
downloadElement.remove();
}
}

View File

@ -9,14 +9,7 @@ $dark-gray: #535f73;
}
}
.calendar-title {
color: $ui-gray-300;
text-transform: uppercase;
font-size: $size-7;
font-weight: $font-weight-semibold;
&.popup-menu-item {
padding: $size-10 $size-8;
}
padding: $size-10 $size-8;
}
.calendar-widget-dropdown {
@extend .button;

View File

@ -2,4 +2,7 @@
color: $link;
text-decoration: none;
font-weight: $font-weight-semibold;
&:hover {
text-decoration: underline !important;
}
}

View File

@ -24,10 +24,6 @@
margin: 0;
}
.is-subtitle-gray {
color: $ui-gray-500;
}
.copy-text {
background-color: $grey-lightest;
padding: $spacing-s;

View File

@ -54,11 +54,6 @@
border-color: darken($ui-gray-300, 5%);
}
}
> a {
&:hover {
text-decoration: underline;
}
}
}
}
@ -224,27 +219,28 @@ p.data-details {
font-size: $size-9;
padding: 6px;
border-radius: $radius-large;
width: 140px;
.bold {
font-weight: $font-weight-bold;
}
.line-chart {
width: 117px;
}
.vertical-chart {
text-align: center;
flex-wrap: nowrap;
width: fit-content;
}
.horizontal-chart {
width: 200px;
padding: $spacing-s;
}
}
.is-label-fit-content {
max-width: fit-content !important;
}
.chart-tooltip-arrow {
width: 0;
height: 0;

View File

@ -16,3 +16,9 @@
.form-section .title {
margin-bottom: $spacing-s;
}
.is-subtitle-gray {
text-transform: uppercase;
font-size: $size-7;
color: $ui-gray-500;
}

View File

@ -11,7 +11,7 @@
</D.Trigger>
<D.Content class="popup-menu-content calendar-content">
<nav class="box menu">
<div class="calendar-title popup-menu-item">Date options</div>
<div class="calendar-title is-subtitle-gray">DATE OPTIONS</div>
<ul class="menu-list">
<li class="action">
<button

View File

@ -13,40 +13,49 @@
</div>
</div>
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@chartLegend={{@chartLegend}}
@clientTotals={{@runningTotals}}
/>
</div>
{{#if (eq @totalUsageCounts.clients 0)}}
<div class="chart-empty-state">
<EmptyState
@title="No data received"
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
/>
</div>
{{else}}
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.totalClientsData}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
/>
</div>
<div class="chart-subTitle">
<p class="chart-subtext">{{this.chartText.totalCopy}}</p>
</div>
<div class="chart-subTitle">
<p class="chart-subtext">{{this.chartText.totalCopy}}</p>
</div>
<div class="data-details-top">
<h3 class="data-details">Top {{lowercase this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-top">
<h3 class="data-details">Top {{this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-bottom">
<h3 class="data-details">Clients in {{lowercase this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.clients}}</p>
</div>
<div class="data-details-bottom">
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
</div>
<div class="timestamp">
Updated
{{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}}
</div>
<div class="timestamp">
Updated
{{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}}
</div>
<div class="legend-center">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div>
<div class="legend-center">
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
</div>
{{/if}}
</div>
{{! MODAL FOR CSV DOWNLOAD BUTTON }}
{{! MODAL FOR CSV DOWNLOAD }}
<Modal
@title="Export attribution data"
@type="info"
@ -63,7 +72,13 @@
<p class="has-bottom-margin-s">{{@startTimeDisplay}} {{if @endTimeDisplay "-"}} {{@endTimeDisplay}}</p>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<DownloadCsv @label="Export" @csvData={{this.getCsvData}} @fileName={{this.getCsvFileName}} />
<button
type="button"
class="button is-primary"
{{on "click" (fn this.exportChartData this.getCsvFileName this.getCsvData)}}
>
Export
</button>
<button type="button" class="button is-secondary" onclick={{action (mut this.showCSVDownloadModal) false}}>
Cancel
</button>

View File

@ -1,6 +1,6 @@
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
<p class="has-bottom-margin-s">
The below data is for the current month starting from the first day. For historical data, see the monthly history tab.
The below data is for the current month starting from the first day. For historical data, see the history tab.
</p>
{{#if (eq @model.config.enabled "Off")}}
<EmptyState
@ -13,21 +13,48 @@
</LinkTo>
{{/if}}
</EmptyState>
{{/if}}
{{#if @isLoading}}
<LayoutLoading />
{{else}}
<Clients::UsageStats @title={{date-format this.responseTimestamp "MMMM"}} @runningTotals={{this.runningTotals}} />
<Clients::Attribution
@chartLegend={{this.chartLegend}}
@topTenNamespaces={{this.topTenNamespaces}}
@runningTotals={{this.runningTotals}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
@isDateRange={{false}}
@timestamp={{this.responseTimestamp}}
/>
{{#if this.topTenChartData}}
<div class="is-subtitle-gray has-bottom-margin-m">
FILTERS
<Toolbar>
<ToolbarFilters>
{{! ARG TODO more filters for namespace here }}
</ToolbarFilters>
</Toolbar>
</div>
{{/if}}
{{! TODO CMB COMMENT IN CONDITIONAL WHEN VERSION ENDPOINT IS COMPLETE }}
{{!-- {{#if (is-after this.upgradeDate this.licenseStartDate)}} --}}
<AlertBanner @type="warning" @title="Warning">
{{#if this.upgradeDate}}
{{concat "You upgraded to Vault 1.9 on " (date-format this.upgradeDate "MMMM d, yyyy.")}}
{{/if}}
How we count clients changed in 1.9, so please keep in mind when looking at the data below. Furthermore, namespace
attribution is available only for 1.9 data.
<DocLink @path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts">
Learn more here.
</DocLink>
</AlertBanner>
{{!-- {{/if}} --}}
{{#if @isLoading}}
<LayoutLoading />
{{else}}
<Clients::UsageStats
@title={{date-format this.responseTimestamp "MMMM"}}
@totalUsageCounts={{this.totalUsageCounts}}
/>
{{#if this.topTenChartData}}
<Clients::Attribution
@chartLegend={{this.chartLegend}}
@totalClientsData={{this.topTenChartData}}
@totalUsageCounts={{this.totalUsageCounts}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
@isDateRange={{false}}
@timestamp={{this.responseTimestamp}}
/>
{{/if}}
{{/if}}
{{/if}}
</div>

View File

@ -1,58 +1,56 @@
{{#if (and (eq @tab "dashboard") (eq @model.config.queriesAvailable false))}}
{{#if (eq @model.config.enabled "On")}}
<EmptyState
@title="No monthly history"
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
/>
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
<p class="has-bottom-margin-xl">
This data is presented by full month. If there is data missing, its possible that tracking was turned off at the time.
Vault will only show data for contiguous blocks of time during which tracking was on. Documentation is available
<DocLink @path="/docs/concepts/client-count">here</DocLink>.
</p>
<h1 data-test-client-count-title class="title is-6 has-bottom-margin-xs">
Billing start month
</h1>
<div class="is-flex-align-baseline">
<p class="is-size-6">{{this.startTimeDisplay}}</p>
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
Edit
</button>
</div>
<p class="is-8 has-text-grey has-bottom-margin-xl">
This date comes from your license, and defines when client counting starts. Without this starting point, the data shown
is not reliable.
</p>
{{#if (eq @model.config.queriesAvailable false)}}
{{#if (eq @model.config.enabled "On")}}
<EmptyState
@title={{concat "No monthly history " (if this.noActivityDate "from ") this.noActivityDate}}
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
/>
{{else}}
<EmptyState
@title="Data tracking is disabled"
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
>
{{#if @model.config.configPath.canUpdate}}
<p>
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="config"}}>
Go to configuration
</LinkTo>
</p>
{{/if}}
</EmptyState>
{{/if}}
{{else}}
<EmptyState
@title="Data tracking is disabled"
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
>
{{#if @model.config.configPath.canUpdate}}
<p>
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="config"}}>
Go to configuration
</LinkTo>
</p>
{{/if}}
</EmptyState>
{{/if}}
{{else}}
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
{{#if (eq @tab "dashboard")}}
<p class="has-bottom-margin-xl">
{{! ARG TODO Add link for documentation "here" }}
This dashboard will surface Vault client usage over time. Clients represent anything that has authenticated to or
communicated with Vault. Documentation is available here.
</p>
{{! Calendar widget and Start Month picker }}
<h1 data-test-client-count-title class="title is-6 has-bottom-margin-xs">
Billing start month
</h1>
<div class="is-flex-align-baseline">
<p class="is-size-6">{{this.startTimeDisplay}}</p>
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
Edit
</button>
</div>
<p class="is-8 has-text-grey has-bottom-margin-xl">
This date comes from your license, and defines when client counting starts. Without this starting point, the data
shown is not reliable.
</p>
{{#if (eq @model.config.enabled "Off")}}
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need
to
<LinkTo @route="vault.cluster.clients.edit">
edit the configuration
</LinkTo>
to enable tracking again.
</AlertBanner>
{{/if}}
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
<div class="calendar-title">Filters</div>
{{#if (eq @model.config.enabled "Off")}}
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will need to
<LinkTo @route="vault.cluster.clients.edit">
edit the configuration
</LinkTo>
to enable tracking again.
</AlertBanner>
{{/if}}
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
<div class="is-subtitle-gray has-bottom-margin-m">
FILTERS
<Toolbar>
<ToolbarFilters>
<CalendarWidget
@ -63,127 +61,145 @@
@handleCurrentBillingPeriod={{this.handleCurrentBillingPeriod}}
@startTimeDisplay={{this.startTimeDisplay}}
/>
{{! ARG TODO more filters for namespace here }}
{{#if this.topTenChartData}}
{{! ARG TODO more filters for namespace here }}
{{/if}}
</ToolbarFilters>
</Toolbar>
{{#if this.responseRangeDiffMessage}}
<AlertBanner @type="warning" @class="has-top-margin-s" @message={{this.responseRangeDiffMessage}} />
{{/if}}
{{#if @isLoading}}
<LayoutLoading />
{{else if this.topTenNamespaces}}
{{! TODO make conditional above more apt }}
</div>
<AlertBanner @type="warning" @title="Warning">
<ul>
{{#if this.responseRangeDiffMessage}}
<li>{{this.responseRangeDiffMessage}}</li>
{{/if}}
{{! COMMENT IN CONDITIONAL WHEN VERSION ENDPOINT IS COMPLETE }}
{{!-- {{#if (is-after this.upgradeDate this.licenseStartDate)}} --}}
<li>
{{#if this.upgradeDate}}
{{concat "You upgraded to Vault 1.9 on " (date-format this.upgradeDate "MMMM d, yyyy.")}}
{{/if}}
How we count clients changed in 1.9, so please keep in mind when looking at the data below. Furthermore,
namespace attribution is available only for 1.9 data.
<DocLink @path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts">
Learn more here.
</DocLink>
</li>
{{!-- {{/if}} --}}
</ul>
</AlertBanner>
{{#if @isLoading}}
<LayoutLoading />
{{else}}
<Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} />
{{#if this.topTenChartData}}
<Clients::Attribution
@chartLegend={{this.chartLegend}}
@topTenNamespaces={{this.topTenNamespaces}}
@runningTotals={{this.runningTotals}}
@totalClientsData={{this.topTenChartData}}
@totalUsageCounts={{this.totalUsageCounts}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{this.startTimeDisplay}}
@endTimeDisplay={{this.endTimeDisplay}}
@isDateRange={{this.isDateRange}}
@timestamp={{this.responseTimestamp}}
/>
{{! If no endTime that means the counters/activity request did not return a payload. }}
{{else if this.endTime}}
<EmptyState
@title="No counter activity data"
@message="There is no data in the activity data yet. We collect it at the end of each month, so your data will be available on the first of next month."
/>
{{else}}
<EmptyState @title="Coming soon" @message="Under construction for the 1.10 binary." />
{{/if}}
{{/if}}
{{! Modal for startTime picker }}
<Modal
@title="Edit start month"
@onClose={{action (mut this.isEditStartMonthOpen) false}}
@isActive={{this.isEditStartMonthOpen}}
@showCloseButton={{true}}
>
<section class="modal-card-body">
<p class="has-bottom-margin-s">
This date comes from your license, and defines when client counting starts. Without this starting point, the data
shown is not reliable.
</p>
<p class="has-bottom-margin-s"><strong>Billing contract start month</strong></p>
<div class="modal-radio-button">
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
{{or this.startMonth "Month"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content class="popup-menu-content is-wide">
<nav class="box menu scroll">
<ul class="menu-list">
{{#each this.months as |month|}}
<button
type="button"
class="link"
{{on "click" (queue (fn this.selectStartMonth month) (action D.actions.close))}}
>
{{month}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
{{or this.startYear "Year"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content class="popup-menu-content is-wide">
<nav class="box menu">
<ul class="menu-list">
{{#each this.years as |year|}}
<button
type="button"
class="link"
{{on "click" (queue (fn this.selectStartYear year) (action D.actions.close))}}
>
{{year}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
</div>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<button
type="button"
class="button is-primary"
onclick={{queue
(action "handleClientActivityQuery" this.startMonth this.startYear "startTime")
(action (mut this.isEditStartMonthOpen) false)
}}
disabled={{if (and this.startMonth this.startYear) false true}}
>
Save
</button>
<button
type="button"
class="button is-secondary"
{{on
"click"
(queue (fn this.handleClientActivityQuery 0 0 "cancel") (action (mut this.isEditStartMonthOpen) false))
}}
>
Cancel
</button>
</footer>
</Modal>
{{else}}
<EmptyState
@title="No billing start date found"
@message="In order to get the most from this data, please enter your billing period start month. This will ensure that the resulting data is accurate."
/>
{{/if}}
</div>
{{/if}}
{{/if}}
{{! BILLING START DATE MODAL }}
<Modal
@title="Edit start month"
@onClose={{action (mut this.isEditStartMonthOpen) false}}
@isActive={{this.isEditStartMonthOpen}}
@showCloseButton={{true}}
>
<section class="modal-card-body">
<p class="has-bottom-margin-s">
This date comes from your license, and defines when client counting starts. Without this starting point, the data
shown is not reliable.
</p>
<p class="has-bottom-margin-s"><strong>Billing contract start month</strong></p>
<div class="modal-radio-button">
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
{{or this.startMonth "Month"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content class="popup-menu-content is-wide">
<nav class="box menu scroll">
<ul class="menu-list">
{{#each this.months as |month|}}
<button
type="button"
class="link"
{{on "click" (queue (fn this.selectStartMonth month) (action D.actions.close))}}
>
{{month}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
<D.Trigger
data-test-popup-menu-trigger="true"
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
@htmlTag="button"
>
{{or this.startYear "Year"}}
<Chevron @direction="down" @isButton={{true}} />
</D.Trigger>
<D.Content class="popup-menu-content is-wide">
<nav class="box menu">
<ul class="menu-list">
{{#each this.years as |year|}}
<button
type="button"
class="link"
{{on "click" (queue (fn this.selectStartYear year) (action D.actions.close))}}
>
{{year}}
</button>
{{/each}}
</ul>
</nav>
</D.Content>
</BasicDropdown>
</div>
</section>
<footer class="modal-card-foot modal-card-foot-outlined">
<button
type="button"
class="button is-primary"
onclick={{queue
(action "handleClientActivityQuery" this.startMonth this.startYear "startTime")
(action (mut this.isEditStartMonthOpen) false)
}}
disabled={{if (and this.startMonth this.startYear) false true}}
>
Save
</button>
<button
type="button"
class="button is-secondary"
{{on
"click"
(queue (fn this.handleClientActivityQuery 0 0 "cancel") (action (mut this.isEditStartMonthOpen) false))
}}
>
Cancel
</button>
</footer>
</Modal>
</div>

View File

@ -17,7 +17,7 @@
attachment="bottom middle"
offset="35px 0"
}}
<div class="chart-tooltip horizontal-chart">
<div class={{concat "chart-tooltip horizontal-chart " (if this.isLabel "is-label-fit-content")}}>
<p>{{this.tooltipText}}</p>
</div>
<div class="chart-tooltip-arrow"></div>

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 794 B

View File

@ -15,7 +15,7 @@
<div class="column" data-test-client-count-stats>
<StatText
@label="Total clients"
@value={{or @runningTotals.clients "0"}}
@value={{or @totalUsageCounts.clients "0"}}
@size="l"
@subText="The sum of entity clientIDs and non-entity clientIDs; this is Vaults primary billing metric."
/>
@ -24,7 +24,7 @@
<StatText
class="column"
@label="Entity clients"
@value={{or @runningTotals.entityClients "0"}}
@value={{or @totalUsageCounts.entity_clients "0"}}
@size="l"
@subText="Representations of a particular user, client, or application that created a token via login."
/>
@ -33,7 +33,7 @@
<StatText
class="column"
@label="Non-entity clients"
@value={{or @runningTotals.nonEntityClients "0"}}
@value={{or @totalUsageCounts.non_entity_clients "0"}}
@size="l"
@subText="Clients created with a shared set of permissions, but not associated with an entity. "
/>

View File

@ -27,7 +27,7 @@
</LinkTo>
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="dashboard"}} @tagName="li" @activeClass="is-active">
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab="dashboard"}} data-test-usage-tab={{true}}>
Monthly history
History
</LinkTo>
</LinkTo>
{{#if this.model.config.configPath.canRead}}

View File

@ -14,3 +14,11 @@ export function formatNumbers(number) {
// replace SI prefix of 'G' for billions to 'B'
return format('.1s')(number).replace('G', 'B');
}
export function formatTooltipNumber(value) {
if (typeof value !== 'number') {
return value;
}
// formats a number according to the locale
return new Intl.NumberFormat().format(value);
}

View File

@ -1,31 +0,0 @@
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

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

View File

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

View File

@ -49,41 +49,42 @@ module('Integration | Component | client count history', function (hooks) {
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
});
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,
non_entity_tokens: 232,
},
});
// TODO CMB fix
// 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,
// non_entity_tokens: 232,
// },
// });
await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
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');
});
// await render(hbs`<Clients::History @tab={{this.tab}} @model={{this.model}} />`);
// 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');
// });
});