diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js index e80038460369f..d2cfc012b61e6 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -168,6 +168,14 @@ export default class History extends Component { return this.getActivityResponse.responseTimestamp; } + get byMonthTotalClients() { + return this.getActivityResponse?.byMonthTotalClients; + } + + get byMonthNewClients() { + return this.getActivityResponse?.byMonthNewClients; + } + get countsIncludeOlderData() { let firstUpgrade = this.args.model.versionHistory[0]; if (!firstUpgrade) { diff --git a/ui/app/components/clients/line-chart.js b/ui/app/components/clients/line-chart.js index 2984a7606a57f..12564c2d5c800 100644 --- a/ui/app/components/clients/line-chart.js +++ b/ui/app/components/clients/line-chart.js @@ -17,7 +17,8 @@ import { LIGHT_AND_DARK_BLUE, SVG_DIMENSIONS, formatNumbers } from '../../utils/ * ```js * * ``` - * @param {array} dataset - dataset is an array of objects + * @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset + * @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset */ export default class LineChart extends Component { @@ -28,7 +29,7 @@ export default class LineChart extends Component { @tracked tooltipNew = ''; get yKey() { - return this.args.yKey || 'clients'; + return this.args.yKey || 'total'; } get xKey() { @@ -41,34 +42,34 @@ export default class LineChart extends Component { @action renderChart(element, args) { - let dataset = args[0]; - let chartSvg = select(element); + const dataset = args[0]; + const chartSvg = select(element); chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions // DEFINE AXES SCALES - let yScale = scaleLinear() + const yScale = scaleLinear() .domain([0, max(dataset.map((d) => d[this.yKey]))]) .range([0, 100]) .nice(); - let yAxisScale = scaleLinear() + const yAxisScale = scaleLinear() .domain([0, max(dataset.map((d) => d[this.yKey]))]) .range([SVG_DIMENSIONS.height, 0]) .nice(); - let xScale = scalePoint() // use scaleTime()? + const xScale = scalePoint() // use scaleTime()? .domain(dataset.map((d) => d[this.xKey])) .range([0, SVG_DIMENSIONS.width]) .padding(0.2); // CUSTOMIZE AND APPEND AXES - let yAxis = axisLeft(yAxisScale) - .ticks(7) + const yAxis = axisLeft(yAxisScale) + .ticks(4) .tickPadding(10) .tickSizeInner(-SVG_DIMENSIONS.width) // makes grid lines length of svg .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})`)); @@ -76,7 +77,7 @@ export default class LineChart extends Component { chartSvg.selectAll('.domain').remove(); // PATH BETWEEN PLOT POINTS - let lineGenerator = line() + const lineGenerator = line() .x((d) => xScale(d[this.xKey])) .y((d) => yAxisScale(d[this.yKey])); @@ -117,14 +118,14 @@ export default class LineChart extends Component { .attr('cx', (d) => xScale(d[this.xKey])) .attr('r', 10); - let hoverCircles = chartSvg.selectAll('.hover-circle'); + const hoverCircles = chartSvg.selectAll('.hover-circle'); // 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?.clients} new clients`; + this.tooltipNew = `${data?.new_clients[this.yKey]} new clients`; let node = hoverCircles.filter((plot) => plot[this.xKey] === data[this.xKey]).node(); this.tooltipTarget = node; }); diff --git a/ui/app/components/clients/vertical-bar-chart.js b/ui/app/components/clients/vertical-bar-chart.js index f6676f2bdef70..e757c969fb235 100644 --- a/ui/app/components/clients/vertical-bar-chart.js +++ b/ui/app/components/clients/vertical-bar-chart.js @@ -25,18 +25,28 @@ import { * ``` * @param {array} dataset - dataset for the chart, must be an array of flattened objects * @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked + * @param {string} xKey - string denoting key for x-axis data (data[xKey]) of dataset + * @param {string} yKey - string denoting key for y-axis data (data[yKey]) of dataset */ export default class VerticalBarChart extends Component { @tracked tooltipTarget = ''; @tracked tooltipTotal = ''; - @tracked uniqueEntities = ''; - @tracked nonEntityTokens = ''; + @tracked entityClients = ''; + @tracked nonEntityClients = ''; get chartLegend() { return this.args.chartLegend; } + get xKey() { + return this.args.xKey || 'month'; + } + + get yKey() { + return this.args.yKey || 'total'; + } + @action registerListener(element, args) { let dataset = args[0]; @@ -47,12 +57,12 @@ export default class VerticalBarChart extends Component { // DEFINE DATA BAR SCALES let yScale = scaleLinear() - .domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data + .domain([0, max(dataset.map((d) => d[this.yKey]))]) .range([0, 100]) .nice(); let xScale = scaleBand() - .domain(dataset.map((d) => d.month)) + .domain(dataset.map((d) => d[this.xKey])) .range([0, SVG_DIMENSIONS.width]) // set width to fix number of pixels .paddingInner(0.85); @@ -71,17 +81,17 @@ export default class VerticalBarChart extends Component { .attr('width', '7px') .attr('class', 'data-bar') .attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`) - .attr('x', ({ data }) => xScale(data.month)) // uses destructuring because was data.data.month + .attr('x', ({ data }) => xScale(data[this.xKey])) // uses destructuring because was data.data.month .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.clients))]) // TODO will need to recalculate when you get the data + .domain([0, max(dataset.map((d) => d[this.yKey]))]) .range([`${SVG_DIMENSIONS.height}`, 0]) .nice(); let yAxis = axisLeft(yAxisScale) - .ticks(7) + .ticks(6) .tickPadding(10) .tickSizeInner(-SVG_DIMENSIONS.width) .tickFormat(formatNumbers); @@ -111,18 +121,14 @@ export default class VerticalBarChart extends Component { .attr('height', '100%') .attr('width', '30px') // three times width .attr('y', '0') // start at bottom - .attr('x', (data) => xScale(data.month)); // not data.data because this is not stacked data + .attr('x', (data) => xScale(data[this.xKey])); // not data.data because this is not stacked data // MOUSE EVENT FOR TOOLTIP tooltipRect.on('mouseover', (data) => { - let hoveredMonth = data.month; - this.tooltipTotal = `${data.clients} ${data.new_clients ? 'total' : 'new'} clients`; - this.uniqueEntities = `${data.entity_clients} unique entities`; - this.nonEntityTokens = `${data.non_entity_clients} non-entity tokens`; - // let node = chartSvg - // .selectAll('rect.tooltip-rect') - // .filter(data => data.month === this.hoveredLabel) - // .node(); + let hoveredMonth = data[this.xKey]; + this.tooltipTotal = `${data[this.yKey]} ${data.new_clients ? 'total' : 'new'} clients`; + this.entityClients = `${data.entity_clients} entity clients`; + this.nonEntityClients = `${data.non_entity_clients} non-entity clients`; let node = chartSvg .selectAll('rect.data-bar') // filter for the top data bar (so y-coord !== 0) with matching month diff --git a/ui/app/routes/vault/cluster/clients/history.js b/ui/app/routes/vault/cluster/clients/history.js index b69987b2e737f..17914db89d34f 100644 --- a/ui/app/routes/vault/cluster/clients/history.js +++ b/ui/app/routes/vault/cluster/clients/history.js @@ -2,6 +2,7 @@ import Route from '@ember/routing/route'; import { isSameMonth } from 'date-fns'; import RSVP from 'rsvp'; import getStorage from 'vault/lib/token-storage'; +import { parseRFC3339 } from 'core/utils/date-formatters'; const INPUTTED_START_DATE = 'vault:ui-inputted-start-date'; export default class HistoryRoute extends Route { @@ -26,17 +27,6 @@ export default class HistoryRoute extends Route { } } - parseRFC3339(timestamp) { - // convert '2021-03-21T00:00:00Z' --> ['2021', 2] (e.g. 2021 March, month is zero indexed) - if (Array.isArray(timestamp)) { - // return if already formatted correctly - return timestamp; - } - return timestamp - ? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1] - : null; - } - async model() { let parentModel = this.modelFor('vault.cluster.clients'); let licenseStart = await this.getLicenseStartTime(); @@ -45,8 +35,8 @@ export default class HistoryRoute extends Route { return RSVP.hash({ config: parentModel.config, activity, - startTimeFromLicense: this.parseRFC3339(licenseStart), - endTimeFromResponse: this.parseRFC3339(activity?.endTime), + startTimeFromLicense: parseRFC3339(licenseStart), + endTimeFromResponse: parseRFC3339(activity?.endTime), versionHistory: parentModel.versionHistory, }); } diff --git a/ui/app/serializers/clients/activity.js b/ui/app/serializers/clients/activity.js index c831f8b9f86f7..5fd5337d57714 100644 --- a/ui/app/serializers/clients/activity.js +++ b/ui/app/serializers/clients/activity.js @@ -1,5 +1,6 @@ import ApplicationSerializer from '../application'; import { formatISO } from 'date-fns'; +import { parseAPITimestamp, parseRFC3339 } from 'core/utils/date-formatters'; export default class ActivitySerializer extends ApplicationSerializer { flattenDataset(byNamespaceArray) { return byNamespaceArray.map((ns) => { @@ -34,10 +35,12 @@ export default class ActivitySerializer extends ApplicationSerializer { // for vault usage - vertical bar chart flattenByMonths(payload, isNewClients = false) { + const sortedPayload = [...payload]; + sortedPayload.reverse(); if (isNewClients) { - return payload.map((m) => { + return sortedPayload?.map((m) => { return { - month: m.timestamp, + month: parseAPITimestamp(m.timestamp, 'M/yy'), entity_clients: m.new_clients.counts.entity_clients, non_entity_clients: m.new_clients.counts.non_entity_clients, total: m.new_clients.counts.clients, @@ -45,9 +48,9 @@ export default class ActivitySerializer extends ApplicationSerializer { }; }); } else { - return payload.map((m) => { + return sortedPayload?.map((m) => { return { - month: m.timestamp, + month: parseAPITimestamp(m.timestamp, 'M/yy'), entity_clients: m.counts.entity_clients, non_entity_clients: m.counts.non_entity_clients, total: m.counts.clients, @@ -88,13 +91,6 @@ export default class ActivitySerializer extends ApplicationSerializer { return object; } - parseRFC3339(timestamp) { - // convert '2021-03-21T00:00:00Z' --> ['2021', 2] (e.g. 2021 March, month is zero indexed) - return timestamp - ? [timestamp.split('-')[0], Number(timestamp.split('-')[1].replace(/^0+/, '')) - 1] - : null; - } - normalizeResponse(store, primaryModelClass, payload, id, requestType) { if (payload.id === 'no-data') { return super.normalizeResponse(store, primaryModelClass, payload, id, requestType); @@ -107,8 +103,8 @@ export default class ActivitySerializer extends ApplicationSerializer { by_month_total_clients: this.flattenByMonths(payload.data.months), by_month_new_clients: this.flattenByMonths(payload.data.months, { isNewClients: true }), total: this.homogenizeClientNaming(payload.data.total), - formatted_end_time: this.parseRFC3339(payload.data.end_time), - formatted_start_time: this.parseRFC3339(payload.data.start_time), + formatted_end_time: parseRFC3339(payload.data.end_time), + formatted_start_time: parseRFC3339(payload.data.start_time), }; delete payload.data.by_namespace; delete payload.data.months; @@ -116,7 +112,6 @@ export default class ActivitySerializer extends ApplicationSerializer { return super.normalizeResponse(store, primaryModelClass, transformedPayload, id, requestType); } } - /* SAMPLE PAYLOAD BEFORE/AFTER: diff --git a/ui/app/templates/components/clients/history.hbs b/ui/app/templates/components/clients/history.hbs index 2dbd7012cd490..6bad4a549530d 100644 --- a/ui/app/templates/components/clients/history.hbs +++ b/ui/app/templates/components/clients/history.hbs @@ -131,6 +131,14 @@ {{else}} {{#if this.totalUsageCounts}} + + {{!-- TODO CMB: remove UsageStats component from history tab (and update associated tests) --}} {{#if this.hasAttributionData}} diff --git a/ui/app/templates/components/clients/running-total.hbs b/ui/app/templates/components/clients/running-total.hbs index b29c3286d78a7..4576b4a4244ae 100644 --- a/ui/app/templates/components/clients/running-total.hbs +++ b/ui/app/templates/components/clients/running-total.hbs @@ -3,9 +3,7 @@

Vault client counts

- A unique active client is any user or service that interacts with Vault. They are made up of direct entities and - non-entity tokens. As Vault’s primary billing metric, your total client count is the number for which you will be - charged at the end of your billing period. + A client is any user or service that interacts with Vault. They are made up of entity clients and non-entity clients. The total client count number is an important consideration for Vault billing.

@@ -20,30 +18,18 @@
{{#let (get this.getTotalClients 0) as |stat|}} -

{{capitalize stat.label}}

+

Entity clients

- {{#if stat.total}} - {{format-number stat.total}} - {{else}} - - Error getting total - - {{/if}} + {{format-number stat.total}}

{{/let}}
{{#let (get this.getTotalClients 1) as |stat|}} -

{{capitalize stat.label}}

+

Non-entity clients

- {{#if stat.total}} - {{format-number stat.total}} - {{else}} - - Error getting total - - {{/if}} + {{format-number stat.total}}

{{/let}}
@@ -68,36 +54,27 @@
{{#let (get this.getAverageNewClients 0) as |stat|}} -

Average new {{stat.label}} per month

+

Average new entity clients per month

- {{#if stat.average}} - {{format-number stat.average}} - {{else}} - - Average cannot be calculated - - {{/if}} + {{format-number stat.average}}

{{/let}}
{{#let (get this.getAverageNewClients 1) as |stat|}} -

Average new {{stat.label}} per month

+

Average new non-entity clients per month

- {{#if stat.average}} - {{format-number stat.average}} - {{else}} - - Average cannot be calculated - - {{/if}} + {{format-number stat.average}}

{{/let}}
- Updated Nov 15 2021, 4:07:32 pm + {{#if @timestamp}} + Updated + {{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}} + {{/if}}
diff --git a/ui/app/templates/components/clients/vertical-bar-chart.hbs b/ui/app/templates/components/clients/vertical-bar-chart.hbs index 185e3e58c2998..d3292e0afa92f 100644 --- a/ui/app/templates/components/clients/vertical-bar-chart.hbs +++ b/ui/app/templates/components/clients/vertical-bar-chart.hbs @@ -3,6 +3,7 @@ class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.registerListener @dataset}} + {{did-update this.renderChart @dataset}} > @@ -21,8 +22,8 @@ }}

{{this.tooltipTotal}}

-

{{this.uniqueEntities}}

-

{{this.nonEntityTokens}}

+

{{this.entityClients}}

+

{{this.nonEntityClients}}

{{/modal-dialog}} diff --git a/ui/lib/core/addon/utils/date-formatters.js b/ui/lib/core/addon/utils/date-formatters.js new file mode 100644 index 0000000000000..c1cb5ac2183fc --- /dev/null +++ b/ui/lib/core/addon/utils/date-formatters.js @@ -0,0 +1,20 @@ +import { format, parseISO } from 'date-fns'; + +// convert RFC3339 timestamp ( '2021-03-21T00:00:00Z' ) to date object, optionally format +export const parseAPITimestamp = (timestamp, style) => { + if (!timestamp) return; + let date = parseISO(timestamp.split('T')[0]); + if (!style) return date; + return format(date, style); +}; + +// convert ISO timestamp '2021-03-21T00:00:00Z' to ['2021', 2] +// (e.g. 2021 March, month is zero indexed) (used by calendar widget) +export const parseRFC3339 = (timestamp) => { + if (Array.isArray(timestamp)) { + // return if already formatted correctly + return timestamp; + } + let date = parseAPITimestamp(timestamp); + return date ? [`${date.getFullYear()}`, date.getMonth()] : null; +}; diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js index fda9813f00984..acccab50dcd87 100644 --- a/ui/mirage/handlers/clients.js +++ b/ui/mirage/handlers/clients.js @@ -1,4 +1,5 @@ -import { formatISO, isBefore, sub } from 'date-fns'; +import { differenceInCalendarMonths, formatISO, formatRFC3339, isBefore, sub } from 'date-fns'; +import { parseAPITimestamp } from 'core/utils/date-formatters'; export default function (server) { // 1.10 API response @@ -47,7 +48,7 @@ export default function (server) { ], license_id: '060d7820-fa59-f95c-832b-395db0aeb9ba', performance_standby_count: 9999, - start_time: '2021-05-17T00:00:00Z', + start_time: '2021-01-17T00:00:00Z', }, persisted_autoload: { expiration_time: '2022-05-17T23:59:59.999Z', @@ -70,7 +71,7 @@ export default function (server) { ], license_id: '060d7820-fa59-f95c-832b-395db0aeb9ba', performance_standby_count: 9999, - start_time: '2021-05-17T00:00:00Z', + start_time: '2021-01-17T00:00:00Z', }, }, }; @@ -91,181 +92,97 @@ export default function (server) { server.get('/sys/internal/counters/activity', (schema, req) => { const { start_time, end_time } = req.queryParams; // fake client counting start date so warning shows if user queries earlier start date - const counts_start = '2020-10-17T00:00:00Z'; - return { - request_id: '25f55fbb-f253-9c46-c6f0-3cdd3ada91ab', - lease_id: '', - renewable: false, - lease_duration: 0, - data: { - by_namespace: [ - { - namespace_id: '96OwG', - namespace_path: 'test-ns/', - counts: { - distinct_entities: 18290, - entity_clients: 18290, - non_entity_tokens: 18738, - non_entity_clients: 18738, - clients: 37028, - }, - mounts: [ - { - mount_path: 'path-1', - counts: { - distinct_entities: 6403, - entity_clients: 6403, - non_entity_tokens: 6300, - non_entity_clients: 6300, - clients: 12703, - }, - }, - { - mount_path: 'path-2', - counts: { - distinct_entities: 5699, - entity_clients: 5699, - non_entity_tokens: 6777, - non_entity_clients: 6777, - clients: 12476, - }, - }, - { - mount_path: 'path-3', - counts: { - distinct_entities: 6188, - entity_clients: 6188, - non_entity_tokens: 5661, - non_entity_clients: 5661, - clients: 11849, - }, - }, - ], - }, + const counts_start = '2020-12-31T00:00:00Z'; + const mockMonthlyData = [ + { + timestamp: '2021-10-01T00:00:00Z', + counts: { + distinct_entities: 0, + entity_clients: 20, + non_entity_tokens: 0, + non_entity_clients: 20, + clients: 40, + }, + namespaces: [ { namespace_id: 'root', namespace_path: '', counts: { - distinct_entities: 19099, - entity_clients: 19099, - non_entity_tokens: 17781, - non_entity_clients: 17781, - clients: 36880, + distinct_entities: 0, + entity_clients: 8, + non_entity_tokens: 0, + non_entity_clients: 7, + clients: 15, }, mounts: [ { - mount_path: 'path-3', + mount_path: 'auth/up2/', counts: { - distinct_entities: 6863, - entity_clients: 6863, - non_entity_tokens: 6801, - non_entity_clients: 6801, - clients: 13664, - }, - }, - { - mount_path: 'path-2', - counts: { - distinct_entities: 6047, - entity_clients: 6047, - non_entity_tokens: 5957, - non_entity_clients: 5957, - clients: 12004, + distinct_entities: 0, + entity_clients: 8, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 8, }, }, { - mount_path: 'path-1', + mount_path: 'auth/up1/', counts: { - distinct_entities: 6189, - entity_clients: 6189, - non_entity_tokens: 5023, - non_entity_clients: 5023, - clients: 11212, + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, + non_entity_clients: 7, + clients: 7, }, }, ], }, - ], - end_time: end_time || formatISO(sub(new Date(), { months: 1 })), - months: [ { - timestamp: '2021-05-01T00:00:00Z', + namespace_id: 's07UR', + namespace_path: 'ns1/', counts: { distinct_entities: 0, - entity_clients: 13, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 12, - clients: 25, + non_entity_clients: 5, + clients: 10, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 8, + entity_clients: 0, non_entity_tokens: 0, - non_entity_clients: 7, - clients: 15, - }, - mounts: [ - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 8, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 8, - }, - }, - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 7, - clients: 7, - }, - }, - ], + non_entity_clients: 5, + clients: 5, + }, }, { - namespace_id: 's07UR', - namespace_path: 'ns1/', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 5, - clients: 10, - }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 5, - clients: 5, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 5, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 5, - }, - }, - ], + non_entity_clients: 0, + clients: 5, + }, }, ], - new_clients: { + }, + ], + new_clients: { + counts: { + distinct_entities: 0, + entity_clients: 3, + non_entity_tokens: 0, + non_entity_clients: 2, + clients: 5, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', counts: { distinct_entities: 0, entity_clients: 3, @@ -273,342 +190,342 @@ export default function (server) { non_entity_clients: 2, clients: 5, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, entity_clients: 3, non_entity_tokens: 0, + non_entity_clients: 0, + clients: 3, + }, + }, + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, non_entity_clients: 2, - clients: 5, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 3, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 3, - }, - }, - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 2, - clients: 2, - }, - }, - ], }, ], }, - }, + ], + }, + }, + { + timestamp: '2021-09-01T00:00:00Z', + counts: { + distinct_entities: 0, + entity_clients: 17, + non_entity_tokens: 0, + non_entity_clients: 18, + clients: 35, + }, + namespaces: [ { - timestamp: '2021-04-01T00:00:00Z', + namespace_id: 'oImjk', + namespace_path: 'ns2/', counts: { distinct_entities: 0, - entity_clients: 10, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 10, - clients: 20, + non_entity_clients: 5, + clients: 10, }, - namespaces: [ + mounts: [ { - namespace_id: 'oImjk', - namespace_path: 'ns2/', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 5, + entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 5, - clients: 10, - }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 5, - clients: 5, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 5, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 5, - }, - }, - ], + clients: 5, + }, }, { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, - entity_clients: 2, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 3, + non_entity_clients: 0, clients: 5, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 3, - clients: 3, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 2, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 2, - }, - }, - ], + }, + ], + }, + { + namespace_id: 'root', + namespace_path: '', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 5, + }, + mounts: [ + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 3, + }, }, { - namespace_id: 's07UR', - namespace_path: 'ns1/', + mount_path: 'auth/up2/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 2, + }, + }, + ], + }, + { + namespace_id: 's07UR', + namespace_path: 'ns1/', + counts: { + distinct_entities: 0, + entity_clients: 3, + non_entity_tokens: 0, + non_entity_clients: 2, + clients: 5, + }, + mounts: [ + { + mount_path: 'auth/up2/', counts: { distinct_entities: 0, entity_clients: 3, non_entity_tokens: 0, + non_entity_clients: 0, + clients: 3, + }, + }, + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, non_entity_clients: 2, - clients: 5, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 3, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 3, - }, - }, - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 2, - clients: 2, - }, - }, - ], }, ], - new_clients: { + }, + ], + new_clients: { + counts: { + distinct_entities: 0, + entity_clients: 10, + non_entity_tokens: 0, + non_entity_clients: 10, + clients: 20, + }, + namespaces: [ + { + namespace_id: 'oImjk', + namespace_path: 'ns2/', counts: { distinct_entities: 0, - entity_clients: 10, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 10, - clients: 20, + non_entity_clients: 5, + clients: 10, }, - namespaces: [ + mounts: [ { - namespace_id: 'oImjk', - namespace_path: 'ns2/', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 5, + entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 5, - clients: 10, + clients: 5, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 5, - clients: 5, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 5, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 5, - }, - }, - ], }, { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, - entity_clients: 2, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 3, + non_entity_clients: 0, clients: 5, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 3, - clients: 3, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 2, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 2, - }, - }, - ], }, + ], + }, + { + namespace_id: 'root', + namespace_path: '', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 5, + }, + mounts: [ { - namespace_id: 's07UR', - namespace_path: 'ns1/', + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 3, + }, + }, + { + mount_path: 'auth/up2/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 2, + }, + }, + ], + }, + { + namespace_id: 's07UR', + namespace_path: 'ns1/', + counts: { + distinct_entities: 0, + entity_clients: 3, + non_entity_tokens: 0, + non_entity_clients: 2, + clients: 5, + }, + mounts: [ + { + mount_path: 'auth/up2/', counts: { distinct_entities: 0, entity_clients: 3, non_entity_tokens: 0, + non_entity_clients: 0, + clients: 3, + }, + }, + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, non_entity_clients: 2, - clients: 5, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 3, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 3, - }, - }, - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 2, - clients: 2, - }, - }, - ], }, ], }, - }, + ], + }, + }, + { + timestamp: '2021-08-01T00:00:00Z', + counts: { + distinct_entities: 0, + entity_clients: 7, + non_entity_tokens: 0, + non_entity_clients: 8, + clients: 15, + }, + namespaces: [ { - timestamp: '2021-03-01T00:00:00Z', + namespace_id: 'root', + namespace_path: '', counts: { distinct_entities: 0, - entity_clients: 7, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 8, - clients: 15, + non_entity_clients: 5, + clients: 10, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 5, + entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 5, - clients: 10, - }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 5, - clients: 5, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 5, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 5, - }, - }, - ], + clients: 5, + }, }, { - namespace_id: 's07UR', - namespace_path: 'ns1/', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, - entity_clients: 2, + entity_clients: 5, non_entity_tokens: 0, - non_entity_clients: 3, + non_entity_clients: 0, clients: 5, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 3, - clients: 3, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 2, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 2, - }, - }, - ], }, ], - new_clients: { + }, + { + namespace_id: 's07UR', + namespace_path: 'ns1/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 5, + }, + mounts: [ + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 3, + }, + }, + { + mount_path: 'auth/up2/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 2, + }, + }, + ], + }, + ], + new_clients: { + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 5, + }, + namespaces: [ + { + namespace_id: 's07UR', + namespace_path: 'ns1/', counts: { distinct_entities: 0, entity_clients: 2, @@ -616,45 +533,45 @@ export default function (server) { non_entity_clients: 3, clients: 5, }, - namespaces: [ + mounts: [ { - namespace_id: 's07UR', - namespace_path: 'ns1/', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 2, + entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 3, - clients: 5, + clients: 3, + }, + }, + { + mount_path: 'auth/up2/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 3, - clients: 3, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 2, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 2, - }, - }, - ], }, ], }, - }, + ], + }, + }, + { + timestamp: '2021-07-01T00:00:00Z', + counts: { + distinct_entities: 0, + entity_clients: 5, + non_entity_tokens: 0, + non_entity_clients: 5, + clients: 10, + }, + namespaces: [ { - timestamp: '2021-02-01T00:00:00Z', + namespace_id: 'root', + namespace_path: '', counts: { distinct_entities: 0, entity_clients: 5, @@ -662,42 +579,42 @@ export default function (server) { non_entity_clients: 5, clients: 10, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, entity_clients: 5, non_entity_tokens: 0, + non_entity_clients: 0, + clients: 5, + }, + }, + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, non_entity_clients: 5, - clients: 10, - }, - mounts: [ - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 5, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 5, - }, - }, - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 5, - clients: 5, - }, - }, - ], + clients: 5, + }, }, ], - new_clients: { + }, + ], + new_clients: { + counts: { + distinct_entities: 0, + entity_clients: 3, + non_entity_tokens: 0, + non_entity_clients: 2, + clients: 5, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', counts: { distinct_entities: 0, entity_clients: 3, @@ -705,45 +622,45 @@ export default function (server) { non_entity_clients: 2, clients: 5, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up2/', counts: { distinct_entities: 0, entity_clients: 3, non_entity_tokens: 0, + non_entity_clients: 0, + clients: 3, + }, + }, + { + mount_path: 'auth/up1/', + counts: { + distinct_entities: 0, + entity_clients: 0, + non_entity_tokens: 0, non_entity_clients: 2, - clients: 5, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 3, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 3, - }, - }, - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 2, - clients: 2, - }, - }, - ], }, ], }, - }, + ], + }, + }, + { + timestamp: '2021-06-01T00:00:00Z', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 5, + }, + namespaces: [ { - timestamp: '2021-01-01T00:00:00Z', + namespace_id: 'root', + namespace_path: '', counts: { distinct_entities: 0, entity_clients: 2, @@ -751,42 +668,42 @@ export default function (server) { non_entity_clients: 3, clients: 5, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 2, + entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 3, - clients: 5, + clients: 3, + }, + }, + { + mount_path: 'auth/up2/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 3, - clients: 3, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 2, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 2, - }, - }, - ], }, ], - new_clients: { + }, + ], + new_clients: { + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 3, + clients: 5, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', counts: { distinct_entities: 0, entity_clients: 2, @@ -794,44 +711,162 @@ export default function (server) { non_entity_clients: 3, clients: 5, }, - namespaces: [ + mounts: [ { - namespace_id: 'root', - namespace_path: '', + mount_path: 'auth/up1/', counts: { distinct_entities: 0, - entity_clients: 2, + entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 3, - clients: 5, + clients: 3, + }, + }, + { + mount_path: 'auth/up2/', + counts: { + distinct_entities: 0, + entity_clients: 2, + non_entity_tokens: 0, + non_entity_clients: 0, + clients: 2, }, - mounts: [ - { - mount_path: 'auth/up1/', - counts: { - distinct_entities: 0, - entity_clients: 0, - non_entity_tokens: 0, - non_entity_clients: 3, - clients: 3, - }, - }, - { - mount_path: 'auth/up2/', - counts: { - distinct_entities: 0, - entity_clients: 2, - non_entity_tokens: 0, - non_entity_clients: 0, - clients: 2, - }, - }, - ], }, ], }, + ], + }, + }, + ]; + const addMonthsWithoutData = (queryStartTimestamp, monthlyData) => { + const queryDate = parseAPITimestamp(queryStartTimestamp); + const startDateByMonth = parseAPITimestamp(monthlyData[monthlyData.length - 1].timestamp); + const transformedMonthlyArray = [...monthlyData]; + if (isBefore(queryDate, startDateByMonth)) { + // no data for months before (upgraded to 1.10 during billing period) + let i = 0; + do { + i++; + let timestamp = formatRFC3339(sub(startDateByMonth, { months: i })); + 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)); + } + return transformedMonthlyArray; + }; + let mockQueriedMonths = addMonthsWithoutData(start_time, mockMonthlyData); + return { + request_id: '25f55fbb-f253-9c46-c6f0-3cdd3ada91ab', + lease_id: '', + renewable: false, + lease_duration: 0, + data: { + by_namespace: [ + { + namespace_id: '96OwG', + namespace_path: 'test-ns/', + counts: { + distinct_entities: 18290, + entity_clients: 18290, + non_entity_tokens: 18738, + non_entity_clients: 18738, + clients: 37028, + }, + mounts: [ + { + mount_path: 'path-1', + counts: { + distinct_entities: 6403, + entity_clients: 6403, + non_entity_tokens: 6300, + non_entity_clients: 6300, + clients: 12703, + }, + }, + { + mount_path: 'path-2', + counts: { + distinct_entities: 5699, + entity_clients: 5699, + non_entity_tokens: 6777, + non_entity_clients: 6777, + clients: 12476, + }, + }, + { + mount_path: 'path-3', + counts: { + distinct_entities: 6188, + entity_clients: 6188, + non_entity_tokens: 5661, + non_entity_clients: 5661, + clients: 11849, + }, + }, + ], + }, + { + namespace_id: 'root', + namespace_path: '', + counts: { + distinct_entities: 19099, + entity_clients: 19099, + non_entity_tokens: 17781, + non_entity_clients: 17781, + clients: 36880, + }, + mounts: [ + { + mount_path: 'path-3', + counts: { + distinct_entities: 6863, + entity_clients: 6863, + non_entity_tokens: 6801, + non_entity_clients: 6801, + clients: 13664, + }, + }, + { + mount_path: 'path-2', + counts: { + distinct_entities: 6047, + entity_clients: 6047, + non_entity_tokens: 5957, + non_entity_clients: 5957, + clients: 12004, + }, + }, + { + mount_path: 'path-1', + counts: { + distinct_entities: 6189, + entity_clients: 6189, + non_entity_tokens: 5023, + non_entity_clients: 5023, + clients: 11212, + }, + }, + ], }, ], + end_time: end_time || formatISO(sub(new Date(), { months: 1 })), + months: mockQueriedMonths || mockMonthlyData, start_time: isBefore(new Date(start_time), new Date(counts_start)) ? counts_start : start_time, total: { distinct_entities: 37389, diff --git a/ui/tests/acceptance/client-history-test.js b/ui/tests/acceptance/client-history-test.js index 5adc61ef28923..19c4b1006ddba 100644 --- a/ui/tests/acceptance/client-history-test.js +++ b/ui/tests/acceptance/client-history-test.js @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { visit, currentURL, click, settled, waitUntil, find } from '@ember/test-helpers'; +import { visit, currentURL, click, settled, find } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import Pretender from 'pretender'; import authPage from 'vault/tests/pages/auth'; @@ -196,7 +196,7 @@ module('Acceptance | clients history tab', function (hooks) { assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15'); assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5'); assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10'); - await waitUntil(() => find('[data-test-horizontal-bar-chart]')); + await settled(); assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart'); assert.dom('[data-test-top-attribution]').includesText('Top auth method');