UI/fix stacked charts tooltip (#26592)

* update selector vertical bar basic

* WIP vertical bar stacked component

* css for bars

* update css

* remove test data

* abstract monthly data getter

* move shared functions to base component

* rename tick formatting helper

* Revert "move shared functions to base component"

This reverts commit 5f931ea6f048df204650f9b4c6ba86195fa668b4.

* fix merge conflicts

* finish typescript declarations

* update chart-helpers test with renamed method

* use timestamp instead of month

* finish typescript

* finish ts cleanup

* add charts to token tab;

* dont blow out scope

* add comments and tests

* update token test

* fix tooltip hover spacing

* cleanup selectors

* one last test!

* delete old chart
This commit is contained in:
claire bontempo
2024-04-24 14:47:07 -07:00
committed by GitHub
parent 15bdb500b9
commit 19786e54b4
32 changed files with 853 additions and 768 deletions

View File

@@ -4,7 +4,7 @@
~}} ~}}
<div> <div>
{{#if this.data}} {{#if this.data}}
<div class="lineal-chart" data-test-chart={{@chartTitle}}> <div class="lineal-chart" data-test-chart={{or @chartTitle "line chart"}}>
<Lineal::Fluid as |width|> <Lineal::Fluid as |width|>
{{#let {{#let
(scale-point domain=this.xDomain range=(array 0 width) padding=0.2) (scale-point domain=this.xDomain range=(array 0 width) padding=0.2)
@@ -12,7 +12,9 @@
(scale-linear range=(array 0 this.chartHeight)) (scale-linear range=(array 0 this.chartHeight))
as |xScale yScale tooltipScale| as |xScale yScale tooltipScale|
}} }}
<svg width={{width}} height={{this.chartHeight}} class="chart has-grid" data-test-line-chart> <svg width={{width}} height={{this.chartHeight}} class="chart has-grid">
<title>{{@chartTitle}}</title>
{{#if (and xScale.isValid yScale.isValid)}} {{#if (and xScale.isValid yScale.isValid)}}
<Lineal::Axis <Lineal::Axis
@scale={{yScale}} @scale={{yScale}}
@@ -79,7 +81,7 @@
fill="#cce3fe" fill="#cce3fe"
stroke="#0c56e9" stroke="#0c56e9"
stroke-width="1.5" stroke-width="1.5"
data-test-line-chart="plot-point" data-test-plot-point
></circle> ></circle>
<circle <circle
role="button" role="button"

View File

@@ -4,7 +4,7 @@
*/ */
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { SVG_DIMENSIONS, formatNumbers } from 'vault/utils/chart-helpers'; import { SVG_DIMENSIONS, numericalAxisLabel } from 'vault/utils/chart-helpers';
import { parseAPITimestamp } from 'core/utils/date-formatters'; import { parseAPITimestamp } from 'core/utils/date-formatters';
import { format, isValid } from 'date-fns'; import { format, isValid } from 'date-fns';
import { debug } from '@ember/debug'; import { debug } from '@ember/debug';
@@ -127,12 +127,12 @@ export default class LineChart extends Component<Args> {
return (datum?.new_clients[this.yKey as keyof TotalClients] as number) || 0; return (datum?.new_clients[this.yKey as keyof TotalClients] as number) || 0;
} }
// TEMPLATE HELPERS
hasValue = (count: number | null) => { hasValue = (count: number | null) => {
return typeof count === 'number' ? true : false; return typeof count === 'number' ? true : false;
}; };
// These functions are used by the tooltip formatCount = (num: number): string => {
formatCount = (count: number) => { return numericalAxisLabel(num) || num.toString();
return formatNumbers([count]);
}; };
formatMonth = (date: Date) => { formatMonth = (date: Date) => {
return format(date, 'M/yy'); return format(date, 'M/yy');

View File

@@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1 SPDX-License-Identifier: BUSL-1.1
~}} ~}}
<div class="lineal-chart" data-test-chart={{@chartTitle}}> <div class="lineal-chart" data-test-chart={{or @chartTitle "vertical bar chart"}}>
<Lineal::Fluid as |width|> <Lineal::Fluid as |width|>
{{#let {{#let
(scale-band domain=this.xDomain range=(array 0 width) padding=0.1) (scale-band domain=this.xDomain range=(array 0 width) padding=0.1)
@@ -11,10 +11,8 @@
(scale-linear range=(array 0 this.chartHeight) domain=this.yDomain) (scale-linear range=(array 0 this.chartHeight) domain=this.yDomain)
as |xScale yScale hScale| as |xScale yScale hScale|
}} }}
<svg width={{width}} height={{this.chartHeight}} data-test-sync-bar-chart> <svg width={{width}} height={{this.chartHeight}}>
<title> <title>{{@chartTitle}}</title>
{{@chartTitle}}
</title>
{{#if (and xScale.isValid yScale.isValid)}} {{#if (and xScale.isValid yScale.isValid)}}
<Lineal::Axis <Lineal::Axis
@@ -99,7 +97,7 @@
<:head as |H|> <:head as |H|>
<H.Tr> <H.Tr>
<H.Th>Month</H.Th> <H.Th>Month</H.Th>
<H.Th>Count of secret syncs</H.Th> <H.Th>{{if @dataKey (humanize @dataKey)}} Count</H.Th>
</H.Tr> </H.Tr>
</:head> </:head>
<:body as |B|> <:body as |B|>

View File

@@ -5,7 +5,7 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { BAR_WIDTH, formatNumbers } from 'vault/utils/chart-helpers'; import { BAR_WIDTH, numericalAxisLabel } from 'vault/utils/chart-helpers';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
import { parseAPITimestamp } from 'core/utils/date-formatters'; import { parseAPITimestamp } from 'core/utils/date-formatters';
@@ -93,6 +93,6 @@ export default class VerticalBarBasic extends Component<Args> {
}; };
formatTicksY = (num: number): string => { formatTicksY = (num: number): string => {
return formatNumbers(num) || num.toString(); return numericalAxisLabel(num) || num.toString();
}; };
} }

View File

