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:
parent
10a70207c7
commit
787ebaebd3
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -44,7 +44,7 @@ export default class VerticalBarChart extends Component {
|
|||
}
|
||||
|
||||
get yKey() {
|
||||
return this.args.yKey || 'total';
|
||||
return this.args.yKey || 'clients';
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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: 1,014 B |
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue