mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
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:
@@ -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"
|
||||||
|
@@ -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');
|
||||||
|
@@ -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|>
|
||||||
|
@@ -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();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
123
ui/app/components/clients/charts/vertical-bar-stacked.hbs
Normal file
123
ui/app/components/clients/charts/vertical-bar-stacked.hbs
Normal 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}}
|
142
ui/app/components/clients/charts/vertical-bar-stacked.ts
Normal file
142
ui/app/components/clients/charts/vertical-bar-stacked.ts
Normal 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();
|
||||||
|
}
|
@@ -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>
|
||||||
|
@@ -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}}
|
||||||
|
@@ -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}}
|
|
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
194
ui/app/styles/components/chart-container.scss
Normal file
194
ui/app/styles/components/chart-container.scss
Normal 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;
|
||||||
|
}
|
@@ -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';
|
||||||
|
48
ui/app/styles/core/charts-lineal.scss
Normal file
48
ui/app/styles/core/charts-lineal.scss
Normal 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);
|
||||||
|
}
|
@@ -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;
|
|
||||||
}
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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",
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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,
|
||||||
|
@@ -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]',
|
||||||
};
|
};
|
||||||
|
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
@@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -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}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user