diff --git a/ui/app/components/clients/current.js b/ui/app/components/clients/current.js index 77d6835e60..ca3ba2a180 100644 --- a/ui/app/components/clients/current.js +++ b/ui/app/components/clients/current.js @@ -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; } diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js index d2cfc012b6..123be15e42 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -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 diff --git a/ui/app/components/clients/horizontal-bar-chart.js b/ui/app/components/clients/horizontal-bar-chart.js index ddd6539f40..377649b18c 100644 --- a/ui/app/components/clients/horizontal-bar-chart.js +++ b/ui/app/components/clients/horizontal-bar-chart.js @@ -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'; /** diff --git a/ui/app/components/clients/line-chart.js b/ui/app/components/clients/line-chart.js index 419dbe2b27..4b8a0e883c 100644 --- a/ui/app/components/clients/line-chart.js +++ b/ui/app/components/clients/line-chart.js @@ -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; }); diff --git a/ui/app/components/clients/monthly-usage.js b/ui/app/components/clients/monthly-usage.js index 9f2e2251c6..235bf3c2aa 100644 --- a/ui/app/components/clients/monthly-usage.js +++ b/ui/app/components/clients/monthly-usage.js @@ -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; } } diff --git a/ui/app/components/clients/running-total.js b/ui/app/components/clients/running-total.js index 80f4310ece..4651a57572 100644 --- a/ui/app/components/clients/running-total.js +++ b/ui/app/components/clients/running-total.js @@ -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 { diff --git a/ui/app/components/clients/vertical-bar-chart.js b/ui/app/components/clients/vertical-bar-chart.js index 2972911926..42a8de1dee 100644 --- a/ui/app/components/clients/vertical-bar-chart.js +++ b/ui/app/components/clients/vertical-bar-chart.js @@ -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') diff --git a/ui/app/models/clients/activity.js b/ui/app/models/clients/activity.js index 2462336989..70d8f49142 100644 --- a/ui/app/models/clients/activity.js +++ b/ui/app/models/clients/activity.js @@ -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; diff --git a/ui/app/models/clients/monthly.js b/ui/app/models/clients/monthly.js index cc02004698..40b3827687 100644 --- a/ui/app/models/clients/monthly.js +++ b/ui/app/models/clients/monthly.js @@ -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; diff --git a/ui/app/routes/vault/cluster/clients.js b/ui/app/routes/vault/cluster/clients.js index ef48445494..84e280887d 100644 --- a/ui/app/routes/vault/cluster/clients.js +++ b/ui/app/routes/vault/cluster/clients.js @@ -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, }); }); diff --git a/ui/app/serializers/clients/activity.js b/ui/app/serializers/clients/activity.js index c3cf93653a..05e8f319a3 100644 --- a/ui/app/serializers/clients/activity.js +++ b/ui/app/serializers/clients/activity.js @@ -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), diff --git a/ui/app/serializers/clients/monthly.js b/ui/app/serializers/clients/monthly.js index 78d1f9b32c..607ed1e30f 100644 --- a/ui/app/serializers/clients/monthly.js +++ b/ui/app/serializers/clients/monthly.js @@ -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), diff --git a/ui/app/templates/components/clients/current.hbs b/ui/app/templates/components/clients/current.hbs index c6affba772..367e040efa 100644 --- a/ui/app/templates/components/clients/current.hbs +++ b/ui/app/templates/components/clients/current.hbs @@ -54,9 +54,15 @@ + {{! TODO CMB this warning should only show if an upgrade to 1.9 or 1.10 happened in current month }} {{#if this.countsIncludeOlderData}} - {{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 read more here. diff --git a/ui/app/templates/components/clients/history.hbs b/ui/app/templates/components/clients/history.hbs index 57f4b83e35..90a7c34f58 100644 --- a/ui/app/templates/components/clients/history.hbs +++ b/ui/app/templates/components/clients/history.hbs @@ -108,13 +108,14 @@ {{#if this.responseRangeDiffMessage}}
  • {{this.responseRangeDiffMessage}}
  • {{/if}} + {{! TODO this warning should only show if data spans an upgrade to 1.9 or 1.10 }} {{#if this.countsIncludeOlderData}}
  • {{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 {{/if}} diff --git a/ui/app/templates/components/clients/line-chart.hbs b/ui/app/templates/components/clients/line-chart.hbs index 4e9f904ffa..142ec9a5cc 100644 --- a/ui/app/templates/components/clients/line-chart.hbs +++ b/ui/app/templates/components/clients/line-chart.hbs @@ -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}} > @@ -24,6 +24,10 @@

    {{this.tooltipMonth}}

    {{this.tooltipTotal}}

    {{this.tooltipNew}}

    + {{#if this.tooltipUpgradeText}} +
    +

    {{this.tooltipUpgradeText}}

    + {{/if}}
    {{/modal-dialog}} diff --git a/ui/app/templates/components/clients/running-total.hbs b/ui/app/templates/components/clients/running-total.hbs index 4168cdbf8b..4aef87b042 100644 --- a/ui/app/templates/components/clients/running-total.hbs +++ b/ui/app/templates/components/clients/running-total.hbs @@ -9,7 +9,7 @@
    - +
    diff --git a/ui/app/utils/chart-helpers.js b/ui/app/utils/chart-helpers.js index 22a30ed69e..6f131a61ce 100644 --- a/ui/app/utils/chart-helpers.js +++ b/ui/app/utils/chart-helpers.js @@ -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'; diff --git a/ui/lib/core/addon/utils/date-formatters.js b/ui/lib/core/addon/utils/date-formatters.js index c1cb5ac218..dec9af4170 100644 --- a/ui/lib/core/addon/utils/date-formatters.js +++ b/ui/lib/core/addon/utils/date-formatters.js @@ -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'); +} diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js index 1cec8bc325..6bdb52390e 100644 --- a/ui/mirage/handlers/clients.js +++ b/ui/mirage/handlers/clients.js @@ -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', }, }, }; diff --git a/ui/tests/integration/components/calendar-widget-test.js b/ui/tests/integration/components/calendar-widget-test.js index 575125ecf1..d6ef443e45 100644 --- a/ui/tests/integration/components/calendar-widget-test.js +++ b/ui/tests/integration/components/calendar-widget-test.js @@ -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());