UI/client count tests (#14162)
This commit is contained in:
parent
a0101257ed
commit
9bb4920497
|
@ -78,6 +78,12 @@ long-form version of the npm script:
|
|||
|
||||
`ember server --proxy=http://localhost:PORT`
|
||||
|
||||
To run yarn with mirage, do:
|
||||
|
||||
- `yarn start:mirage handlername`
|
||||
|
||||
Where `handlername` is one of the options exported in `mirage/handlers/index`
|
||||
|
||||
### Code Generators
|
||||
|
||||
Make use of the many generators for code, try `ember help generate` for more details. If you're using a component that can be widely-used, consider making it an `addon` component instead (see [this PR](https://github.com/hashicorp/vault/pull/6629) for more details)
|
||||
|
|
|
@ -39,6 +39,9 @@ export default class Attribution extends Component {
|
|||
}
|
||||
|
||||
get isSingleNamespace() {
|
||||
if (!this.args.totalClientsData) {
|
||||
return 'no data';
|
||||
}
|
||||
// if a namespace is selected, then we're viewing top 10 auth methods (mounts)
|
||||
return !!this.args.selectedNamespace;
|
||||
}
|
||||
|
@ -61,7 +64,16 @@ export default class Attribution extends Component {
|
|||
|
||||
get chartText() {
|
||||
let dateText = this.isDateRange ? 'date range' : 'month';
|
||||
if (!this.isSingleNamespace) {
|
||||
switch (this.isSingleNamespace) {
|
||||
case true:
|
||||
return {
|
||||
description:
|
||||
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
|
||||
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
|
||||
${dateText === 'date range' ? ' over time.' : '.'}`,
|
||||
totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `,
|
||||
};
|
||||
case false:
|
||||
return {
|
||||
description:
|
||||
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.',
|
||||
|
@ -70,20 +82,12 @@ export default class Attribution extends Component {
|
|||
${dateText === 'date range' ? ' over time.' : '.'}`,
|
||||
totalCopy: `The total clients in the namespace for this ${dateText}. This number is useful for identifying overall usage volume.`,
|
||||
};
|
||||
} else if (this.isSingleNamespace) {
|
||||
return {
|
||||
description:
|
||||
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.',
|
||||
newCopy: `The new clients used by the auth method for this ${dateText}. This aids in understanding which auth methods create and use new clients
|
||||
${dateText === 'date range' ? ' over time.' : '.'}`,
|
||||
totalCopy: `The total clients used by the auth method for this ${dateText}. This number is useful for identifying overall usage volume. `,
|
||||
};
|
||||
} else {
|
||||
case 'no data':
|
||||
return {
|
||||
description: 'There is a problem gathering data',
|
||||
newCopy: 'There is a problem gathering data',
|
||||
totalCopy: 'There is a problem gathering data',
|
||||
};
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,11 @@ export default class Current extends Component {
|
|||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod;
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData;
|
||||
}
|
||||
|
||||
get filteredActivity() {
|
||||
|
|
|
@ -62,10 +62,12 @@ export default class History extends Component {
|
|||
|
||||
// SEARCH SELECT
|
||||
@tracked selectedNamespace = null;
|
||||
@tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => ({
|
||||
@tracked namespaceArray = this.getActivityResponse.byNamespace
|
||||
? this.getActivityResponse.byNamespace.map((namespace) => ({
|
||||
name: namespace.label,
|
||||
id: namespace.label,
|
||||
}));
|
||||
}))
|
||||
: [];
|
||||
|
||||
// TEMPLATE MESSAGING
|
||||
@tracked noActivityDate = '';
|
||||
|
@ -102,7 +104,11 @@ export default class History extends Component {
|
|||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod;
|
||||
if (this.selectedAuthMethod) return false;
|
||||
if (this.selectedNamespace) {
|
||||
return this.authMethodOptions.length > 0;
|
||||
}
|
||||
return !!this.totalClientsData && this.totalUsageCounts && this.totalUsageCounts.clients !== 0;
|
||||
}
|
||||
|
||||
get startTimeDisplay() {
|
||||
|
|
|
@ -21,8 +21,6 @@ import { tracked } from '@glimmer/tracking';
|
|||
* @param {array} chartLegend - array of objects with key names 'key' and 'label' so data can be stacked
|
||||
*/
|
||||
|
||||
// TODO CMB: delete original bar chart component
|
||||
|
||||
// SIZING CONSTANTS
|
||||
const CHART_MARGIN = { top: 10, left: 95 }; // makes space for y-axis legend
|
||||
const TRANSLATE = { down: 14, left: 99 };
|
||||
|
@ -104,7 +102,7 @@ export default class HorizontalBarChart extends Component {
|
|||
.append('rect')
|
||||
.attr('class', 'data-bar')
|
||||
.style('cursor', 'pointer')
|
||||
.attr('width', (chartData) => `${xScale(chartData[1] - chartData[0])}%`)
|
||||
.attr('width', (chartData) => `${xScale(Math.abs(chartData[1] - chartData[0]))}%`)
|
||||
.attr('height', yScale.bandwidth())
|
||||
.attr('x', (chartData) => `${xScale(chartData[0])}%`)
|
||||
.attr('y', ({ data }) => yScale(data[labelKey]))
|
||||
|
|
|
@ -27,6 +27,14 @@ export default class LineChart extends Component {
|
|||
@tracked tooltipTotal = '';
|
||||
@tracked tooltipNew = '';
|
||||
|
||||
get yKey() {
|
||||
return this.args.yKey || 'clients';
|
||||
}
|
||||
|
||||
get xKey() {
|
||||
return this.args.xKey || 'month';
|
||||
}
|
||||
|
||||
@action removeTooltip() {
|
||||
this.tooltipTarget = null;
|
||||
}
|
||||
|
@ -39,15 +47,17 @@ export default class LineChart extends Component {
|
|||
|
||||
// DEFINE AXES SCALES
|
||||
let yScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.clients))])
|
||||
.range([0, 100]);
|
||||
.domain([0, max(dataset.map((d) => d[this.yKey]))])
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
|
||||
let yAxisScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.clients))])
|
||||
.range([SVG_DIMENSIONS.height, 0]);
|
||||
.domain([0, max(dataset.map((d) => d[this.yKey]))])
|
||||
.range([SVG_DIMENSIONS.height, 0])
|
||||
.nice();
|
||||
|
||||
let xScale = scalePoint() // use scaleTime()?
|
||||
.domain(dataset.map((d) => d.month))
|
||||
.domain(dataset.map((d) => d[this.xKey]))
|
||||
.range([0, SVG_DIMENSIONS.width])
|
||||
.padding(0.2);
|
||||
|
||||
|
@ -67,8 +77,8 @@ export default class LineChart extends Component {
|
|||
|
||||
// PATH BETWEEN PLOT POINTS
|
||||
let lineGenerator = line()
|
||||
.x((d) => xScale(d.month))
|
||||
.y((d) => yAxisScale(d.clients));
|
||||
.x((d) => xScale(d[this.xKey]))
|
||||
.y((d) => yAxisScale(d[this.yKey]));
|
||||
|
||||
chartSvg
|
||||
.append('g')
|
||||
|
@ -86,8 +96,8 @@ export default class LineChart extends Component {
|
|||
.enter()
|
||||
.append('circle')
|
||||
.attr('class', 'data-plot')
|
||||
.attr('cy', (d) => `${100 - yScale(d.clients)}%`)
|
||||
.attr('cx', (d) => xScale(d.month))
|
||||
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
|
||||
.attr('cx', (d) => xScale(d[this.xKey]))
|
||||
.attr('r', 3.5)
|
||||
.attr('fill', LIGHT_AND_DARK_BLUE[0])
|
||||
.attr('stroke', LIGHT_AND_DARK_BLUE[1])
|
||||
|
@ -103,18 +113,19 @@ export default class LineChart extends Component {
|
|||
.attr('class', 'hover-circle')
|
||||
.style('cursor', 'pointer')
|
||||
.style('opacity', '0')
|
||||
.attr('cy', (d) => `${100 - yScale(d.clients)}%`)
|
||||
.attr('cx', (d) => xScale(d.month))
|
||||
.attr('cy', (d) => `${100 - yScale(d[this.yKey])}%`)
|
||||
.attr('cx', (d) => xScale(d[this.xKey]))
|
||||
.attr('r', 10);
|
||||
|
||||
let hoverCircles = chartSvg.selectAll('.hover-circle');
|
||||
|
||||
// MOUSE EVENT FOR TOOLTIP
|
||||
hoverCircles.on('mouseover', (data) => {
|
||||
this.tooltipMonth = data.month;
|
||||
this.tooltipTotal = `${data.clients} total clients`;
|
||||
this.tooltipNew = `${data.new_clients.clients} new clients`;
|
||||
let node = hoverCircles.filter((plot) => plot.month === data.month).node();
|
||||
// TODO: how to genericize this?
|
||||
this.tooltipMonth = data[this.xKey];
|
||||
this.tooltipTotal = `${data[this.yKey]} total clients`;
|
||||
this.tooltipNew = `${data.new_clients?.clients} new clients`;
|
||||
let node = hoverCircles.filter((plot) => plot[this.xKey] === data[this.xKey]).node();
|
||||
this.tooltipTarget = node;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class VerticalBarChart extends Component {
|
|||
|
||||
// DEFINE DATA BAR SCALES
|
||||
let yScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data
|
||||
.domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data
|
||||
.range([0, 100])
|
||||
.nice();
|
||||
|
||||
|
@ -76,7 +76,7 @@ export default class VerticalBarChart extends Component {
|
|||
|
||||
// MAKE AXES //
|
||||
let yAxisScale = scaleLinear()
|
||||
.domain([0, max(dataset.map((d) => d.total))]) // TODO will need to recalculate when you get the data
|
||||
.domain([0, max(dataset.map((d) => d.clients))]) // TODO will need to recalculate when you get the data
|
||||
.range([`${SVG_DIMENSIONS.height}`, 0])
|
||||
.nice();
|
||||
|
||||
|
@ -116,7 +116,7 @@ export default class VerticalBarChart extends Component {
|
|||
// MOUSE EVENT FOR TOOLTIP
|
||||
tooltipRect.on('mouseover', (data) => {
|
||||
let hoveredMonth = data.month;
|
||||
this.tooltipTotal = `${data.total} ${data.new_clients ? 'total' : 'new'} clients`;
|
||||
this.tooltipTotal = `${data.clients} ${data.new_clients ? 'total' : 'new'} clients`;
|
||||
this.uniqueEntities = `${data.entity_clients} unique entities`;
|
||||
this.nonEntityTokens = `${data.non_entity_clients} non-entity tokens`;
|
||||
// let node = chartSvg
|
||||
|
|
|
@ -8,7 +8,7 @@ import { tracked } from '@glimmer/tracking';
|
|||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <DateDropdown @handleDateSelection={this.actionFromParent} @name={{"startTime"}}/>
|
||||
* <DateDropdown @handleDateSelection={this.actionFromParent} @name={{"startTime"}} @submitText="Save"/>
|
||||
* ```
|
||||
* @param {function} handleDateSelection - is the action from the parent that the date picker triggers
|
||||
* @param {string} [name] - optional argument passed from date dropdown to parent function
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { isSameMonth } from 'date-fns';
|
||||
import RSVP from 'rsvp';
|
||||
import getStorage from 'vault/lib/token-storage';
|
||||
|
||||
const INPUTTED_START_DATE = 'vault:ui-inputted-start-date';
|
||||
export default class HistoryRoute extends Route {
|
||||
async getActivity(start_time) {
|
||||
try {
|
||||
// on init ONLY make network request if we have a start time from the license
|
||||
// otherwise user needs to manually input
|
||||
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
|
||||
} catch (e) {
|
||||
// returns 400 when license start date is in the current month
|
||||
if (e.httpStatus === 400) {
|
||||
if (isSameMonth(new Date(start_time), new Date())) {
|
||||
// triggers empty state to manually enter date if license begins in current month
|
||||
return { isLicenseDateError: true };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
// on init ONLY make network request if we have a start_time
|
||||
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
|
||||
}
|
||||
|
||||
async getLicenseStartTime() {
|
||||
|
|
|
@ -9,8 +9,6 @@
|
|||
.stacked-charts {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
// grid-template-columns: 1fr;
|
||||
// grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.single-chart-grid {
|
||||
|
@ -63,8 +61,8 @@
|
|||
grid-row-start: 2;
|
||||
grid-row-end: span 3;
|
||||
justify-self: center;
|
||||
height: 341px;
|
||||
max-width: 730px;
|
||||
height: 300px;
|
||||
max-width: 700px;
|
||||
|
||||
svg.chart {
|
||||
width: 100%;
|
||||
|
@ -107,8 +105,13 @@
|
|||
}
|
||||
|
||||
.chart-empty-state {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: -1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
grid-row-end: span 3;
|
||||
grid-column-end: span 3;
|
||||
> div {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-subTitle {
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
<div class="chart-wrapper single-chart-grid">
|
||||
<div class="chart-wrapper single-chart-grid" data-test-clients-attribution>
|
||||
<div class="chart-header has-header-link has-bottom-margin-m">
|
||||
<div class="header-left">
|
||||
<h2 class="chart-title">Attribution</h2>
|
||||
<p class="chart-description">{{this.chartText.description}}</p>
|
||||
<p class="chart-description" data-test-attribution-description>{{this.chartText.description}}</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{{#if @totalClientsData}}
|
||||
<button type="button" class="button is-secondary" {{on "click" (fn (mut this.showCSVDownloadModal) true)}}>
|
||||
<button
|
||||
data-test-attribution-export-button
|
||||
type="button"
|
||||
class="button is-secondary"
|
||||
{{on "click" (fn (mut this.showCSVDownloadModal) true)}}
|
||||
>
|
||||
Export attribution data
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.barChartTotalClients}}
|
||||
<div class="chart-container-wide">
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.barChartTotalClients}}
|
||||
|
@ -20,30 +25,35 @@
|
|||
@totalUsageCounts={{@totalUsageCounts}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chart-subTitle">
|
||||
<p class="chart-subtext">{{this.chartText.totalCopy}}</p>
|
||||
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.totalCopy}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-top">
|
||||
<div class="data-details-top" data-test-top-attribution>
|
||||
<h3 class="data-details">Top {{this.attributionBreakdown}}</h3>
|
||||
<p class="data-details">{{this.topClientCounts.label}}</p>
|
||||
</div>
|
||||
|
||||
<div class="data-details-bottom">
|
||||
<div class="data-details-bottom" data-test-top-counts>
|
||||
<h3 class="data-details">Clients in {{this.attributionBreakdown}}</h3>
|
||||
<p class="data-details">{{format-number this.topClientCounts.clients}}</p>
|
||||
</div>
|
||||
|
||||
<div class="timestamp">
|
||||
Updated
|
||||
{{date-format @timestamp "MMM dd yyyy, h:mm:ss aaa"}}
|
||||
</div>
|
||||
|
||||
<div class="legend-center">
|
||||
<span class="light-dot"></span><span class="legend-label">{{capitalize @chartLegend.0.label}}</span>
|
||||
<span class="dark-dot"></span><span class="legend-label">{{capitalize @chartLegend.1.label}}</span>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="chart-empty-state">
|
||||
<EmptyState @icon="skip" @title="No data found" @bottomBorder={{true}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="timestamp" data-test-attribution-timestamp>
|
||||
{{#if @timestamp}}
|
||||
Updated
|
||||
{{date-format @timestamp "MMM d yyyy, h:mm:ss aaa"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{! MODAL FOR CSV DOWNLOAD }}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<ToolbarFilters data-test-clients-filter-bar>
|
||||
<SearchSelect
|
||||
@id="namespace-search-select-monthly"
|
||||
@options={{this.namespaceArray}}
|
||||
|
@ -34,7 +34,7 @@
|
|||
@displayInherit={{true}}
|
||||
class="is-marginless"
|
||||
/>
|
||||
{{#if this.selectedNamespace}}
|
||||
{{#if (not (is-empty this.authMethodOptions))}}
|
||||
<SearchSelect
|
||||
@id="auth-method-search-select"
|
||||
@options={{this.authMethodOptions}}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</h2>
|
||||
<div data-test-start-date-editor class="is-flex-align-baseline">
|
||||
{{#if this.startTimeDisplay}}
|
||||
<p class="is-size-6">{{this.startTimeDisplay}}</p>
|
||||
<p class="is-size-6" data-test-date-display>{{this.startTimeDisplay}}</p>
|
||||
<button type="button" class="button is-link" {{on "click" (fn (mut this.isEditStartMonthOpen) true)}}>
|
||||
Edit
|
||||
</button>
|
||||
|
@ -59,11 +59,10 @@
|
|||
to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
{{! check for startTimeFromLicense or startTimeFromResponse otherwise emptyState}}
|
||||
{{#if (or @model.startTimeFromLicense this.startTimeFromResponse)}}
|
||||
{{#if (or this.totalUsageCounts this.hasAttributionData)}}
|
||||
<div class="is-subtitle-gray has-bottom-margin-m">
|
||||
FILTERS
|
||||
<Toolbar>
|
||||
<Toolbar data-test-clients-filter-bar>
|
||||
<ToolbarFilters>
|
||||
<CalendarWidget
|
||||
@arrayOfMonths={{this.arrayOfMonths}}
|
||||
|
@ -86,7 +85,7 @@
|
|||
class="is-marginless"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if this.selectedNamespace}}
|
||||
{{#if (not (is-empty this.authMethodOptions))}}
|
||||
<SearchSelect
|
||||
@id="auth-method-search-select"
|
||||
@options={{this.authMethodOptions}}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
<svg class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.renderChart @dataset}}>
|
||||
<svg
|
||||
data-test-line-chart
|
||||
class="chart has-grid"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.renderChart @dataset}}
|
||||
>
|
||||
</svg>
|
||||
|
||||
{{! TOOLTIP }}
|
||||
|
|
Before Width: | Height: | Size: 778 B After Width: | Height: | Size: 808 B |
|
@ -18,6 +18,7 @@
|
|||
@value={{or @totalUsageCounts.clients "0"}}
|
||||
@size="l"
|
||||
@subText="The sum of entity clientIDs and non-entity clientIDs; this is Vault’s primary billing metric."
|
||||
data-test-stat-text="total-clients"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
@ -27,6 +28,7 @@
|
|||
@value={{or @totalUsageCounts.entity_clients "0"}}
|
||||
@size="l"
|
||||
@subText="Representations of a particular user, client, or application that created a token via login."
|
||||
data-test-stat-text="entity-clients"
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
@ -36,6 +38,7 @@
|
|||
@value={{or @totalUsageCounts.non_entity_clients "0"}}
|
||||
@size="l"
|
||||
@subText="Clients created with a shared set of permissions, but not associated with an entity. "
|
||||
data-test-stat-text="non-entity-clients"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
<svg class="chart has-grid" {{on "mouseleave" this.removeTooltip}} {{did-insert this.registerListener @dataset}}>
|
||||
<svg
|
||||
data-test-vertical-bar-chart
|
||||
class="chart has-grid"
|
||||
{{on "mouseleave" this.removeTooltip}}
|
||||
{{did-insert this.registerListener @dataset}}
|
||||
>
|
||||
</svg>
|
||||
|
||||
{{! TOOLTIP }}
|
||||
|
|
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 819 B |
|
@ -1,6 +1,6 @@
|
|||
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||
<D.Trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
data-test-popup-menu-trigger="month"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
|
@ -9,7 +9,7 @@
|
|||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu scroll">
|
||||
<ul class="menu-list">
|
||||
<ul data-test-month-list class="menu-list">
|
||||
{{#each this.months as |month index|}}
|
||||
<button
|
||||
type="button"
|
||||
|
@ -26,7 +26,7 @@
|
|||
</BasicDropdown>
|
||||
<BasicDropdown @class="popup-menu" @horizontalPosition="auto-right" @verticalPosition="below" as |D|>
|
||||
<D.Trigger
|
||||
data-test-popup-menu-trigger="true"
|
||||
data-test-popup-menu-trigger="year"
|
||||
class={{concat "toolbar-link" (if D.isOpen " is-active")}}
|
||||
@htmlTag="button"
|
||||
>
|
||||
|
@ -35,7 +35,7 @@
|
|||
</D.Trigger>
|
||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||
<nav class="box menu">
|
||||
<ul class="menu-list">
|
||||
<ul data-test-year-list class="menu-list">
|
||||
{{#each this.years as |year|}}
|
||||
<button
|
||||
type="button"
|
||||
|
@ -51,6 +51,7 @@
|
|||
</D.Content>
|
||||
</BasicDropdown>
|
||||
<button
|
||||
data-test-date-dropdown-submit
|
||||
type="button"
|
||||
class="button is-primary"
|
||||
disabled={{if (and this.startMonth this.startYear) false true}}
|
||||
|
|
|
@ -11,8 +11,10 @@ export const SVG_DIMENSIONS = { height: 190, width: 500 };
|
|||
|
||||
// Reference for tickFormat https://www.youtube.com/watch?v=c3MCROTNN8g
|
||||
export function formatNumbers(number) {
|
||||
if (number < 1000) return number;
|
||||
if (number < 10000) return format('.1s')(number);
|
||||
// replace SI prefix of 'G' for billions to 'B'
|
||||
return format('.1s')(number).replace('G', 'B');
|
||||
return format('.2s')(number).replace('G', 'B');
|
||||
}
|
||||
|
||||
export function formatTooltipNumber(value) {
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<div class={{concat "stat-text-container " @size (unless @subText "-no-subText")}} data-test-stat-text-container>
|
||||
<div
|
||||
class={{concat "stat-text-container " @size (unless @subText "-no-subText")}}
|
||||
data-test-stat-text-container
|
||||
...attributes
|
||||
>
|
||||
<div class="stat-label has-bottom-margin-xs">{{@label}}</div>
|
||||
{{#if @subText}}
|
||||
<div class="stat-text">{{@subText}}</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<nav class="toolbar">
|
||||
<nav class="toolbar" ...attributes>
|
||||
<div class="toolbar-scroller">
|
||||
{{yield}}
|
||||
</div>
|
||||
|
|
|
@ -34,12 +34,14 @@ export default function (server) {
|
|||
|
||||
server.get(
|
||||
'/sys/internal/counters/activity',
|
||||
function () {
|
||||
function (_, req) {
|
||||
const start_time = req.queryParams.start_time || '2021-03-17T00:00:00Z';
|
||||
const end_time = req.queryParams.end_time || '2021-12-31T23:59:59Z';
|
||||
return {
|
||||
request_id: '26be5ab9-dcac-9237-ec12-269a8ca647d5',
|
||||
data: {
|
||||
start_time: '2021-03-17T00:00:00Z',
|
||||
end_time: '2021-12-31T23:59:59Z',
|
||||
start_time,
|
||||
end_time,
|
||||
total: {
|
||||
_comment1: 'total client counts',
|
||||
clients: 3637,
|
||||
|
|
|
@ -871,7 +871,7 @@ export default function (server) {
|
|||
},
|
||||
mounts: [
|
||||
{
|
||||
path: 'auth/method/uMGBU',
|
||||
mount_path: 'auth/method/uMGBU',
|
||||
counts: {
|
||||
clients: 35,
|
||||
entity_clients: 20,
|
||||
|
@ -879,7 +879,7 @@ export default function (server) {
|
|||
},
|
||||
},
|
||||
{
|
||||
path: 'auth/method/woiej',
|
||||
mount_path: 'auth/method/woiej',
|
||||
counts: {
|
||||
clients: 35,
|
||||
entity_clients: 20,
|
||||
|
@ -898,7 +898,7 @@ export default function (server) {
|
|||
},
|
||||
mounts: [
|
||||
{
|
||||
path: 'auth/method/ABCD1',
|
||||
mount_path: 'auth/method/ABCD1',
|
||||
counts: {
|
||||
clients: 35,
|
||||
entity_clients: 20,
|
||||
|
@ -906,7 +906,7 @@ export default function (server) {
|
|||
},
|
||||
},
|
||||
{
|
||||
path: 'auth/method/ABCD2',
|
||||
mount_path: 'auth/method/ABCD2',
|
||||
counts: {
|
||||
clients: 35,
|
||||
entity_clients: 20,
|
||||
|
@ -925,7 +925,7 @@ export default function (server) {
|
|||
},
|
||||
mounts: [
|
||||
{
|
||||
path: 'auth/method/XYZZ2',
|
||||
mount_path: 'auth/method/XYZZ2',
|
||||
counts: {
|
||||
clients: 35,
|
||||
entity_clients: 20,
|
||||
|
@ -933,7 +933,7 @@ export default function (server) {
|
|||
},
|
||||
},
|
||||
{
|
||||
path: 'auth/method/XYZZ1',
|
||||
mount_path: 'auth/method/XYZZ1',
|
||||
counts: {
|
||||
clients: 35,
|
||||
entity_clients: 20,
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, settled, click } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import Pretender from 'pretender';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import ss from 'vault/tests/pages/components/search-select';
|
||||
import {
|
||||
generateConfigResponse,
|
||||
generateCurrentMonthResponse,
|
||||
SELECTORS,
|
||||
sendResponse,
|
||||
} from '../helpers/clients';
|
||||
|
||||
const searchSelect = create(ss);
|
||||
|
||||
module('Acceptance | clients current', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.server.shutdown();
|
||||
});
|
||||
|
||||
test('shows empty state when config disabled, no data', async function (assert) {
|
||||
const config = generateConfigResponse({ enabled: 'default-disable' });
|
||||
const monthly = generateCurrentMonthResponse();
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({}));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/current');
|
||||
assert.equal(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('Tracking is disabled');
|
||||
});
|
||||
|
||||
test('shows empty state when config enabled, no data', async function (assert) {
|
||||
const config = generateConfigResponse();
|
||||
const monthly = generateCurrentMonthResponse();
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/current');
|
||||
assert.equal(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No data received');
|
||||
});
|
||||
|
||||
test('filters correctly on current with full data', async function (assert) {
|
||||
const config = generateConfigResponse();
|
||||
const monthly = generateCurrentMonthResponse(3);
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/current');
|
||||
assert.equal(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
|
||||
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
|
||||
const { clients, entity_clients, non_entity_clients } = monthly.data;
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText(non_entity_clients.toString());
|
||||
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
// Filter by namespace
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Still shows attribution bar chart');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top auth method');
|
||||
// Filter by auth method
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('5');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('3');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('2');
|
||||
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
|
||||
// Delete auth filter goes back to filtered only on namespace
|
||||
await click('#auth-method-search-select [data-test-selected-list-button="delete"]');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Still shows attribution bar chart');
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
// Delete namespace filter with auth filter on
|
||||
await click('#namespace-search-select-monthly [data-test-selected-list-button="delete"]');
|
||||
// Goes back to no filters
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText(non_entity_clients.toString());
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
});
|
||||
|
||||
test('filters correctly on current with no auth mounts', async function (assert) {
|
||||
const config = generateConfigResponse();
|
||||
const monthly = generateCurrentMonthResponse(3, true /* skip mounts */);
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/internal/counters/activity/monthly', () => sendResponse(monthly));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/current');
|
||||
assert.equal(currentURL(), '/vault/clients/current');
|
||||
assert.dom(SELECTORS.activeTab).hasText('Current month', 'current month tab is active');
|
||||
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
|
||||
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
|
||||
const { clients, entity_clients, non_entity_clients } = monthly.data;
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText(non_entity_clients.toString());
|
||||
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
// Filter by namespace
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
||||
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution');
|
||||
assert.dom('#auth-method-search-select').doesNotExist('Auth method filter is not shown');
|
||||
// Remove namespace filter
|
||||
await click('#namespace-search-select-monthly [data-test-selected-list-button="delete"]');
|
||||
// Goes back to no filters
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText(clients.toString());
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText(entity_clients.toString());
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText(non_entity_clients.toString());
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,278 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, settled } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import Pretender from 'pretender';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { addMonths, format, formatRFC3339, startOfMonth, subMonths } from 'date-fns';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { clickTrigger } from 'ember-power-select/test-support/helpers';
|
||||
import ss from 'vault/tests/pages/components/search-select';
|
||||
import {
|
||||
generateActivityResponse,
|
||||
generateConfigResponse,
|
||||
generateLicenseResponse,
|
||||
SELECTORS,
|
||||
sendResponse,
|
||||
} from '../helpers/clients';
|
||||
|
||||
const searchSelect = create(ss);
|
||||
|
||||
module('Acceptance | clients history tab', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.server.shutdown();
|
||||
});
|
||||
|
||||
test('shows warning when config off, no data, queries available', async function (assert) {
|
||||
const licenseStart = startOfMonth(subMonths(new Date(), 6));
|
||||
const licenseEnd = addMonths(new Date(), 6);
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
const config = generateConfigResponse({ enabled: 'default-disable' });
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => sendResponse(null, 204));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history');
|
||||
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
|
||||
|
||||
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No data received');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Shows filter bar to search previous dates');
|
||||
assert.dom(SELECTORS.usageStats).doesNotExist('No usage stats');
|
||||
});
|
||||
|
||||
test('shows warning when config off, no data, queries unavailable', async function (assert) {
|
||||
const licenseStart = startOfMonth(subMonths(new Date(), 6));
|
||||
const licenseEnd = addMonths(new Date(), 6);
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
const config = generateConfigResponse({ enabled: 'default-disable', queries_available: false });
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => sendResponse(null, 204));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history');
|
||||
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('Data tracking is disabled');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Filter bar is hidden when no data available');
|
||||
});
|
||||
|
||||
test('shows empty state when config on and no queries', async function (assert) {
|
||||
const licenseStart = startOfMonth(subMonths(new Date(), 6));
|
||||
const licenseEnd = addMonths(new Date(), 6);
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
const config = generateConfigResponse({ queries_available: false });
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => sendResponse(null, 204));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
// History Tab
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history');
|
||||
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
|
||||
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No monthly history');
|
||||
assert.dom(SELECTORS.filterBar).doesNotExist('Does not show filter bar');
|
||||
});
|
||||
|
||||
test('visiting history tab config on and data with mounts', async function (assert) {
|
||||
const licenseStart = startOfMonth(subMonths(new Date(), 6));
|
||||
const licenseEnd = addMonths(new Date(), 6);
|
||||
const lastMonth = addMonths(new Date(), -1);
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
const config = generateConfigResponse();
|
||||
const activity = generateActivityResponse(5, licenseStart, lastMonth);
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => sendResponse(activity));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history');
|
||||
assert
|
||||
.dom(SELECTORS.dateDisplay)
|
||||
.hasText(format(licenseStart, 'MMMM yyyy'), 'billing start month is correctly parsed from license');
|
||||
assert
|
||||
.dom(SELECTORS.rangeDropdown)
|
||||
.hasText(
|
||||
`${format(licenseStart, 'MMMM yyyy')} - ${format(lastMonth, 'MMMM yyyy')}`,
|
||||
'Date range shows dates correctly parsed activity response'
|
||||
);
|
||||
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
|
||||
const { clients, entity_clients, non_entity_clients } = activity.data.total;
|
||||
assert
|
||||
.dom('[data-test-stat-text="total-clients"] .stat-value')
|
||||
.hasText(clients.toString(), 'total clients stat is correct');
|
||||
assert
|
||||
.dom('[data-test-stat-text="entity-clients"] .stat-value')
|
||||
.hasText(entity_clients.toString(), 'entity clients stat is correct');
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText(non_entity_clients.toString(), 'non-entity clients stat is correct');
|
||||
assert.dom('[data-test-clients-attribution]').exists('Shows attribution area');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
});
|
||||
|
||||
test('filters correctly on history with full data', async function (assert) {
|
||||
const licenseStart = startOfMonth(subMonths(new Date(), 6));
|
||||
const licenseEnd = addMonths(new Date(), 6);
|
||||
const lastMonth = addMonths(new Date(), -1);
|
||||
const config = generateConfigResponse();
|
||||
const activity = generateActivityResponse(5, licenseStart, lastMonth);
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => sendResponse(activity));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
|
||||
assert.dom(SELECTORS.usageStats).exists('usage stats block exists');
|
||||
assert.dom('[data-test-stat-text-container]').exists({ count: 3 }, '3 stat texts exist');
|
||||
const { clients } = activity.data.total;
|
||||
// Filter by namespace
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.ok(true, 'Filter by first namespace');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('15');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('5');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('10');
|
||||
// await this.pauseTest();
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Shows attribution bar chart');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top auth method');
|
||||
// Filter by auth method
|
||||
await clickTrigger();
|
||||
await searchSelect.options.objectAt(0).click();
|
||||
await settled();
|
||||
assert.ok(true, 'Filter by first auth method');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('5');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('3');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"] .stat-value').hasText('2');
|
||||
assert.dom(SELECTORS.attributionBlock).doesNotExist('Does not show attribution block');
|
||||
|
||||
await click('#namespace-search-select [data-test-selected-list-button="delete"]');
|
||||
assert.ok(true, 'Remove namespace filter without first removing auth method filter');
|
||||
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
|
||||
assert
|
||||
.dom('[data-test-stat-text="total-clients"] .stat-value')
|
||||
.hasText(clients.toString(), 'total clients stat is back to unfiltered value');
|
||||
});
|
||||
|
||||
test('shows warning if upgrade happened within license period', async function (assert) {
|
||||
const licenseStart = startOfMonth(subMonths(new Date(), 6));
|
||||
const licenseEnd = addMonths(new Date(), 6);
|
||||
const lastMonth = addMonths(new Date(), -1);
|
||||
const config = generateConfigResponse();
|
||||
const activity = generateActivityResponse(5, licenseStart, lastMonth);
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => sendResponse(activity));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () =>
|
||||
sendResponse({
|
||||
keys: ['1.9.0'],
|
||||
key_info: {
|
||||
'1.9.0': {
|
||||
previous_version: '1.8.3',
|
||||
timestamp_installed: formatRFC3339(addMonths(new Date(), -2)),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
|
||||
assert.dom('[data-test-flash-message] .message-actions').containsText(`You upgraded to Vault 1.9.0`);
|
||||
});
|
||||
|
||||
test('Shows empty if license start date is current month', async function (assert) {
|
||||
const licenseStart = new Date();
|
||||
const licenseEnd = addMonths(new Date(), 12);
|
||||
const config = generateConfigResponse();
|
||||
const license = generateLicenseResponse(licenseStart, licenseEnd);
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(license));
|
||||
this.get('/v1/sys/internal/counters/activity', () => this.passthrough);
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () =>
|
||||
sendResponse({
|
||||
keys: [],
|
||||
})
|
||||
);
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.emptyStateTitle).hasText('No data for this billing period');
|
||||
assert
|
||||
.dom(SELECTORS.dateDisplay)
|
||||
.hasText(format(licenseStart, 'MMMM yyyy'), 'Shows license date, gives ability to edit');
|
||||
assert.dom('[data-test-popup-menu-trigger="month"]').exists('Dropdown exists to select month');
|
||||
assert.dom('[data-test-popup-menu-trigger="year"]').exists('Dropdown exists to select year');
|
||||
});
|
||||
|
||||
test('shows correct interface if no permissions on license', async function (assert) {
|
||||
const config = generateConfigResponse();
|
||||
this.server = new Pretender(function () {
|
||||
this.get('/v1/sys/license/status', () => sendResponse(null, 403));
|
||||
this.get('/v1/sys/internal/counters/config', () => sendResponse(config));
|
||||
this.get('/v1/sys/version-history', () => sendResponse({ keys: [] }));
|
||||
this.get('/v1/sys/health', this.passthrough);
|
||||
this.get('/v1/sys/seal-status', this.passthrough);
|
||||
this.post('/v1/sys/capabilities-self', this.passthrough);
|
||||
this.get('/v1/sys/internal/ui/feature-flags', this.passthrough);
|
||||
});
|
||||
await visit('/vault/clients/history');
|
||||
assert.equal(currentURL(), '/vault/clients/history', 'clients/history URL is correct');
|
||||
assert.dom(SELECTORS.activeTab).hasText('History', 'history tab is active');
|
||||
// Message changes depending on ent or OSS
|
||||
assert.dom(SELECTORS.emptyStateTitle).exists('Empty state exists');
|
||||
assert.dom('[data-test-popup-menu-trigger="month"]').exists('Dropdown exists to select month');
|
||||
assert.dom('[data-test-popup-menu-trigger="year"]').exists('Dropdown exists to select year');
|
||||
});
|
||||
});
|
|
@ -6,7 +6,7 @@ import logout from 'vault/tests/pages/logout';
|
|||
import authForm from 'vault/tests/pages/components/auth-form';
|
||||
import enablePage from 'vault/tests/pages/settings/auth/enable';
|
||||
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
import { visit, settled, currentURL, find } from '@ember/test-helpers';
|
||||
import { visit, settled, currentURL } from '@ember/test-helpers';
|
||||
|
||||
const consoleComponent = create(consoleClass);
|
||||
const authFormComponent = create(authForm);
|
||||
|
@ -156,10 +156,9 @@ module('Acceptance | oidc provider', function (hooks) {
|
|||
await settled();
|
||||
assert.equal(currentURL(), url, 'URL is as expected after login');
|
||||
assert.dom('[data-test-oidc-redirect]').exists('redirect text exists');
|
||||
assert.ok(
|
||||
find('[data-test-oidc-redirect]').textContent.includes(`${callback}?code=`),
|
||||
'Successful redirect to callback'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-oidc-redirect]')
|
||||
.hasTextContaining(`${callback}?code=`, 'Successful redirect to callback');
|
||||
});
|
||||
|
||||
test('OIDC Provider redirects to auth if current token and prompt = login', async function (assert) {
|
||||
|
@ -178,10 +177,9 @@ module('Acceptance | oidc provider', function (hooks) {
|
|||
await authFormComponent.password(USER_PASSWORD);
|
||||
await authFormComponent.login();
|
||||
await settled();
|
||||
assert.ok(
|
||||
find('[data-test-oidc-redirect]').textContent.includes(`${callback}?code=`),
|
||||
'Successful redirect to callback'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-oidc-redirect]')
|
||||
.hasTextContaining(`${callback}?code=`, 'Successful redirect to callback');
|
||||
});
|
||||
|
||||
test('OIDC Provider shows consent form when prompt = consent', async function (assert) {
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
import { formatRFC3339 } from 'date-fns';
|
||||
|
||||
/** Scenarios
|
||||
* Config off, no data
|
||||
* * queries available (hist only)
|
||||
* * queries unavailable (hist only)
|
||||
* Config on, no data
|
||||
* Config on, with data
|
||||
* Filtering (data with mounts)
|
||||
* Filtering (data without mounts)
|
||||
* -- HISTORY ONLY --
|
||||
* No permissions for license
|
||||
* Version (hist only)
|
||||
* License start date this month
|
||||
*/
|
||||
|
||||
export const SELECTORS = {
|
||||
activeTab: '.nav-tab-link.is-active',
|
||||
emptyStateTitle: '[data-test-empty-state-title]',
|
||||
usageStats: '[data-test-usage-stats]',
|
||||
dateDisplay: '[data-test-date-display]',
|
||||
attributionBlock: '[data-test-clients-attribution]',
|
||||
filterBar: '[data-test-clients-filter-bar]',
|
||||
rangeDropdown: '[data-test-popup-menu-trigger]',
|
||||
};
|
||||
|
||||
export function sendResponse(data, httpStatus = 200) {
|
||||
if (httpStatus === 403) {
|
||||
return [
|
||||
httpStatus,
|
||||
{ 'Content-Type': 'application/json' },
|
||||
JSON.stringify({ errors: ['permission denied'] }),
|
||||
];
|
||||
}
|
||||
if (httpStatus === 204) {
|
||||
// /activity endpoint returns 204 when no data, while
|
||||
// /activity/monthly returns 200 with zero values on data
|
||||
return [httpStatus, { 'Content-Type': 'application/json' }];
|
||||
}
|
||||
return [httpStatus, { 'Content-Type': 'application/json' }, JSON.stringify(data)];
|
||||
}
|
||||
|
||||
export function generateConfigResponse(overrides = {}) {
|
||||
return {
|
||||
request_id: 'some-config-id',
|
||||
data: {
|
||||
default_report_months: 12,
|
||||
enabled: 'default-enable',
|
||||
queries_available: true,
|
||||
retention_months: 24,
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateNamespaceBlock(idx = 0, skipMounts = false) {
|
||||
let mountCount = 1;
|
||||
const nsBlock = {
|
||||
namespace_id: `${idx}UUID`,
|
||||
namespace_path: `my-namespace-${idx}/`,
|
||||
counts: {
|
||||
clients: mountCount * 15,
|
||||
entity_clients: mountCount * 5,
|
||||
non_entity_clients: mountCount * 10,
|
||||
distinct_entities: mountCount * 5,
|
||||
non_entity_tokens: mountCount * 10,
|
||||
},
|
||||
};
|
||||
if (!skipMounts) {
|
||||
mountCount = Math.floor((Math.random() + idx) * 20);
|
||||
let mounts = [];
|
||||
if (!skipMounts) {
|
||||
Array.from(Array(mountCount)).forEach((v, index) => {
|
||||
mounts.push({
|
||||
mount_path: `auth/authid${index}`,
|
||||
counts: {
|
||||
clients: 5,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 2,
|
||||
distinct_entities: 3,
|
||||
non_entity_tokens: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
nsBlock.mounts = mounts;
|
||||
}
|
||||
return nsBlock;
|
||||
}
|
||||
|
||||
export function generateActivityResponse(nsCount = 1, startDate, endDate) {
|
||||
if (nsCount === 0) {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
start_time: formatRFC3339(startDate),
|
||||
end_time: formatRFC3339(endDate),
|
||||
total: {
|
||||
clients: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
},
|
||||
by_namespace: [
|
||||
{
|
||||
namespace_id: `root`,
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
clients: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
// months: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
let namespaces = Array.from(Array(nsCount)).map((v, idx) => {
|
||||
return generateNamespaceBlock(idx);
|
||||
});
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
start_time: formatRFC3339(startDate),
|
||||
end_time: formatRFC3339(endDate),
|
||||
total: {
|
||||
clients: 999,
|
||||
entity_clients: 666,
|
||||
non_entity_clients: 333,
|
||||
},
|
||||
by_namespace: namespaces,
|
||||
// months: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateLicenseResponse(startDate, endDate) {
|
||||
return {
|
||||
request_id: 'my-license-request-id',
|
||||
data: {
|
||||
autoloaded: {
|
||||
license_id: 'my-license-id',
|
||||
start_time: formatRFC3339(startDate),
|
||||
expiration_time: formatRFC3339(endDate),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateCurrentMonthResponse(namespaceCount, skipMounts = false) {
|
||||
if (!namespaceCount) {
|
||||
return {
|
||||
request_id: 'monthly-response-id',
|
||||
data: {
|
||||
by_namespace: [],
|
||||
clients: 0,
|
||||
entity_clients: 0,
|
||||
non_entity_clients: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
// generate by_namespace data
|
||||
const by_namespace = Array.from(Array(namespaceCount)).map((ns, idx) =>
|
||||
generateNamespaceBlock(idx, skipMounts)
|
||||
);
|
||||
const counts = by_namespace.reduce(
|
||||
(prev, curr) => {
|
||||
return {
|
||||
clients: prev.clients + curr.counts.clients,
|
||||
entity_clients: prev.entity_clients + curr.counts.entity_clients,
|
||||
non_entity_clients: prev.non_entity_clients + curr.counts.non_entity_clients,
|
||||
};
|
||||
},
|
||||
{ clients: 0, entity_clients: 0, non_entity_clients: 0 }
|
||||
);
|
||||
return {
|
||||
request_id: 'monthly-response-id',
|
||||
data: {
|
||||
by_namespace,
|
||||
...counts,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import { module, test } from 'qunit';
|
||||
import EmberObject from '@ember/object';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | client count current', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
hooks.beforeEach(function () {
|
||||
let model = EmberObject.create({
|
||||
config: {},
|
||||
monthly: {},
|
||||
versionHistory: [],
|
||||
});
|
||||
this.model = model;
|
||||
});
|
||||
|
||||
test('it shows empty state when disabled and no data available', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'Off', queriesAvailable: false });
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />
|
||||
`);
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('Tracking is disabled');
|
||||
});
|
||||
|
||||
test('it shows empty state when enabled and no data', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'On', queriesAvailable: false });
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />`);
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data received');
|
||||
});
|
||||
|
||||
test('it shows zeroed data when enabled but no counts', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'On' });
|
||||
Object.assign(this.model.monthly, {
|
||||
byNamespace: [{ label: 'root', clients: 0, entity_clients: 0, non_entity_clients: 0 }],
|
||||
total: { clients: 0, entity_clients: 0, non_entity_clients: 0 },
|
||||
});
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />
|
||||
`);
|
||||
assert.dom('[data-test-component="empty-state"]').doesNotExist('Empty state does not exist');
|
||||
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
|
||||
assert.dom('[data-test-stat-text-container]').includesText('0');
|
||||
});
|
||||
|
||||
test('it shows data when available from query', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||
Object.assign(this.model.monthly, {
|
||||
total: {
|
||||
clients: 1234,
|
||||
entity_clients: 234,
|
||||
non_entity_clients: 232,
|
||||
},
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Current @model={{this.model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').doesNotExist('Date range component should not exists');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
|
||||
});
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
import { module, test } from 'qunit';
|
||||
import EmberObject from '@ember/object';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | client count history', function (hooks) {
|
||||
// TODO CMB add tests for calendar widget showing
|
||||
setupRenderingTest(hooks);
|
||||
hooks.beforeEach(function () {
|
||||
let model = EmberObject.create({
|
||||
config: {},
|
||||
activity: {},
|
||||
versionHistory: [],
|
||||
});
|
||||
this.model = model;
|
||||
});
|
||||
|
||||
test('it shows empty state when disabled and no data available', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'Off', queriesAvailable: false });
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('Data tracking is disabled');
|
||||
});
|
||||
|
||||
test('it shows empty state when enabled and no data available', async function (assert) {
|
||||
Object.assign(this.model.config, { enabled: 'On', queriesAvailable: false });
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No monthly history');
|
||||
});
|
||||
|
||||
test('it shows empty state when no data for queried date range', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true });
|
||||
Object.assign(this.model, { startTimeFromLicense: ['2021', 5] });
|
||||
Object.assign(this.model.activity, {
|
||||
byNamespace: [
|
||||
{
|
||||
label: 'namespace24/',
|
||||
clients: 8301,
|
||||
entity_clients: 4387,
|
||||
non_entity_clients: 3914,
|
||||
mounts: [],
|
||||
},
|
||||
{
|
||||
label: 'namespace88/',
|
||||
clients: 7752,
|
||||
entity_clients: 3632,
|
||||
non_entity_clients: 4120,
|
||||
mounts: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data received');
|
||||
});
|
||||
|
||||
test('it shows warning when disabled and data available', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'Off' });
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
|
||||
assert.dom('[data-test-tracking-disabled]').exists('Flash message exists');
|
||||
assert.dom('[data-test-tracking-disabled] .message-title').hasText('Tracking is disabled');
|
||||
});
|
||||
|
||||
test('it shows data when available from query', async function (assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||
Object.assign(this.model, { startTimeFromLicense: ['2021', 5] });
|
||||
Object.assign(this.model.activity, {
|
||||
byNamespace: [
|
||||
{ label: 'nsTest5/', clients: 2725, entity_clients: 1137, non_entity_clients: 1588 },
|
||||
{ label: 'nsTest1/', clients: 200, entity_clients: 100, non_entity_clients: 100 },
|
||||
],
|
||||
total: {
|
||||
clients: 1234,
|
||||
entity_clients: 234,
|
||||
non_entity_clients: 232,
|
||||
},
|
||||
});
|
||||
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::History @model={{this.model}} />`);
|
||||
|
||||
assert.dom('[data-test-start-date-editor]').exists('Billing start date editor exists');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.dom('[data-test-usage-stats]').exists('Client count data exists');
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('Horizontal bar chart exists');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,144 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { formatRFC3339 } from 'date-fns';
|
||||
import { click } from '@ember/test-helpers';
|
||||
|
||||
module('Integration | Component | clients/attribution', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.set('timestamp', formatRFC3339(new Date()));
|
||||
this.set('selectedNamespace', null);
|
||||
this.set('isDateRange', true);
|
||||
this.set('chartLegend', [
|
||||
{ label: 'entity clients', key: 'entity_clients' },
|
||||
{ label: 'non-entity clients', key: 'non_entity_clients' },
|
||||
]);
|
||||
this.set('totalUsageCounts', { clients: 15, entity_clients: 10, non_entity_clients: 5 });
|
||||
this.set('totalClientsData', [
|
||||
{ label: 'second', clients: 10, entity_clients: 7, non_entity_clients: 3 },
|
||||
{ label: 'first', clients: 5, entity_clients: 3, non_entity_clients: 2 },
|
||||
]);
|
||||
this.set('totalMountsData', { clients: 5, entity_clients: 3, non_entity_clients: 2 });
|
||||
this.set('namespaceMountsData', [
|
||||
{ label: 'auth1/', clients: 3, entity_clients: 2, non_entity_clients: 1 },
|
||||
{ label: 'auth2/', clients: 2, entity_clients: 1, non_entity_clients: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('it renders empty state with no data', async function (assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution @chartLegend={{chartLegend}} />
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').exists();
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data found');
|
||||
assert.dom('[data-test-attribution-description]').hasText('There is a problem gathering data');
|
||||
assert.dom('[data-test-attribution-export-button]').doesNotExist();
|
||||
assert.dom('[data-test-attribution-timestamp]').doesNotHaveTextContaining('Updated');
|
||||
});
|
||||
|
||||
test('it renders with data for namespaces', async function (assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{chartLegend}}
|
||||
@totalClientsData={{totalClientsData}}
|
||||
@totalUsageCounts={{totalUsageCounts}}
|
||||
@timestamp={{timestamp}}
|
||||
@selectedNamespace={{selectedNamespace}}
|
||||
@isDateRange={{isDateRange}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').doesNotExist();
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('chart displays');
|
||||
assert.dom('[data-test-attribution-export-button]').exists();
|
||||
assert
|
||||
.dom('[data-test-attribution-description]')
|
||||
.hasText(
|
||||
'This data shows the top ten namespaces by client count and can be used to understand where clients are originating. Namespaces are identified by path. To see all namespaces, export this data.'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-attribution-subtext]')
|
||||
.hasText(
|
||||
'The total clients in the namespace for this date range. This number is useful for identifying overall usage volume.'
|
||||
);
|
||||
assert.dom('[data-test-top-attribution]').includesText('namespace').includesText('second');
|
||||
assert.dom('[data-test-top-counts]').includesText('namespace').includesText('10');
|
||||
});
|
||||
|
||||
test('it renders correct text for a single month', async function (assert) {
|
||||
this.set('isDateRange', false);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{chartLegend}}
|
||||
@totalClientsData={{totalClientsData}}
|
||||
@totalUsageCounts={{totalUsageCounts}}
|
||||
@timestamp={{timestamp}}
|
||||
@selectedNamespace={{selectedNamespace}}
|
||||
@isDateRange={{isDateRange}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert
|
||||
.dom('[data-test-attribution-subtext]')
|
||||
.includesText('namespace for this month', 'renders monthly namespace text');
|
||||
|
||||
this.set('selectedNamespace', 'second');
|
||||
assert
|
||||
.dom('[data-test-attribution-subtext]')
|
||||
.includesText('auth method for this month', 'renders monthly auth method text');
|
||||
});
|
||||
|
||||
test('it renders with data for selected namespace auth methods for a date range', async function (assert) {
|
||||
this.set('selectedNamespace', 'second');
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{chartLegend}}
|
||||
@totalClientsData={{namespaceMountsData}}
|
||||
@totalUsageCounts={{totalUsageCounts}}
|
||||
@timestamp={{timestamp}}
|
||||
@selectedNamespace={{selectedNamespace}}
|
||||
@isDateRange={{isDateRange}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-component="empty-state"]').doesNotExist();
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists('chart displays');
|
||||
assert.dom('[data-test-attribution-export-button]').exists();
|
||||
assert
|
||||
.dom('[data-test-attribution-description]')
|
||||
.hasText(
|
||||
'This data shows the top ten authentication methods by client count within this namespace, and can be used to understand where clients are originating. Authentication methods are organized by path.'
|
||||
);
|
||||
assert
|
||||
.dom('[data-test-attribution-subtext]')
|
||||
.hasText(
|
||||
'The total clients used by the auth method for this date range. This number is useful for identifying overall usage volume.'
|
||||
);
|
||||
assert.dom('[data-test-top-attribution]').includesText('auth method').includesText('auth1/');
|
||||
assert.dom('[data-test-top-counts]').includesText('auth method').includesText('3');
|
||||
});
|
||||
|
||||
test('it renders modal', async function (assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<Clients::Attribution
|
||||
@chartLegend={{chartLegend}}
|
||||
@totalClientsData={{namespaceMountsData}}
|
||||
@timestamp={{timestamp}}
|
||||
@startTimeDisplay={{"January 2022"}}
|
||||
@endTimeDisplay={{"February 2022"}}
|
||||
/>
|
||||
`);
|
||||
await click('[data-test-attribution-export-button]');
|
||||
assert.dom('.modal.is-active .title').hasText('Export attribution data', 'modal appears to export csv');
|
||||
assert.dom('.modal.is-active').includesText('January 2022 - February 2022');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { findAll, render, triggerEvent } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | clients/horizontal-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', async function (assert) {
|
||||
const totalObject = { clients: 5, entity_clients: 2, non_entity_clients: 3 };
|
||||
const dataArray = [
|
||||
{ label: 'second', clients: 3, entity_clients: 1, non_entity_clients: 2 },
|
||||
{ label: 'first', clients: 2, entity_clients: 1, non_entity_clients: 1 },
|
||||
];
|
||||
this.set('totalUsageCounts', totalObject);
|
||||
this.set('totalClientsData', dataArray);
|
||||
|
||||
await render(hbs`
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.totalClientsData}}
|
||||
@chartLegend={{chartLegend}}
|
||||
@totalUsageCounts={{totalUsageCounts}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists();
|
||||
const dataBars = findAll('[data-test-horizontal-bar-chart] rect.data-bar');
|
||||
const actionBars = findAll('[data-test-horizontal-bar-chart] rect.action-bar');
|
||||
|
||||
assert.equal(actionBars.length, dataArray.length, 'renders correct number of hover bars');
|
||||
assert.equal(dataBars.length, dataArray.length * 2, 'renders correct number of data bars');
|
||||
|
||||
const textLabels = this.element.querySelectorAll('[data-test-horizontal-bar-chart] .tick text');
|
||||
const textTotals = this.element.querySelectorAll('[data-test-horizontal-bar-chart] text.total-value');
|
||||
textLabels.forEach((label, index) => {
|
||||
assert.dom(label).hasText(dataArray[index].label, 'label renders correct text');
|
||||
});
|
||||
textTotals.forEach((label, index) => {
|
||||
assert.dom(label).hasText(`${dataArray[index].clients}`, 'total value renders correct number');
|
||||
});
|
||||
|
||||
for (let [i, bar] of actionBars.entries()) {
|
||||
let percent = Math.round((dataArray[i].clients / totalObject.clients) * 100);
|
||||
await triggerEvent(bar, 'mouseover');
|
||||
let tooltip = document.querySelector('.ember-modal-dialog');
|
||||
assert.dom(tooltip).includesText(`${percent}%`, 'tooltip renders correct percentage');
|
||||
}
|
||||
});
|
||||
|
||||
test('it renders data with a large range', async function (assert) {
|
||||
const totalObject = { clients: 5929393, entity_clients: 1391997, non_entity_clients: 4537396 };
|
||||
const dataArray = [
|
||||
{ label: 'second', clients: 5929093, entity_clients: 1391896, non_entity_clients: 4537100 },
|
||||
{ label: 'first', clients: 300, entity_clients: 101, non_entity_clients: 296 },
|
||||
];
|
||||
this.set('totalUsageCounts', totalObject);
|
||||
this.set('totalClientsData', dataArray);
|
||||
|
||||
await render(hbs`
|
||||
<Clients::HorizontalBarChart
|
||||
@dataset={{this.totalClientsData}}
|
||||
@chartLegend={{chartLegend}}
|
||||
@totalUsageCounts={{totalUsageCounts}}
|
||||
/>`);
|
||||
|
||||
assert.dom('[data-test-horizontal-bar-chart]').exists();
|
||||
const dataBars = findAll('[data-test-horizontal-bar-chart] rect.data-bar');
|
||||
const actionBars = findAll('[data-test-horizontal-bar-chart] rect.action-bar');
|
||||
|
||||
assert.equal(actionBars.length, dataArray.length, 'renders correct number of hover bars');
|
||||
assert.equal(dataBars.length, dataArray.length * 2, 'renders correct number of data bars');
|
||||
|
||||
for (let [i, bar] of actionBars.entries()) {
|
||||
let percent = Math.round((dataArray[i].clients / totalObject.clients) * 100);
|
||||
await triggerEvent(bar, 'mouseover');
|
||||
let tooltip = document.querySelector('.ember-modal-dialog');
|
||||
assert.dom(tooltip).includesText(`${percent}%`, 'tooltip renders correct percentage');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | clients/line-chart', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.set('dataset', [
|
||||
{
|
||||
foo: 1,
|
||||
bar: 4,
|
||||
},
|
||||
{
|
||||
foo: 2,
|
||||
bar: 8,
|
||||
},
|
||||
{
|
||||
foo: 3,
|
||||
bar: 14,
|
||||
},
|
||||
{
|
||||
foo: 4,
|
||||
bar: 10,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`
|
||||
<div class="chart-container-wide">
|
||||
<Clients::LineChart @dataset={{dataset}} @xKey="foo" @yKey="bar" />
|
||||
</div>
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-line-chart]').exists('Chart is rendered');
|
||||
assert.dom('.hover-circle').exists({ count: 4 }, 'Renders dot for each data point');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
module('Integration | Component | clients/usage-stats', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders defaults', async function (assert) {
|
||||
await render(hbs`<Clients::UsageStats />`);
|
||||
|
||||
assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed');
|
||||
assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('0', 'Value defaults to zero');
|
||||
assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists');
|
||||
assert.dom('[data-test-stat-text="entity-clients"] .stat-value').hasText('0', 'Value defaults to zero');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists');
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText('0', 'Value defaults to zero');
|
||||
assert.dom('a').hasAttribute('href', 'https://learn.hashicorp.com/tutorials/vault/usage-metrics');
|
||||
});
|
||||
|
||||
test('it renders with data', async function (assert) {
|
||||
this.set('counts', {
|
||||
clients: 17,
|
||||
entity_clients: 7,
|
||||
non_entity_clients: 10,
|
||||
});
|
||||
await render(hbs`<Clients::UsageStats @totalUsageCounts={{counts}} />`);
|
||||
|
||||
assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed');
|
||||
assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists');
|
||||
assert
|
||||
.dom('[data-test-stat-text="total-clients"] .stat-value')
|
||||
.hasText('17', 'Total clients shows passed value');
|
||||
assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists');
|
||||
assert
|
||||
.dom('[data-test-stat-text="entity-clients"] .stat-value')
|
||||
.hasText('7', 'entity clients shows passed value');
|
||||
assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists');
|
||||
assert
|
||||
.dom('[data-test-stat-text="non-entity-clients"] .stat-value')
|
||||
.hasText('10', 'non entity clients shows passed value');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } 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', async function (assert) {
|
||||
const barChartData = [
|
||||
{ month: 'january', clients: 200, entity_clients: 91, non_entity_clients: 50, new_clients: 5 },
|
||||
{ month: 'february', clients: 300, entity_clients: 101, non_entity_clients: 150, new_clients: 5 },
|
||||
];
|
||||
this.set('barChartData', barChartData);
|
||||
|
||||
await render(hbs`
|
||||
<Clients::VerticalBarChart
|
||||
@dataset={{barChartData}}
|
||||
@chartLegend={{chartLegend}}
|
||||
/>
|
||||
`);
|
||||
assert.dom('[data-test-vertical-bar-chart]').exists();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { click, render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
const ARRAY_OF_MONTHS = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
const CURRENT_DATE = new Date();
|
||||
const CURRENT_YEAR = CURRENT_DATE.getFullYear(); // integer of year
|
||||
const CURRENT_MONTH = CURRENT_DATE.getMonth(); // index of month
|
||||
|
||||
module('Integration | Component | date-dropdown', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders dropdown', async function (assert) {
|
||||
this.set('text', 'Save');
|
||||
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
assert.dom('[data-test-date-dropdown-submit]').hasText('Submit', 'button renders default text');
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown @submitText={{text}}/>
|
||||
</div>
|
||||
`);
|
||||
assert.dom('[data-test-date-dropdown-submit]').hasText('Save', 'button renders passed in text');
|
||||
});
|
||||
|
||||
test('it renders dropdown and selects month and year', async function (assert) {
|
||||
let parentAction = (month, year) => {
|
||||
assert.equal(month, 'January', 'sends correct month to parent callback');
|
||||
assert.equal(year, CURRENT_YEAR, 'sends correct year to parent callback');
|
||||
};
|
||||
this.set('parentAction', parentAction);
|
||||
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown
|
||||
@handleDateSelection={{parentAction}} />
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
|
||||
const submitButton = this.element.querySelector('[data-test-date-dropdown-submit]');
|
||||
|
||||
assert.strictEqual(submitButton.disabled, true, 'button is disabled when no month or year selected');
|
||||
|
||||
await click(monthDropdown);
|
||||
let dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
|
||||
assert.equal(dropdownListMonths.length, 12, 'dropdown has 12 months');
|
||||
for (let [index, month] of ARRAY_OF_MONTHS.entries()) {
|
||||
assert.dom(dropdownListMonths[index]).hasText(`${month}`, `dropdown includes ${month}`);
|
||||
}
|
||||
|
||||
await click(dropdownListMonths[0]);
|
||||
assert.dom(monthDropdown).hasText('January', 'dropdown selects January');
|
||||
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting month');
|
||||
|
||||
await click(yearDropdown);
|
||||
let dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
|
||||
assert.equal(dropdownListYears.length, 5, 'dropdown has 5 years');
|
||||
|
||||
for (let [index, year] of dropdownListYears.entries()) {
|
||||
let comparisonYear = CURRENT_YEAR - index;
|
||||
assert.dom(year).hasText(`${comparisonYear}`, `dropdown includes ${comparisonYear}`);
|
||||
}
|
||||
|
||||
await click(dropdownListYears[0]);
|
||||
assert.dom(yearDropdown).hasText(`${CURRENT_YEAR}`, `dropdown selects ${CURRENT_YEAR}`);
|
||||
assert.dom('.ember-basic-dropdown-content').doesNotExist('dropdown closes after selecting year');
|
||||
assert.strictEqual(submitButton.disabled, false, 'button enabled when month and year selected');
|
||||
|
||||
await click(submitButton);
|
||||
});
|
||||
|
||||
test('it disables correct years when selecting month first', async function (assert) {
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await click(monthDropdown);
|
||||
let dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
|
||||
await click(dropdownListMonths[i]);
|
||||
|
||||
await click(yearDropdown);
|
||||
let dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
|
||||
|
||||
if (i < CURRENT_MONTH) {
|
||||
for (let year of dropdownListYears) {
|
||||
assert.strictEqual(year.disabled, false, `${ARRAY_OF_MONTHS[i]} ${year.innerText} valid`);
|
||||
}
|
||||
} else {
|
||||
for (let [yearIndex, year] of dropdownListYears.entries()) {
|
||||
if (yearIndex === 0) {
|
||||
assert.strictEqual(year.disabled, true, `${ARRAY_OF_MONTHS[i]} ${year.innerText} disabled`);
|
||||
} else {
|
||||
assert.strictEqual(year.disabled, false, `${ARRAY_OF_MONTHS[i]} ${year.innerText} valid`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await click(yearDropdown);
|
||||
}
|
||||
});
|
||||
|
||||
test('it disables correct months when selecting year first', async function (assert) {
|
||||
await render(hbs`
|
||||
<div class="is-flex-align-baseline">
|
||||
<DateDropdown/>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const monthDropdown = this.element.querySelector('[data-test-popup-menu-trigger="month"]');
|
||||
const yearDropdown = this.element.querySelector('[data-test-popup-menu-trigger="year"]');
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await click(yearDropdown);
|
||||
let dropdownListYears = this.element.querySelectorAll('[data-test-year-list] button');
|
||||
await click(dropdownListYears[i]);
|
||||
|
||||
await click(monthDropdown);
|
||||
let dropdownListMonths = this.element.querySelectorAll('[data-test-month-list] button');
|
||||
|
||||
if (i === 0) {
|
||||
for (let [monthIndex, month] of dropdownListMonths.entries()) {
|
||||
if (monthIndex < CURRENT_MONTH) {
|
||||
assert.strictEqual(
|
||||
month.disabled,
|
||||
false,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} valid`
|
||||
);
|
||||
} else {
|
||||
assert.strictEqual(
|
||||
month.disabled,
|
||||
true,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} disabled`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let [monthIndex, month] of dropdownListMonths.entries()) {
|
||||
assert.strictEqual(
|
||||
month.disabled,
|
||||
false,
|
||||
`${ARRAY_OF_MONTHS[monthIndex]} ${dropdownListYears[i].innerText.trim()} valid`
|
||||
);
|
||||
}
|
||||
}
|
||||
await click(monthDropdown);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import { formatNumbers, formatTooltipNumber } from 'vault/utils/chart-helpers';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
const SMALL_NUMBERS = [0, 7, 27, 103, 999];
|
||||
const LARGE_NUMBERS = {
|
||||
1001: '1k',
|
||||
33777: '34k',
|
||||
532543: '530k',
|
||||
2100100: '2.1M',
|
||||
54500200100: '55B',
|
||||
};
|
||||
|
||||
module('Unit | Utility | chart-helpers', function () {
|
||||
test('formatNumbers renders number correctly', function (assert) {
|
||||
const method = formatNumbers();
|
||||
assert.ok(method);
|
||||
SMALL_NUMBERS.forEach(function (num) {
|
||||
assert.equal(formatNumbers(num), num, `Does not format small number ${num}`);
|
||||
});
|
||||
Object.keys(LARGE_NUMBERS).forEach(function (num) {
|
||||
const expected = LARGE_NUMBERS[num];
|
||||
assert.equal(formatNumbers(num), expected, `Formats ${num} as ${expected}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('formatTooltipNumber renders number correctly', function (assert) {
|
||||
const formatted = formatTooltipNumber(120300200100);
|
||||
assert.equal(formatted.length, 15, 'adds punctuation at proper place for large numbers');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue