mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 20:17:59 +00:00
UI/Add upgrade indicator client charts (#15083)
* clean up activity serailizer * fix line chart so only plot months with data * cleanup monthly serializer * account for empty months in vertical bar chart * tidy version upgrade info * fix version history model typo * extract const into helper * add upgrade indicator to line chart * fix tests * add todos
This commit is contained in:
@@ -7,9 +7,6 @@ export default class Current extends Component {
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
{ key: 'non_entity_clients', label: 'non-entity clients' },
|
||||
];
|
||||
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0
|
||||
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp
|
||||
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked namespaceArray = this.byNamespaceTotalClients.map((namespace) => {
|
||||
return { name: namespace['label'], id: namespace['label'] };
|
||||
@@ -18,6 +15,12 @@ export default class Current extends Component {
|
||||
@tracked selectedAuthMethod = null;
|
||||
@tracked authMethodOptions = [];
|
||||
|
||||
get latestUpgradeData() {
|
||||
// e.g. {id: '1.9.0', previousVersion: null, timestampInstalled: '2021-11-03T10:23:16Z'}
|
||||
// version id is 1.9.0 or earliest upgrade post 1.9.0, timestamp is RFC3339
|
||||
return this.args.model.versionHistory[0] || null;
|
||||
}
|
||||
|
||||
// Response total client count data by namespace for current/partial month
|
||||
get byNamespaceTotalClients() {
|
||||
return this.args.model.monthly?.byNamespaceTotalClients || [];
|
||||
@@ -74,11 +77,10 @@ export default class Current extends Component {
|
||||
}
|
||||
|
||||
get countsIncludeOlderData() {
|
||||
let firstUpgrade = this.args.model.versionHistory[0];
|
||||
if (!firstUpgrade) {
|
||||
if (!this.latestUpgradeData) {
|
||||
return false;
|
||||
}
|
||||
let versionDate = new Date(firstUpgrade.timestampInstalled);
|
||||
let versionDate = new Date(this.latestUpgradeData.timestampInstalled);
|
||||
// compare against this month and this year to show message or not.
|
||||
return isAfter(versionDate, startOfMonth(new Date())) ? versionDate : false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { isSameMonth, isAfter } from 'date-fns';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
|
||||
const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
|
||||
|
||||
@@ -11,20 +12,7 @@ export default class History extends Component {
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
arrayOfMonths = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
arrayOfMonths = ARRAY_OF_MONTHS;
|
||||
|
||||
chartLegend = [
|
||||
{ key: 'entity_clients', label: 'entity clients' },
|
||||
@@ -56,10 +44,6 @@ export default class History extends Component {
|
||||
@tracked startTimeRequested = null;
|
||||
@tracked queriedActivityResponse = null;
|
||||
|
||||
// VERSION/UPGRADE INFO
|
||||
@tracked firstUpgradeVersion = this.args.model.versionHistory[0].id || null; // return 1.9.0 or earliest upgrade post 1.9.0
|
||||
@tracked upgradeDate = this.args.model.versionHistory[0].timestampInstalled || null; // returns RFC3339 timestamp
|
||||
|
||||
// SEARCH SELECT
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked namespaceArray = this.getActivityResponse.byNamespace
|
||||
@@ -98,17 +82,17 @@ export default class History extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
// on init API response uses license start_date, getter updates when user queries dates
|
||||
get getActivityResponse() {
|
||||
return this.queriedActivityResponse || this.args.model.activity;
|
||||
get isDateRange() {
|
||||
return !isSameMonth(
|
||||
new Date(this.getActivityResponse.startTime),
|
||||
new Date(this.getActivityResponse.endTime)
|
||||
);
|
||||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return !!this.totalClientsData && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
|
||||
get latestUpgradeData() {
|
||||
// {id: '1.9.0', previousVersion: null, timestampInstalled: '2021-11-03T10:23:16Z'}
|
||||
// version id is 1.9.0 or earliest upgrade post 1.9.0, timestamp is RFC3339
|
||||
return this.args.model.versionHistory[0] || null;
|
||||
}
|
||||
|
||||
get startTimeDisplay() {
|
||||
@@ -129,25 +113,19 @@ export default class History extends Component {
|
||||
return `${this.arrayOfMonths[month]} ${year}`;
|
||||
}
|
||||
|
||||
get filteredActivity() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse;
|
||||
}
|
||||
if (!auth) {
|
||||
return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace);
|
||||
}
|
||||
return this.getActivityResponse.byNamespace
|
||||
.find((ns) => ns.label === namespace)
|
||||
.mounts?.find((mount) => mount.label === auth);
|
||||
// GETTERS FOR RESPONSE & DATA
|
||||
|
||||
// on init API response uses license start_date, getter updates when user queries dates
|
||||
get getActivityResponse() {
|
||||
return this.queriedActivityResponse || this.args.model.activity;
|
||||
}
|
||||
|
||||
get isDateRange() {
|
||||
return !isSameMonth(
|
||||
new Date(this.getActivityResponse.startTime),
|
||||
new Date(this.getActivityResponse.endTime)
|
||||
);
|
||||
get hasAttributionData() {
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return !!this.totalClientsData && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
|
||||
}
|
||||
|
||||
// top level TOTAL client counts for given date range
|
||||
@@ -169,25 +147,35 @@ export default class History extends Component {
|
||||
}
|
||||
|
||||
get byMonthTotalClients() {
|
||||
return this.getActivityResponse?.byMonthTotalClients;
|
||||
return this.getActivityResponse?.byMonth;
|
||||
}
|
||||
|
||||
get byMonthNewClients() {
|
||||
return this.getActivityResponse?.byMonthNewClients;
|
||||
return this.byMonthTotalClients.map((m) => m.new_clients);
|
||||
}
|
||||
|
||||
get countsIncludeOlderData() {
|
||||
let firstUpgrade = this.args.model.versionHistory[0];
|
||||
if (!firstUpgrade) {
|
||||
if (!this.latestUpgradeData) {
|
||||
return false;
|
||||
}
|
||||
let versionDate = new Date(firstUpgrade.timestampInstalled);
|
||||
let startTimeFromResponseAsDateObject = new Date(
|
||||
Number(this.startTimeFromResponse[0]),
|
||||
this.startTimeFromResponse[1]
|
||||
);
|
||||
// compare against this startTimeFromResponse to show message or not.
|
||||
return isAfter(versionDate, startTimeFromResponseAsDateObject) ? versionDate : false;
|
||||
let versionDate = new Date(this.latestUpgradeData.timestampInstalled);
|
||||
let startTimeFromResponse = new Date(this.getActivityResponse.startTime);
|
||||
// compare against this start date returned from API to show message or not.
|
||||
return isAfter(versionDate, startTimeFromResponse) ? versionDate : false;
|
||||
}
|
||||
|
||||
get filteredActivity() {
|
||||
const namespace = this.selectedNamespace;
|
||||
const auth = this.selectedAuthMethod;
|
||||
if (!namespace && !auth) {
|
||||
return this.getActivityResponse;
|
||||
}
|
||||
if (!auth) {
|
||||
return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace);
|
||||
}
|
||||
return this.getActivityResponse.byNamespace
|
||||
.find((ns) => ns.label === namespace)
|
||||
.mounts?.find((mount) => mount.label === auth);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -6,7 +6,7 @@ import { select, event, selectAll } from 'd3-selection';
|
||||
import { scaleLinear, scaleBand } from 'd3-scale';
|
||||
import { axisLeft } from 'd3-axis';
|
||||
import { max, maxIndex } from 'd3-array';
|
||||
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from '../../utils/chart-helpers';
|
||||
import { BAR_COLOR_HOVER, GREY, LIGHT_AND_DARK_BLUE, formatTooltipNumber } from 'vault/utils/chart-helpers';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,13 @@ import { select, selectAll, node } from 'd3-selection';
|
||||
import { axisLeft, axisBottom } from 'd3-axis';
|
||||
import { scaleLinear, scalePoint } from 'd3-scale';
|
||||
import { line } from 'd3-shape';
|
||||
import { LIGHT_AND_DARK_BLUE, SVG_DIMENSIONS, formatNumbers } from '../../utils/chart-helpers';
|
||||
import {
|
||||
LIGHT_AND_DARK_BLUE,
|
||||
UPGRADE_WARNING,
|
||||
SVG_DIMENSIONS,
|
||||
formatNumbers,
|
||||
} from 'vault/utils/chart-helpers';
|
||||
import { parseAPITimestamp, formatChartDate } from 'core/utils/date-formatters';
|
||||
|
||||
/**
|
||||
* @module LineChart
|
||||
@@ -26,6 +32,7 @@ export default class LineChart extends Component {
|
||||
@tracked tooltipMonth = '';
|
||||
@tracked tooltipTotal = '';
|
||||
@tracked tooltipNew = '';
|
||||
@tracked tooltipUpgradeText = '';
|
||||
|
||||
get yKey() {
|
||||
return this.args.yKey || 'clients';
|
||||
@@ -42,21 +49,29 @@ export default class LineChart extends Component {
|
||||
@action
|
||||
renderChart(element, args) {
|
||||
const dataset = args[0];
|
||||
let upgradeMonth, currentVersion, previousVersion;
|
||||
if (args[1]) {
|
||||
upgradeMonth = parseAPITimestamp(args[1].timestampInstalled, 'M/yy');
|
||||
currentVersion = args[1].id;
|
||||
previousVersion = args[1].previousVersion;
|
||||
}
|
||||
const filteredData = dataset.filter((e) => Object.keys(e).includes(this.yKey)); // months with data will contain a 'clients' key (otherwise only a timestamp)
|
||||
const chartSvg = select(element);
|
||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||
|
||||
// DEFINE AXES SCALES
|
||||
const yScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d[this.yKey]))])
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
|
||||
const yAxisScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d[this.yKey]))])
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([SVG_DIMENSIONS.height, 0])
|
||||
.nice();
|
||||
|
||||
const xScale = scalePoint() // use scaleTime()?
|
||||
// use full dataset (instead of filteredData) so x-axis spans months with and without data
|
||||
const xScale = scalePoint()
|
||||
.domain(dataset.map((d) => d[this.xKey]))
|
||||
.range([0, SVG_DIMENSIONS.width])
|
||||
.padding(0.2);
|
||||
@@ -75,6 +90,20 @@ export default class LineChart extends Component {
|
||||
|
||||
chartSvg.selectAll('.domain').remove();
|
||||
|
||||
// VERSION UPGRADE INDICATOR
|
||||
chartSvg
|
||||
.append('g')
|
||||
.selectAll('circle')
|
||||
.data(filteredData)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'upgrade-circle')
|
||||
.attr('fill', UPGRADE_WARNING)
|
||||
.style('opacity', (d) => (d[this.xKey] === upgradeMonth ? '1' : '0'))
|
||||
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
|
||||
.attr('cx', (d) => xScale(d[this.xKey]))
|
||||
.attr('r', 10);
|
||||
|
||||
// PATH BETWEEN PLOT POINTS
|
||||
const lineGenerator = line()
|
||||
.x((d) => xScale(d[this.xKey]))
|
||||
@@ -86,13 +115,13 @@ export default class LineChart extends Component {
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', LIGHT_AND_DARK_BLUE[1])
|
||||
.attr('stroke-width', 0.5)
|
||||
.attr('d', lineGenerator(dataset));
|
||||
.attr('d', lineGenerator(filteredData));
|
||||
|
||||
// LINE PLOTS (CIRCLES)
|
||||
chartSvg
|
||||
.append('g')
|
||||
.selectAll('circle')
|
||||
.data(dataset)
|
||||
.data(filteredData)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'data-plot')
|
||||
@@ -107,7 +136,7 @@ export default class LineChart extends Component {
|
||||
chartSvg
|
||||
.append('g')
|
||||
.selectAll('circle')
|
||||
.data(dataset)
|
||||
.data(filteredData)
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'hover-circle')
|
||||
@@ -122,9 +151,13 @@ export default class LineChart extends Component {
|
||||
// MOUSE EVENT FOR TOOLTIP
|
||||
hoverCircles.on('mouseover', (data) => {
|
||||
// TODO: how to genericize this?
|
||||
this.tooltipMonth = data[this.xKey];
|
||||
this.tooltipTotal = `${data[this.yKey]} total clients`;
|
||||
this.tooltipNew = `${data?.new_clients[this.yKey]} new clients`;
|
||||
this.tooltipMonth = formatChartDate(data[this.xKey]);
|
||||
this.tooltipTotal = data[this.yKey] + ' total clients';
|
||||
this.tooltipNew = data?.new_clients[this.yKey] + ' new clients';
|
||||
this.tooltipUpgradeText =
|
||||
data[this.xKey] === upgradeMonth
|
||||
? `Vault was upgraded ${previousVersion ? 'from ' + previousVersion : ''} to ${currentVersion}`
|
||||
: '';
|
||||
let node = hoverCircles.filter((plot) => plot[this.xKey] === data[this.xKey]).node();
|
||||
this.tooltipTarget = node;
|
||||
});
|
||||
|
||||
@@ -21,27 +21,25 @@ import { mean } from 'd3-array';
|
||||
month: '1/22',
|
||||
entity_clients: 23,
|
||||
non_entity_clients: 45,
|
||||
total: 68,
|
||||
clients: 68,
|
||||
namespaces: [],
|
||||
new_clients: {
|
||||
entity_clients: 11,
|
||||
non_entity_clients: 36,
|
||||
total: 47,
|
||||
clients: 47,
|
||||
namespaces: [],
|
||||
},
|
||||
}
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
*/
|
||||
export default class MonthlyUsage extends Component {
|
||||
barChartData = this.args.verticalBarChartData;
|
||||
|
||||
get averageTotalClients() {
|
||||
let average = mean(this.barChartData?.map((d) => d.total));
|
||||
let average = mean(this.args.verticalBarChartData?.map((d) => d.clients));
|
||||
return Math.round(average) || null;
|
||||
}
|
||||
|
||||
get averageNewClients() {
|
||||
let average = mean(this.barChartData?.map((d) => d.new_clients.total));
|
||||
let average = mean(this.args.verticalBarChartData?.map((d) => d.new_clients.clients));
|
||||
return Math.round(average) || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { mean } from 'd3-array';
|
||||
@barChartData={{this.byMonthNewClients}}
|
||||
@lineChartData={{this.byMonth}}
|
||||
@runningTotals={{this.runningTotals}}
|
||||
@upgradeData={{if this.countsIncludeOlderData this.latestUpgradeData}}
|
||||
/>
|
||||
* ```
|
||||
|
||||
@@ -35,6 +36,7 @@ import { mean } from 'd3-array';
|
||||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
* @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'}
|
||||
*
|
||||
*/
|
||||
export default class RunningTotal extends Component {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SVG_DIMENSIONS,
|
||||
TRANSLATE,
|
||||
formatNumbers,
|
||||
} from '../../utils/chart-helpers';
|
||||
} from 'vault/utils/chart-helpers';
|
||||
|
||||
/**
|
||||
* @module VerticalBarChart
|
||||
@@ -49,24 +49,25 @@ export default class VerticalBarChart extends Component {
|
||||
|
||||
@action
|
||||
registerListener(element, args) {
|
||||
let dataset = args[0];
|
||||
let stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
|
||||
let stackedData = stackFunction(dataset);
|
||||
let chartSvg = select(element);
|
||||
const dataset = args[0];
|
||||
const filteredData = dataset.filter((e) => Object.keys(e).includes('clients')); // months with data will contain a 'clients' key (otherwise only a timestamp)
|
||||
const stackFunction = stack().keys(this.chartLegend.map((l) => l.key));
|
||||
const stackedData = stackFunction(filteredData);
|
||||
const chartSvg = select(element);
|
||||
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
|
||||
|
||||
// DEFINE DATA BAR SCALES
|
||||
let yScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d[this.yKey]))])
|
||||
const yScale = scaleLinear()
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
|
||||
let xScale = scaleBand()
|
||||
const xScale = scaleBand()
|
||||
.domain(dataset.map((d) => d[this.xKey]))
|
||||
.range([0, SVG_DIMENSIONS.width]) // set width to fix number of pixels
|
||||
.paddingInner(0.85);
|
||||
|
||||
let dataBars = chartSvg
|
||||
const dataBars = chartSvg
|
||||
.selectAll('g')
|
||||
.data(stackedData)
|
||||
.enter()
|
||||
@@ -85,18 +86,18 @@ export default class VerticalBarChart extends Component {
|
||||
.attr('y', (data) => `${100 - yScale(data[1])}%`); // subtract higher than 100% to give space for x axis ticks
|
||||
|
||||
// MAKE AXES //
|
||||
let yAxisScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d[this.yKey]))])
|
||||
const yAxisScale = scaleLinear()
|
||||
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
|
||||
.range([`${SVG_DIMENSIONS.height}`, 0])
|
||||
.nice();
|
||||
|
||||
let yAxis = axisLeft(yAxisScale)
|
||||
const yAxis = axisLeft(yAxisScale)
|
||||
.ticks(4)
|
||||
.tickPadding(10)
|
||||
.tickSizeInner(-SVG_DIMENSIONS.width)
|
||||
.tickFormat(formatNumbers);
|
||||
|
||||
let xAxis = axisBottom(xScale).tickSize(0);
|
||||
const xAxis = axisBottom(xScale).tickSize(0);
|
||||
|
||||
yAxis(chartSvg.append('g'));
|
||||
xAxis(chartSvg.append('g').attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`));
|
||||
@@ -104,16 +105,16 @@ export default class VerticalBarChart extends Component {
|
||||
chartSvg.selectAll('.domain').remove(); // remove domain lines
|
||||
|
||||
// WIDER SELECTION AREA FOR TOOLTIP HOVER
|
||||
let greyBars = chartSvg
|
||||
const greyBars = chartSvg
|
||||
.append('g')
|
||||
.attr('transform', `translate(${TRANSLATE.left})`)
|
||||
.style('fill', `${GREY}`)
|
||||
.style('opacity', '0')
|
||||
.style('mix-blend-mode', 'multiply');
|
||||
|
||||
let tooltipRect = greyBars
|
||||
const tooltipRect = greyBars
|
||||
.selectAll('rect')
|
||||
.data(dataset)
|
||||
.data(filteredData)
|
||||
.enter()
|
||||
.append('rect')
|
||||
.style('cursor', 'pointer')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
export default class Activity extends Model {
|
||||
@attr('array') byMonthTotalClients;
|
||||
@attr('array') byMonthNewClients;
|
||||
@attr('array') byMonth;
|
||||
@attr('array') byNamespace;
|
||||
@attr('object') total;
|
||||
@attr('array') formattedEndTime;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Model, { attr } from '@ember-data/model';
|
||||
export default class MonthlyModel extends Model {
|
||||
@attr('string') responseTimestamp;
|
||||
@attr('array') byNamespace;
|
||||
@attr('object') total; // total clients during the current/partial month
|
||||
@attr('object') new; // total NEW clients during the current/partial
|
||||
@attr('array') byNamespaceTotalClients;
|
||||
|
||||
@@ -12,7 +12,7 @@ export default class ClientsRoute extends Route {
|
||||
response.forEach((model) => {
|
||||
arrayOfModels.push({
|
||||
id: model.id,
|
||||
perviousVersion: model.previousVersion,
|
||||
previousVersion: model.previousVersion,
|
||||
timestampInstalled: model.timestampInstalled,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,30 +2,28 @@ import ApplicationSerializer from '../application';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { parseAPITimestamp, parseRFC3339 } from 'core/utils/date-formatters';
|
||||
export default class ActivitySerializer extends ApplicationSerializer {
|
||||
flattenDataset(namespaceArray) {
|
||||
return namespaceArray.map((ns) => {
|
||||
flattenDataset(object) {
|
||||
let flattenedObject = {};
|
||||
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
|
||||
return this.homogenizeClientNaming(flattenedObject);
|
||||
}
|
||||
|
||||
formatByNamespace(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'];
|
||||
let flattenedNs = {};
|
||||
// we don't want client counts nested within the 'counts' object for stacked charts
|
||||
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
|
||||
flattenedNs = this.homogenizeClientNaming(flattenedNs);
|
||||
|
||||
let flattenedNs = this.flattenDataset(ns);
|
||||
// if no mounts, mounts will be an empty array
|
||||
flattenedNs.mounts = ns.mounts
|
||||
? ns.mounts.map((mount) => {
|
||||
let flattenedMount = {};
|
||||
let label = mount['mount_path'];
|
||||
Object.keys(mount['counts']).forEach((key) => (flattenedMount[key] = mount['counts'][key]));
|
||||
flattenedMount = this.homogenizeClientNaming(flattenedMount);
|
||||
return {
|
||||
label,
|
||||
...flattenedMount,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
flattenedNs.mounts = [];
|
||||
if (ns?.mounts && ns.mounts.length > 0) {
|
||||
flattenedNs.mounts = ns.mounts.map((mount) => {
|
||||
return {
|
||||
label: mount['mount_path'],
|
||||
...this.flattenDataset(mount),
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
label,
|
||||
...flattenedNs,
|
||||
@@ -33,36 +31,34 @@ export default class ActivitySerializer extends ApplicationSerializer {
|
||||
});
|
||||
}
|
||||
|
||||
flattenByMonths(payload, isNewClients = false) {
|
||||
const sortedPayload = [...payload];
|
||||
formatByMonths(monthsArray) {
|
||||
const sortedPayload = [...monthsArray];
|
||||
// months are always returned from the API: [mostRecent...oldestMonth]
|
||||
sortedPayload.reverse();
|
||||
if (isNewClients) {
|
||||
return sortedPayload?.map((m) => {
|
||||
return sortedPayload.map((m) => {
|
||||
if (Object.keys(m).includes('counts')) {
|
||||
let totalClients = this.flattenDataset(m);
|
||||
let newClients = this.flattenDataset(m.new_clients);
|
||||
return {
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
entity_clients: m.new_clients.counts.entity_clients,
|
||||
non_entity_clients: m.new_clients.counts.non_entity_clients,
|
||||
clients: m.new_clients.counts.clients,
|
||||
namespaces: this.flattenDataset(m.new_clients.namespaces),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return sortedPayload?.map((m) => {
|
||||
return {
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
entity_clients: m.counts.entity_clients,
|
||||
non_entity_clients: m.counts.non_entity_clients,
|
||||
clients: m.counts.clients,
|
||||
namespaces: this.flattenDataset(m.namespaces),
|
||||
...totalClients,
|
||||
namespaces: this.formatByNamespace(m.namespaces),
|
||||
new_clients: {
|
||||
entity_clients: m.new_clients.counts.entity_clients,
|
||||
non_entity_clients: m.new_clients.counts.non_entity_clients,
|
||||
clients: m.new_clients.counts.clients,
|
||||
namespaces: this.flattenDataset(m.new_clients.namespaces),
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
...newClients,
|
||||
namespaces: this.formatByNamespace(m.new_clients.namespaces),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO CMB below is an assumption, need to test
|
||||
// if no monthly data (no counts key), month object will just contain a timestamp
|
||||
return {
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
new_clients: {
|
||||
month: parseAPITimestamp(m.timestamp, 'M/yy'),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// In 1.10 'distinct_entities' changed to 'entity_clients' and
|
||||
@@ -86,7 +82,6 @@ export default class ActivitySerializer extends ApplicationSerializer {
|
||||
non_entity_clients: non_entity_tokens,
|
||||
};
|
||||
}
|
||||
// TODO CMB: test what to return if neither key exists
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -98,9 +93,8 @@ export default class ActivitySerializer extends ApplicationSerializer {
|
||||
let transformedPayload = {
|
||||
...payload,
|
||||
response_timestamp,
|
||||
by_namespace: this.flattenDataset(payload.data.by_namespace),
|
||||
by_month_total_clients: this.flattenByMonths(payload.data.months),
|
||||
by_month_new_clients: this.flattenByMonths(payload.data.months, { isNewClients: true }),
|
||||
by_namespace: this.formatByNamespace(payload.data.by_namespace),
|
||||
by_month: this.formatByMonths(payload.data.months),
|
||||
total: this.homogenizeClientNaming(payload.data.total),
|
||||
formatted_end_time: parseRFC3339(payload.data.end_time),
|
||||
formatted_start_time: parseRFC3339(payload.data.start_time),
|
||||
|
||||
@@ -2,30 +2,28 @@ import ApplicationSerializer from '../application';
|
||||
import { formatISO } from 'date-fns';
|
||||
|
||||
export default class MonthlySerializer extends ApplicationSerializer {
|
||||
flattenDataset(namespaceArray) {
|
||||
flattenDataset(object) {
|
||||
let flattenedObject = {};
|
||||
Object.keys(object['counts']).forEach((key) => (flattenedObject[key] = object['counts'][key]));
|
||||
return this.homogenizeClientNaming(flattenedObject);
|
||||
}
|
||||
|
||||
formatByNamespace(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'];
|
||||
let flattenedNs = {};
|
||||
// we don't want client counts nested within the 'counts' object for stacked charts
|
||||
Object.keys(ns['counts']).forEach((key) => (flattenedNs[key] = ns['counts'][key]));
|
||||
flattenedNs = this.homogenizeClientNaming(flattenedNs);
|
||||
|
||||
let flattenedNs = this.flattenDataset(ns);
|
||||
// if no mounts, mounts will be an empty array
|
||||
flattenedNs.mounts = ns.mounts
|
||||
? ns.mounts.map((mount) => {
|
||||
let flattenedMount = {};
|
||||
let label = mount['mount_path'];
|
||||
Object.keys(mount['counts']).forEach((key) => (flattenedMount[key] = mount['counts'][key]));
|
||||
flattenedMount = this.homogenizeClientNaming(flattenedMount);
|
||||
return {
|
||||
label,
|
||||
...flattenedMount,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
flattenedNs.mounts = [];
|
||||
if (ns?.mounts && ns.mounts.length > 0) {
|
||||
flattenedNs.mounts = ns.mounts.map((mount) => {
|
||||
return {
|
||||
label: mount['mount_path'],
|
||||
...this.flattenDataset(mount),
|
||||
};
|
||||
});
|
||||
}
|
||||
return {
|
||||
label,
|
||||
...flattenedNs,
|
||||
@@ -67,7 +65,7 @@ export default class MonthlySerializer extends ApplicationSerializer {
|
||||
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);
|
||||
by_namespace_new_clients = this.formatByNamespace(newClientsData.namespaces);
|
||||
new_clients = this.homogenizeClientNaming(newClientsData.counts);
|
||||
} else {
|
||||
by_namespace_new_clients = [];
|
||||
@@ -76,7 +74,7 @@ export default class MonthlySerializer extends ApplicationSerializer {
|
||||
let transformedPayload = {
|
||||
...payload,
|
||||
response_timestamp,
|
||||
by_namespace_total_clients: this.flattenDataset(payload.data.by_namespace),
|
||||
by_namespace_total_clients: this.formatByNamespace(payload.data.by_namespace),
|
||||
by_namespace_new_clients,
|
||||
// nest within 'total' object to mimic /activity response shape
|
||||
total: this.homogenizeClientNaming(payload.data),
|
||||
|
||||
@@ -54,9 +54,15 @@
|
||||
</ToolbarFilters>
|
||||
</Toolbar>
|
||||
</div>
|
||||
{{! TODO CMB this warning should only show if an upgrade to 1.9 or 1.10 happened in current month }}
|
||||
{{#if this.countsIncludeOlderData}}
|
||||
<AlertBanner @type="warning" @title="Warning">
|
||||
{{concat "You upgraded to Vault " this.firstUpgradeVersion " on " (date-format this.upgradeDate "MMMM d, yyyy.")}}
|
||||
{{concat
|
||||
"You upgraded to Vault "
|
||||
this.latestUpgradeData.id
|
||||
" on "
|
||||
(date-format this.latestUpgradeData.timestampInstalled "MMMM d, yyyy.")
|
||||
}}
|
||||
How we count clients changed in 1.9, so please keep that in mind when looking at the data below, and you can
|
||||
<DocLink @path="/docs/concepts/client-count/faq#q-which-vault-version-reflects-the-most-accurate-client-counts">
|
||||
read more here.
|
||||
|
||||
@@ -108,13 +108,14 @@
|
||||
{{#if this.responseRangeDiffMessage}}
|
||||
<li>{{this.responseRangeDiffMessage}}</li>
|
||||
{{/if}}
|
||||
{{! TODO this warning should only show if data spans an upgrade to 1.9 or 1.10 }}
|
||||
{{#if this.countsIncludeOlderData}}
|
||||
<li>
|
||||
{{concat
|
||||
"You upgraded to Vault "
|
||||
this.firstUpgradeVersion
|
||||
this.latestUpgradeData.id
|
||||
" on "
|
||||
(date-format this.upgradeDate "MMMM d, yyyy.")
|
||||
(date-format this.latestUpgradeData.timestampInstalled "MMMM d, yyyy.")
|
||||
}}
|
||||
How we count clients changed in 1.9, so please keep that in mind when looking at the data below, and you can
|
||||
<DocLink
|
||||
@@ -140,6 +141,7 @@
|
||||
@lineChartData={{this.byMonthTotalClients}}
|
||||
@barChartData={{this.byMonthNewClients}}
|
||||
@runningTotals={{this.totalUsageCounts}}
|
||||
@upgradeData={{if this.countsIncludeOlderData this.latestUpgradeData}}
|
||||
@timestamp={{this.responseTimestamp}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
data-test-line-chart
|
||||
class="chart has-grid"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.renderChart @dataset}}
|
||||
{{did-update this.renderChart @dataset}}
|
||||
{{did-insert this.renderChart @dataset @upgradeData}}
|
||||
{{did-update this.renderChart @dataset @upgradeData}}
|
||||
>
|
||||
</svg>
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
<p class="bold">{{this.tooltipMonth}}</p>
|
||||
<p>{{this.tooltipTotal}}</p>
|
||||
<p>{{this.tooltipNew}}</p>
|
||||
{{#if this.tooltipUpgradeText}}
|
||||
<br />
|
||||
<p class="has-text-highlight">{{this.tooltipUpgradeText}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="chart-tooltip-arrow"></div>
|
||||
{{/modal-dialog}}
|
||||
|
||||
|
Before Width: | Height: | Size: 851 B After Width: | Height: | Size: 1014 B |
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
|
||||
<div class="chart-container-wide">
|
||||
<Clients::LineChart @dataset={{@lineChartData}} />
|
||||
<Clients::LineChart @dataset={{@lineChartData}} @upgradeData={{@upgradeData}} />
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { format } from 'd3-format';
|
||||
|
||||
// COLOR THEME:
|
||||
export const LIGHT_AND_DARK_BLUE = ['#BFD4FF', '#1563FF'];
|
||||
export const UPGRADE_WARNING = '#FDEEBA';
|
||||
export const BAR_COLOR_HOVER = ['#1563FF', '#0F4FD1'];
|
||||
export const GREY = '#EBEEF2';
|
||||
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
import { format, parseISO } from 'date-fns';
|
||||
|
||||
export const ARRAY_OF_MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
// convert RFC3339 timestamp ( '2021-03-21T00:00:00Z' ) to date object, optionally format
|
||||
export const parseAPITimestamp = (timestamp, style) => {
|
||||
if (!timestamp) return;
|
||||
@@ -18,3 +33,11 @@ export const parseRFC3339 = (timestamp) => {
|
||||
let date = parseAPITimestamp(timestamp);
|
||||
return date ? [`${date.getFullYear()}`, date.getMonth()] : null;
|
||||
};
|
||||
|
||||
// convert MM/yy (format of dates in charts) to 'Month yyyy' (format in tooltip)
|
||||
export function formatChartDate(date) {
|
||||
let array = date.split('/');
|
||||
array.splice(1, 0, '01');
|
||||
let dateString = array.join('/');
|
||||
return format(new Date(dateString), 'MMMM yyyy');
|
||||
}
|
||||
|
||||
@@ -665,25 +665,7 @@ const handleMockQuery = (queryStartTimestamp, monthlyData) => {
|
||||
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: {
|
||||
distinct_entities: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 0,
|
||||
},
|
||||
namespaces: [],
|
||||
new_clients: {
|
||||
counts: {
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 0,
|
||||
},
|
||||
namespaces: [],
|
||||
},
|
||||
});
|
||||
transformedMonthlyArray.push({ timestamp });
|
||||
} while (i < differenceInCalendarMonths(startDateByMonth, queryDate));
|
||||
}
|
||||
if (isAfter(queryDate, startDateByMonth)) {
|
||||
@@ -696,19 +678,19 @@ export default function (server) {
|
||||
// 1.10 API response
|
||||
server.get('sys/version-history', function () {
|
||||
return {
|
||||
keys: ['1.9.0', '1.9.1', '1.9.2'],
|
||||
keys: ['1.10.0', '1.9.0', '1.9.1', '1.9.2'],
|
||||
key_info: {
|
||||
'1.9.0': {
|
||||
previous_version: null,
|
||||
timestamp_installed: '2021-11-03T10:23:16Z',
|
||||
timestamp_installed: '2021-07-03T10:23:16Z',
|
||||
},
|
||||
'1.9.1': {
|
||||
previous_version: '1.9.0',
|
||||
timestamp_installed: '2021-12-03T10:23:16Z',
|
||||
timestamp_installed: '2021-09-03T10:23:16Z',
|
||||
},
|
||||
'1.9.2': {
|
||||
'1.10.0': {
|
||||
previous_version: '1.9.1',
|
||||
timestamp_installed: '2021-01-03T10:23:16Z',
|
||||
timestamp_installed: '2021-10-03T10:23:16Z',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,25 +4,11 @@ import { render, click } from '@ember/test-helpers';
|
||||
import sinon from 'sinon';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import calendarDropdown from 'vault/tests/pages/components/calendar-widget';
|
||||
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
|
||||
|
||||
module('Integration | Component | calendar-widget', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const ARRAY_OF_MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.set('handleClientActivityQuery', sinon.spy());
|
||||
this.set('handleCurrentBillingPeriod', sinon.spy());
|
||||
|
||||
Reference in New Issue
Block a user