From 9bb49204979a7d873214659e92c4eaf7ac662a82 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Thu, 24 Feb 2022 14:04:40 -0600 Subject: [PATCH] UI/client count tests (#14162) --- ui/README.md | 6 + ui/app/components/clients/attribution.js | 46 +-- ui/app/components/clients/current.js | 6 +- ui/app/components/clients/history.js | 16 +- .../clients/horizontal-bar-chart.js | 4 +- ui/app/components/clients/line-chart.js | 41 ++- .../components/clients/vertical-bar-chart.js | 6 +- ui/app/components/date-dropdown.js | 2 +- .../routes/vault/cluster/clients/history.js | 16 +- ui/app/styles/core/charts.scss | 15 +- .../components/clients/attribution.hbs | 72 +++-- .../templates/components/clients/current.hbs | 4 +- .../templates/components/clients/history.hbs | 9 +- .../components/clients/line-chart.hbs | 7 +- .../components/clients/usage-stats.hbs | 3 + .../components/clients/vertical-bar-chart.hbs | 7 +- ui/app/templates/components/date-dropdown.hbs | 9 +- ui/app/utils/chart-helpers.js | 4 +- .../addon/templates/components/stat-text.hbs | 6 +- .../addon/templates/components/toolbar.hbs | 2 +- ui/mirage/handlers/activity.js | 8 +- ui/mirage/handlers/clients.js | 12 +- ui/tests/acceptance/client-current-test.js | 172 +++++++++++ ui/tests/acceptance/client-history-test.js | 278 ++++++++++++++++++ ui/tests/acceptance/oidc-provider-test.js | 16 +- ui/tests/helpers/clients.js | 183 ++++++++++++ .../components/clients-current-test.js | 69 ----- .../components/clients-history-test.js | 103 ------- .../components/clients/attribution-test.js | 144 +++++++++ .../config-test.js} | 0 .../clients/horizontal-bar-chart-test.js | 85 ++++++ .../components/clients/line-chart-test.js | 40 +++ .../components/clients/usage-stats-test.js | 46 +++ .../clients/vertical-bar-chart-test.js | 30 ++ .../components/date-dropdown-test.js | 174 +++++++++++ ui/tests/unit/utils/chart-helpers-test.js | 30 ++ 36 files changed, 1369 insertions(+), 302 deletions(-) create mode 100644 ui/tests/acceptance/client-current-test.js create mode 100644 ui/tests/acceptance/client-history-test.js create mode 100644 ui/tests/helpers/clients.js delete mode 100644 ui/tests/integration/components/clients-current-test.js delete mode 100644 ui/tests/integration/components/clients-history-test.js create mode 100644 ui/tests/integration/components/clients/attribution-test.js rename ui/tests/integration/components/{clients-config-test.js => clients/config-test.js} (100%) create mode 100644 ui/tests/integration/components/clients/horizontal-bar-chart-test.js create mode 100644 ui/tests/integration/components/clients/line-chart-test.js create mode 100644 ui/tests/integration/components/clients/usage-stats-test.js create mode 100644 ui/tests/integration/components/clients/vertical-bar-chart-test.js create mode 100644 ui/tests/integration/components/date-dropdown-test.js create mode 100644 ui/tests/unit/utils/chart-helpers-test.js diff --git a/ui/README.md b/ui/README.md index d8b8a72aa..62a7983bb 100644 --- a/ui/README.md +++ b/ui/README.md @@ -78,6 +78,12 @@ long-form version of the npm script: `ember server --proxy=http://localhost:PORT` +To run yarn with mirage, do: + +- `yarn start:mirage handlername` + +Where `handlername` is one of the options exported in `mirage/handlers/index` + ### Code Generators Make use of the many generators for code, try `ember help generate` for more details. If you're using a component that can be widely-used, consider making it an `addon` component instead (see [this PR](https://github.com/hashicorp/vault/pull/6629) for more details) diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js index 75bcde84d..edafc1001 100644 --- a/ui/app/components/clients/attribution.js +++ b/ui/app/components/clients/attribution.js @@ -39,6 +39,9 @@ export default class Attribution extends Component { } get isSingleNamespace() { + if (!this.args.totalClientsData) { + return 'no data'; + } // if a namespace is selected, then we're viewing top 10 auth methods (mounts) return !!this.args.selectedNamespace; } @@ -61,29 +64,30 @@ export default class Attribution extends Component { get chartText() { let dateText = this.isDateRange ? 'date range' : 'month'; - if (!this.isSingleNamespace) { - return { - description: - 'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.', - newCopy: `The new clients in the namespace for this ${dateText}. + switch (this.isSingleNamespace) { + case true: + return { + description: + 'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.', + newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients + ${dateText === 'date range' ? ' over time.' : '.'}`, + totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `, + }; + case false: + return { + description: + 'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.', + newCopy: `The new clients in the namespace for this ${dateText}. This aids in understanding which namespaces create and use new clients ${dateText === 'date range' ? ' over time.' : '.'}`, - totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`, - }; - } else if (this.isSingleNamespace) { - return { - description: - 'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.', - newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients - ${dateText === 'date range' ? ' over time.' : '.'}`, - totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `, - }; - } else { - return { - description: 'There is a problem gathering data', - newCopy: 'There is a problem gathering data', - totalCopy: 'There is a problem gathering data', - }; + totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`, + }; + case 'no data': + return { + description: 'There is a problem gathering data', + }; + default: + return ''; } } diff --git a/ui/app/components/clients/current.js b/ui/app/components/clients/current.js index 1e8a49080..3f00c3945 100644 --- a/ui/app/components/clients/current.js +++ b/ui/app/components/clients/current.js @@ -29,7 +29,11 @@ export default class Current extends Component { } get hasAttributionData() { - return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod; + if (this.selectedAuthMethod) return false; + if (this.selectedNamespace) { + return this.authMethodOptions.length > 0; + } + return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData; } get filteredActivity() { diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js index fdd163956..07a59ff10 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -62,10 +62,12 @@ export default class History extends Component { // SEARCH SELECT @tracked selectedNamespace = null; - @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => ({ - name: namespace.label, - id: namespace.label, - })); + @tracked namespaceArray = this.getActivityResponse.byNamespace + ? this.getActivityResponse.byNamespace.map((namespace) => ({ + name: namespace.label, + id: namespace.label, + })) + : []; // TEMPLATE MESSAGING @tracked noActivityDate = ''; @@ -102,7 +104,11 @@ export default class History extends Component { } get hasAttributionData() { - return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod; + if (this.selectedAuthMethod) return false; + if (this.selectedNamespace) { + return this.authMethodOptions.length > 0; + } + return !!this.totalClientsData && this.totalUsageCounts && this.totalUsageCounts.clients !== 0; } get startTimeDisplay() { diff --git a/ui/app/components/clients/horizontal-bar-chart.js b/ui/app/components/clients/horizontal-bar-chart.js index f387c0ea1..429465357 100644 --- a/ui/app/components/clients/horizontal-bar-chart.js +++ b/ui/app/components/clients/horizontal-bar-chart.js @@ -21,8 +21,6 @@ import { tracked } from '@glimmer/tracking'; * @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked */ -// TODO CMB: delete original bar chart component - // SIZING CONSTANTS const CHART_MARGIN = { top: 10, left: 95 }; // makes space for y-axis legend const TRANSLATE = { down: 14, left: 99 }; @@ -104,7 +102,7 @@ export default class HorizontalBarChart extends Component { .append('rect') .attr('class', 'data-bar') .style('cursor', 'pointer') - .attr('width', (chartData) => `${xScale(chartData[1] - chartData[0])}%`) + .attr('width', (chartData) => `${xScale(Math.abs(chartData[1] - chartData[0]))}%`) .attr('height', yScale.bandwidth()) .attr('x', (chartData) => `${xScale(chartData[0])}%`) .attr('y', ({ data }) => yScale(data[labelKey])) diff --git a/ui/app/components/clients/line-chart.js b/ui/app/components/clients/line-chart.js index 64c55ef0f..2984a7606 100644 --- a/ui/app/components/clients/line-chart.js +++ b/ui/app/components/clients/line-chart.js @@ -27,6 +27,14 @@ export default class LineChart extends Component { @tracked tooltipTotal = ''; @tracked tooltipNew = ''; + get yKey() { + return this.args.yKey || 'clients'; + } + + get xKey() { + return this.args.xKey || 'month'; + } + @action removeTooltip() { this.tooltipTarget = null; } @@ -39,15 +47,17 @@ export default class LineChart extends Component { // DEFINE AXES SCALES let yScale = scaleLinear() - .domain([0, max(dataset.map((d) => d.clients))]) - .range([0, 100]); + .domain([0, max(dataset.map((d) => d[this.yKey]))]) + .range([0, 100]) + .nice(); let yAxisScale = scaleLinear() - .domain([0, max(dataset.map((d) => d.clients))]) - .range([SVG_DIMENSIONS.height, 0]); + .domain([0, max(dataset.map((d) => d[this.yKey]))]) + .range([SVG_DIMENSIONS.height, 0]) + .nice(); let xScale = scalePoint() // use scaleTime()? - .domain(dataset.map((d) => d.month)) + .domain(dataset.map((d) => d[this.xKey])) .range([0, SVG_DIMENSIONS.width]) .padding(0.2); @@ -67,8 +77,8 @@ export default class LineChart extends Component { // PATH BETWEEN PLOT POINTS let lineGenerator = line() - .x((d) => xScale(d.month)) - .y((d) => yAxisScale(d.clients)); + .x((d) => xScale(d[this.xKey])) + .y((d) => yAxisScale(d[this.yKey])); chartSvg .append('g') @@ -86,8 +96,8 @@ export default class LineChart extends Component { .enter() .append('circle') .attr('class', 'data-plot') - .attr('cy', (d) => `${100 - yScale(d.clients)}%`) - .attr('cx', (d) => xScale(d.month)) + .attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`) + .attr('cx', (d) => xScale(d[this.xKey])) .attr('r', 3.5) .attr('fill', LIGHT_AND_DARK_BLUE[0]) .attr('stroke', LIGHT_AND_DARK_BLUE[1]) @@ -103,18 +113,19 @@ export default class LineChart extends Component { .attr('class', 'hover-circle') .style('cursor', 'pointer') .style('opacity', '0') - .attr('cy', (d) => `${100 - yScale(d.clients)}%`) - .attr('cx', (d) => xScale(d.month)) + .attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`) + .attr('cx', (d) => xScale(d[this.xKey])) .attr('r', 10); let hoverCircles = chartSvg.selectAll('.hover-circle'); // MOUSE EVENT FOR TOOLTIP hoverCircles.on('mouseover', (data) => { - this.tooltipMonth = data.month; - this.tooltipTotal = `${data.clients} total clients`; - this.tooltipNew = `${data.new_clients.clients} new clients`; - let node = hoverCircles.filter((plot) => plot.month === data.month).node(); + // 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`; + 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 c77214622..f6676f2bd 100644 --- a/ui/app/components/clients/vertical-bar-chart.js +++ b/ui/app/components/clients/vertical-bar-chart.js @@ -47,7 +47,7 @@ export default class VerticalBarChart extends Component { // DEFINE DATA BAR SCALES let yScale = scaleLinear() - .domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data + .domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data .range([0, 100]) .nice(); @@ -76,7 +76,7 @@ export default class VerticalBarChart extends Component { // MAKE AXES // let yAxisScale = scaleLinear() - .domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data + .domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data .range([`${SVG_DIMENSIONS.height}`, 0]) .nice(); @@ -116,7 +116,7 @@ export default class VerticalBarChart extends Component { // MOUSE EVENT FOR TOOLTIP tooltipRect.on('mouseover', (data) => { let hoveredMonth = data.month; - this.tooltipTotal = `${data.total} ${data.new_clients ? 'total' : 'new'} clients`; + 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 diff --git a/ui/app/components/date-dropdown.js b/ui/app/components/date-dropdown.js index c40b412f2..bf9df3754 100644 --- a/ui/app/components/date-dropdown.js +++ b/ui/app/components/date-dropdown.js @@ -8,7 +8,7 @@ import { tracked } from '@glimmer/tracking'; * * @example * ```js - * + * * ``` * @param {function} handleDateSelection - is the action from the parent that the date picker triggers * @param {string} [name] - optional argument passed from date dropdown to parent function diff --git a/ui/app/routes/vault/cluster/clients/history.js b/ui/app/routes/vault/cluster/clients/history.js index db7661bc1..b69987b2e 100644 --- a/ui/app/routes/vault/cluster/clients/history.js +++ b/ui/app/routes/vault/cluster/clients/history.js @@ -1,21 +1,17 @@ import Route from '@ember/routing/route'; +import { isSameMonth } from 'date-fns'; import RSVP from 'rsvp'; import getStorage from 'vault/lib/token-storage'; const INPUTTED_START_DATE = 'vault:ui-inputted-start-date'; export default class HistoryRoute extends Route { async getActivity(start_time) { - try { - // on init ONLY make network request if we have a start time from the license - // otherwise user needs to manually input - return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {}; - } catch (e) { - // returns 400 when license start date is in the current month - if (e.httpStatus === 400) { - return { isLicenseDateError: true }; - } - throw e; + if (isSameMonth(new Date(start_time), new Date())) { + // triggers empty state to manually enter date if license begins in current month + return { isLicenseDateError: true }; } + // on init ONLY make network request if we have a start_time + return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {}; } async getLicenseStartTime() { diff --git a/ui/app/styles/core/charts.scss b/ui/app/styles/core/charts.scss index eb154bf88..3cf8f650e 100644 --- a/ui/app/styles/core/charts.scss +++ b/ui/app/styles/core/charts.scss @@ -9,8 +9,6 @@ .stacked-charts { display: grid; width: 100%; - // grid-template-columns: 1fr; - // grid-template-rows: 1fr; } .single-chart-grid { @@ -63,8 +61,8 @@ grid-row-start: 2; grid-row-end: span 3; justify-self: center; - height: 341px; - max-width: 730px; + height: 300px; + max-width: 700px; svg.chart { width: 100%; @@ -107,8 +105,13 @@ } .chart-empty-state { - grid-column-start: 1; - grid-column-end: -1; + height: 100%; + width: 100%; + grid-row-end: span 3; + grid-column-end: span 3; + > div { + box-shadow: none !important; + } } .chart-subTitle { diff --git a/ui/app/templates/components/clients/attribution.hbs b/ui/app/templates/components/clients/attribution.hbs index 2a81542f2..bfbf27aeb 100644 --- a/ui/app/templates/components/clients/attribution.hbs +++ b/ui/app/templates/components/clients/attribution.hbs @@ -1,48 +1,58 @@ -
+
+ {{#if this.barChartTotalClients}} +
+ +
+
+

{{this.chartText.totalCopy}}

+
-
- -
+
+

Top {{this.attributionBreakdown}}

+

{{this.topClientCounts.label}}

+
-
-

{{this.chartText.totalCopy}}

-
+
+

Clients in {{this.attributionBreakdown}}

+

{{format-number this.topClientCounts.clients}}

+
-
-

Top {{this.attributionBreakdown}}

-

{{this.topClientCounts.label}}

-
- -
-

Clients in {{this.attributionBreakdown}}

-

{{format-number this.topClientCounts.clients}}

-
- -
- Updated - {{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}} -
- -
- {{capitalize @chartLegend.0.label}} - {{capitalize @chartLegend.1.label}} +
+ {{capitalize @chartLegend.0.label}} + {{capitalize @chartLegend.1.label}} +
+ {{else}} +
+ +
+ {{/if}} +
+ {{#if @timestamp}} + Updated + {{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}} + {{/if}}
diff --git a/ui/app/templates/components/clients/current.hbs b/ui/app/templates/components/clients/current.hbs index 324ca3f43..8803426b9 100644 --- a/ui/app/templates/components/clients/current.hbs +++ b/ui/app/templates/components/clients/current.hbs @@ -22,7 +22,7 @@
FILTERS - + - {{#if this.selectedNamespace}} + {{#if (not (is-empty this.authMethodOptions))}}
{{#if this.startTimeDisplay}} -

{{this.startTimeDisplay}}

+

{{this.startTimeDisplay}}

@@ -59,11 +59,10 @@ to enable tracking again. {{/if}} - {{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}} - {{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}} + {{#if (or this.totalUsageCounts this.hasAttributionData)}}
FILTERS - + {{/if}} - {{#if this.selectedNamespace}} + {{#if (not (is-empty this.authMethodOptions))}} + {{! TOOLTIP }} diff --git a/ui/app/templates/components/clients/usage-stats.hbs b/ui/app/templates/components/clients/usage-stats.hbs index 94dfb3449..de76ee88b 100644 --- a/ui/app/templates/components/clients/usage-stats.hbs +++ b/ui/app/templates/components/clients/usage-stats.hbs @@ -18,6 +18,7 @@ @value={{or @totalUsageCounts.clients "0"}} @size="l" @subText="The sum of entity clientIDs and non-entity clientIDs; this is Vault’s primary billing metric." + data-test-stat-text="total-clients" />
@@ -27,6 +28,7 @@ @value={{or @totalUsageCounts.entity_clients "0"}} @size="l" @subText="Representations of a particular user, client, or application that created a token via login." + data-test-stat-text="entity-clients" />
@@ -36,6 +38,7 @@ @value={{or @totalUsageCounts.non_entity_clients "0"}} @size="l" @subText="Clients created with a shared set of permissions, but not associated with an entity. " + data-test-stat-text="non-entity-clients" />
diff --git a/ui/app/templates/components/clients/vertical-bar-chart.hbs b/ui/app/templates/components/clients/vertical-bar-chart.hbs index b11047945..185e3e58c 100644 --- a/ui/app/templates/components/clients/vertical-bar-chart.hbs +++ b/ui/app/templates/components/clients/vertical-bar-chart.hbs @@ -1,4 +1,9 @@ - + {{! TOOLTIP }} diff --git a/ui/app/templates/components/date-dropdown.hbs b/ui/app/templates/components/date-dropdown.hbs index ecda827ea..aaef773c5 100644 --- a/ui/app/templates/components/date-dropdown.hbs +++ b/ui/app/templates/components/date-dropdown.hbs @@ -1,6 +1,6 @@ @@ -9,7 +9,7 @@