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:
claire bontempo
2022-04-20 08:35:57 -07:00
committed by GitHub
parent 50afd00b0b
commit e74c1b29b3
20 changed files with 230 additions and 212 deletions

View File

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

View File

@@ -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

View File

@@ -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';
/**

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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')

View File

@@ -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;

View File

@@ -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;

View File

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

View File

@@ -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),

View File

@@ -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),

View File

@@ -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.

View File

@@ -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}}

View File

@@ -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

View File

@@ -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">

View File

@@ -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';

View File

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

View File

@@ -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',
},
},
};

View File

@@ -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());