UI/filter monthly graphs (#15279)
* alphabetize utils * add util to add namespace key * finish filtering * add fake data for filtering * address comments * add empty state for no new client counts, when filtered by namespace * fix mirage clients linting * re-add namespaces to month object * clean up filtering * add tests and refactor accordingly * fix tooltip bug and chart new month client chart not rendering * filter out undefined * optional method chaining * add filter and fix ticks for line chart * fix axes domains * fix average calculation
This commit is contained in:
parent
45efa37c4a
commit
999d243544
|
@ -122,6 +122,7 @@ export default class Attribution extends Component {
|
|||
...otherColumns,
|
||||
];
|
||||
}
|
||||
|
||||
generateCsvData() {
|
||||
const totalAttribution = this.args.totalClientAttribution;
|
||||
const newAttribution = this.barChartNewClients ? this.args.newClientAttribution : null;
|
||||
|
|
|
@ -185,12 +185,19 @@ export default class History extends Component {
|
|||
return this.queriedActivityResponse || this.args.model.activity;
|
||||
}
|
||||
|
||||
get byMonthTotalClients() {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
get byMonthActivityData() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredActivityByMonth;
|
||||
} else {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
}
|
||||
|
||||
get byMonthNewClients() {
|
||||
return this.byMonthTotalClients.map((m) => m.new_clients);
|
||||
if (this.byMonthActivityData) {
|
||||
return this.byMonthActivityData?.map((m) => m.new_clients);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
|
@ -201,37 +208,34 @@ export default class History extends Component {
|
|||
return !!this.totalClientAttribution && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
|
||||
}
|
||||
|
||||
// top level TOTAL client counts for given date range
|
||||
// (object) top level TOTAL client counts for given date range
|
||||
get totalUsageCounts() {
|
||||
return this.selectedNamespace ? this.filteredActivity : this.getActivityResponse.total;
|
||||
return this.selectedNamespace ? this.filteredActivityByNamespace : this.getActivityResponse.total;
|
||||
}
|
||||
|
||||
get newUsageCounts() {
|
||||
return this.selectedNamespace
|
||||
? this.filteredNewClientAttribution
|
||||
: this.byMonthTotalClients[0]?.new_clients;
|
||||
// (object) single month new client data with total counts + array of namespace breakdown
|
||||
get newClientCounts() {
|
||||
return this.isDateRange ? null : this.byMonthActivityData[0]?.new_clients;
|
||||
}
|
||||
|
||||
// total client data for horizontal bar chart in attribution component
|
||||
get totalClientAttribution() {
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredActivity?.mounts || null;
|
||||
return this.filteredActivityByNamespace?.mounts || null;
|
||||
} else {
|
||||
return this.getActivityResponse?.byNamespace;
|
||||
return this.getActivityResponse?.byNamespace || null;
|
||||
}
|
||||
}
|
||||
|
||||
// new client data for horizontal bar chart
|
||||
get newClientAttribution() {
|
||||
// new client attribution only available in a single, historical month
|
||||
if (this.isDateRange) {
|
||||
return null;
|
||||
}
|
||||
// only a single month is returned from the api
|
||||
// new client attribution only available in a single, historical month (not a date range)
|
||||
if (this.isDateRange) return null;
|
||||
|
||||
if (this.selectedNamespace) {
|
||||
return this.filteredNewClientAttribution?.mounts || null;
|
||||
return this.newClientCounts?.mounts || null;
|
||||
} else {
|
||||
return this.byMonthTotalClients[0]?.new_clients.namespaces || null;
|
||||
return this.newClientCounts?.namespaces || null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,7 +243,8 @@ export default class History extends Component {
|
|||
return this.getActivityResponse.responseTimestamp;
|
||||
}
|
||||
|
||||
get filteredActivity() {
|
||||
// FILTERS
|
||||
get filteredActivityByNamespace() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
|
@ -253,19 +258,22 @@ export default class History extends Component {
|
|||
.mounts?.find((mount) => mount.label === auth);
|
||||
}
|
||||
|
||||
get filteredNewClientAttribution() {
|
||||
get filteredActivityByMonth() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
// new client data is only available by month
|
||||
const newClientsData = this.byMonthTotalClients[0]?.new_clients;
|
||||
if (!newClientsData) return null;
|
||||
if (this.isDateRange) return null;
|
||||
if (!namespace && !auth) return newClientsData;
|
||||
|
||||
const foundNamespace = newClientsData.namespaces.find((ns) => ns.label === namespace);
|
||||
if (!foundNamespace) return null;
|
||||
if (!auth) return foundNamespace;
|
||||
return foundNamespace.mounts?.find((mount) => mount.label === auth);
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
const namespaceData = this.getActivityResponse?.byMonth
|
||||
.map((m) => m.namespaces_by_key[namespace])
|
||||
.filter((d) => d !== undefined);
|
||||
if (!auth) {
|
||||
return namespaceData.length === 0 ? null : namespaceData;
|
||||
}
|
||||
const mountData = namespaceData
|
||||
.map((namespace) => namespace.mounts_by_key[auth])
|
||||
.filter((d) => d !== undefined);
|
||||
return mountData.length === 0 ? null : mountData;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -346,7 +354,7 @@ export default class History extends Component {
|
|||
this.selectedAuthMethod = null;
|
||||
} else {
|
||||
// Side effect: set auth namespaces
|
||||
const mounts = this.filteredActivity.mounts?.map((mount) => ({
|
||||
const mounts = this.filteredActivityByNamespace.mounts?.map((mount) => ({
|
||||
id: mount.label,
|
||||
name: mount.label,
|
||||
}));
|
||||
|
|
|
@ -56,6 +56,8 @@ export default class LineChart extends Component {
|
|||
);
|
||||
}
|
||||
const filteredData = dataset.filter((e) => Object.keys(e).includes(this.yKey)); // months with data will contain a 'clients' key (otherwise only a timestamp)
|
||||
const dataMax = max(filteredData.map((d) => d[this.yKey]));
|
||||
const domainMax = Math.ceil(dataMax / 10) * 10; // we want to round UP to the nearest tens place ex. dataMax = 102, domainMax = 110
|
||||
const chartSvg = select(element);
|
||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||
|
||||
|
@ -63,15 +65,8 @@ export default class LineChart extends Component {
|
|||
chartSvg.selectAll('g').remove().exit().data(filteredData).enter();
|
||||
|
||||
// DEFINE AXES SCALES
|
||||
const yScale = scaleLinear()
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
|
||||
const yAxisScale = scaleLinear()
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([SVG_DIMENSIONS.height, 0])
|
||||
.nice();
|
||||
const yScale = scaleLinear().domain([0, domainMax]).range([0, 100]).nice();
|
||||
const yAxisScale = scaleLinear().domain([0, domainMax]).range([SVG_DIMENSIONS.height, 0]).nice();
|
||||
|
||||
// use full dataset (instead of filteredData) so x-axis spans months with and without data
|
||||
const xScale = scalePoint()
|
||||
|
@ -158,10 +153,9 @@ export default class LineChart extends Component {
|
|||
// MOUSE EVENT FOR TOOLTIP
|
||||
hoverCircles.on('mouseover', (data) => {
|
||||
// TODO: how to generalize this?
|
||||
let { new_clients } = data || null;
|
||||
this.tooltipMonth = formatChartDate(data[this.xKey]);
|
||||
this.tooltipTotal = data[this.yKey] + ' total clients';
|
||||
this.tooltipNew = (new_clients ? new_clients[this.yKey] : '0') + ' new clients';
|
||||
this.tooltipNew = (data?.new_clients[this.yKey] || '0') + ' new clients';
|
||||
this.tooltipUpgradeText = '';
|
||||
let upgradeInfo = findUpgradeData(data);
|
||||
if (upgradeInfo) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { mean } from 'd3-array';
|
||||
import { calculateAverageClients } from 'vault/utils/chart-helpers';
|
||||
|
||||
/**
|
||||
* @module MonthlyUsage
|
||||
|
@ -10,7 +10,7 @@ import { mean } from 'd3-array';
|
|||
<Clients::MonthlyUsage
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
@verticalBarChartData={{this.byMonthTotalClients}}
|
||||
@verticalBarChartData={{this.byMonthActivityData}}
|
||||
/>
|
||||
* ```
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
|
@ -34,12 +34,15 @@ import { mean } from 'd3-array';
|
|||
*/
|
||||
export default class MonthlyUsage extends Component {
|
||||
get averageTotalClients() {
|
||||
let average = mean(this.args.verticalBarChartData?.map((d) => d.clients));
|
||||
return Math.round(average) || null;
|
||||
return calculateAverageClients(this.args.verticalBarChartData, 'clients') || '0';
|
||||
}
|
||||
|
||||
get averageNewClients() {
|
||||
let average = mean(this.args.verticalBarChartData?.map((d) => d.new_clients.clients));
|
||||
return Math.round(average) || null;
|
||||
return (
|
||||
calculateAverageClients(
|
||||
this.args.verticalBarChartData.map((d) => d.new_clients),
|
||||
'clients'
|
||||
) || '0'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { mean } from 'd3-array';
|
||||
import { calculateAverageClients } from 'vault/utils/chart-helpers';
|
||||
|
||||
/**
|
||||
* @module RunningTotal
|
||||
|
@ -11,6 +11,7 @@ import { mean } from 'd3-array';
|
|||
* ```js
|
||||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@barChartData={{this.byMonthNewClients}}
|
||||
@lineChartData={{this.byMonth}}
|
||||
@runningTotals={{this.runningTotals}}
|
||||
|
@ -18,7 +19,9 @@ import { mean } from 'd3-array';
|
|||
/>
|
||||
* ```
|
||||
|
||||
* @param {array} chartData - array of objects from /activity response
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
* @param {string} selectedAuthMethod - string of auth method label for empty state message in bar chart
|
||||
* @param {array} barChartData - array of objects from /activity response, from the 'months' key
|
||||
object example: {
|
||||
month: '1/22',
|
||||
entity_clients: 23,
|
||||
|
@ -32,24 +35,38 @@ import { mean } from 'd3-array';
|
|||
namespaces: [],
|
||||
},
|
||||
};
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
* @param {array} lineChartData - array of objects from /activity response, from the 'months' key
|
||||
* @param {object} runningTotals - top level totals from /activity response { clients: 3517, entity_clients: 1593, non_entity_clients: 1924 }
|
||||
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response
|
||||
* @param {object} upgradeData - object containing version upgrade data e.g.: {id: '1.9.0', previousVersion: null, timestampInstalled: '2021-11-03T10:23:16Z'}
|
||||
* @param {string} timestamp - ISO timestamp created in serializer to timestamp the response
|
||||
*
|
||||
*/
|
||||
export default class RunningTotal extends Component {
|
||||
get entityClientData() {
|
||||
return {
|
||||
runningTotal: this.args.runningTotals.entity_clients,
|
||||
averageNewClients: Math.round(mean(this.args.barChartData?.map((d) => d.entity_clients))),
|
||||
averageNewClients: calculateAverageClients(this.args.barChartData, 'entity_clients') || '0',
|
||||
};
|
||||
}
|
||||
|
||||
get nonEntityClientData() {
|
||||
return {
|
||||
runningTotal: this.args.runningTotals.non_entity_clients,
|
||||
averageNewClients: Math.round(mean(this.args.barChartData?.map((d) => d.non_entity_clients))),
|
||||
averageNewClients: calculateAverageClients(this.args.barChartData, 'non_entity_clients') || '0',
|
||||
};
|
||||
}
|
||||
|
||||
get hasRunningTotalClients() {
|
||||
return (
|
||||
typeof this.entityClientData.runningTotal === 'number' ||
|
||||
typeof this.nonEntityClientData.runningTotal === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
get hasAverageNewClients() {
|
||||
return (
|
||||
typeof this.entityClientData.averageNewClients === 'number' ||
|
||||
typeof this.nonEntityClientData.averageNewClients === 'number'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,13 +54,13 @@ export default class VerticalBarChart extends Component {
|
|||
const stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
|
||||
const stackedData = stackFunction(filteredData);
|
||||
const chartSvg = select(element);
|
||||
const dataMax = max(filteredData.map((d) => d[this.yKey]));
|
||||
const domainMax = Math.ceil(dataMax / 10) * 10; // we want to round UP to the nearest tens place ex. dataMax = 102, domainMax = 110
|
||||
|
||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||
|
||||
// DEFINE DATA BAR SCALES
|
||||
const yScale = scaleLinear()
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
const yScale = scaleLinear().domain([0, domainMax]).range([0, 100]).nice();
|
||||
|
||||
const xScale = scaleBand()
|
||||
.domain(dataset.map((d) => d[this.xKey]))
|
||||
|
|
|
@ -101,10 +101,13 @@
|
|||
}
|
||||
|
||||
.chart-empty-state {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
place-self: center stretch;
|
||||
grid-row-end: span 3;
|
||||
grid-column-end: span 3;
|
||||
max-width: none;
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
|
||||
> div {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,6 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{! MODAL FOR CSV DOWNLOAD }}
|
||||
<Modal
|
||||
@title="Export attribution data"
|
||||
|
|
|
@ -127,14 +127,14 @@
|
|||
<LayoutLoading />
|
||||
{{else}}
|
||||
{{#if this.totalUsageCounts}}
|
||||
{{#unless this.byMonthTotalClients}}
|
||||
{{#unless this.byMonthActivityData}}
|
||||
<Clients::UsageStats @title="Total usage" @totalUsageCounts={{this.totalUsageCounts}} />
|
||||
{{/unless}}
|
||||
{{#if this.byMonthTotalClients}}
|
||||
{{#if this.byMonthActivityData}}
|
||||
<Clients::RunningTotal
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
@lineChartData={{this.byMonthTotalClients}}
|
||||
@selectedAuthMethod={{this.selectedAuthMethod}}
|
||||
@lineChartData={{this.byMonthActivityData}}
|
||||
@barChartData={{this.byMonthNewClients}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{this.upgradeDuringActivity}}
|
||||
|
@ -145,7 +145,7 @@
|
|||
<Clients::Attribution
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@totalUsageCounts={{this.totalUsageCounts}}
|
||||
@newUsageCounts={{this.newUsageCounts}}
|
||||
@newUsageCounts={{this.newClientCounts}}
|
||||
@totalClientAttribution={{this.totalClientAttribution}}
|
||||
@newClientAttribution={{this.newClientAttribution}}
|
||||
@selectedNamespace={{this.selectedNamespace}}
|
||||
|
@ -155,10 +155,10 @@
|
|||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.byMonthTotalClients}}
|
||||
{{#if this.byMonthActivityData}}
|
||||
<Clients::MonthlyUsage
|
||||
@chartLegend={{this.chartLegend}}
|
||||
@verticalBarChartData={{this.byMonthTotalClients}}
|
||||
@verticalBarChartData={{this.byMonthActivityData}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{{! TODO Add conditional, show charts if data available, otherwise show stat text boxes }}
|
||||
<div class="chart-wrapper stacked-charts">
|
||||
<div class="single-chart-grid">
|
||||
<div class="chart-header has-bottom-margin-xl">
|
||||
|
@ -37,8 +38,17 @@
|
|||
</div>
|
||||
|
||||
<div class="single-chart-grid">
|
||||
<div class="chart-container-wide">
|
||||
<Clients::VerticalBarChart @dataset={{@barChartData}} @chartLegend={{@chartLegend}} />
|
||||
<div class={{concat (unless this.hasAverageNewClients "chart-empty-state ") "chart-container-wide"}}>
|
||||
<Clients::VerticalBarChart
|
||||
@dataset={{if this.hasAverageNewClients @barChartData false}}
|
||||
@chartLegend={{@chartLegend}}
|
||||
@noDataTitle="No new clients"
|
||||
@noDataMessage={{concat
|
||||
"There are no new clients for this "
|
||||
(if @selectedAuthMethod "auth method" "namespace")
|
||||
" in this date range"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
|
@ -74,4 +84,5 @@
|
|||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,11 +1,17 @@
|
|||
<svg
|
||||
data-test-vertical-bar-chart
|
||||
class="chart has-grid"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.renderChart @dataset}}
|
||||
{{did-update this.renderChart @dataset}}
|
||||
>
|
||||
</svg>
|
||||
{{#if @dataset}}
|
||||
<svg
|
||||
data-test-vertical-bar-chart
|
||||
class="chart has-grid"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.renderChart @dataset}}
|
||||
{{did-update this.renderChart @dataset}}
|
||||
>
|
||||
</svg>
|
||||
{{else}}
|
||||
<div class="chart-empty-state">
|
||||
<EmptyState @title={{@noDataTitle}} @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{! TOOLTIP }}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 857 B After Width: | Height: | Size: 1.0 KiB |
|
@ -1,4 +1,5 @@
|
|||
import { format } from 'd3-format';
|
||||
import { mean } from 'd3-array';
|
||||
|
||||
// COLOR THEME:
|
||||
export const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#1563FF'];
|
||||
|
@ -25,3 +26,10 @@ export function formatTooltipNumber(value) {
|
|||
// formats a number according to the locale
|
||||
return new Intl.NumberFormat().format(value);
|
||||
}
|
||||
|
||||
export function calculateAverageClients(dataset, objectKey) {
|
||||
// dataset is an array of objects (consumed by the chart components)
|
||||
// objectKey is the key of the integer we want to calculate, ex: 'entity_clients', 'non_entity_clients', 'clients'
|
||||
let getIntegers = dataset.map((d) => (d[objectKey] ? d[objectKey] : 0)); // if undefined no data, so return 0
|
||||
return getIntegers.length !== 0 ? Math.round(mean(getIntegers)) : null;
|
||||
}
|
||||
|
|
|
@ -1,38 +1,24 @@
|
|||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { compareAsc } from 'date-fns';
|
||||
|
||||
export const flattenDataset = (object) => {
|
||||
if (Object.keys(object).includes('counts') && object.counts) {
|
||||
let flattenedObject = {};
|
||||
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
|
||||
return homogenizeClientNaming(flattenedObject);
|
||||
}
|
||||
return object;
|
||||
};
|
||||
|
||||
export const sortMonthsByTimestamp = (monthsArray) => {
|
||||
// backend is working on a fix to sort months by date
|
||||
// right now months are ordered in descending client count number
|
||||
const sortedPayload = [...monthsArray];
|
||||
return sortedPayload.sort((a, b) =>
|
||||
compareAsc(parseAPITimestamp(a.timestamp), parseAPITimestamp(b.timestamp))
|
||||
);
|
||||
};
|
||||
|
||||
export const formatByMonths = (monthsArray) => {
|
||||
if (!Array.isArray(monthsArray)) return monthsArray;
|
||||
const sortedPayload = sortMonthsByTimestamp(monthsArray);
|
||||
return sortedPayload.map((m) => {
|
||||
const month = parseAPITimestamp(m.timestamp, 'M/yy');
|
||||
let totalClientsByNamespace = formatByNamespace(m.namespaces);
|
||||
let newClientsByNamespace = formatByNamespace(m.new_clients?.namespaces);
|
||||
if (Object.keys(m).includes('counts')) {
|
||||
let totalClients = flattenDataset(m);
|
||||
let newClients = m.new_clients ? flattenDataset(m.new_clients) : {};
|
||||
let totalCounts = flattenDataset(m);
|
||||
let newCounts = m.new_clients ? flattenDataset(m.new_clients) : {};
|
||||
return {
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
...totalClients,
|
||||
month,
|
||||
...totalCounts,
|
||||
namespaces: formatByNamespace(m.namespaces),
|
||||
namespaces_by_key: namespaceArrayToObject(totalClientsByNamespace, newClientsByNamespace, month),
|
||||
new_clients: {
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
...newClients,
|
||||
month,
|
||||
...newCounts,
|
||||
namespaces: formatByNamespace(m.new_clients?.namespaces) || [],
|
||||
},
|
||||
};
|
||||
|
@ -87,3 +73,175 @@ export const homogenizeClientNaming = (object) => {
|
|||
}
|
||||
return object;
|
||||
};
|
||||
|
||||
export const flattenDataset = (object) => {
|
||||
// TODO CMB revisit when backend has finished ticket VAULT-6035
|
||||
if (object?.counts) {
|
||||
let flattenedObject = {};
|
||||
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
|
||||
return homogenizeClientNaming(flattenedObject);
|
||||
}
|
||||
return object;
|
||||
};
|
||||
|
||||
export const sortMonthsByTimestamp = (monthsArray) => {
|
||||
// backend is working on a fix to sort months by date
|
||||
// right now months are ordered in descending client count number
|
||||
const sortedPayload = [...monthsArray];
|
||||
return sortedPayload.sort((a, b) =>
|
||||
compareAsc(parseAPITimestamp(a.timestamp), parseAPITimestamp(b.timestamp))
|
||||
);
|
||||
};
|
||||
|
||||
export const namespaceArrayToObject = (totalClientsByNamespace, newClientsByNamespace, month) => {
|
||||
// all 'new_client' data resides within a separate key of each month (see data structure below)
|
||||
// FIRST: iterate and nest respective 'new_clients' data within each namespace and mount object
|
||||
// note: this is happening within the month object
|
||||
const nestNewClientsWithinNamespace = totalClientsByNamespace.map((ns) => {
|
||||
let newNamespaceCounts = newClientsByNamespace?.find((n) => n.label === ns.label);
|
||||
if (newNamespaceCounts) {
|
||||
let { label, clients, entity_clients, non_entity_clients } = newNamespaceCounts;
|
||||
let newClientsByMount = [...newNamespaceCounts?.mounts];
|
||||
let nestNewClientsWithinMounts = ns.mounts.map((mount) => {
|
||||
let new_clients = newClientsByMount?.find((m) => m.label === mount.label) || {};
|
||||
return {
|
||||
...mount,
|
||||
new_clients,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...ns,
|
||||
new_clients: {
|
||||
label,
|
||||
clients,
|
||||
entity_clients,
|
||||
non_entity_clients,
|
||||
},
|
||||
mounts: [...nestNewClientsWithinMounts],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...ns,
|
||||
new_clients: {},
|
||||
};
|
||||
});
|
||||
|
||||
// SECOND: create a new object (namespace_by_key) in which each namespace label is a key
|
||||
let namespaces_by_key = {};
|
||||
nestNewClientsWithinNamespace.forEach((namespaceObject) => {
|
||||
// THIRD: make another object within the namespace where each mount label is a key
|
||||
let mounts_by_key = {};
|
||||
namespaceObject.mounts.forEach((mountObject) => {
|
||||
mounts_by_key[mountObject.label] = {
|
||||
month,
|
||||
...mountObject,
|
||||
new_clients: { month, ...mountObject.new_clients },
|
||||
};
|
||||
});
|
||||
|
||||
let { label, clients, entity_clients, non_entity_clients, new_clients } = namespaceObject;
|
||||
namespaces_by_key[label] = {
|
||||
month,
|
||||
clients,
|
||||
entity_clients,
|
||||
non_entity_clients,
|
||||
new_clients: { month, ...new_clients },
|
||||
mounts_by_key,
|
||||
};
|
||||
});
|
||||
return namespaces_by_key;
|
||||
// structure of object returned
|
||||
// namespace_by_key: {
|
||||
// "namespace_label": {
|
||||
// month: "3/22",
|
||||
// clients: 32,
|
||||
// entity_clients: 16,
|
||||
// non_entity_clients: 16,
|
||||
// new_clients: {
|
||||
// month: "3/22",
|
||||
// clients: 5,
|
||||
// entity_clients: 2,
|
||||
// non_entity_clients: 3,
|
||||
// },
|
||||
// mounts_by_key: {
|
||||
// "mount_label": {
|
||||
// month: "3/22",
|
||||
// clients: 3,
|
||||
// entity_clients: 2,
|
||||
// non_entity_clients: 1,
|
||||
// new_clients: {
|
||||
// month: "3/22",
|
||||
// clients: 5,
|
||||
// entity_clients: 2,
|
||||
// non_entity_clients: 3,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
};
|
||||
|
||||
// API RESPONSE STRUCTURE:
|
||||
// data: {
|
||||
// ** by_namespace organized in descending order of client count number **
|
||||
// by_namespace: [
|
||||
// {
|
||||
// namespace_id: '96OwG',
|
||||
// namespace_path: 'test-ns/',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'path-1', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// ** months organized in ascending order of timestamps, oldest to most recent
|
||||
// months: [
|
||||
// {
|
||||
// timestamp: '2022-03-01T00:00:00Z',
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// new_clients: {
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// timestamp: '2022-04-01T00:00:00Z',
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// new_clients: {
|
||||
// counts: {},
|
||||
// namespaces: [
|
||||
// {
|
||||
// namespace_id: 'root',
|
||||
// namespace_path: '',
|
||||
// counts: {},
|
||||
// mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// start_time: 'start timestamp string',
|
||||
// end_time: 'end timestamp string',
|
||||
// total: { clients: 300, non_entity_clients: 100, entity_clients: 400} ,
|
||||
// }
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
startOfMonth,
|
||||
} from 'date-fns';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
|
||||
const MOCK_MONTHLY_DATA = [
|
||||
{
|
||||
timestamp: '2021-05-01T00:00:00Z',
|
||||
|
@ -16,7 +17,7 @@ const MOCK_MONTHLY_DATA = [
|
|||
distinct_entities: 0,
|
||||
entity_clients: 25,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 20,
|
||||
non_entity_clients: 25,
|
||||
clients: 50,
|
||||
},
|
||||
namespaces: [
|
||||
|
@ -985,6 +986,26 @@ export default function (server) {
|
|||
clients: 11212,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 50,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 23,
|
||||
clients: 73,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 25,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 15,
|
||||
clients: 40,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,857 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
flattenDataset,
|
||||
formatByMonths,
|
||||
formatByNamespace,
|
||||
homogenizeClientNaming,
|
||||
sortMonthsByTimestamp,
|
||||
namespaceArrayToObject,
|
||||
} from 'core/utils/client-count-utils';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import isBefore from 'date-fns/isBefore';
|
||||
import isAfter from 'date-fns/isAfter';
|
||||
|
||||
// import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
// import ENV from 'vault/config/environment';
|
||||
// import { formatRFC3339 } from 'date-fns';
|
||||
|
||||
module('Integration | Util | client count utils', function (hooks) {
|
||||
setupTest(hooks);
|
||||
// setupMirage(hooks);
|
||||
|
||||
// TODO: wire up to stubbed API/mirage?
|
||||
// hooks.before(function () {
|
||||
// ENV['ember-cli-mirage'].handler = 'clients';
|
||||
// });
|
||||
// hooks.after(function () {
|
||||
// ENV['ember-cli-mirage'].handler = null;
|
||||
// });
|
||||
|
||||
/* MONTHS array contains: (update when backend work done on months )
|
||||
- one month with only old client naming
|
||||
*/
|
||||
|
||||
const MONTHS = [
|
||||
{
|
||||
timestamp: '2021-05-01T00:00:00Z',
|
||||
counts: {
|
||||
distinct_entities: 25,
|
||||
non_entity_tokens: 25,
|
||||
clients: 50,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 13,
|
||||
non_entity_tokens: 7,
|
||||
clients: 20,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 8,
|
||||
non_entity_tokens: 0,
|
||||
clients: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
non_entity_tokens: 7,
|
||||
clients: 7,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 's07UR',
|
||||
namespace_path: 'ns1/',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 5,
|
||||
clients: 10,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
non_entity_tokens: 5,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
non_entity_tokens: 0,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {
|
||||
distinct_entities: 3,
|
||||
non_entity_tokens: 2,
|
||||
clients: 5,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 3,
|
||||
non_entity_tokens: 2,
|
||||
clients: 5,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 3,
|
||||
non_entity_tokens: 0,
|
||||
clients: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
non_entity_tokens: 2,
|
||||
clients: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '2021-10-01T00:00:00Z',
|
||||
counts: {
|
||||
distinct_entities: 20,
|
||||
entity_clients: 20,
|
||||
non_entity_tokens: 20,
|
||||
non_entity_clients: 20,
|
||||
clients: 40,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 8,
|
||||
entity_clients: 8,
|
||||
non_entity_tokens: 7,
|
||||
non_entity_clients: 7,
|
||||
clients: 15,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 8,
|
||||
entity_clients: 8,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 8,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 7,
|
||||
non_entity_clients: 7,
|
||||
clients: 7,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 's07UR',
|
||||
namespace_path: 'ns1/',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
entity_clients: 5,
|
||||
non_entity_tokens: 5,
|
||||
non_entity_clients: 5,
|
||||
clients: 10,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 5,
|
||||
non_entity_clients: 5,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 5,
|
||||
entity_clients: 5,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {
|
||||
distinct_entities: 3,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 2,
|
||||
non_entity_clients: 2,
|
||||
clients: 5,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 3,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 2,
|
||||
non_entity_clients: 2,
|
||||
clients: 5,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 3,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 2,
|
||||
non_entity_clients: 2,
|
||||
clients: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamp: '2021-09-01T00:00:00Z',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 17,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 18,
|
||||
clients: 35,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'oImjk',
|
||||
namespace_path: 'ns2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 5,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 5,
|
||||
clients: 10,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 5,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 5,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 2,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 3,
|
||||
clients: 5,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 3,
|
||||
clients: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 2,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 's07UR',
|
||||
namespace_path: 'ns1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 2,
|
||||
clients: 5,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 2,
|
||||
clients: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 10,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 10,
|
||||
clients: 20,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'oImjk',
|
||||
namespace_path: 'ns2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 5,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 5,
|
||||
clients: 10,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 5,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 5,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 5,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 2,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 3,
|
||||
clients: 5,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 3,
|
||||
clients: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 2,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 's07UR',
|
||||
namespace_path: 'ns1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 2,
|
||||
clients: 5,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 2,
|
||||
clients: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const BY_NAMESPACE = [
|
||||
{
|
||||
namespace_id: '96OwG',
|
||||
namespace_path: 'test-ns/',
|
||||
counts: {
|
||||
distinct_entities: 18290,
|
||||
entity_clients: 18290,
|
||||
non_entity_tokens: 18738,
|
||||
non_entity_clients: 18738,
|
||||
clients: 37028,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'path-1',
|
||||
counts: {
|
||||
distinct_entities: 6403,
|
||||
entity_clients: 6403,
|
||||
non_entity_tokens: 6300,
|
||||
non_entity_clients: 6300,
|
||||
clients: 12703,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'path-2',
|
||||
counts: {
|
||||
distinct_entities: 5699,
|
||||
entity_clients: 5699,
|
||||
non_entity_tokens: 6777,
|
||||
non_entity_clients: 6777,
|
||||
clients: 12476,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'path-3',
|
||||
counts: {
|
||||
distinct_entities: 6188,
|
||||
entity_clients: 6188,
|
||||
non_entity_tokens: 5661,
|
||||
non_entity_clients: 5661,
|
||||
clients: 11849,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 19099,
|
||||
entity_clients: 19099,
|
||||
non_entity_tokens: 17781,
|
||||
non_entity_clients: 17781,
|
||||
clients: 36880,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
mount_path: 'path-3',
|
||||
counts: {
|
||||
distinct_entities: 6863,
|
||||
entity_clients: 6863,
|
||||
non_entity_tokens: 6801,
|
||||
non_entity_clients: 6801,
|
||||
clients: 13664,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'path-2',
|
||||
counts: {
|
||||
distinct_entities: 6047,
|
||||
entity_clients: 6047,
|
||||
non_entity_tokens: 5957,
|
||||
non_entity_clients: 5957,
|
||||
clients: 12004,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'path-1',
|
||||
counts: {
|
||||
distinct_entities: 6189,
|
||||
entity_clients: 6189,
|
||||
non_entity_tokens: 5023,
|
||||
non_entity_clients: 5023,
|
||||
clients: 11212,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up2/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 50,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 23,
|
||||
clients: 73,
|
||||
},
|
||||
},
|
||||
{
|
||||
mount_path: 'auth/up1/',
|
||||
counts: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 25,
|
||||
non_entity_tokens: 0,
|
||||
non_entity_clients: 15,
|
||||
clients: 40,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SOME_OBJECT = { foo: 'bar' };
|
||||
|
||||
test('formatByMonths: formats the months array', async function (assert) {
|
||||
assert.expect(101);
|
||||
const keyNameAssertions = (object, objectName) => {
|
||||
const objectKeys = Object.keys(object);
|
||||
assert.false(objectKeys.includes('counts'), `${objectName} doesn't include 'counts' key`);
|
||||
assert.true(objectKeys.includes('clients'), `${objectName} includes 'clients' key`);
|
||||
assert.true(objectKeys.includes('entity_clients'), `${objectName} includes 'entity_clients' key`);
|
||||
assert.true(
|
||||
objectKeys.includes('non_entity_clients'),
|
||||
`${objectName} includes 'non_entity_clients' key`
|
||||
);
|
||||
};
|
||||
const assertClientCounts = (object, originalObject) => {
|
||||
let newObjectKeys = ['clients', 'entity_clients', 'non_entity_clients'];
|
||||
let originalKeys = Object.keys(originalObject.counts).includes('entity_clients')
|
||||
? newObjectKeys
|
||||
: ['clients', 'distinct_entities', 'non_entity_tokens'];
|
||||
|
||||
newObjectKeys.forEach((key, i) => {
|
||||
assert.equal(
|
||||
object[key],
|
||||
originalObject.counts[originalKeys[i]],
|
||||
`${object.month} ${key} equal original counts`
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const formattedMonths = formatByMonths(MONTHS);
|
||||
assert.notEqual(formattedMonths, MONTHS, 'does not modify original array');
|
||||
|
||||
formattedMonths.forEach((month) => {
|
||||
const originalMonth = MONTHS.find((m) => month.month === parseAPITimestamp(m.timestamp, 'M/yy'));
|
||||
// if originalMonth is found (not undefined) then the formatted month has an accurate, parsed timestamp
|
||||
assert.ok(originalMonth, `month has parsed timestamp of ${month.month}`);
|
||||
assert.ok(month.namespaces_by_key, `month includes 'namespaces_by_key' key`);
|
||||
|
||||
keyNameAssertions(month, 'formatted month');
|
||||
assertClientCounts(month, originalMonth);
|
||||
|
||||
assert.ok(month.new_clients.month, 'new clients key has a month key');
|
||||
keyNameAssertions(month.new_clients, 'formatted month new_clients');
|
||||
assertClientCounts(month.new_clients, originalMonth.new_clients);
|
||||
|
||||
month.namespaces.forEach((namespace) => keyNameAssertions(namespace, 'namespace within month'));
|
||||
month.new_clients.namespaces.forEach((namespace) =>
|
||||
keyNameAssertions(namespace, 'new client namespaces within month')
|
||||
);
|
||||
});
|
||||
|
||||
assert.equal(formatByMonths(SOME_OBJECT), SOME_OBJECT, 'it returns if arg is not an array');
|
||||
});
|
||||
|
||||
test('formatByNamespace: formats namespace arrays with and without mounts', async function (assert) {
|
||||
assert.expect(102);
|
||||
const keyNameAssertions = (object, objectName) => {
|
||||
const objectKeys = Object.keys(object);
|
||||
assert.false(objectKeys.includes('counts'), `${objectName} doesn't include 'counts' key`);
|
||||
assert.true(objectKeys.includes('label'), `${objectName} includes 'label' key`);
|
||||
assert.true(objectKeys.includes('clients'), `${objectName} includes 'clients' key`);
|
||||
assert.true(objectKeys.includes('entity_clients'), `${objectName} includes 'entity_clients' key`);
|
||||
assert.true(
|
||||
objectKeys.includes('non_entity_clients'),
|
||||
`${objectName} includes 'non_entity_clients' key`
|
||||
);
|
||||
};
|
||||
const keyValueAssertions = (object, pathName, originalObject) => {
|
||||
const keysToAssert = ['clients', 'entity_clients', 'non_entity_clients'];
|
||||
assert.equal(object.label, originalObject[pathName], `${pathName} matches label`);
|
||||
|
||||
keysToAssert.forEach((key) => {
|
||||
assert.equal(object[key], originalObject.counts[key], `number of ${key} equal original`);
|
||||
});
|
||||
};
|
||||
|
||||
const formattedNamespaces = formatByNamespace(BY_NAMESPACE);
|
||||
assert.notEqual(formattedNamespaces, MONTHS, 'does not modify original array');
|
||||
|
||||
formattedNamespaces.forEach((namespace) => {
|
||||
let origNamespace = BY_NAMESPACE.find((ns) => ns.namespace_path === namespace.label);
|
||||
keyNameAssertions(namespace, 'formatted namespace');
|
||||
keyValueAssertions(namespace, 'namespace_path', origNamespace);
|
||||
|
||||
namespace.mounts.forEach((mount) => {
|
||||
let origMount = origNamespace.mounts.find((m) => m.mount_path === mount.label);
|
||||
keyNameAssertions(mount, 'formatted mount');
|
||||
keyValueAssertions(mount, 'mount_path', origMount);
|
||||
});
|
||||
});
|
||||
|
||||
const nsWithoutMounts = {
|
||||
namespace_id: '96OwG',
|
||||
namespace_path: 'no-mounts-ns/',
|
||||
counts: {
|
||||
distinct_entities: 18290,
|
||||
entity_clients: 18290,
|
||||
non_entity_tokens: 18738,
|
||||
non_entity_clients: 18738,
|
||||
clients: 37028,
|
||||
},
|
||||
mounts: [],
|
||||
};
|
||||
|
||||
let formattedNsWithoutMounts = formatByNamespace([nsWithoutMounts])[0];
|
||||
keyNameAssertions(formattedNsWithoutMounts, 'namespace without mounts');
|
||||
keyValueAssertions(formattedNsWithoutMounts, 'namespace_path', nsWithoutMounts);
|
||||
assert.equal(formattedNsWithoutMounts.mounts.length, 0, 'formatted namespace has no mounts');
|
||||
|
||||
assert.equal(formatByNamespace(SOME_OBJECT), SOME_OBJECT, 'it returns if arg is not an array');
|
||||
});
|
||||
|
||||
test('homogenizeClientNaming: homogenizes key names when both old and new keys exist, or just old key names', async function (assert) {
|
||||
assert.expect(168);
|
||||
const keyNameAssertions = (object, objectName) => {
|
||||
const objectKeys = Object.keys(object);
|
||||
assert.false(
|
||||
objectKeys.includes('distinct_entities'),
|
||||
`${objectName} doesn't include 'distinct_entities' key`
|
||||
);
|
||||
assert.false(
|
||||
objectKeys.includes('non_entity_tokens'),
|
||||
`${objectName} doesn't include 'non_entity_tokens' key`
|
||||
);
|
||||
assert.true(objectKeys.includes('entity_clients'), `${objectName} includes 'entity_clients' key`);
|
||||
assert.true(
|
||||
objectKeys.includes('non_entity_clients'),
|
||||
`${objectName} includes 'non_entity_clients' key`
|
||||
);
|
||||
};
|
||||
|
||||
let transformedMonths = [...MONTHS];
|
||||
transformedMonths.forEach((month) => {
|
||||
month.counts = homogenizeClientNaming(month.counts);
|
||||
keyNameAssertions(month.counts, 'month counts');
|
||||
|
||||
month.new_clients.counts = homogenizeClientNaming(month.new_clients.counts);
|
||||
keyNameAssertions(month.new_clients.counts, 'month new counts');
|
||||
|
||||
month.namespaces.forEach((ns) => {
|
||||
ns.counts = homogenizeClientNaming(ns.counts);
|
||||
keyNameAssertions(ns.counts, 'namespace counts');
|
||||
|
||||
ns.mounts.forEach((mount) => {
|
||||
mount.counts = homogenizeClientNaming(mount.counts);
|
||||
keyNameAssertions(mount.counts, 'mount counts');
|
||||
});
|
||||
});
|
||||
|
||||
month.new_clients.namespaces.forEach((ns) => {
|
||||
ns.counts = homogenizeClientNaming(ns.counts);
|
||||
keyNameAssertions(ns.counts, 'namespace new counts');
|
||||
|
||||
ns.mounts.forEach((mount) => {
|
||||
mount.counts = homogenizeClientNaming(mount.counts);
|
||||
keyNameAssertions(mount.counts, 'mount new counts');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('flattenDataset: removes the counts key and flattens the dataset', async function (assert) {
|
||||
assert.expect(18);
|
||||
const flattenedNamespace = flattenDataset(BY_NAMESPACE[0]);
|
||||
const flattenedMount = flattenDataset(BY_NAMESPACE[0].mounts[0]);
|
||||
const flattenedMonth = flattenDataset(MONTHS[0]);
|
||||
const flattenedNewMonthClients = flattenDataset(MONTHS[0].new_clients);
|
||||
const objectNullCounts = { counts: null, foo: 'bar' };
|
||||
|
||||
const keyNameAssertions = (object, objectName) => {
|
||||
const objectKeys = Object.keys(object);
|
||||
assert.false(objectKeys.includes('counts'), `${objectName} doesn't include 'counts' key`);
|
||||
assert.true(objectKeys.includes('clients'), `${objectName} includes 'clients' key`);
|
||||
assert.true(objectKeys.includes('entity_clients'), `${objectName} includes 'entity_clients' key`);
|
||||
assert.true(
|
||||
objectKeys.includes('non_entity_clients'),
|
||||
`${objectName} includes 'non_entity_clients' key`
|
||||
);
|
||||
};
|
||||
|
||||
keyNameAssertions(flattenedNamespace, 'namespace object');
|
||||
keyNameAssertions(flattenedMount, 'mount object');
|
||||
keyNameAssertions(flattenedMonth, 'month object');
|
||||
keyNameAssertions(flattenedNewMonthClients, 'month new_clients object');
|
||||
|
||||
assert.equal(
|
||||
flattenDataset(SOME_OBJECT),
|
||||
SOME_OBJECT,
|
||||
"it returns original object if counts key doesn't exist"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
flattenDataset(objectNullCounts),
|
||||
objectNullCounts,
|
||||
'it returns original object if counts are null'
|
||||
);
|
||||
});
|
||||
|
||||
test('sortMonthsByTimestamp: sorts timestamps chronologically, oldest to most recent', async function (assert) {
|
||||
assert.expect(3);
|
||||
const sortedMonths = sortMonthsByTimestamp(MONTHS);
|
||||
assert.ok(
|
||||
isBefore(parseAPITimestamp(sortedMonths[0].timestamp), parseAPITimestamp(sortedMonths[1].timestamp)),
|
||||
'first timestamp date is earlier than second'
|
||||
);
|
||||
assert.ok(
|
||||
isAfter(parseAPITimestamp(sortedMonths[2].timestamp), parseAPITimestamp(sortedMonths[1].timestamp)),
|
||||
'third timestamp date is later second'
|
||||
);
|
||||
assert.notEqual(sortedMonths, MONTHS, 'it does not modify original array');
|
||||
});
|
||||
|
||||
test('namespaceArrayToObject: transforms data without modifying original', async function (assert) {
|
||||
assert.expect(29);
|
||||
|
||||
const assertClientCounts = (object, originalObject) => {
|
||||
let valuesToCheck = ['clients', 'entity_clients', 'non_entity_clients'];
|
||||
|
||||
valuesToCheck.forEach((key) => {
|
||||
assert.equal(object[key], originalObject[key], `${key} equal original counts`);
|
||||
});
|
||||
};
|
||||
const totalClientsByNamespace = formatByNamespace(MONTHS[1].namespaces);
|
||||
const newClientsByNamespace = formatByNamespace(MONTHS[1].new_clients.namespaces);
|
||||
|
||||
const byNamespaceKeyObject = namespaceArrayToObject(
|
||||
totalClientsByNamespace,
|
||||
newClientsByNamespace,
|
||||
'10/21'
|
||||
);
|
||||
|
||||
assert.propEqual(
|
||||
totalClientsByNamespace,
|
||||
formatByNamespace(MONTHS[1].namespaces),
|
||||
'it does not modify original array'
|
||||
);
|
||||
assert.propEqual(
|
||||
newClientsByNamespace,
|
||||
formatByNamespace(MONTHS[1].new_clients.namespaces),
|
||||
'it does not modify original array'
|
||||
);
|
||||
|
||||
let namespaceKeys = Object.keys(byNamespaceKeyObject);
|
||||
namespaceKeys.forEach((nsKey) => {
|
||||
const newNsObject = byNamespaceKeyObject[nsKey];
|
||||
let originalNsData = totalClientsByNamespace.find((ns) => ns.label === nsKey);
|
||||
assertClientCounts(newNsObject, originalNsData);
|
||||
let mountKeys = Object.keys(newNsObject.mounts_by_key);
|
||||
mountKeys.forEach((mKey) => {
|
||||
let mountData = originalNsData.mounts.find((m) => m.label === mKey);
|
||||
assertClientCounts(newNsObject.mounts_by_key[mKey], mountData);
|
||||
});
|
||||
});
|
||||
|
||||
namespaceKeys.forEach((nsKey) => {
|
||||
const newNsObject = byNamespaceKeyObject[nsKey];
|
||||
let originalNsData = newClientsByNamespace.find((ns) => ns.label === nsKey);
|
||||
if (!originalNsData) return;
|
||||
assertClientCounts(newNsObject.new_clients, originalNsData);
|
||||
let mountKeys = Object.keys(newNsObject.mounts_by_key);
|
||||
|
||||
mountKeys.forEach((mKey) => {
|
||||
let mountData = originalNsData.mounts.find((m) => m.label === mKey);
|
||||
assertClientCounts(newNsObject.mounts_by_key[mKey].new_clients, mountData);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue