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