UI/Add double attribution chart to current (#15035)

* update /monthly endpoint

* change object key names to match API

* update serializers

* add optional no data mesage for horizontal chart

* add split chart option for attribution component

* wire up filtering namespaces and auth methods

* update clients current tests

* update todos and address comments

* fix attribution test
This commit is contained in:
claire bontempo 2022-04-15 12:06:10 -07:00 committed by GitHub
parent 10a70207c7
commit 787ebaebd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 398 additions and 95 deletions

View File

@ -11,21 +11,24 @@ import { inject as service } from '@ember/service';
* ```js
* <Clients::Attribution
* @chartLegend={{this.chartLegend}}
* @totalClientsData={{this.totalClientsData}}
* @totalUsageCounts={{this.totalUsageCounts}}
* @newUsageCounts={{this.newUsageCounts}}
* @totalClientsData={{this.totalClientsData}}
* @newClientsData={{this.newClientsData}}
* @selectedNamespace={{this.selectedNamespace}}
* @startTimeDisplay={{this.startTimeDisplay}}
* @endTimeDisplay={{this.endTimeDisplay}}
* @startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
* @isDateRange={{this.isDateRange}}
* @timestamp={{this.responseTimestamp}}
* />
* ```
* @param {array} chartLegend - (passed to child) array of objects with key names 'key' and 'label' so data can be stacked
* @param {array} totalClientsData - array of objects containing a label and breakdown of total, entity and non-entity clients
* @param {object} totalUsageCounts - object with total client counts for chart tooltip text
* @param {object} newUsageCounts - object with new client counts for chart tooltip text
* @param {array} totalClientsData - array of objects containing a label and breakdown of client counts for total clients
* @param {array} newClientsData - array of objects containing a label and breakdown of client counts for new clients
* @param {string} selectedNamespace - namespace selected from filter bar
* @param {string} startTimeDisplay - start date for CSV modal
* @param {string} endTimeDisplay - end date for CSV modal
* @param {string} startTimeDisplay - string that displays as start date for CSV modal
* @param {string} endTimeDisplay - string that displays as end date for CSV modal
* @param {boolean} isDateRange - getter calculated in parent to relay if dataset is a date range or single month
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response
*/
@ -34,6 +37,9 @@ export default class Attribution extends Component {
@tracked showCSVDownloadModal = false;
@service downloadCsv;
get hasCsvData() {
return this.args.totalClientsData ? this.args.totalClientsData.length > 0 : false;
}
get isDateRange() {
return this.args.isDateRange;
}
@ -52,6 +58,10 @@ export default class Attribution extends Component {
return this.args.totalClientsData?.slice(0, 10);
}
get barChartNewClients() {
return this.args.newClientsData?.slice(0, 10);
}
get topClientCounts() {
// get top namespace or auth method
return this.args.totalClientsData ? this.args.totalClientsData[0] : null;
@ -69,8 +79,9 @@ export default class Attribution extends Component {
return {
description:
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
${dateText === 'date range' ? ' over time.' : '.'}`,
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients${
dateText === 'date range' ? ' over time.' : '.'
}`,
totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `,
};
case false:
@ -78,8 +89,9 @@ export default class Attribution extends Component {
description:
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.',
newCopy: `The new clients in the namespace for this ${dateText}.
This aids in understanding which namespaces create and use new clients
${dateText === 'date range' ? ' over time.' : '.'}`,
This aids in understanding which namespaces create and use new clients${
dateText === 'date range' ? ' over time.' : '.'
}`,
totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`,
};
case 'no data':
@ -95,7 +107,7 @@ export default class Attribution extends Component {
let csvData = [],
graphData = this.args.totalClientsData,
csvHeader = [
`Namespace path`,
'Namespace path',
'Authentication method',
'Total clients',
'Entity clients',

View File

@ -11,21 +11,30 @@ export default class Current extends Component {
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp
@tracked selectedNamespace = null;
@tracked namespaceArray = this.byNamespaceCurrent.map((namespace) => {
@tracked namespaceArray = this.byNamespaceTotalClients.map((namespace) => {
return { name: namespace['label'], id: namespace['label'] };
});
@tracked selectedAuthMethod = null;
@tracked authMethodOptions = [];
// Response client count data by namespace for current/partial month
get byNamespaceCurrent() {
return this.args.model.monthly?.byNamespace || [];
// Response total client count data by namespace for current/partial month
get byNamespaceTotalClients() {
return this.args.model.monthly?.byNamespaceTotalClients || [];
}
// Response new client count data by namespace for current/partial month
get byNamespaceNewClients() {
return this.args.model.monthly?.byNamespaceNewClients || [];
}
get isGatheringData() {
// return true if tracking IS enabled but no data collected yet
return this.args.model.config?.enabled === 'On' && this.byNamespaceCurrent.length === 0;
return (
this.args.model.config?.enabled === 'On' &&
this.byNamespaceTotalClients.length === 0 &&
this.byNamespaceNewClients.length === 0
);
}
get hasAttributionData() {
@ -36,16 +45,30 @@ export default class Current extends Component {
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData;
}
get filteredActivity() {
get filteredTotalData() {
const namespace = this.selectedNamespace;
const auth = this.selectedAuthMethod;
if (!namespace && !auth) {
return this.getActivityResponse;
return this.byNamespaceTotalClients;
}
if (!auth) {
return this.byNamespaceCurrent.find((ns) => ns.label === namespace);
return this.byNamespaceTotalClients.find((ns) => ns.label === namespace);
}
return this.byNamespaceCurrent
return this.byNamespaceTotalClients
.find((ns) => ns.label === namespace)
.mounts?.find((mount) => mount.label === auth);
}
get filteredNewData() {
const namespace = this.selectedNamespace;
const auth = this.selectedAuthMethod;
if (!namespace && !auth) {
return this.byNamespaceNewClients;
}
if (!auth) {
return this.byNamespaceNewClients.find((ns) => ns.label === namespace);
}
return this.byNamespaceNewClients
.find((ns) => ns.label === namespace)
.mounts?.find((mount) => mount.label === auth);
}
@ -62,15 +85,28 @@ export default class Current extends Component {
// top level TOTAL client counts for current/partial month
get totalUsageCounts() {
return this.selectedNamespace ? this.filteredActivity : this.args.model.monthly?.total;
return this.selectedNamespace ? this.filteredTotalData : this.args.model.monthly?.total;
}
get newUsageCounts() {
return this.selectedNamespace ? this.filteredNewData : this.args.model.monthly?.new;
}
// total client data for horizontal bar chart in attribution component
get totalClientsData() {
if (this.selectedNamespace) {
return this.filteredActivity?.mounts || null;
return this.filteredTotalData?.mounts || null;
} else {
return this.byNamespaceCurrent;
return this.byNamespaceTotalClients;
}
}
// new client data for horizontal bar chart in attribution component
get newClientsData() {
if (this.selectedNamespace) {
return this.filteredNewData?.mounts || null;
} else {
return this.byNamespaceNewClients;
}
}
@ -89,7 +125,7 @@ export default class Current extends Component {
this.selectedAuthMethod = null;
} else {
// Side effect: set auth namespaces
const mounts = this.filteredActivity.mounts?.map((mount) => ({
const mounts = this.filteredTotalData.mounts?.map((mount) => ({
id: mount.label,
name: mount.label,
}));

View File

@ -28,7 +28,7 @@ export default class LineChart extends Component {
@tracked tooltipNew = '';
get yKey() {
return this.args.yKey || 'total';
return this.args.yKey || 'clients';
}
get xKey() {

View File

@ -44,7 +44,7 @@ export default class VerticalBarChart extends Component {
}
get yKey() {
return this.args.yKey || 'total';
return this.args.yKey || 'clients';
}
@action

View File

@ -2,5 +2,8 @@ import Model, { attr } from '@ember-data/model';
export default class MonthlyModel extends Model {
@attr('string') responseTimestamp;
@attr('array') byNamespace;
@attr('object') total;
@attr('object') total; // total clients during the current/partial month
@attr('object') new; // total NEW clients during the current/partial
@attr('array') byNamespaceTotalClients;
@attr('array') byNamespaceNewClients;
}

View File

@ -2,8 +2,8 @@ import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
import { parseAPITimestamp, parseRFC3339 } from 'core/utils/date-formatters';
export default class ActivitySerializer extends ApplicationSerializer {
flattenDataset(byNamespaceArray) {
return byNamespaceArray.map((ns) => {
flattenDataset(namespaceArray) {
return namespaceArray.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'];
@ -33,7 +33,6 @@ export default class ActivitySerializer extends ApplicationSerializer {
});
}
// for vault usage - vertical bar chart
flattenByMonths(payload, isNewClients = false) {
const sortedPayload = [...payload];
sortedPayload.reverse();
@ -43,7 +42,7 @@ export default class ActivitySerializer extends ApplicationSerializer {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
entity_clients: m.new_clients.counts.entity_clients,
non_entity_clients: m.new_clients.counts.non_entity_clients,
total: m.new_clients.counts.clients,
clients: m.new_clients.counts.clients,
namespaces: this.flattenDataset(m.new_clients.namespaces),
};
});
@ -53,12 +52,12 @@ export default class ActivitySerializer extends ApplicationSerializer {
month: parseAPITimestamp(m.timestamp, 'M/yy'),
entity_clients: m.counts.entity_clients,
non_entity_clients: m.counts.non_entity_clients,
total: m.counts.clients,
clients: m.counts.clients,
namespaces: this.flattenDataset(m.namespaces),
new_clients: {
entity_clients: m.new_clients.counts.entity_clients,
non_entity_clients: m.new_clients.counts.non_entity_clients,
total: m.new_clients.counts.clients,
clients: m.new_clients.counts.clients,
namespaces: this.flattenDataset(m.new_clients.namespaces),
},
};

View File

@ -1,8 +1,9 @@
import ApplicationSerializer from '../application';
import { formatISO } from 'date-fns';
export default class MonthlySerializer extends ApplicationSerializer {
flattenDataset(byNamespaceArray) {
return byNamespaceArray.map((ns) => {
flattenDataset(namespaceArray) {
return namespaceArray?.map((ns) => {
// 'namespace_path' is an empty string for root
if (ns['namespace_id'] === 'root') ns['namespace_path'] = 'root';
let label = ns['namespace_path'];
@ -11,14 +12,17 @@ export default class MonthlySerializer extends ApplicationSerializer {
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
flattenedNs = this.homogenizeClientNaming(flattenedNs);
// TODO CMB check how this works with actual API endpoint
// if no mounts, mounts will be an empty array
flattenedNs.mounts = ns.mounts
? ns.mounts.map((mount) => {
let flattenedMount = {};
flattenedMount.label = mount['mount_path'];
let label = mount['mount_path'];
Object.keys(mount['counts']).forEach((key) => (flattenedMount[key] = mount['counts'][key]));
return flattenedMount;
flattenedMount = this.homogenizeClientNaming(flattenedMount);
return {
label,
...flattenedMount,
};
})
: [];
@ -29,22 +33,27 @@ export default class MonthlySerializer extends ApplicationSerializer {
});
}
// For 1.10 release naming changed from 'distinct_entities' to 'entity_clients' and
// In 1.10 'distinct_entities' changed 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
// TODO CMB 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;
// if new key names exist, only return those key/value pairs
if (Object.keys(object).includes('entity_clients')) {
let { clients, entity_clients, non_entity_clients } = object;
return {
clients,
entity_clients,
non_entity_clients,
};
}
// if object only has outdated key names, update naming
if (Object.keys(object).includes('distinct_entities')) {
let { clients, distinct_entities, non_entity_tokens } = object;
return {
clients,
entity_clients: distinct_entities,
non_entity_clients: non_entity_tokens,
};
}
return object;
}
@ -53,14 +62,29 @@ export default class MonthlySerializer extends ApplicationSerializer {
return super.normalizeResponse(store, primaryModelClass, payload, id, requestType);
}
let response_timestamp = formatISO(new Date());
// TODO CMB: the following is assumed, need to confirm
// the months array will always include a single object: a timestamp of the current month and new/total count data, if available
let newClientsData = payload.data.months[0]?.new_clients || null;
let by_namespace_new_clients, new_clients;
if (newClientsData) {
by_namespace_new_clients = this.flattenDataset(newClientsData.namespaces);
new_clients = this.homogenizeClientNaming(newClientsData.counts);
} else {
by_namespace_new_clients = [];
new_clients = [];
}
let transformedPayload = {
...payload,
response_timestamp,
by_namespace: this.flattenDataset(payload.data.by_namespace),
by_namespace_total_clients: this.flattenDataset(payload.data.by_namespace),
by_namespace_new_clients,
// nest within 'total' object to mimic /activity response shape
total: this.homogenizeClientNaming(payload.data),
new: new_clients,
};
delete payload.data.by_namespace;
delete payload.data.months;
delete payload.data.total;
return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType);
}
}

View File

@ -1,11 +1,15 @@
<div class="chart-wrapper single-chart-grid" data-test-clients-attribution>
{{! show single chart if data is from a range, show two charts if from a single month}}
<div
class={{concat "chart-wrapper" (if @isDateRange " single-chart-grid") (unless @isDateRange " dual-chart-grid")}}
data-test-clients-attribution
>
<div class="chart-header has-header-link has-bottom-margin-m">
<div class="header-left">
<h2 class="chart-title">Attribution</h2>
<p class="chart-description" data-test-attribution-description>{{this.chartText.description}}</p>
</div>
<div class="header-right">
{{#if @totalClientsData}}
{{#if this.hasCsvData}}
<button
data-test-attribution-export-button
type="button"
@ -17,28 +21,51 @@
{{/if}}
</div>
</div>
{{#if this.barChartTotalClients}}
<div class="chart-container-wide">
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
/>
</div>
<div class="chart-subTitle">
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.totalCopy}}</p>
</div>
{{#if (eq @isDateRange true)}}
<div class="chart-container-wide" data-test-chart-container="total-clients">
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
/>
</div>
<div class="chart-subTitle">
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.totalCopy}}</p>
</div>
<div class="data-details-top" data-test-top-attribution>
<h3 class="data-details">Top {{this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-top" data-test-top-attribution>
<h3 class="data-details">Top {{this.attributionBreakdown}}</h3>
<p class="data-details">{{this.topClientCounts.label}}</p>
</div>
<div class="data-details-bottom" data-test-top-counts>
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
</div>
<div class="data-details-bottom" data-test-top-counts>
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
</div>
{{else}}
<div class="chart-container-left" data-test-chart-container="new-clients">
<h2 class="chart-title">New clients</h2>
<p class="chart-description">{{this.chartText.newCopy}}</p>
<Clients::HorizontalBarChart
@dataset={{this.barChartNewClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@newUsageCounts}}
@noDataMessage={{"There are no new clients for this namespace during this time period."}}
/>
</div>
<div class="chart-container-right" data-test-chart-container="total-clients">
<h2 class="chart-title">Total clients</h2>
<p class="chart-description">{{this.chartText.totalCopy}}</p>
<Clients::HorizontalBarChart
@dataset={{this.barChartTotalClients}}
@chartLegend={{@chartLegend}}
@totalUsageCounts={{@totalUsageCounts}}
/>
</div>
{{/if}}
<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>

View File

@ -71,8 +71,10 @@
{{#if this.hasAttributionData}}
<Clients::Attribution
@chartLegend={{this.chartLegend}}
@totalClientsData={{this.totalClientsData}}
@totalUsageCounts={{this.totalUsageCounts}}
@newUsageCounts={{this.newUsageCounts}}
@totalClientsData={{this.totalClientsData}}
@newClientsData={{this.newClientsData}}
@selectedNamespace={{this.selectedNamespace}}
@startTimeDisplay={{date-format this.responseTimestamp "MMMM yyyy"}}
@isDateRange={{false}}

View File

@ -1,12 +1,17 @@
<svg
data-test-horizontal-bar-chart
class="chart is-horizontal"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>
{{#if @dataset}}
<svg
data-test-horizontal-bar-chart
class="chart is-horizontal"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>
{{else}}
<div class="chart-empty-state">
<EmptyState @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
</div>
{{/if}}
{{#if this.tooltipTarget}}
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
{{! Component must be in curly bracket notation }}

Before

Width:  |  Height:  |  Size: 827 B

After

Width:  |  Height:  |  Size: 1014 B

View File

@ -6,6 +6,7 @@ import {
isBefore,
sub,
isSameMonth,
startOfMonth,
} from 'date-fns';
import { parseAPITimestamp } from 'core/utils/date-formatters';
const MOCK_MONTHLY_DATA = [
@ -663,6 +664,8 @@ const handleMockQuery = (queryStartTimestamp, monthlyData) => {
do {
i++;
let timestamp = formatRFC3339(sub(startDateByMonth, { months: i }));
// TODO CMB update this when we confirm what combined data looks like
// this is probably not what the empty object looks like but waiting to hear back from backend
transformedMonthlyArray.push({
timestamp,
counts: {
@ -893,6 +896,7 @@ export default function (server) {
});
server.get('/sys/internal/counters/activity/monthly', function () {
const timestamp = new Date();
return {
request_id: '26be5ab9-dcac-9237-ec12-269a8ca64742',
lease_id: '',
@ -982,8 +986,127 @@ export default function (server) {
],
},
],
months: [
{
timestamp: startOfMonth(timestamp).toISOString(),
counts: {
distinct_entities: 0,
entity_clients: 4,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 4,
},
namespaces: [
{
namespace_id: 'lHmap',
namespace_path: 'education/',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
mounts: [
{
mount_path: 'auth_userpass_a36c8125',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
},
],
},
{
namespace_id: 'root',
namespace_path: '',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
mounts: [
{
mount_path: 'auth_userpass_3158c012',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
},
],
},
],
new_clients: {
counts: {
distinct_entities: 0,
entity_clients: 4,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 4,
},
namespaces: [
{
namespace_id: 'root',
namespace_path: '',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
mounts: [
{
mount_path: 'auth_userpass_3158c012',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
},
],
},
{
namespace_id: 'lHmap',
namespace_path: 'education/',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
mounts: [
{
mount_path: 'auth_userpass_a36c8125',
counts: {
distinct_entities: 0,
entity_clients: 2,
non_entity_tokens: 0,
non_entity_clients: 0,
clients: 2,
},
},
],
},
],
},
},
],
distinct_entities: 132,
entity_clients: 132,
non_entity_tokens: 43,
non_entity_clients: 43,
clients: 175,
},
wrap_info: null,

View File

@ -29,7 +29,7 @@ module('Acceptance | clients current', function (hooks) {
test('shows empty state when config disabled, no data', async function (assert) {
const config = generateConfigResponse({ enabled: 'default-disable' });
const monthly = generateCurrentMonthResponse();
const monthly = generateCurrentMonthResponse({ configEnabled: false });
this.server = new Pretender(function () {
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
@ -89,8 +89,20 @@ module('Acceptance | clients current', function (hooks) {
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert
.dom('[data-test-chart-container="total-clients"] [data-test-horizontal-bar-chart]')
.exists('Shows totals attribution bar chart');
assert
// TODO CMB - this assertion should be updated so the response includes new client counts
// TODO then move somewhere to assert empty state shows when filtering a namespace with no new clients
.dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]')
.includesText(
'There are no new clients for this namespace during this time period.',
'Shows empty state if no new client counts'
);
// check chart displays correct elements and values
for (const key in CHART_ELEMENTS) {
@ -120,8 +132,17 @@ module('Acceptance | clients current', function (hooks) {
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
assert.dom('[data-test-horizontal-bar-chart]').exists('Still shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top auth method');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert
.dom('[data-test-chart-container="total-clients"] [data-test-horizontal-bar-chart]')
.exists('Still shows totals attribution bar chart');
assert
.dom('[data-test-chart-container="total-clients"] .chart-description')
.includesText('The total clients used by the auth method for this month.');
assert
.dom('[data-test-chart-container="new-clients"] .chart-description')
.includesText('The new clients used by the auth method for this month.');
// check chart displays correct elements and values
for (const key in CHART_ELEMENTS) {
@ -158,8 +179,9 @@ module('Acceptance | clients current', function (hooks) {
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
await settled();
await waitUntil(() => find('[data-test-horizontal-bar-chart]'));
assert.dom('[data-test-horizontal-bar-chart]').exists('Still shows attribution bar chart');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert.dom(SELECTORS.attributionBlock).exists('Still shows attribution block');
await clickTrigger();
await searchSelect.options.objectAt(0).click();
await settled();
@ -171,7 +193,11 @@ module('Acceptance | clients current', function (hooks) {
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert
.dom('[data-test-chart-container="new-clients"] [data-test-empty-state-subtext]')
.includesText('There are no new clients', 'Shows empty state if no new client counts');
});
test('filters correctly on current with no auth mounts', async function (assert) {
@ -198,8 +224,15 @@ module('Acceptance | clients current', function (hooks) {
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
assert
.dom('[data-test-chart-container="total-clients"] [data-test-horizontal-bar-chart]')
.exists('Shows totals attribution bar chart');
assert
.dom('[data-test-chart-container="total-clients"] .chart-description')
.includesText('The total clients in the namespace for this month.');
// Filter by namespace
await clickTrigger();
await searchSelect.options.objectAt(0).click();
@ -217,7 +250,8 @@ module('Acceptance | clients current', function (hooks) {
assert
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
.hasText(non_entity_clients.toString());
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
assert.dom('[data-test-chart-container="new-clients"] .chart-title').includesText('New clients');
assert.dom('[data-test-chart-container="total-clients"] .chart-title').includesText('Total clients');
});
test('shows correct empty state when config off but no read on config', async function (assert) {

View File

@ -158,15 +158,23 @@ export function generateLicenseResponse(startDate, endDate) {
};
}
export function generateCurrentMonthResponse(namespaceCount, skipMounts = false) {
export function generateCurrentMonthResponse(namespaceCount, skipMounts = false, configEnabled = true) {
if (!configEnabled) {
return {
data: { id: 'no-data' },
};
}
if (!namespaceCount) {
return {
request_id: 'monthly-response-id',
data: {
by_namespace: [],
clients: 0,
distinct_entities: 0,
entity_clients: 0,
non_entity_clients: 0,
non_entity_tokens: 0,
months: [],
},
};
}
@ -189,6 +197,7 @@ export function generateCurrentMonthResponse(namespaceCount, skipMounts = false)
data: {
by_namespace,
...counts,
months: [],
},
};
}

View File

@ -84,15 +84,44 @@ module('Integration | Component | clients/attribution', function (hooks) {
@isDateRange={{isDateRange}}
/>
`);
assert
.dom('[data-test-attribution-subtext]')
.includesText('namespace for this month', 'renders monthly namespace text');
.dom('[data-test-attribution-description]')
.includesText(
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.',
'renders correct auth attribution description'
);
assert
.dom('[data-test-chart-container="total-clients"] .chart-description')
.includesText(
'The total clients in the namespace for this month. This number is useful for identifying overall usage volume.',
'renders total monthly namespace text'
);
assert
.dom('[data-test-chart-container="new-clients"] .chart-description')
.includesText(
'The new clients in the namespace for this month. This aids in understanding which namespaces create and use new clients.',
'renders new monthly namespace text'
);
this.set('selectedNamespace', 'second');
assert
.dom('[data-test-attribution-subtext]')
.includesText('auth method for this month', 'renders monthly auth method text');
.dom('[data-test-attribution-description]')
.includesText(
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
'renders correct auth attribution description'
);
assert
.dom('[data-test-chart-container="total-clients"] .chart-description')
.includesText(
'The total clients used by the auth method for this month. This number is useful for identifying overall usage volume.',
'renders total monthly auth method text'
);
assert
.dom('[data-test-chart-container="new-clients"] .chart-description')
.includesText(
'The new clients used by the auth method for this month. This aids in understanding which auth methods create and use new clients.',
'renders new monthly auth method text'
);
});
test('it renders with data for selected namespace auth methods for a date range', async function (assert) {