Skip to content

Commit

Permalink
UI/Add double attribution chart to current (#15035)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hellobontempo authored and Matt Schultz committed Apr 27, 2022
1 parent 999b5ca commit 9acb139
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 95 deletions.
34 changes: 23 additions & 11 deletions ui/app/components/clients/attribution.js
Expand Up @@ -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
*/
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -69,17 +79,19 @@ 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:
return {
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':
Expand All @@ -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',
Expand Down
62 changes: 49 additions & 13 deletions ui/app/components/clients/current.js
Expand Up @@ -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() {
Expand All @@ -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);
}
Expand All @@ -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.byNamespaceTotalClients;
}
}

// new client data for horizontal bar chart in attribution component
get newClientsData() {
if (this.selectedNamespace) {
return this.filteredNewData?.mounts || null;
} else {
return this.byNamespaceCurrent;
return this.byNamespaceNewClients;
}
}

Expand All @@ -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,
}));
Expand Down
2 changes: 1 addition & 1 deletion ui/app/components/clients/line-chart.js
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/components/clients/vertical-bar-chart.js
Expand Up @@ -44,7 +44,7 @@ export default class VerticalBarChart extends Component {
}
get yKey() {
return this.args.yKey || 'total';
return this.args.yKey || 'clients';
}
@action
Expand Down
5 changes: 4 additions & 1 deletion ui/app/models/clients/monthly.js
Expand Up @@ -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;
}
11 changes: 5 additions & 6 deletions ui/app/serializers/clients/activity.js
Expand Up @@ -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'];
Expand Down Expand Up @@ -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();
Expand All @@ -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),
};
});
Expand All @@ -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),
},
};
Expand Down
52 changes: 38 additions & 14 deletions ui/app/serializers/clients/monthly.js
@@ -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'];
Expand All @@ -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,
};
})
: [];

Expand All @@ -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;
}

Expand All @@ -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);
}
}

0 comments on commit 9acb139

Please sign in to comment.