@@ -0,0 +1,123 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div class="lineal-chart" data-test-chart={{or @chartTitle "stacked vertical bar chart"}}>
<Lineal::Fluid as |width|>
{{#let
(scale-band domain=this.xBounds range=(array 0 width) padding=0.1)
(scale-linear range=(array this.chartHeight 0) domain=this.yBounds)
(scale-linear range=(array 0 this.chartHeight) domain=this.yBounds)
as |xScale yScale hScale|
}}
<svg width={{width}} height={{this.chartHeight}}>
<title>{{@chartTitle}}</title>
{{#if (and xScale.isValid yScale.isValid)}}
<Lineal::Axis
@includeDomain={{false}}
@orientation="left"
@scale={{yScale}}
@tickCount="4"
@tickFormat={{this.formatTicksY}}
@tickPadding={{10}}
@tickSizeInner={{concat "-" width}}
class="lineal-axis"
data-test-y-axis
/>
<Lineal::Axis
@includeDomain={{false}}
@orientation="bottom"
@scale={{xScale}}
@tickFormat={{this.formatTicksX}}
@tickPadding={{10}}
@tickSize="0"
class="lineal-axis"
transform="translate(0,{{yScale.range.min}})"
data-test-x-axis
/>
{{/if}}
<Lineal::VBars
@data={{this.chartData}}
@x="timestamp"
@y="counts"
@width={{this.barWidth}}
@xScale={{xScale}}
@yScale={{yScale}}
@color="clientType"
@colorScale="blue-bar"
transform="translate({{this.barOffset xScale.bandwidth}},0)"
data-test-vertical-bar
/>
{{! TOOLTIP target rectangles }}
{{#if (and xScale.isValid yScale.isValid)}}
{{#each this.aggregatedData as |d|}}
<rect
role="button"
aria-label="Show exact counts for {{d.legendX}}"
x="0"
y="0"
height={{this.chartHeight}}
width={{xScale.bandwidth}}
fill="transparent"
stroke="transparent"
transform="translate({{xScale.compute d.x}})"
{{on "mouseover" (fn (mut this.activeDatum) d)}}
{{on "mouseout" (fn (mut this.activeDatum) null)}}
data-test-interactive-area={{d.x}}
/>
{{/each}}
{{/if}}
</svg>
{{#if this.activeDatum}}
<div
class="lineal-tooltip-position chart-tooltip"
role="status"
{{style
--x=(this.tooltipX (xScale.compute this.activeDatum.x) xScale.bandwidth)
--y=(this.tooltipY (hScale.compute this.activeDatum.y))
}}
>
<div data-test-tooltip>
<p class="bold">{{this.activeDatum.legendX}}</p>
{{#each this.activeDatum.legendY as |stat|}}
<p>{{stat}}</p>
{{/each}}
</div>
<div class="chart-tooltip-arrow"></div>
</div>
{{/if}}
{{/let}}
</Lineal::Fluid>
</div>
{{#if @showTable}}
<details data-test-underlying-data>
<summary>{{@chartTitle}} data</summary>
<Hds::Table @caption="Underlying data">
<:head as |H|>
<H.Tr>
<H.Th>Timestamp</H.Th>
{{#each this.dataKeys as |key|}}
<H.Th>{{humanize key}}</H.Th>
{{/each}}
</H.Tr>
</:head>
<:body as |B|>
{{#each @data as |row|}}
<B.Tr>
<B.Td>{{row.timestamp}}</B.Td>
{{#each this.dataKeys as |key|}}
<B.Td>{{or (get row key) "-"}}</B.Td>
{{/each}}
</B.Tr>
{{/each}}
</:body>
</Hds::Table>
</details>
{{/if}}

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { BAR_WIDTH, numericalAxisLabel } from 'vault/utils/chart-helpers';
import { formatNumber } from 'core/helpers/format-number';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { flatGroup } from 'd3-array';
import type { MonthlyChartData } from 'vault/vault/charts/client-counts';
import type { ClientTypes } from 'core/utils/client-count-utils';
interface Args {
chartHeight?: number;
chartLegend: Legend[];
chartTitle: string;
data: MonthlyChartData[];
}
interface Legend {
key: ClientTypes;
label: string;
}
interface AggregatedDatum {
x: string;
y: number;
legendX: string;
legendY: string[];
}
interface DatumBase {
timestamp: string;
clientType: string;
}
// separated because "A mapped type may not declare properties or methods."
type ChartDatum = DatumBase & {
[key in ClientTypes]?: number | undefined;
};
/**
* @module VerticalBarStacked
* Renders a stacked bar chart of counts for different client types over time. Which client types render
* is mapped from the "key" values of the @legend arg
*
* @example
* <Clients::Charts::VerticalBarStacked
* @chartTitle="Total monthly usage"
* @data={{this.byMonthActivityData}}
* @chartLegend={{this.legend}}
* @chartHeight={{250}}
* />
*/
export default class VerticalBarStacked extends Component<Args> {
barWidth = BAR_WIDTH;
@tracked activeDatum: AggregatedDatum | null = null;
get chartHeight() {
return this.args.chartHeight || 190;
}
get dataKeys(): ClientTypes[] {
return this.args.chartLegend.map((l: Legend) => l.key);
}
label(legendKey: string) {
return this.args.chartLegend.find((l: Legend) => l.key === legendKey).label;
}
get chartData() {
let dataset: [string, number | undefined, string, ChartDatum[]][] = [];
// each datum needs to be its own object
for (const key of this.dataKeys) {
const chartData: ChartDatum[] = this.args.data.map((d: MonthlyChartData) => ({
timestamp: d.timestamp,
clientType: key,
[key]: d[key],
}));
const group = flatGroup(
chartData,
// order here must match destructure order in return below
(d) => d.timestamp,
(d) => d[key],
(d) => d.clientType
);
dataset = [...dataset, ...group];
}
return dataset.map(([timestamp, counts, clientType]) => ({
timestamp, // x value
counts, // y value
clientType, // corresponds to chart's @color arg
}));
}
// for yBounds scale, tooltip target area and tooltip text data
get aggregatedData(): AggregatedDatum[] {
return this.args.data.map((datum: MonthlyChartData) => {
const values = this.dataKeys
.map((k: string) => datum[k as ClientTypes])
.filter((count) => Number.isInteger(count));
const sum = values.length ? values.reduce((sum, currentValue) => sum + currentValue, 0) : null;
const xValue = datum.timestamp;
return {
x: xValue,
y: sum ?? 0, // y-axis point where tooltip renders
legendX: parseAPITimestamp(xValue, 'MMMM yyyy') as string,
legendY:
sum === null
? ['No data']
: this.dataKeys.map((k) => `${formatNumber([datum[k]])} ${this.label(k)}`),
};
});
}
get yBounds() {
const counts: number[] = this.aggregatedData
.map((d) => d.y)
.flatMap((num) => (typeof num === 'number' ? [num] : []));
const max = Math.max(...counts);
// if max is <=4, hardcode 4 which is the y-axis tickCount so y-axes are not decimals
return [0, max <= 4 ? 4 : max];
}
get xBounds() {
const domain = this.chartData.map((d) => d.timestamp);
return new Set(domain);
}
// TEMPLATE HELPERS
barOffset = (bandwidth: number) => (bandwidth - this.barWidth) / 2;
tooltipX = (original: number, bandwidth: number) => (original + bandwidth / 2).toString();
tooltipY = (original: number) => (!original ? '0' : `${original}`);
formatTicksX = (timestamp: string): string => parseAPITimestamp(timestamp, 'M/yy');
formatTicksY = (num: number): string => numericalAxisLabel(num) || num.toString();
}

View File

@@ -31,7 +31,12 @@
</:stats> </:stats>
<:chart> <:chart>
<Clients::VerticalBarChart @dataset={{this.byMonthActivityData}} @chartLegend={{this.legend}} /> <Clients::Charts::VerticalBarStacked
@chartTitle="Entity/Non-entity clients usage"
@data={{this.byMonthActivityData}}
@chartLegend={{this.legend}}
@chartHeight={{250}}
/>
</:chart> </:chart>
</Clients::ChartContainer> </Clients::ChartContainer>
@@ -60,7 +65,12 @@
</:stats> </:stats>
<:chart> <:chart>
<Clients::VerticalBarChart @dataset={{this.byMonthNewClients}} @chartLegend={{this.legend}} /> <Clients::Charts::VerticalBarStacked
@chartTitle="Monthly new"
@data={{this.byMonthNewClients}}
@chartLegend={{this.legend}}
@chartHeight={{250}}
/>
</:chart> </:chart>
<:emptyState> <:emptyState>

View File

@@ -37,7 +37,12 @@
</:stats> </:stats>
<:chart> <:chart>
<Clients::Charts::Line @dataset={{@byMonthActivityData}} @upgradeData={{@upgradeData}} @chartHeight="250" /> <Clients::Charts::Line
@dataset={{@byMonthActivityData}}
@upgradeData={{@upgradeData}}
@chartHeight="250"
@chartTitle="Vault client counts line chart"
/>
</:chart> </:chart>
</Clients::ChartContainer> </Clients::ChartContainer>
{{else}} {{else}}

View File

@@ -1,36 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if @dataset}}
<svg
data-test-vertical-bar-chart
class="chart has-grid"
{{on "mouseleave" this.removeTooltip}}
{{did-insert this.renderChart @dataset}}
{{did-update this.renderChart @dataset}}
>
</svg>
{{else}}
<EmptyState @title={{@noDataTitle}} @subTitle={{or @noDataMessage "No data to display"}} @bottomBorder={{true}} />
{{/if}}
{{! TOOLTIP }}
{{#if this.tooltipTarget}}
{{! Required to set tag name = div https://github.com/yapplabs/ember-modal-dialog/issues/290 }}
{{! Component must be in curly bracket notation }}
{{! template-lint-disable no-curly-component-invocation }}
{{#modal-dialog
tagName="div" tetherTarget=this.tooltipTarget targetAttachment="bottom middle" attachment="bottom middle" offset="10px 0"
}}
<div class="chart-tooltip">
<p class="bold">{{this.tooltipTotal}}</p>
{{#each this.tooltipStats as |stat|}}
<p>{{stat}}</p>
{{/each}}
</div>
<div class="chart-tooltip-arrow"></div>
{{/modal-dialog}}
{{/if}}

View File

@@ -1,174 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { max } from 'd3-array';
// eslint-disable-next-line no-unused-vars
import { select, selectAll, node } from 'd3-selection';
import { axisLeft, axisBottom } from 'd3-axis';
import { scaleLinear, scalePoint } from 'd3-scale';
import { stack } from 'd3-shape';
import {
BAR_WIDTH,
GREY,
BAR_PALETTE,
SVG_DIMENSIONS,
TRANSLATE,
calculateSum,
formatNumbers,
} from 'vault/utils/chart-helpers';
import { formatNumber } from 'core/helpers/format-number';
/**
* @module VerticalBarChart
* VerticalBarChart components are used to display stacked data in a vertical bar chart with accompanying tooltip
*
* @example
* ```js
* <VerticalBarChart @dataset={dataset} @chartLegend={chartLegend} />
* ```
* @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
* @param {string} [noDataMessage] - custom empty state message that displays when no dataset is passed to the chart
*/
export default class VerticalBarChart extends Component {
@tracked tooltipTarget = '';
@tracked tooltipTotal = '';
@tracked tooltipStats = [];
get xKey() {
return this.args.xKey || 'month';
}
get yKey() {
return this.args.yKey || 'clients';
}
@action
renderChart(element, [chartData]) {
const dataset = chartData;
const filteredData = dataset.filter((e) => Object.keys(e).includes('clients')); // months with data will contain a 'clients' key (otherwise only a timestamp)
const stackFunction = stack().keys(this.args.chartLegend.map((l) => l.key));
const stackedData = stackFunction(filteredData);
const chartSvg = select(element);
const domainMax = max(filteredData.map((d) => d[this.yKey]));
chartSvg.attr('viewBox', `-50 20 600 ${SVG_DIMENSIONS.height}`); // set svg dimensions
// DEFINE DATA BAR SCALES
const yScale = scaleLinear().domain([0, domainMax]).range([0, 100]).nice();
const xScale = scalePoint()
.domain(dataset.map((d) => d[this.xKey]))
.range([0, SVG_DIMENSIONS.width]) // set width to fix number of pixels
.padding(0.2);
// clear out DOM before appending anything
chartSvg.selectAll('g').remove().exit().data(stackedData).enter();
const dataBars = chartSvg
.selectAll('g')
.data(stackedData)
.enter()
.append('g')
.style('fill', (d, i) => BAR_PALETTE[i]);
dataBars
.selectAll('rect')
.data((stackedData) => stackedData)
.enter()
.append('rect')
.attr('width', `${BAR_WIDTH}px`)
.attr('class', 'data-bar')
.attr('data-test-vertical-chart', 'data-bar')
.attr('height', (stackedData) => `${yScale(stackedData[1] - stackedData[0])}%`)
.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
const tooltipTether = chartSvg
.append('g')
.attr('transform', `translate(${BAR_WIDTH / 2})`)
.attr('data-test-vertical-chart', 'tooltip-tethers')
.selectAll('circle')
.data(filteredData)
.enter()
.append('circle')
.style('opacity', '0')
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
.attr('cx', (d) => xScale(d[this.xKey]))
.attr('r', 1);
// MAKE AXES //
const yAxisScale = scaleLinear()
.domain([0, max(filteredData.map((d) => d[this.yKey]))])
.range([`${SVG_DIMENSIONS.height}`, 0])
.nice();
const yAxis = axisLeft(yAxisScale)
.ticks(4)
.tickPadding(10)
.tickSizeInner(-SVG_DIMENSIONS.width)
.tickFormat(formatNumbers);
const xAxis = axisBottom(xScale).tickSize(0);
yAxis(chartSvg.append('g').attr('data-test-vertical-chart', 'y-axis-labels'));
xAxis(
chartSvg
.append('g')
.attr('transform', `translate(0, ${SVG_DIMENSIONS.height + 10})`)
.attr('data-test-vertical-chart', 'x-axis-labels')
);
chartSvg.selectAll('.domain').remove(); // remove domain lines
// WIDER SELECTION AREA FOR TOOLTIP HOVER
const greyBars = chartSvg
.append('g')
.attr('transform', `translate(${TRANSLATE.left})`)
.style('fill', `${GREY}`)
.style('opacity', '0')
.style('mix-blend-mode', 'multiply');
const tooltipRect = greyBars
.selectAll('rect')
.data(filteredData)
.enter()
.append('rect')
.style('cursor', 'pointer')
.attr('class', 'tooltip-rect')
.attr('height', '100%')
.attr('width', '30px') // three times width
.attr('y', '0') // start at bottom
.attr('x', (data) => xScale(data[this.xKey])); // not data.data because this is not stacked data
// MOUSE EVENT FOR TOOLTIP
tooltipRect.on('mouseover', (data) => {
const hoveredMonth = data[this.xKey];
const stackedNumbers = []; // accumulates stacked dataset values to calculate total
this.tooltipStats = []; // clear stats
this.args.chartLegend.forEach(({ key, label }) => {
stackedNumbers.push(data[key]);
// since we're relying on D3 not ember reactivity,
// pushing directly to this.tooltipStats updates the DOM
this.tooltipStats.push(`${formatNumber([data[key]])} ${label}`);
});
this.tooltipTotal = `${formatNumber([calculateSum(stackedNumbers)])} ${
data.new_clients ? 'total' : 'new'
} clients`;
// filter for the tether point that matches the hoveredMonth
const hoveredElement = tooltipTether.filter((data) => data.month === hoveredMonth).node();
this.tooltipTarget = hoveredElement; // grab the node from the list of rects
});
}
@action removeTooltip() {
this.tooltipTarget = null;
}
}

View File

@@ -0,0 +1,194 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.chart-wrapper {
border: $light-border;
border-radius: $radius-large;
padding: $spacing-12 $spacing-24;
margin-bottom: $spacing-16;
}
// GRID LAYOUT //
.single-chart-grid {
display: grid;
grid-template-columns: 1fr 0.3fr 3.7fr;
grid-template-rows: 0.5fr 1fr 1fr 1fr 0.25fr;
width: 100%;
&.no-legend {
grid-template-rows: 0.5fr 1fr 1fr 0.25fr;
}
}
.dual-chart-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: 0.7fr 1fr 1fr 1fr 0.3fr;
width: 100%;
}
.chart-header {
grid-column-start: 1;
grid-column-end: span col4-end;
grid-row-start: 1;
box-shadow: inset 0 -1px 0 $ui-gray-200;
}
.has-header-link {
display: grid;
grid-template-columns: 4fr 1fr;
.header-right {
text-align: right;
> button {
&:hover,
&:focus {
background-color: transparent;
background-color: darken($ui-gray-050, 5%);
border-color: darken($ui-gray-300, 5%);
}
}
}
}
.chart-container-wide {
grid-column-start: 3;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: span 3;
justify-self: center;
height: 300px;
max-width: 700px;
width: 100%;
svg.chart {
width: 100%;
height: 100%;
}
}
.chart-container-left {
grid-column-start: 1;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: 5;
padding-bottom: $spacing-36;
margin-bottom: $spacing-12;
box-shadow: inset 0 -1px 0 $ui-gray-200;
> h2 {
padding-left: 18px;
}
> p {
padding-left: 18px;
}
}
.chart-container-right {
grid-column-start: 4;
grid-column-end: 8;
grid-row-start: 2;
grid-row-end: 5;
padding-bottom: $spacing-36;
margin-bottom: $spacing-12;
box-shadow: inset 0 -1px 0 $ui-gray-200;
> h2 {
padding-left: 18px;
}
> p {
padding-left: 18px;
}
}
.chart-empty-state {
place-self: center stretch;
grid-row-end: span 2;
grid-column-start: 1;
grid-column-end: span 3;
max-width: none;
padding-right: 20px;
padding-left: 20px;
display: flex;
> div {
box-shadow: none !important;
}
> div.empty-state {
white-space: nowrap;
align-self: stretch;
width: 100%;
}
}
.chart-subTitle {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 2;
}
.data-details-top {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 3;
}
.data-details-bottom {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 4;
}
.timestamp {
grid-column: 1 / span 2;
grid-row-start: -1;
color: $ui-gray-500;
font-size: $size-9;
align-self: end;
}
.legend {
grid-row-start: 5;
grid-column-start: 2;
grid-column-end: 6;
align-self: center;
justify-self: center;
font-size: $size-9;
}
// FONT STYLES //
h2.chart-title {
font-weight: $font-weight-bold;
font-size: $size-5;
line-height: $spacing-24;
margin-bottom: $spacing-4;
}
p.chart-description {
color: $ui-gray-700;
font-size: 14px;
line-height: 18px;
margin-bottom: $spacing-8;
}
p.chart-subtext {
color: $ui-gray-500;
font-size: $size-8;
line-height: 16px;
margin-top: $spacing-8;
}
h3.data-details {
font-weight: $font-weight-bold;
font-size: $size-8;
line-height: 14px;
margin-bottom: $spacing-8;
}
p.data-details {
font-weight: $font-weight-normal;
font-size: $size-4;
}

View File

@@ -23,6 +23,7 @@
@import './core/box'; @import './core/box';
@import './core/buttons'; @import './core/buttons';
@import './core/charts'; @import './core/charts';
@import './core/charts-lineal';
@import './core/checkbox-and-radio'; @import './core/checkbox-and-radio';
@import './core/columns'; @import './core/columns';
@import './core/containers'; @import './core/containers';
@@ -57,6 +58,7 @@
@import './components/b64-toggle'; @import './components/b64-toggle';
@import './components/box-label'; @import './components/box-label';
@import './components/calendar-widget'; @import './components/calendar-widget';
@import './components/chart-container';
@import './components/cluster-banners'; @import './components/cluster-banners';
@import './components/codemirror'; @import './components/codemirror';
@import './components/console-ui-panel'; @import './components/console-ui-panel';

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
// LINEAL STYLING //
.lineal-chart {
position: relative;
padding: 10px 10px 20px 50px;
width: 100%;
svg {
overflow: visible;
}
}
.lineal-chart-bar {
fill: var(--token-color-palette-blue-300);
}
.lineal-axis {
color: $ui-gray-500;
text {
font-size: 0.75rem;
}
line {
color: $ui-gray-300;
}
}
.lineal-tooltip-position {
position: absolute;
transform-style: preserve-3d;
bottom: 30px;
left: -20px;
pointer-events: none;
width: 140px;
transform: translate(calc(1px * var(--x, 0)), calc(-1px * var(--y, 0)));
transform-origin: bottom left;
z-index: 100;
margin-bottom: $spacing-8;
}
// @colorScale arg for Lineal::VBars is "blue-bar", indices are added by lineal
.blue-bar-1 {
color: var(--token-color-palette-blue-100);
fill: var(--token-color-palette-blue-100);
}
.blue-bar-2 {
color: var(--token-color-palette-blue-200);
fill: var(--token-color-palette-blue-200);
}

View File

@@ -3,197 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
.chart-wrapper { // MISC STYLES for non-lineal charts
border: $light-border;
border-radius: $radius-large;
padding: $spacing-12 $spacing-24;
margin-bottom: $spacing-16;
}
// GRID LAYOUT //
.single-chart-grid {
display: grid;
grid-template-columns: 1fr 0.3fr 3.7fr;
grid-template-rows: 0.5fr 1fr 1fr 1fr 0.25fr;
width: 100%;
&.no-legend {
grid-template-rows: 0.5fr 1fr 1fr 0.25fr;
}
}
.dual-chart-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: 0.7fr 1fr 1fr 1fr 0.3fr;
width: 100%;
}
.chart-header {
grid-column-start: 1;
grid-column-end: span col4-end;
grid-row-start: 1;
box-shadow: inset 0 -1px 0 $ui-gray-200;
}
.has-header-link {
display: grid;
grid-template-columns: 4fr 1fr;
.header-right {
text-align: right;
> button {
&:hover,
&:focus {
background-color: transparent;
background-color: darken($ui-gray-050, 5%);
border-color: darken($ui-gray-300, 5%);
}
}
}
}
.chart-container-wide {
grid-column-start: 3;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: span 3;
justify-self: center;
height: 300px;
max-width: 700px;
width: 100%;
svg.chart {
width: 100%;
height: 100%;
}
}
.chart-container-left {
grid-column-start: 1;
grid-column-end: 4;
grid-row-start: 2;
grid-row-end: 5;
padding-bottom: $spacing-36;
margin-bottom: $spacing-12;
box-shadow: inset 0 -1px 0 $ui-gray-200;
> h2 {
padding-left: 18px;
}
> p {
padding-left: 18px;
}
}
.chart-container-right {
grid-column-start: 4;
grid-column-end: 8;
grid-row-start: 2;
grid-row-end: 5;
padding-bottom: $spacing-36;
margin-bottom: $spacing-12;
box-shadow: inset 0 -1px 0 $ui-gray-200;
> h2 {
padding-left: 18px;
}
> p {
padding-left: 18px;
}
}
.chart-empty-state {
place-self: center stretch;
grid-row-end: span 2;
grid-column-start: 1;
grid-column-end: span 3;
max-width: none;
padding-right: 20px;
padding-left: 20px;
display: flex;
> div {
box-shadow: none !important;
}
> div.empty-state {
white-space: nowrap;
align-self: stretch;
width: 100%;
}
}
.chart-subTitle {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 2;
}
.data-details-top {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 3;
}
.data-details-bottom {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 4;
}
.timestamp {
grid-column: 1 / span 2;
grid-row-start: -1;
color: $ui-gray-500;
font-size: $size-9;
align-self: end;
}
.legend {
grid-row-start: 5;
grid-column-start: 2;
grid-column-end: 6;
align-self: center;
justify-self: center;
font-size: $size-9;
}
// FONT STYLES //
h2.chart-title {
font-weight: $font-weight-bold;
font-size: $size-5;
line-height: $spacing-24;
margin-bottom: $spacing-4;
}
p.chart-description {
color: $ui-gray-700;
font-size: 14px;
line-height: 18px;
margin-bottom: $spacing-8;
}
p.chart-subtext {
color: $ui-gray-500;
font-size: $size-8;
line-height: 16px;
margin-top: $spacing-8;
}
h3.data-details {
font-weight: $font-weight-bold;
font-size: $size-8;
line-height: 14px;
margin-bottom: $spacing-8;
}
p.data-details {
font-weight: $font-weight-normal;
font-size: $size-4;
}
// MISC STYLES
.legend-colors { .legend-colors {
height: 10px; height: 10px;
@@ -302,36 +112,3 @@ p.data-details {
grid-row-start: 4; grid-row-start: 4;
} }
} }
// LINEAL STYLING //
.lineal-chart {
position: relative;
padding: 10px 10px 20px 50px;
width: 100%;
svg {
overflow: visible;
}
}
.lineal-chart-bar {
fill: var(--token-color-palette-blue-300);
}
.lineal-axis {
color: $ui-gray-500;
text {
font-size: 0.75rem;
}
line {
color: $ui-gray-300;
}
}
.lineal-tooltip-position {
position: absolute;
transform-style: preserve-3d;
bottom: 30px;
left: -20px;
pointer-events: none;
width: 140px;
transform: translate(calc(1px * var(--x, 0)), calc(-1px * var(--y, 0)));
transform-origin: bottom left;
z-index: 100;
}

View File

@@ -18,7 +18,7 @@ export const SVG_DIMENSIONS = { height: 190, width: 500 };
export const BAR_WIDTH = 7; // data bar width is 7 pixels export const BAR_WIDTH = 7; // data bar width is 7 pixels
// Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g // Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g
export function formatNumbers(number) { export function numericalAxisLabel(number) {
if (number < 1000) return number; if (number < 1000) return number;
if (number < 1100) return format('.1s')(number); if (number < 1100) return format('.1s')(number);
if (number < 2000) return format('.2s')(number); // between 1k and 2k, show 2 decimals if (number < 2000) return format('.2s')(number); // between 1k and 2k, show 2 decimals

View File

@@ -24,7 +24,7 @@ export const CLIENT_TYPES = [
'secret_syncs', 'secret_syncs',
] as const; ] as const;
type ClientTypes = (typeof CLIENT_TYPES)[number]; export type ClientTypes = (typeof CLIENT_TYPES)[number];
// returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10 // returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10
// that occurred between timestamps (i.e. queried activity data) // that occurred between timestamps (i.e. queried activity data)

View File

@@ -69,6 +69,7 @@
"@icholy/duration": "^5.1.0", "@icholy/duration": "^5.1.0",
"@lineal-viz/lineal": "^0.5.1", "@lineal-viz/lineal": "^0.5.1",
"@tsconfig/ember": "^2.0.0", "@tsconfig/ember": "^2.0.0",
"@types/d3-array": "^3.2.1",
"@types/ember": "^4.0.2", "@types/ember": "^4.0.2",
"@types/ember-data": "^4.4.6", "@types/ember-data": "^4.4.6",
"@types/ember-data__adapter": "^4.0.1", "@types/ember-data__adapter": "^4.0.1",

View File

@@ -12,7 +12,7 @@ import sinon from 'sinon';
import timestamp from 'core/utils/timestamp'; import timestamp from 'core/utils/timestamp';
import authPage from 'vault/tests/pages/auth'; import authPage from 'vault/tests/pages/auth';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { ACTIVITY_RESPONSE_STUB, assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers'; import { ACTIVITY_RESPONSE_STUB, assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
@@ -146,7 +146,7 @@ module('Acceptance | clients | counts | acme', function (hooks) {
// no data because this is an auth mount (acme_clients come from pki mounts) // no data because this is an auth mount (acme_clients come from pki mounts)
await click(searchSelect.option(searchSelect.optionIndex('auth/authid/0'))); await click(searchSelect.option(searchSelect.optionIndex('auth/authid/0')));
assert.dom(CLIENT_COUNT.statText('Total ACME clients')).hasTextContaining('0'); assert.dom(CLIENT_COUNT.statText('Total ACME clients')).hasTextContaining('0');
assert.dom(`${CLIENT_COUNT.charts.chart('ACME usage')} ${CLIENT_COUNT.charts.dataBar}`).isNotVisible(); assert.dom(`${CHARTS.chart('ACME usage')} ${CHARTS.verticalBar}`).isNotVisible();
assert.dom(CLIENT_COUNT.chartContainer('Monthly new')).doesNotExist(); assert.dom(CHARTS.container('Monthly new')).doesNotExist();
}); });
}); });

View File

@@ -12,7 +12,7 @@ import { visit, click, findAll, settled } from '@ember/test-helpers';
import authPage from 'vault/tests/pages/auth'; import authPage from 'vault/tests/pages/auth';
import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters'; import { ARRAY_OF_MONTHS } from 'core/utils/date-formatters';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { create } from 'ember-cli-page-object'; import { create } from 'ember-cli-page-object';
import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { clickTrigger } from 'ember-power-select/test-support/helpers';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
@@ -55,16 +55,12 @@ module('Acceptance | clients | overview', function (hooks) {
.hasText('Jul 2023 - Jan 2024', 'Date range shows dates correctly parsed activity response'); .hasText('Jul 2023 - Jan 2024', 'Date range shows dates correctly parsed activity response');
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area'); assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
assert assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts')) .dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts'); .exists('Shows running totals with monthly breakdown charts');
assert assert
.dom(CLIENT_COUNT.charts.line.xAxisLabel) .dom(`${CHARTS.container('Vault client counts')} ${CHARTS.xAxisLabel}`)
.hasText('7/23', 'x-axis labels start with billing start date'); .hasText('7/23', 'x-axis labels start with billing start date');
assert.strictEqual( assert.strictEqual(findAll(CHARTS.plotPoint).length, 5, 'line chart plots 5 points to match query');
findAll('[data-test-line-chart="plot-point"]').length,
5,
'line chart plots 5 points to match query'
);
}); });
test('it should update charts when querying date ranges', async function (assert) { test('it should update charts when querying date ranges', async function (assert) {
@@ -77,7 +73,7 @@ module('Acceptance | clients | overview', function (hooks) {
.dom(CLIENT_COUNT.usageStats('Vault client counts')) .dom(CLIENT_COUNT.usageStats('Vault client counts'))
.doesNotExist('running total single month stat boxes do not show'); .doesNotExist('running total single month stat boxes do not show');
assert assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts')) .dom(CHARTS.container('Vault client counts'))
.doesNotExist('running total month over month charts do not show'); .doesNotExist('running total month over month charts do not show');
assert.dom(CLIENT_COUNT.attributionBlock).exists('attribution area shows'); assert.dom(CLIENT_COUNT.attributionBlock).exists('attribution area shows');
assert assert
@@ -101,16 +97,12 @@ module('Acceptance | clients | overview', function (hooks) {
await click('[data-test-date-dropdown-submit]'); await click('[data-test-date-dropdown-submit]');
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area'); assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
assert assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts')) .dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts'); .exists('Shows running totals with monthly breakdown charts');
assert assert
.dom(CLIENT_COUNT.charts.line.xAxisLabel) .dom(`${CHARTS.container('Vault client counts')} ${CHARTS.xAxisLabel}`)
.hasText('9/23', 'x-axis labels start with queried start month (upgrade date)'); .hasText('9/23', 'x-axis labels start with queried start month (upgrade date)');
assert.strictEqual( assert.strictEqual(findAll(CHARTS.plotPoint).length, 5, 'line chart plots 5 points to match query');
findAll('[data-test-line-chart="plot-point"]').length,
5,
'line chart plots 5 points to match query'
);
// query for single, historical month (upgrade month) // query for single, historical month (upgrade month)
await click(CLIENT_COUNT.rangeDropdown); await click(CLIENT_COUNT.rangeDropdown);
@@ -122,11 +114,11 @@ module('Acceptance | clients | overview', function (hooks) {
.dom(CLIENT_COUNT.usageStats('Vault client counts')) .dom(CLIENT_COUNT.usageStats('Vault client counts'))
.exists('running total single month usage stats show'); .exists('running total single month usage stats show');
assert assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts')) .dom(CHARTS.container('Vault client counts'))
.doesNotExist('running total month over month charts do not show'); .doesNotExist('running total month over month charts do not show');
assert.dom(CLIENT_COUNT.attributionBlock).exists('attribution area shows'); assert.dom(CLIENT_COUNT.attributionBlock).exists('attribution area shows');
assert.dom('[data-test-chart-container="new-clients"]').exists('new client attribution chart shows'); assert.dom(CHARTS.container('new-clients')).exists('new client attribution chart shows');
assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows'); assert.dom(CHARTS.container('total-clients')).exists('total client attribution chart shows');
// query historical date range (from September 2023 to December 2023) // query historical date range (from September 2023 to December 2023)
await click(CLIENT_COUNT.rangeDropdown); await click(CLIENT_COUNT.rangeDropdown);
@@ -135,14 +127,10 @@ module('Acceptance | clients | overview', function (hooks) {
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area'); assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
assert assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts')) .dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts'); .exists('Shows running totals with monthly breakdown charts');
assert.strictEqual( assert.strictEqual(findAll(CHARTS.plotPoint).length, 4, 'line chart plots 4 points to match query');
findAll('[data-test-line-chart="plot-point"]').length, const xAxisLabels = findAll(CHARTS.xAxisLabel);
4,
'line chart plots 4 points to match query'
);
const xAxisLabels = findAll(CLIENT_COUNT.charts.line.xAxisLabel);
assert assert
.dom(xAxisLabels[xAxisLabels.length - 1]) .dom(xAxisLabels[xAxisLabels.length - 1])
.hasText('12/23', 'x-axis labels end with queried end month'); .hasText('12/23', 'x-axis labels end with queried end month');
@@ -167,7 +155,7 @@ module('Acceptance | clients | overview', function (hooks) {
test('totals filter correctly with full data', async function (assert) { test('totals filter correctly with full data', async function (assert) {
assert assert
.dom(CLIENT_COUNT.chartContainer('Vault client counts')) .dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts'); .exists('Shows running totals with monthly breakdown charts');
assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area'); assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area');
@@ -193,7 +181,7 @@ module('Acceptance | clients | overview', function (hooks) {
}; };
for (const label in expectedStats) { for (const label in expectedStats) {
assert assert
.dom(CLIENT_COUNT.charts.statTextValue(label)) .dom(CLIENT_COUNT.statTextValue(label))
.includesText(`${expectedStats[label]}`, `label: ${label} renders accurate namespace client counts`); .includesText(`${expectedStats[label]}`, `label: ${label} renders accurate namespace client counts`);
} }
@@ -213,7 +201,7 @@ module('Acceptance | clients | overview', function (hooks) {
}; };
for (const label in expectedStats) { for (const label in expectedStats) {
assert assert
.dom(CLIENT_COUNT.charts.statTextValue(label)) .dom(CLIENT_COUNT.statTextValue(label))
.includesText(`${expectedStats[label]}`, `label: "${label} "renders accurate mount client counts`); .includesText(`${expectedStats[label]}`, `label: "${label} "renders accurate mount client counts`);
} }
@@ -235,7 +223,7 @@ module('Acceptance | clients | overview', function (hooks) {
}; };
for (const label in expectedStats) { for (const label in expectedStats) {
assert assert
.dom(CLIENT_COUNT.charts.statTextValue(label)) .dom(CLIENT_COUNT.statTextValue(label))
.includesText(`${expectedStats[label]}`, `label: ${label} is back to unfiltered value`); .includesText(`${expectedStats[label]}`, `label: ${label} is back to unfiltered value`);
} }
}); });
@@ -273,13 +261,13 @@ module('Acceptance | clients | overview | sync in license, activated', function
}); });
test('it should show secrets sync data in overview and tab', async function (assert) { test('it should show secrets sync data in overview and tab', async function (assert) {
assert.dom(CLIENT_COUNT.charts.statTextValue('Secret sync')).exists('shows secret sync data on overview'); assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists('shows secret sync data on overview');
await click(GENERAL.tab('sync')); await click(GENERAL.tab('sync'));
assert.dom(GENERAL.tab('sync')).hasClass('active'); assert.dom(GENERAL.tab('sync')).hasClass('active');
assert.dom(GENERAL.emptyStateTitle).doesNotExist(); assert.dom(GENERAL.emptyStateTitle).doesNotExist();
assert assert
.dom(CLIENT_COUNT.charts.chart('Secrets sync usage')) .dom(CHARTS.chart('Secrets sync usage'))
.exists('chart is shown because feature is active and has data'); .exists('chart is shown because feature is active and has data');
}); });
}); });
@@ -302,7 +290,7 @@ module('Acceptance | clients | overview | sync in license, not activated', funct
test('it should hide secrets sync charts', async function (assert) { test('it should hide secrets sync charts', async function (assert) {
assert assert
.dom(CLIENT_COUNT.charts.chart('Secrets sync usage')) .dom(CHARTS.chart('Secrets sync usage'))
.doesNotExist('chart is hidden because feature is not activated'); .doesNotExist('chart is hidden because feature is not activated');
assert.dom('[data-test-stat-text="secret-syncs"]').doesNotExist(); assert.dom('[data-test-stat-text="secret-syncs"]').doesNotExist();
@@ -327,7 +315,7 @@ module('Acceptance | clients | overview | sync not in license', function (hooks)
}); });
test('it should hide secrets sync charts', async function (assert) { test('it should hide secrets sync charts', async function (assert) {
assert.dom(CLIENT_COUNT.charts.chart('Secrets sync usage')).doesNotExist(); assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist();
assert.dom('[data-test-stat-text="secret-syncs"]').doesNotExist(); assert.dom('[data-test-stat-text="secret-syncs"]').doesNotExist();
}); });

View File

@@ -13,7 +13,7 @@ import sinon from 'sinon';
import timestamp from 'core/utils/timestamp'; import timestamp from 'core/utils/timestamp';
import authPage from 'vault/tests/pages/auth'; import authPage from 'vault/tests/pages/auth';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
module('Acceptance | clients | sync | activated', function (hooks) { module('Acceptance | clients | sync | activated', function (hooks) {
setupApplicationTest(hooks); setupApplicationTest(hooks);
@@ -35,9 +35,7 @@ module('Acceptance | clients | sync | activated', function (hooks) {
test('it should render charts when secrets sync is activated', async function (assert) { test('it should render charts when secrets sync is activated', async function (assert) {
syncHandler(this.server); syncHandler(this.server);
assert assert.dom(CHARTS.chart('Secrets sync usage')).exists('Secrets sync usage chart is rendered');
.dom(CLIENT_COUNT.charts.chart('Secrets sync usage'))
.exists('Secrets sync usage chart is rendered');
assert.dom(CLIENT_COUNT.statText('Total sync clients')).exists('Total sync clients chart is rendered'); assert.dom(CLIENT_COUNT.statText('Total sync clients')).exists('Total sync clients chart is rendered');
assert.dom(GENERAL.emptyStateTitle).doesNotExist(); assert.dom(GENERAL.emptyStateTitle).doesNotExist();
}); });

View File

@@ -7,7 +7,7 @@ import { click, findAll } from '@ember/test-helpers';
import { LICENSE_START } from 'vault/mirage/handlers/clients'; import { LICENSE_START } from 'vault/mirage/handlers/clients';
import { addMonths } from 'date-fns'; import { addMonths } from 'date-fns';
import { CLIENT_COUNT } from './client-count-selectors'; import { CLIENT_COUNT, CHARTS } from './client-count-selectors';
export async function dateDropdownSelect(month, year) { export async function dateDropdownSelect(month, year) {
const { dateDropdown, counts } = CLIENT_COUNT; const { dateDropdown, counts } = CLIENT_COUNT;
@@ -19,18 +19,18 @@ export async function dateDropdownSelect(month, year) {
await click(dateDropdown.submit); await click(dateDropdown.submit);
} }
export function assertBarChart(assert, chartName, byMonthData) { export function assertBarChart(assert, chartName, byMonthData, isStacked = false) {
// assertion count is byMonthData.length + 2 // assertion count is byMonthData.length, plus 2
const chart = CLIENT_COUNT.charts.chart(chartName); const chart = CHARTS.chart(chartName);
const dataBars = findAll(`${chart} ${CLIENT_COUNT.charts.dataBar}`).filter((b) => b.hasAttribute('height')); const dataBars = findAll(`${chart} ${CHARTS.verticalBar}`).filter(
const xAxisLabels = findAll(`${chart} ${CLIENT_COUNT.charts.xAxisLabel}`); (b) => b.hasAttribute('height') && b.getAttribute('height') !== '0'
assert.strictEqual(
dataBars.length,
byMonthData.filter((m) => m.clients).length,
`${chartName}: it renders bars for each non-zero month`
); );
const xAxisLabels = findAll(`${chart} ${CHARTS.xAxisLabel}`);
let count = byMonthData.filter((m) => m.clients).length;
if (isStacked) count = count * 2;
assert.strictEqual(dataBars.length, count, `${chartName}: it renders bars for each non-zero month`);
assert.strictEqual( assert.strictEqual(
xAxisLabels.length, xAxisLabels.length,
byMonthData.length, byMonthData.length,

View File

@@ -17,28 +17,8 @@ export const CLIENT_COUNT = {
startDiscrepancy: '[data-test-counts-start-discrepancy]', startDiscrepancy: '[data-test-counts-start-discrepancy]',
}, },
statText: (label: string) => `[data-test-stat-text="${label}"]`, statText: (label: string) => `[data-test-stat-text="${label}"]`,
chartContainer: (title: string) => `[data-test-chart-container="${title}"]`, statTextValue: (label: string) =>
charts: { label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
chart: (title: string) => `[data-test-chart="${title}"]`, // newer lineal charts
statTextValue: (label: string) =>
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
legend: '[data-test-chart-container-legend]',
legendLabel: (nth: number) => `.legend-label:nth-child(${nth * 2})`, // nth * 2 accounts for dots in legend
timestamp: '[data-test-chart-container-timestamp]',
dataBar: '[data-test-vertical-bar]',
xAxisLabel: '[data-test-x-axis] text',
// selectors for old d3 charts
verticalBar: '[data-test-vertical-bar-chart]',
lineChart: '[data-test-line-chart]',
bar: {
xAxisLabel: '[data-test-vertical-chart="x-axis-labels"] text',
dataBar: '[data-test-vertical-chart="data-bar"]',
},
line: {
xAxisLabel: '[data-test-line-chart] [data-test-x-axis] text',
plotPoint: '[data-test-line-chart="plot-point"]',
},
},
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`, usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
dateDisplay: '[data-test-date-display]', dateDisplay: '[data-test-date-display]',
attributionBlock: '[data-test-clients-attribution]', attributionBlock: '[data-test-clients-attribution]',
@@ -68,11 +48,21 @@ export const CLIENT_COUNT = {
upgradeWarning: '[data-test-clients-upgrade-warning]', upgradeWarning: '[data-test-clients-upgrade-warning]',
}; };
export const CHART_ELEMENTS = { export const CHARTS = {
entityClientDataBars: '[data-test-group="entity_clients"]', // container
nonEntityDataBars: '[data-test-group="non_entity_clients"]', container: (title: string) => `[data-test-chart-container="${title}"]`,
yLabels: '[data-test-group="y-labels"]', timestamp: '[data-test-chart-container-timestamp]',
actionBars: '[data-test-group="action-bars"]', legend: '[data-test-chart-container-legend]',
labelActionBars: '[data-test-group="label-action-bars"]', legendLabel: (nth: number) => `.legend-label:nth-child(${nth * 2})`, // nth * 2 accounts for dots in legend
totalValues: '[data-test-group="total-values"]',
// chart elements
chart: (title: string) => `[data-test-chart="${title}"]`,
hover: (area: string) => `[data-test-interactive-area="${area}"]`,
table: '[data-test-underlying-data]',
tooltip: '[data-test-tooltip]',
verticalBar: '[data-test-vertical-bar]',
xAxis: '[data-test-x-axis]',
yAxis: '[data-test-y-axis]',
xAxisLabel: '[data-test-x-axis] text',
plotPoint: '[data-test-plot-point]',
}; };

View File

@@ -44,8 +44,10 @@ module('Integration | Component | clients/charts/vertical-bar-basic', function (
test('it renders when some months have no data', async function (assert) { test('it renders when some months have no data', async function (assert) {
assert.expect(10); assert.expect(10);
await render(hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" />`); await render(
assert.dom('[data-test-sync-bar-chart]').exists('renders chart container'); hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @chartTitle="My chart"/>`
);
assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
assert.dom('[data-test-vertical-bar]').exists({ count: 3 }, 'renders 3 vertical bars'); assert.dom('[data-test-vertical-bar]').exists({ count: 3 }, 'renders 3 vertical bars');
// Tooltips // Tooltips
@@ -88,9 +90,11 @@ module('Integration | Component | clients/charts/vertical-bar-basic', function (
secret_syncs: 0, secret_syncs: 0,
}, },
]; ];
await render(hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" />`); await render(
hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @chartTitle="My chart"/>`
);
assert.dom('[data-test-sync-bar-chart]').exists('renders chart container'); assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
assert.dom('[data-test-vertical-bar]').exists({ count: 2 }, 'renders 2 vertical bars'); assert.dom('[data-test-vertical-bar]').exists({ count: 2 }, 'renders 2 vertical bars');
assert.dom('[data-test-vertical-bar]').hasAttribute('height', '0', 'rectangles have 0 height'); assert.dom('[data-test-vertical-bar]').hasAttribute('height', '0', 'rectangles have 0 height');
// Tooltips // Tooltips
@@ -108,12 +112,12 @@ module('Integration | Component | clients/charts/vertical-bar-basic', function (
test('it renders underlying data', async function (assert) { test('it renders underlying data', async function (assert) {
assert.expect(3); assert.expect(3);
await render( await render(
hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @showTable={{true}} />` hbs`<Clients::Charts::VerticalBarBasic @data={{this.data}} @dataKey="secret_syncs" @showTable={{true}} @chartTitle="My chart"/>`
); );
assert.dom('[data-test-sync-bar-chart]').exists('renders chart container'); assert.dom('[data-test-chart="My chart"]').exists('renders chart container');
assert.dom('[data-test-underlying-data]').exists('renders underlying data when showTable=true'); assert.dom('[data-test-underlying-data]').exists('renders underlying data when showTable=true');
assert assert
.dom('[data-test-underlying-data] thead') .dom('[data-test-underlying-data] thead')
.hasText('Month Count of secret syncs', 'renders correct table headers'); .hasText('Month Secret syncs Count', 'renders correct table headers');
}); });
}); });

View File

@@ -0,0 +1,144 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { findAll, render, triggerEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
const EXAMPLE = [
{
timestamp: '2022-09-01T00:00:00',
total: null,
fuji_apples: null,
gala_apples: null,
red_delicious: null,
},
{
timestamp: '2022-10-01T00:00:00',
total: 6440,
fuji_apples: 1471,
gala_apples: 4389,
red_delicious: 4207,
},
{
timestamp: '2022-11-01T00:00:00',
total: 9583,
fuji_apples: 149,
gala_apples: 20,
red_delicious: 5802,
},
];
module('Integration | Component | clients/charts/vertical-bar-stacked', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.data = EXAMPLE;
this.legend = [
{ key: 'fuji_apples', label: 'Fuji counts' },
{ key: 'gala_apples', label: 'Gala counts' },
];
});
test('it renders when some months have no data', async function (assert) {
assert.expect(10);
await render(
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @chartTitle="My chart"/>`
);
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
const visibleBars = findAll(CHARTS.verticalBar).filter((e) => e.getAttribute('height') !== '0');
const count = this.data.filter((d) => d.total !== null).length * 2;
assert.strictEqual(visibleBars.length, count, `renders ${count} vertical bars`);
// Tooltips
await triggerEvent(CHARTS.hover('2022-09-01T00:00:00'), 'mouseover');
assert.dom(CHARTS.tooltip).isVisible('renders tooltip on mouseover');
assert
.dom(CHARTS.tooltip)
.hasText('September 2022 No data', 'renders formatted timestamp with no data message');
await triggerEvent(CHARTS.hover('2022-09-01T00:00:00'), 'mouseout');
assert.dom(CHARTS.tooltip).doesNotExist('removes tooltip on mouseout');
await triggerEvent(CHARTS.hover('2022-10-01T00:00:00'), 'mouseover');
assert
.dom(CHARTS.tooltip)
.hasText('October 2022 1,471 Fuji counts 4,389 Gala counts', 'October tooltip has exact count');
await triggerEvent(CHARTS.hover('2022-10-01T00:00:00'), 'mouseout');
await triggerEvent(CHARTS.hover('2022-11-01T00:00:00'), 'mouseover');
assert
.dom(CHARTS.tooltip)
.hasText('November 2022 149 Fuji counts 20 Gala counts', 'November tooltip has exact count');
await triggerEvent(CHARTS.hover('2022-11-01T00:00:00'), 'mouseout');
// Axis
assert.dom(CHARTS.xAxis).hasText('9/22 10/22 11/22', 'renders x-axis labels');
assert.dom(CHARTS.yAxis).hasText('0 2k 4k', 'renders y-axis labels');
// Table
assert.dom(CHARTS.table).doesNotExist('does not render underlying data by default');
});
// 0 is different than null (no data)
test('it renders when all months have 0 clients', async function (assert) {
assert.expect(14);
this.data = [
{
month: '10/22',
timestamp: '2022-10-01T00:00:00',
total: 40,
fuji_apples: 0,
gala_apples: 0,
red_delicious: 40,
},
{
month: '11/22',
timestamp: '2022-11-01T00:00:00',
total: 180,
fuji_apples: 0,
gala_apples: 0,
red_delicious: 180,
},
];
await render(
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @chartTitle="My chart"/>`
);
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
findAll(CHARTS.verticalBar).forEach((b, idx) =>
assert.dom(b).isNotVisible(`bar: ${idx} does not render`)
);
findAll(CHARTS.verticalBar).forEach((b, idx) =>
assert.dom(b).hasAttribute('height', '0', `rectangle: ${idx} have 0 height`)
);
// Tooltips
await triggerEvent(CHARTS.hover('2022-10-01T00:00:00'), 'mouseover');
assert.dom(CHARTS.tooltip).isVisible('renders tooltip on mouseover');
assert.dom(CHARTS.tooltip).hasText('October 2022 0 Fuji counts 0 Gala counts', 'tooltip has 0 counts');
await triggerEvent(CHARTS.hover('2022-10-01T00:00:00'), 'mouseout');
assert.dom(CHARTS.tooltip).isNotVisible('removes tooltip on mouseout');
// Axis
assert.dom(CHARTS.xAxis).hasText('10/22 11/22', 'renders x-axis labels');
assert.dom(CHARTS.yAxis).hasText('0 1 2 3 4', 'renders y-axis labels');
});
test('it renders underlying data', async function (assert) {
assert.expect(3);
await render(
hbs`<Clients::Charts::VerticalBarStacked @data={{this.data}} @chartLegend={{this.legend}} @showTable={{true}} @chartTitle="My chart"/>`
);
assert.dom(CHARTS.chart('My chart')).exists('renders chart container');
assert.dom(CHARTS.table).exists('renders underlying data when showTable=true');
assert
.dom(`${CHARTS.table} thead`)
.hasText('Timestamp Fuji apples Gala apples', 'renders correct table headers');
});
});

View File

@@ -53,9 +53,9 @@ module('Integration | Component | clients/line-chart', function (hooks) {
</div> </div>
`); `);
assert.dom('[data-test-line-chart]').exists('Chart is rendered'); assert.dom('[data-test-chart="line chart"]').exists('Chart is rendered');
assert assert
.dom('[data-test-line-chart="plot-point"]') .dom('[data-test-plot-point]')
.exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`); .exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`);
findAll('[data-test-x-axis] text').forEach((e, i) => { findAll('[data-test-x-axis] text').forEach((e, i) => {
// For some reason the first axis label is not rendered // For some reason the first axis label is not rendered
@@ -110,9 +110,9 @@ module('Integration | Component | clients/line-chart', function (hooks) {
/> />
</div> </div>
`); `);
assert.dom('[data-test-line-chart]').exists('Chart is rendered'); assert.dom('[data-test-chart="line chart"]').exists('Chart is rendered');
assert assert
.dom('[data-test-line-chart="plot-point"]') .dom('[data-test-plot-point]')
.exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`); .exists({ count: this.dataset.length }, `renders ${this.dataset.length} plot points`);
assert assert
.dom(find(`[data-test-line-chart="upgrade-${this.dataset[2].month}"]`)) .dom(find(`[data-test-line-chart="upgrade-${this.dataset[2].month}"]`))
@@ -242,7 +242,7 @@ module('Integration | Component | clients/line-chart', function (hooks) {
`); `);
assert assert
.dom('[data-test-line-chart="plot-point"]') .dom('[data-test-plot-point]')
.exists({ count: this.dataset.length }, 'chart still renders when upgradeData is not an array'); .exists({ count: this.dataset.length }, 'chart still renders when upgradeData is not an array');
}); });
@@ -260,7 +260,7 @@ module('Integration | Component | clients/line-chart', function (hooks) {
`); `);
assert assert
.dom('[data-test-line-chart="plot-point"]') .dom('[data-test-plot-point]')
.exists({ count: this.dataset.length }, 'chart still renders when upgradeData has incorrect keys'); .exists({ count: this.dataset.length }, 'chart still renders when upgradeData has incorrect keys');
}); });

View File

@@ -12,7 +12,7 @@ import hbs from 'htmlbars-inline-precompile';
import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
import { getUnixTime } from 'date-fns'; import { getUnixTime } from 'date-fns';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
import { calculateAverage } from 'vault/utils/chart-helpers'; import { calculateAverage } from 'vault/utils/chart-helpers';
import { dateFormat } from 'core/helpers/date-format'; import { dateFormat } from 'core/helpers/date-format';
@@ -20,7 +20,7 @@ import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers
const START_TIME = getUnixTime(LICENSE_START); const START_TIME = getUnixTime(LICENSE_START);
const END_TIME = getUnixTime(STATIC_NOW); const END_TIME = getUnixTime(STATIC_NOW);
const { statText, chartContainer, charts, usageStats } = CLIENT_COUNT; const { statText, usageStats } = CLIENT_COUNT;
module('Integration | Component | clients | Clients::Page::Acme', function (hooks) { module('Integration | Component | clients | Clients::Page::Acme', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@@ -81,7 +81,7 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], {
withTimeZone: true, withTimeZone: true,
}); });
assert.dom(charts.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp'); assert.dom(CHARTS.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp');
assertBarChart(assert, 'ACME usage', this.activity.byMonth); assertBarChart(assert, 'ACME usage', this.activity.byMonth);
assertBarChart(assert, 'Monthly new', this.activity.byMonth); assertBarChart(assert, 'Monthly new', this.activity.byMonth);
@@ -94,8 +94,8 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
const expectedTotal = formatNumber([this.activity.total.acme_clients]); const expectedTotal = formatNumber([this.activity.total.acme_clients]);
await this.renderComponent(); await this.renderComponent();
assert.dom(charts.chart('ACME usage')).doesNotExist('total usage chart does not render'); assert.dom(CHARTS.chart('ACME usage')).doesNotExist('total usage chart does not render');
assert.dom(chartContainer('Monthly new')).doesNotExist('monthly new chart does not render'); assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render');
assert.dom(statText('Average ACME clients per month')).doesNotExist(); assert.dom(statText('Average ACME clients per month')).doesNotExist();
assert.dom(statText('Average new ACME clients per month')).doesNotExist(); assert.dom(statText('Average new ACME clients per month')).doesNotExist();
assert assert
@@ -121,8 +121,8 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
.dom(GENERAL.emptyStateMessage) .dom(GENERAL.emptyStateMessage)
.hasText('There is no ACME client data available for this date range.'); .hasText('There is no ACME client data available for this date range.');
assert.dom(charts.chart('ACME usage')).doesNotExist('vertical bar chart does not render'); assert.dom(CHARTS.chart('ACME usage')).doesNotExist('vertical bar chart does not render');
assert.dom(chartContainer('Monthly new')).doesNotExist('monthly new chart does not render'); assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render');
assert.dom(statText('Total ACME clients')).doesNotExist(); assert.dom(statText('Total ACME clients')).doesNotExist();
assert.dom(statText('Average ACME clients per month')).doesNotExist(); assert.dom(statText('Average ACME clients per month')).doesNotExist();
assert.dom(statText('Average new ACME clients per month')).doesNotExist(); assert.dom(statText('Average new ACME clients per month')).doesNotExist();
@@ -179,11 +179,11 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
await this.renderComponent(); await this.renderComponent();
assert.dom(charts.chart('ACME usage')).exists('renders empty ACME usage chart'); assert.dom(CHARTS.chart('ACME usage')).exists('renders empty ACME usage chart');
assert assert
.dom(statText('Total ACME clients')) .dom(statText('Total ACME clients'))
.hasTextContaining('The total number of ACME requests made to Vault during this time period. 0'); .hasTextContaining('The total number of ACME requests made to Vault during this time period. 0');
findAll(`${charts.chart('ACME usage')} ${charts.xAxisLabel}`).forEach((e, i) => { findAll(`${CHARTS.chart('ACME usage')} ${CHARTS.xAxisLabel}`).forEach((e, i) => {
assert assert
.dom(e) .dom(e)
.hasText( .hasText(
@@ -191,11 +191,13 @@ module('Integration | Component | clients | Clients::Page::Acme', function (hook
`renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}`
); );
}); });
findAll(`${charts.chart('ACME usage')} ${charts.dataBar}`).forEach((e, i) => { findAll(`${CHARTS.chart('ACME usage')} ${CHARTS.verticalBar}`).forEach((e, i) => {
assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`);
}); });
assert.dom(chartContainer('Monthly new')).doesNotExist('empty monthly new chart does not render at all'); assert
.dom(CHARTS.container('Monthly new'))
.doesNotExist('empty monthly new chart does not render at all');
assert.dom(statText('Average ACME clients per month')).doesNotExist(); assert.dom(statText('Average ACME clients per month')).doesNotExist();
assert.dom(statText('Average new ACME clients per month')).doesNotExist(); assert.dom(statText('Average new ACME clients per month')).doesNotExist();
}); });

View File

@@ -11,7 +11,7 @@ import hbs from 'htmlbars-inline-precompile';
import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
import { getUnixTime } from 'date-fns'; import { getUnixTime } from 'date-fns';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
import { calculateAverage } from 'vault/utils/chart-helpers'; import { calculateAverage } from 'vault/utils/chart-helpers';
import { dateFormat } from 'core/helpers/date-format'; import { dateFormat } from 'core/helpers/date-format';
@@ -19,7 +19,7 @@ import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers
const START_TIME = getUnixTime(LICENSE_START); const START_TIME = getUnixTime(LICENSE_START);
const END_TIME = getUnixTime(STATIC_NOW); const END_TIME = getUnixTime(STATIC_NOW);
const { charts, chartContainer, statText, usageStats } = CLIENT_COUNT; const { statText, usageStats } = CLIENT_COUNT;
module('Integration | Component | clients | Clients::Page::Sync', function (hooks) { module('Integration | Component | clients | Clients::Page::Sync', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@@ -81,7 +81,7 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], {
withTimeZone: true, withTimeZone: true,
}); });
assert.dom(charts.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp'); assert.dom(CHARTS.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp');
assertBarChart(assert, 'Secrets sync usage', this.activity.byMonth); assertBarChart(assert, 'Secrets sync usage', this.activity.byMonth);
assertBarChart(assert, 'Monthly new', this.activity.byMonth); assertBarChart(assert, 'Monthly new', this.activity.byMonth);
@@ -94,8 +94,8 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
const expectedTotal = formatNumber([this.activity.total.secret_syncs]); const expectedTotal = formatNumber([this.activity.total.secret_syncs]);
await this.renderComponent(); await this.renderComponent();
assert.dom(charts.chart('Secrets sync usage')).doesNotExist('total usage chart does not render'); assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('total usage chart does not render');
assert.dom(chartContainer('Monthly new')).doesNotExist('monthly new chart does not render'); assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render');
assert.dom(statText('Average sync clients per month')).doesNotExist(); assert.dom(statText('Average sync clients per month')).doesNotExist();
assert.dom(statText('Average new sync clients per month')).doesNotExist(); assert.dom(statText('Average new sync clients per month')).doesNotExist();
assert assert
@@ -119,8 +119,8 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
assert.dom(GENERAL.emptyStateTitle).hasText('No secrets sync clients'); assert.dom(GENERAL.emptyStateTitle).hasText('No secrets sync clients');
assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this date range.'); assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this date range.');
assert.dom(charts.chart('Secrets sync usage')).doesNotExist('vertical bar chart does not render'); assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('vertical bar chart does not render');
assert.dom(chartContainer('Monthly new')).doesNotExist('monthly new chart does not render'); assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render');
assert.dom(statText('Total sync clients')).doesNotExist(); assert.dom(statText('Total sync clients')).doesNotExist();
assert.dom(statText('Average sync clients per month')).doesNotExist(); assert.dom(statText('Average sync clients per month')).doesNotExist();
assert.dom(statText('Average new sync clients per month')).doesNotExist(); assert.dom(statText('Average new sync clients per month')).doesNotExist();
@@ -148,7 +148,7 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
.hasText('No data is available because Secrets Sync has not been activated.'); .hasText('No data is available because Secrets Sync has not been activated.');
assert.dom(GENERAL.emptyStateActions).hasText('Activate Secrets Sync'); assert.dom(GENERAL.emptyStateActions).hasText('Activate Secrets Sync');
assert.dom(charts.chart('Secrets sync usage')).doesNotExist(); assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist();
assert.dom(statText('Total sync clients')).doesNotExist(); assert.dom(statText('Total sync clients')).doesNotExist();
assert.dom(statText('Average sync clients per month')).doesNotExist(); assert.dom(statText('Average sync clients per month')).doesNotExist();
}); });
@@ -193,7 +193,7 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
assert.expect(6 + monthCount * 2); assert.expect(6 + monthCount * 2);
await this.renderComponent(); await this.renderComponent();
assert.dom(charts.chart('Secrets sync usage')).exists('renders empty sync usage chart'); assert.dom(CHARTS.chart('Secrets sync usage')).exists('renders empty sync usage chart');
assert assert
.dom(statText('Total sync clients')) .dom(statText('Total sync clients'))
.hasText( .hasText(
@@ -202,7 +202,7 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
assert assert
.dom(statText('Average sync clients per month')) .dom(statText('Average sync clients per month'))
.doesNotExist('Does not render average if the calculation is 0'); .doesNotExist('Does not render average if the calculation is 0');
findAll(`${charts.chart('Secrets sync usage')} ${charts.xAxisLabel}`).forEach((e, i) => { findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.xAxisLabel}`).forEach((e, i) => {
assert assert
.dom(e) .dom(e)
.hasText( .hasText(
@@ -210,11 +210,13 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook
`renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}`
); );
}); });
findAll(`${charts.chart('Secrets sync usage')} ${charts.dataBar}`).forEach((e, i) => { findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.verticalBar}`).forEach((e, i) => {
assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`);
}); });
assert.dom(chartContainer('Monthly new')).doesNotExist('empty monthly new chart does not render at all'); assert
.dom(CHARTS.container('Monthly new'))
.doesNotExist('empty monthly new chart does not render at all');
assert.dom(statText('Average sync clients per month')).doesNotExist(); assert.dom(statText('Average sync clients per month')).doesNotExist();
assert.dom(statText('Average new sync clients per month')).doesNotExist(); assert.dom(statText('Average new sync clients per month')).doesNotExist();
}); });

View File

@@ -7,7 +7,7 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support';
import { setRunOptions } from 'ember-a11y-testing/test-support'; import { setRunOptions } from 'ember-a11y-testing/test-support';
import { render, findAll } from '@ember/test-helpers'; import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
import { getUnixTime } from 'date-fns'; import { getUnixTime } from 'date-fns';
@@ -15,7 +15,8 @@ import { calculateAverage } from 'vault/utils/chart-helpers';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
import { dateFormat } from 'core/helpers/date-format'; import { dateFormat } from 'core/helpers/date-format';
import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { assertBarChart } from 'vault/tests/helpers/clients/client-count-helpers';
const START_TIME = getUnixTime(LICENSE_START); const START_TIME = getUnixTime(LICENSE_START);
const END_TIME = getUnixTime(STATIC_NOW); const END_TIME = getUnixTime(STATIC_NOW);
@@ -67,6 +68,8 @@ module('Integration | Component | clients | Clients::Page::Token', function (hoo
}); });
test('it should render monthly total chart', async function (assert) { test('it should render monthly total chart', async function (assert) {
const count = this.activity.byMonth.length;
assert.expect(count + 7);
const getAverage = (data) => { const getAverage = (data) => {
const average = ['entity_clients', 'non_entity_clients'].reduce((count, key) => { const average = ['entity_clients', 'non_entity_clients'].reduce((count, key) => {
return (count += calculateAverage(data, key) || 0); return (count += calculateAverage(data, key) || 0);
@@ -74,83 +77,51 @@ module('Integration | Component | clients | Clients::Page::Token', function (hoo
return formatNumber([average]); return formatNumber([average]);
}; };
const expectedTotal = getAverage(this.activity.byMonth); const expectedTotal = getAverage(this.activity.byMonth);
const chart = CLIENT_COUNT.chartContainer('Entity/Non-entity clients usage'); const chart = CHARTS.container('Entity/Non-entity clients usage');
await this.renderComponent(); await this.renderComponent();
assert assert
.dom(CLIENT_COUNT.charts.statTextValue('Average total clients per month')) .dom(CLIENT_COUNT.statTextValue('Average total clients per month'))
.hasText(expectedTotal, 'renders correct total clients'); .hasText(expectedTotal, 'renders correct total clients');
// assert bar chart is correct // assert bar chart is correct
findAll(`${chart} ${CLIENT_COUNT.charts.bar.xAxisLabel}`).forEach((e, i) => { assert.dom(`${chart} ${CHARTS.xAxis}`).hasText('7/23 8/23 9/23 10/23 11/23 12/23 1/24');
assert assertBarChart(assert, 'Entity/Non-entity clients usage', this.activity.byMonth, true);
.dom(e)
.hasText(
`${this.activity.byMonth[i].month}`,
`renders x-axis labels for bar chart: ${this.activity.byMonth[i].month}`
);
});
assert
.dom(`${chart} ${CLIENT_COUNT.charts.bar.dataBar}`)
.exists(
{ count: this.activity.byMonth.filter((m) => m.clients).length * 2 },
'renders two stacked data bars of entity/non-entity clients for each month'
);
const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], {
withTimeZone: true, withTimeZone: true,
}); });
assert assert.dom(`${chart} ${CHARTS.timestamp}`).hasText(`Updated ${formattedTimestamp}`, 'renders timestamp');
.dom(`${chart} ${CLIENT_COUNT.charts.timestamp}`) assert.dom(`${chart} ${CHARTS.legendLabel(1)}`).hasText('Entity clients', 'Legend label renders');
.hasText(`Updated ${formattedTimestamp}`, 'renders timestamp'); assert.dom(`${chart} ${CHARTS.legendLabel(2)}`).hasText('Non-entity clients', 'Legend label renders');
assert
.dom(`${chart} ${CLIENT_COUNT.charts.legendLabel(1)}`)
.hasText('Entity clients', 'Legend label renders');
assert
.dom(`${chart} ${CLIENT_COUNT.charts.legendLabel(2)}`)
.hasText('Non-entity clients', 'Legend label renders');
}); });
test('it should render monthly new chart', async function (assert) { test('it should render monthly new chart', async function (assert) {
const count = this.newActivity.length;
assert.expect(count + 8);
const expectedNewEntity = formatNumber([calculateAverage(this.newActivity, 'entity_clients')]); const expectedNewEntity = formatNumber([calculateAverage(this.newActivity, 'entity_clients')]);
const expectedNewNonEntity = formatNumber([calculateAverage(this.newActivity, 'non_entity_clients')]); const expectedNewNonEntity = formatNumber([calculateAverage(this.newActivity, 'non_entity_clients')]);
const chart = CLIENT_COUNT.chartContainer('Monthly new'); const chart = CHARTS.container('Monthly new');
await this.renderComponent(); await this.renderComponent();
assert assert
.dom(CLIENT_COUNT.charts.statTextValue('Average new entity clients per month')) .dom(CLIENT_COUNT.statTextValue('Average new entity clients per month'))
.hasText(expectedNewEntity, 'renders correct new entity clients'); .hasText(expectedNewEntity, 'renders correct new entity clients');
assert assert
.dom(CLIENT_COUNT.charts.statTextValue('Average new non-entity clients per month')) .dom(CLIENT_COUNT.statTextValue('Average new non-entity clients per month'))
.hasText(expectedNewNonEntity, 'renders correct new nonentity clients'); .hasText(expectedNewNonEntity, 'renders correct new nonentity clients');
// assert bar chart is correct
findAll(`${chart} ${CLIENT_COUNT.charts.bar.xAxisLabel}`).forEach((e, i) => {
assert
.dom(e)
.hasText(
`${this.activity.byMonth[i].month}`,
`renders x-axis labels for bar chart: ${this.activity.byMonth[i].month}`
);
});
assert
.dom(`${chart} ${CLIENT_COUNT.charts.bar.dataBar}`)
.exists(
{ count: this.activity.byMonth.filter((m) => m.clients).length * 2 },
'renders two stacked bars of new entity/non-entity clients for each month'
);
const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], {
withTimeZone: true, withTimeZone: true,
}); });
assert assert.dom(`${chart} ${CHARTS.timestamp}`).hasText(`Updated ${formattedTimestamp}`, 'renders timestamp');
.dom(`${chart} ${CLIENT_COUNT.charts.timestamp}`) assert.dom(`${chart} ${CHARTS.legendLabel(1)}`).hasText('Entity clients', 'Legend label renders');
.hasText(`Updated ${formattedTimestamp}`, 'renders timestamp'); assert.dom(`${chart} ${CHARTS.legendLabel(2)}`).hasText('Non-entity clients', 'Legend label renders');
assert
.dom(`${chart} ${CLIENT_COUNT.charts.legendLabel(1)}`) // assert bar chart is correct
.hasText('Entity clients', 'Legend label renders'); assert.dom(`${chart} ${CHARTS.xAxis}`).hasText('7/23 8/23 9/23 10/23 11/23 12/23 1/24');
assert assertBarChart(assert, 'Monthly new', this.newActivity, true);
.dom(`${chart} ${CLIENT_COUNT.charts.legendLabel(2)}`)
.hasText('Non-entity clients', 'Legend label renders');
}); });
test('it should render empty state for no new monthly data', async function (assert) { test('it should render empty state for no new monthly data', async function (assert) {
@@ -158,12 +129,12 @@ module('Integration | Component | clients | Clients::Page::Token', function (hoo
...d, ...d,
new_clients: { month: d.month }, new_clients: { month: d.month },
})); }));
const chart = CLIENT_COUNT.charts.chart('monthly-new'); const chart = CHARTS.container('Monthly new');
await this.renderComponent(); await this.renderComponent();
assert.dom(`${chart} ${CLIENT_COUNT.charts.verticalBar}`).doesNotExist('Chart does not render'); assert.dom(`${chart} ${CHARTS.verticalBar}`).doesNotExist('Chart does not render');
assert.dom(`${chart} ${CLIENT_COUNT.charts.legend}`).doesNotExist('Legend does not render'); assert.dom(`${chart} ${CHARTS.legend}`).doesNotExist('Legend does not render');
assert.dom(GENERAL.emptyStateTitle).hasText('No new clients'); assert.dom(GENERAL.emptyStateTitle).hasText('No new clients');
assert assert
.dom(CLIENT_COUNT.statText('Average new entity clients per month')) .dom(CLIENT_COUNT.statText('Average new entity clients per month'))
@@ -183,13 +154,13 @@ module('Integration | Component | clients | Clients::Page::Token', function (hoo
const checkUsage = () => { const checkUsage = () => {
assert assert
.dom(CLIENT_COUNT.charts.statTextValue('Total clients')) .dom(CLIENT_COUNT.statTextValue('Total clients'))
.hasText(formatNumber([entity_clients + non_entity_clients]), 'Total clients value renders'); .hasText(formatNumber([entity_clients + non_entity_clients]), 'Total clients value renders');
assert assert
.dom(CLIENT_COUNT.charts.statTextValue('Entity')) .dom(CLIENT_COUNT.statTextValue('Entity'))
.hasText(formatNumber([entity_clients]), 'Entity value renders'); .hasText(formatNumber([entity_clients]), 'Entity value renders');
assert assert
.dom(CLIENT_COUNT.charts.statTextValue('Non-entity')) .dom(CLIENT_COUNT.statTextValue('Non-entity'))
.hasText(formatNumber([non_entity_clients]), 'Non-entity value renders'); .hasText(formatNumber([non_entity_clients]), 'Non-entity value renders');
}; };

View File

@@ -15,7 +15,7 @@ import { findAll } from '@ember/test-helpers';
import { formatNumber } from 'core/helpers/format-number'; import { formatNumber } from 'core/helpers/format-number';
import timestamp from 'core/utils/timestamp'; import timestamp from 'core/utils/timestamp';
import { setRunOptions } from 'ember-a11y-testing/test-support'; import { setRunOptions } from 'ember-a11y-testing/test-support';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors';
const START_TIME = getUnixTime(LICENSE_START); const START_TIME = getUnixTime(LICENSE_START);
@@ -73,8 +73,8 @@ module('Integration | Component | clients/running-total', function (hooks) {
test('it renders with full monthly activity data', async function (assert) { test('it renders with full monthly activity data', async function (assert) {
await this.renderComponent(); await this.renderComponent();
assert.dom(CLIENT_COUNT.chartContainer('Vault client counts')).exists('running total component renders'); assert.dom(CHARTS.container('Vault client counts')).exists('running total component renders');
assert.dom(CLIENT_COUNT.charts.lineChart).exists('line chart renders'); assert.dom(CHARTS.chart('Vault client counts line chart')).exists('line chart renders');
const expectedValues = { const expectedValues = {
'Running client total': formatNumber([this.totalUsageCounts.clients]), 'Running client total': formatNumber([this.totalUsageCounts.clients]),
@@ -85,7 +85,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
}; };
for (const label in expectedValues) { for (const label in expectedValues) {
assert assert
.dom(CLIENT_COUNT.charts.statTextValue(label)) .dom(CLIENT_COUNT.statTextValue(label))
.hasText( .hasText(
`${expectedValues[label]}`, `${expectedValues[label]}`,
`stat label: ${label} renders correct total: ${expectedValues[label]}` `stat label: ${label} renders correct total: ${expectedValues[label]}`
@@ -93,7 +93,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
} }
// assert line chart is correct // assert line chart is correct
findAll(CLIENT_COUNT.charts.line.xAxisLabel).forEach((e, i) => { findAll(CHARTS.xAxisLabel).forEach((e, i) => {
assert assert
.dom(e) .dom(e)
.hasText( .hasText(
@@ -102,7 +102,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
); );
}); });
assert assert
.dom(CLIENT_COUNT.charts.line.plotPoint) .dom(CHARTS.plotPoint)
.exists( .exists(
{ count: this.byMonthActivity.filter((m) => m.clients).length }, { count: this.byMonthActivity.filter((m) => m.clients).length },
'renders correct number of plot points' 'renders correct number of plot points'
@@ -117,8 +117,8 @@ module('Integration | Component | clients/running-total', function (hooks) {
await this.renderComponent(); await this.renderComponent();
assert.dom(CLIENT_COUNT.chartContainer('Vault client counts')).exists('running total component renders'); assert.dom(CHARTS.container('Vault client counts')).exists('running total component renders');
assert.dom(CLIENT_COUNT.charts.lineChart).exists('line chart renders'); assert.dom(CHARTS.chart('Vault client counts line chart')).exists('line chart renders');
const expectedValues = { const expectedValues = {
Entity: formatNumber([this.totalUsageCounts.entity_clients]), Entity: formatNumber([this.totalUsageCounts.entity_clients]),
@@ -128,7 +128,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
}; };
for (const label in expectedValues) { for (const label in expectedValues) {
assert assert
.dom(CLIENT_COUNT.charts.statTextValue(label)) .dom(CLIENT_COUNT.statTextValue(label))
.hasText( .hasText(
`${expectedValues[label]}`, `${expectedValues[label]}`,
`stat label: ${label} renders correct total: ${expectedValues[label]}` `stat label: ${label} renders correct total: ${expectedValues[label]}`
@@ -153,7 +153,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
}; };
for (const label in expectedStats) { for (const label in expectedStats) {
assert assert
.dom(`[data-test-total] ${CLIENT_COUNT.charts.statTextValue(label)}`) .dom(`[data-test-total] ${CLIENT_COUNT.statTextValue(label)}`)
.hasText( .hasText(
`${expectedStats[label]}`, `${expectedStats[label]}`,
`stat label: ${label} renders single month total: ${expectedStats[label]}` `stat label: ${label} renders single month total: ${expectedStats[label]}`
@@ -169,14 +169,14 @@ module('Integration | Component | clients/running-total', function (hooks) {
}; };
for (const label in expectedStats) { for (const label in expectedStats) {
assert assert
.dom(`[data-test-new] ${CLIENT_COUNT.charts.statTextValue(label)}`) .dom(`[data-test-new] ${CLIENT_COUNT.statTextValue(label)}`)
.hasText( .hasText(
`${expectedStats[label]}`, `${expectedStats[label]}`,
`stat label: ${label} renders single month new clients: ${expectedStats[label]}` `stat label: ${label} renders single month new clients: ${expectedStats[label]}`
); );
} }
assert.dom(CLIENT_COUNT.charts.lineChart).doesNotExist('line chart does not render'); assert.dom(CHARTS.chart('Vault client counts line chart')).doesNotExist('line chart does not render');
assert.dom(CLIENT_COUNT.charts.statTextValue()).exists({ count: 10 }, 'renders 10 stat text containers'); assert.dom(CLIENT_COUNT.statTextValue()).exists({ count: 10 }, 'renders 10 stat text containers');
}); });
test('it hides secret sync totals when feature is not activated', async function (assert) { test('it hides secret sync totals when feature is not activated', async function (assert) {
@@ -184,10 +184,10 @@ module('Integration | Component | clients/running-total', function (hooks) {
await this.renderComponent(); await this.renderComponent();
assert.dom(CLIENT_COUNT.chartContainer('Vault client counts')).exists('running total component renders'); assert.dom(CHARTS.container('Vault client counts')).exists('running total component renders');
assert.dom(CLIENT_COUNT.charts.lineChart).exists('line chart renders'); assert.dom(CHARTS.chart('Vault client counts line chart')).exists('line chart renders');
assert.dom(CLIENT_COUNT.charts.statTextValue('Entity')).exists(); assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists();
assert.dom(CLIENT_COUNT.charts.statTextValue('Non-entity')).exists(); assert.dom(CLIENT_COUNT.statTextValue('Non-entity')).exists();
assert.dom(CLIENT_COUNT.charts.statTextValue('Secret sync')).doesNotExist('does not render secret syncs'); assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).doesNotExist('does not render secret syncs');
}); });
}); });

View File

@@ -1,114 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, findAll, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | clients/vertical-bar-chart', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.set('chartLegend', [
{ label: 'entity clients', key: 'entity_clients' },
{ label: 'non-entity clients', key: 'non_entity_clients' },
]);
});
test('it renders chart and tooltip for total clients', async function (assert) {
const barChartData = [
{ month: 'january', clients: 141, entity_clients: 91, non_entity_clients: 50, new_clients: 5 },
{ month: 'february', clients: 251, entity_clients: 101, non_entity_clients: 150, new_clients: 5 },
];
this.set('barChartData', barChartData);
await render(hbs`
<div class="chart-container-wide">
<Clients::VerticalBarChart
@dataset={{this.barChartData}}
@chartLegend={{this.chartLegend}}
/>
</div>
`);
assert.dom('[data-test-vertical-bar-chart]').exists('renders chart');
assert
.dom('[data-test-vertical-chart="data-bar"]')
.exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked
assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => {
assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`);
});
// FLAKY after adding a11y testing, skip for now
// const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect');
// for (const [i, bar] of tooltipHoverBars.entries()) {
// await triggerEvent(bar, 'mouseover');
// const tooltip = document.querySelector('.ember-modal-dialog');
// assert
// .dom(tooltip)
// .includesText(
// `${barChartData[i].clients} total clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`,
// 'tooltip text is correct'
// );
// }
});
test('it renders chart and tooltip for new clients', async function (assert) {
const barChartData = [
{ month: 'january', entity_clients: 91, non_entity_clients: 50, clients: 0 },
{ month: 'february', entity_clients: 101, non_entity_clients: 150, clients: 110 },
];
this.set('barChartData', barChartData);
await render(hbs`
<div class="chart-container-wide">
<Clients::VerticalBarChart
@dataset={{this.barChartData}}
@chartLegend={{this.chartLegend}}
/>
</div>
`);
assert.dom('[data-test-vertical-bar-chart]').exists('renders chart');
assert
.dom('[data-test-vertical-chart="data-bar"]')
.exists({ count: barChartData.length * 2 }, 'renders correct number of bars'); // multiply length by 2 because bars are stacked
assert.dom(find('[data-test-vertical-chart="y-axis-labels"] text')).hasText('0', `y-axis starts at 0`);
findAll('[data-test-vertical-chart="x-axis-labels"] text').forEach((e, i) => {
assert.dom(e).hasText(`${barChartData[i].month}`, `renders x-axis label: ${barChartData[i].month}`);
});
// FLAKY after adding a11y testing, skip for now
// const tooltipHoverBars = findAll('[data-test-vertical-bar-chart] rect.tooltip-rect');
// for (const [i, bar] of tooltipHoverBars.entries()) {
// await triggerEvent(bar, 'mouseover');
// const tooltip = document.querySelector('.ember-modal-dialog');
// assert
// .dom(tooltip)
// .includesText(
// `${barChartData[i].clients} new clients ${barChartData[i].entity_clients} entity clients ${barChartData[i].non_entity_clients} non-entity clients`,
// 'tooltip text is correct'
// );
// }
});
test('it renders empty state when no dataset', async function (assert) {
await render(hbs`
<div class="chart-container-wide">
<Clients::VerticalBarChart @noDataMessage="this is a custom message to explain why you're not seeing a vertical bar chart"/>
</div>
`);
assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data');
assert
.dom('[data-test-empty-state-subtext]')
.hasText(
`this is a custom message to explain why you're not seeing a vertical bar chart`,
'custom message renders'
);
});
});

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1 * SPDX-License-Identifier: BUSL-1.1
*/ */
import { formatNumbers, calculateAverage, calculateSum } from 'vault/utils/chart-helpers'; import { numericalAxisLabel, calculateAverage, calculateSum } from 'vault/utils/chart-helpers';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
const SMALL_NUMBERS = [0, 7, 27, 103, 999]; const SMALL_NUMBERS = [0, 7, 27, 103, 999];
@@ -17,16 +17,16 @@ const LARGE_NUMBERS = {
}; };
module('Unit | Utility | chart-helpers', function () { module('Unit | Utility | chart-helpers', function () {
test('formatNumbers renders number correctly', function (assert) { test('numericalAxisLabel renders number correctly', function (assert) {
assert.expect(12); assert.expect(12);
const method = formatNumbers(); const method = numericalAxisLabel();
assert.ok(method); assert.ok(method);
SMALL_NUMBERS.forEach(function (num) { SMALL_NUMBERS.forEach(function (num) {
assert.strictEqual(formatNumbers(num), num, `Does not format small number ${num}`); assert.strictEqual(numericalAxisLabel(num), num, `Does not format small number ${num}`);
}); });
Object.keys(LARGE_NUMBERS).forEach(function (num) { Object.keys(LARGE_NUMBERS).forEach(function (num) {
const expected = LARGE_NUMBERS[num]; const expected = LARGE_NUMBERS[num];
assert.strictEqual(formatNumbers(num), expected, `Formats ${num} as ${expected}`); assert.strictEqual(numericalAxisLabel(num), expected, `Formats ${num} as ${expected}`);
}); });
}); });

View File

@@ -2774,6 +2774,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/d3-array@npm:^3.2.1":
version: 3.2.1
resolution: "@types/d3-array@npm:3.2.1"
checksum: 8a41cee0969e53bab3f56cc15c4e6c9d76868d6daecb2b7d8c9ce71e0ececccc5a8239697cc52dadf5c665f287426de5c8ef31a49e7ad0f36e8846889a383df4
languageName: node
linkType: hard
"@types/ember-data@npm:*, @types/ember-data@npm:^4.4.6": "@types/ember-data@npm:*, @types/ember-data@npm:^4.4.6":
version: 4.4.16 version: 4.4.16
resolution: "@types/ember-data@npm:4.4.16" resolution: "@types/ember-data@npm:4.4.16"
@@ -19384,6 +19391,7 @@ __metadata:
"@icholy/duration": ^5.1.0 "@icholy/duration": ^5.1.0
"@lineal-viz/lineal": ^0.5.1 "@lineal-viz/lineal": ^0.5.1
"@tsconfig/ember": ^2.0.0 "@tsconfig/ember": ^2.0.0
"@types/d3-array": ^3.2.1
"@types/ember": ^4.0.2 "@types/ember": ^4.0.2
"@types/ember-data": ^4.4.6 "@types/ember-data": ^4.4.6
"@types/ember-data__adapter": ^4.0.1 "@types/ember-data__adapter": ^4.0.1