Client count updates (#12554)
* Client count updates - Added Current month tab which leverages partial monthly activity api - Refactored Vault usage to Monthly history - New client count history component based on StatText and BarChart component - Restrict bar chart to showcase only top 10 namespaces - Removed config route, as config and history component will be rendered based on query param - Updated all metrics reference to clients - Removed old tests and added integration test for current month * Fixed navbar permission - Added changelog * Updated the model for current month data * Fixed current month tests * Fixed indentation and chart label
This commit is contained in:
parent
b2418a3a8c
commit
35c188fba7
|
@ -0,0 +1,3 @@
|
|||
```release-note:feature
|
||||
ui: client count monthly view
|
||||
```
|
|
@ -5,7 +5,11 @@ export default Application.extend({
|
|||
return 'internal/counters/activity';
|
||||
},
|
||||
queryRecord(store, type, query) {
|
||||
const url = this.urlForQuery(null, type);
|
||||
let url = this.urlForQuery(null, type);
|
||||
if (query.tab === 'current') {
|
||||
url = `${url}/monthly`;
|
||||
query = null;
|
||||
}
|
||||
// API accepts start and end as query params
|
||||
return this.ajax(url, 'GET', { data: query }).then(resp => {
|
||||
let response = resp || {};
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* @module PricingMetricsConfig
|
||||
* PricingMetricsConfig components are used to show and edit the pricing metrics config information.
|
||||
* @module ClientsConfig
|
||||
* ClientsConfig components are used to show and edit the client count config information.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <PricingMetricsConfig @model={{model}} @mode="edit" />
|
||||
* <Clients::Config @model={{model}} @mode="edit" />
|
||||
* ```
|
||||
* @param {object} model - model is the DS metrics/config model which should be passed in
|
||||
* @param {object} model - model is the DS clients/config model which should be passed in
|
||||
* @param {string} [mode=show] - mode is either show or edit. Show results in a table with the config, show has a form.
|
||||
*/
|
||||
|
||||
|
@ -16,7 +16,7 @@ import { inject as service } from '@ember/service';
|
|||
import { tracked } from '@glimmer/tracking';
|
||||
import { task } from 'ember-concurrency';
|
||||
|
||||
export default class PricingMetricsConfigComponent extends Component {
|
||||
export default class ConfigComponent extends Component {
|
||||
@service router;
|
||||
@tracked mode = 'show';
|
||||
@tracked modalOpen = false;
|
||||
|
@ -57,7 +57,7 @@ export default class PricingMetricsConfigComponent extends Component {
|
|||
this.error = err.message;
|
||||
return;
|
||||
}
|
||||
this.router.transitionTo('vault.cluster.metrics.config');
|
||||
this.router.transitionTo('vault.cluster.clients.index');
|
||||
}).drop())
|
||||
save;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import Component from '@glimmer/component';
|
||||
|
||||
export default class HistoryComponent extends Component {
|
||||
max_namespaces = 10;
|
||||
|
||||
get hasClientData() {
|
||||
if (this.args.tab === 'current') {
|
||||
return this.args.model.activity && this.args.model.activity.clients;
|
||||
}
|
||||
return this.args.model.activity && this.args.model.activity.total;
|
||||
}
|
||||
|
||||
get barChartDataset() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
let dataset = this.args.model.activity.byNamespace;
|
||||
// Filter out root data
|
||||
dataset = dataset.filter(item => {
|
||||
return item.namespace_id !== 'root';
|
||||
});
|
||||
// Show only top 10 namespaces
|
||||
dataset = dataset.slice(0, this.max_namespaces);
|
||||
return dataset.map(d => {
|
||||
return {
|
||||
label: d['namespace_path'],
|
||||
non_entity_tokens: d['counts']['non_entity_tokens'],
|
||||
distinct_entities: d['counts']['distinct_entities'],
|
||||
total: d['counts']['clients'],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get showGraphs() {
|
||||
if (!this.args.model.activity || !this.args.model.activity.byNamespace) {
|
||||
return null;
|
||||
}
|
||||
return this.args.model.activity.byNamespace.length > 1;
|
||||
}
|
||||
}
|
|
@ -18,16 +18,7 @@
|
|||
import { set, computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
isValid,
|
||||
subMonths,
|
||||
startOfToday,
|
||||
format,
|
||||
endOfMonth,
|
||||
startOfMonth,
|
||||
isBefore,
|
||||
} from 'date-fns';
|
||||
import { subMonths, startOfToday, format, endOfMonth, startOfMonth, isBefore } from 'date-fns';
|
||||
import layout from '../templates/components/pricing-metrics-dates';
|
||||
import { parseDateString } from 'vault/helpers/parse-date-string';
|
||||
|
||||
|
@ -69,35 +60,6 @@ export default Component.extend({
|
|||
}
|
||||
}),
|
||||
|
||||
// We don't want the warning to show when inputs are being updated before query is made
|
||||
/* eslint-disable-next-line ember/require-computed-property-dependencies */
|
||||
showResultsWarning: computed('resultEnd', 'resultStart', function() {
|
||||
if (!this.queryStart || !this.queryEnd || !this.resultStart || !this.resultEnd) {
|
||||
return false;
|
||||
}
|
||||
const resultStart = new Date(this.resultStart);
|
||||
const resultEnd = new Date(this.resultEnd);
|
||||
let queryStart, queryEnd;
|
||||
try {
|
||||
queryStart = parseDateString(this.queryStart, '-');
|
||||
queryEnd = parseDateString(this.queryEnd, '-');
|
||||
} catch (e) {
|
||||
// Log error for debugging purposes
|
||||
console.debug(e);
|
||||
}
|
||||
|
||||
if (!queryStart || !queryEnd || !isValid(resultStart) || !isValid(resultEnd)) {
|
||||
return false;
|
||||
}
|
||||
if (Math.abs(differenceInSeconds(queryStart, resultStart)) >= 86400) {
|
||||
return true;
|
||||
}
|
||||
if (Math.abs(differenceInSeconds(resultEnd, endOfMonth(queryEnd))) >= 86400) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
|
||||
error: computed('end', 'endDate', 'retentionMonths', 'start', 'startDate', function() {
|
||||
if (!this.startDate) {
|
||||
return 'Start date is invalid. Please use format MM/yyyy';
|
||||
|
@ -148,7 +110,7 @@ export default Component.extend({
|
|||
handleQuery() {
|
||||
const start = format(this.startDate, 'MM-yyyy');
|
||||
const end = format(this.endDate, 'MM-yyyy');
|
||||
this.router.transitionTo('vault.cluster.metrics', {
|
||||
this.router.transitionTo('vault.cluster.clients', {
|
||||
queryParams: {
|
||||
start,
|
||||
end,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ClientsController extends Controller {
|
||||
queryParams = ['tab', 'start', 'end'];
|
||||
tab = null;
|
||||
start = null;
|
||||
end = null;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({
|
||||
queryParams: ['start', 'end'],
|
||||
|
||||
start: null,
|
||||
end: null,
|
||||
});
|
|
@ -2,6 +2,10 @@ import Model, { attr } from '@ember-data/model';
|
|||
|
||||
export default Model.extend({
|
||||
total: attr('object'),
|
||||
byNamespace: attr('array'),
|
||||
endTime: attr('string'),
|
||||
startTime: attr('string'),
|
||||
clients: attr('number'),
|
||||
distinct_entities: attr('number'),
|
||||
non_entity_tokens: attr('number'),
|
||||
});
|
|
@ -15,9 +15,8 @@ Router.map(function() {
|
|||
this.route('logout');
|
||||
this.mount('open-api-explorer', { path: '/api-explorer' });
|
||||
this.route('license');
|
||||
this.route('metrics', function() {
|
||||
this.route('clients', function() {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('config');
|
||||
this.route('edit');
|
||||
});
|
||||
this.route('storage', { path: '/storage/raft' });
|
||||
|
|
|
@ -2,6 +2,6 @@ import Route from '@ember/routing/route';
|
|||
|
||||
export default Route.extend({
|
||||
model() {
|
||||
return this.store.queryRecord('metrics/config', {});
|
||||
return this.store.queryRecord('clients/config', {});
|
||||
},
|
||||
});
|
|
@ -4,10 +4,13 @@ import { hash } from 'rsvp';
|
|||
import { getTime } from 'date-fns';
|
||||
import { parseDateString } from 'vault/helpers/parse-date-string';
|
||||
|
||||
const getActivityParams = ({ start, end }) => {
|
||||
const getActivityParams = ({ tab, start, end }) => {
|
||||
// Expects MM-yyyy format
|
||||
// TODO: minStart, maxEnd
|
||||
let params = {};
|
||||
if (tab === 'current') {
|
||||
params.tab = tab;
|
||||
} else if (tab === 'history') {
|
||||
if (start) {
|
||||
let startDate = parseDateString(start);
|
||||
if (startDate) {
|
||||
|
@ -23,11 +26,15 @@ const getActivityParams = ({ start, end }) => {
|
|||
params.end_time = getTime(endDate) / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
queryParams: {
|
||||
tab: {
|
||||
refreshModel: true,
|
||||
},
|
||||
start: {
|
||||
refreshModel: true,
|
||||
},
|
||||
|
@ -37,13 +44,13 @@ export default Route.extend(ClusterRoute, {
|
|||
},
|
||||
|
||||
model(params) {
|
||||
let config = this.store.queryRecord('metrics/config', {}).catch(e => {
|
||||
let config = this.store.queryRecord('clients/config', {}).catch(e => {
|
||||
console.debug(e);
|
||||
// swallowing error so activity can show if no config permissions
|
||||
return {};
|
||||
});
|
||||
const activityParams = getActivityParams(params);
|
||||
let activity = this.store.queryRecord('metrics/activity', activityParams);
|
||||
let activity = this.store.queryRecord('clients/activity', activityParams);
|
||||
|
||||
return hash({
|
||||
queryStart: params.start,
|
|
@ -1,8 +0,0 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
model() {
|
||||
return this.store.queryRecord('metrics/config', {});
|
||||
},
|
||||
});
|
|
@ -29,7 +29,7 @@ const API_PATHS = {
|
|||
seal: 'sys/seal',
|
||||
raft: 'sys/storage/raft/configuration',
|
||||
},
|
||||
metrics: {
|
||||
clients: {
|
||||
activity: 'sys/internal/counters/activity',
|
||||
config: 'sys/internal/counters/config',
|
||||
},
|
||||
|
|
|
@ -7,14 +7,11 @@
|
|||
|
||||
> div.is-border {
|
||||
border: 0.3px solid $ui-gray-200;
|
||||
width: 94%;
|
||||
margin-left: 3%;
|
||||
margin-bottom: $spacing-xxs;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
margin-left: $spacing-l;
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
|
||||
|
@ -47,7 +44,7 @@
|
|||
}
|
||||
|
||||
.bar-chart-container {
|
||||
padding: $spacing-m $spacing-l $spacing-m $spacing-l;
|
||||
padding: $spacing-m 0;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
.stat-text-container {
|
||||
line-height: normal;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.l,
|
||||
&.m {
|
||||
|
@ -13,6 +16,7 @@
|
|||
font-weight: $font-weight-normal;
|
||||
color: $ui-gray-700;
|
||||
line-height: inherit;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: $size-3;
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
Save
|
||||
</button>
|
||||
<LinkTo
|
||||
@route="vault.cluster.metrics.config"
|
||||
@route="vault.cluster.clients.index"
|
||||
@query={{hash tab="config"}}
|
||||
class="button">
|
||||
Cancel
|
||||
</LinkTo>
|
|
@ -0,0 +1,149 @@
|
|||
{{#if (eq @model.config.queriesAvailable false)}}
|
||||
{{#if (eq @model.config.enabled 'On')}}
|
||||
<EmptyState
|
||||
@title="No monthly history"
|
||||
@message="There is no data in the monthly history yet. We collect it at the end of each month, so your data will be available on the first of next month."
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="Data tracking is disabled"
|
||||
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<p>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='config'}}>
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
</p>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{#if (eq @tab 'current')}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Current month
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
The below data is for the current month starting from the first day. For historical data, see the monthly
|
||||
history tab.
|
||||
</p>
|
||||
{{#if (eq @model.config.enabled 'Off')}}
|
||||
<EmptyState
|
||||
@title="Tracking is disabled"
|
||||
@message="Tracking is disabled and data is not being collected. To turn it on edit the configuration."
|
||||
>
|
||||
{{#if @model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
Go to configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (eq @model.config.enabled 'Off')}}
|
||||
<AlertBanner data-test-tracking-disabled @type="warning" @title="Tracking is disabled">
|
||||
Tracking is currently disabled and data is not being collected. Historical data can be searched, but you will
|
||||
need to
|
||||
<LinkTo @route="vault.cluster.clients.edit">
|
||||
edit the configuration
|
||||
</LinkTo>
|
||||
to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
<h1 data-test-client-count-title class="title is-4 has-bottom-margin-s">
|
||||
Monthly history
|
||||
</h1>
|
||||
<p class="has-bottom-margin-s">
|
||||
This data is presented by full month. If there is data missing, it's possible that tracking was turned off at
|
||||
the time. Vault will only show data for contiguous blocks of time during which tracking was on.
|
||||
</p>
|
||||
<PricingMetricsDates
|
||||
@queryStart={{@model.queryStart}}
|
||||
@queryEnd={{@model.queryEnd}}
|
||||
@resultStart={{@model.activity.startTime}}
|
||||
@resultEnd={{@model.activity.endTime}}
|
||||
@defaultSpan={{or @model.config.defaultReportMonths 12}}
|
||||
@retentionMonths={{@model.config.retentionMonths}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#unless this.hasClientData}}
|
||||
{{#if (eq @tab 'current')}}
|
||||
{{#if (eq @model.config.enabled 'On')}}
|
||||
<EmptyState
|
||||
@title="No data received"
|
||||
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<EmptyState
|
||||
@title="No data received"
|
||||
@message="No data exists for that query period. Try searching again."
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="card has-bottom-margin-m">
|
||||
<div class="card-content">
|
||||
<div class="is-flex is-flex-center">
|
||||
<div class="is-flex-1">
|
||||
<h2 class="title is-5 is-marginless">
|
||||
Total usage
|
||||
</h2>
|
||||
<p class="sub-text">
|
||||
These totals are within this namespace and all its children.
|
||||
</p>
|
||||
</div>
|
||||
<LearnLink @path="/tutorials/vault/usage-metrics">
|
||||
Learn more
|
||||
</LearnLink>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="columns">
|
||||
<div class="column" data-test-client-count-stats>
|
||||
<StatText
|
||||
@label="Total active Clients"
|
||||
@value={{or @model.activity.clients @model.activity.total.clients}}
|
||||
@size="l"
|
||||
@subText="The sum of unique entities and active direct tokens; Vault's primary billing metric."
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
class="column"
|
||||
@label="Unique entities"
|
||||
@value={{or @model.activity.distinct_entities @model.activity.total.distinct_entities}}
|
||||
@size="l"
|
||||
@subText="Representation of a particular user, client or application that created a token via login."
|
||||
/>
|
||||
</div>
|
||||
<div class="column">
|
||||
<StatText
|
||||
class="column"
|
||||
@label="Active direct tokens"
|
||||
@value={{or @model.activity.non_entity_tokens @model.activity.total.non_entity_tokens}}
|
||||
@size="l"
|
||||
@subText="Tokens created via a method that is not associated with an entity."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if this.showGraphs}}
|
||||
<div class="columns has-bottom-margin-m">
|
||||
<div class="column is-two-thirds">
|
||||
<BarChart
|
||||
@title="Top 10 Namespaces"
|
||||
@description="Each namespace's client count includes clients in child namespaces."
|
||||
@dataset={{this.barChartDataset}}
|
||||
@mapLegend={{array
|
||||
(hash key='non_entity_tokens' label='Active direct tokens')
|
||||
(hash key='distinct_entities' label='Unique entities')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="column"></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -116,12 +116,12 @@
|
|||
{{/if}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{#if ( and (has-permission 'metrics' routeParams='activity') (not @cluster.dr.isSecondary) this.auth.currentToken)}}
|
||||
{{#if ( and (has-permission 'clients' routeParams='activity') (not @cluster.dr.isSecondary) this.auth.currentToken)}}
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
<LinkTo @route="vault.cluster.metrics" @invokeAction={{@onLinkClick}}>
|
||||
<LinkTo @route="vault.cluster.clients" @query={{hash tab="current"}} @invokeAction={{@onLinkClick}}>
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">Metrics</span>
|
||||
<span class="level-left">Client count</span>
|
||||
<Chevron class="has-text-grey-light level-right" />
|
||||
</div>
|
||||
</LinkTo>
|
||||
|
|
|
@ -41,14 +41,4 @@
|
|||
{{date-format resultStart "MMM dd, yyyy"}} through {{date-format resultEnd "MMM dd, yyyy"}}
|
||||
</h2>
|
||||
{{/if}}
|
||||
{{#if showResultsWarning}}
|
||||
<div class="access-information" data-test-results-date-warning>
|
||||
<Icon @glyph="info-circle-fill" class="has-text-info"/>
|
||||
<p>This data may not reflect your search exactly. This is because Vault will only show data for contiguous blocks of time during which tracking was on.
|
||||
<LearnLink @path="/tutorials/vault/usage-metrics">
|
||||
Learn more here
|
||||
</LearnLink>
|
||||
</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -72,15 +72,16 @@
|
|||
<StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} />
|
||||
</div>
|
||||
<div class="navbar-separator is-hidden-mobile"></div>
|
||||
{{else if (and (has-permission 'metrics' routeParams='activity') (not cluster.dr.isSecondary) auth.currentToken)}}
|
||||
{{else if (and (has-permission 'clients' routeParams='activity') (not cluster.dr.isSecondary) auth.currentToken)}}
|
||||
<div class="navbar-sections">
|
||||
<div class="{{if (is-active-route 'vault.cluster.metrics') 'is-active'}}">
|
||||
<div class="{{if (is-active-route 'vault.cluster.clients') 'is-active'}}">
|
||||
{{#link-to
|
||||
"vault.cluster.metrics"
|
||||
current-when="vault.cluster.metrics"
|
||||
"vault.cluster.clients"
|
||||
(query-params tab="history")
|
||||
current-when="vault.cluster.clients"
|
||||
data-test-navbar-item='metrics'
|
||||
}}
|
||||
Metrics
|
||||
Client count
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<PricingMetricsConfig @model={{model}} @mode="edit" />
|
||||
<Clients::Config @model={{model}} @mode="edit" />
|
|
@ -0,0 +1,46 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Vault Client Count
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-pricing-metrics>
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='current' start=null end=null}} @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='current' start=null end=null}} data-test-usage-tab={{true}}>
|
||||
Current month
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='history'}} @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='history'}} data-test-usage-tab={{true}}>
|
||||
Monthly history
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{#if model.config.configPath.canRead}}
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='config' start=null end=null}} @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.clients.index" @query={{hash tab='config' start=null end=null}} data-test-configuration-tab={{true}}>
|
||||
Configuration
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{#if (eq tab "config")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if model.config.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.clients.edit" class="toolbar-link">
|
||||
Edit configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
<Clients::Config @model={{model.config}} />
|
||||
{{else}}
|
||||
<Clients::History @tab={{tab}} @model={{model}} />
|
||||
{{/if}}
|
|
@ -1,36 +0,0 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Metrics
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless">
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="vault.cluster.metrics.index" @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.metrics.index" data-test-configuration-tab={{false}}>
|
||||
Vault usage
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
<LinkTo @route="vault.cluster.metrics.config" @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.metrics.config" data-test-configuration-tab={{true}}>
|
||||
Configuration
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{#if model.configPath.canUpdate}}
|
||||
<LinkTo @route="vault.cluster.metrics.edit" class="toolbar-link">
|
||||
Edit configuration
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
<PricingMetricsConfig @model={{model}} />
|
|
@ -1,78 +0,0 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
Metrics
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
<div class="tabs-container box is-bottomless is-marginless is-fullwidth is-paddingless" data-test-pricing-metrics>
|
||||
<nav class="tabs">
|
||||
<ul>
|
||||
<LinkTo @route="vault.cluster.metrics.index" @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.metrics.index" data-test-usage-tab={{true}}>
|
||||
Vault usage
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{#if model.config.configPath.canRead}}
|
||||
<LinkTo @route="vault.cluster.metrics.config" @tagName="li" @activeClass="is-active">
|
||||
<LinkTo @route="vault.cluster.metrics.config" data-test-configuration-tab={{true}}>
|
||||
Configuration
|
||||
</LinkTo>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{#if (eq model.config.queriesAvailable false)}}
|
||||
{{#if (eq model.config.enabled "On")}}
|
||||
<EmptyState @title="No data is being received" @message="We haven't yet gathered enough data to display here. We collect it at the end of each month, so your data will be available on the first of next month. It will include the current namespace and all its children." />
|
||||
{{else}}
|
||||
<EmptyState @title="No data is being received" @message='Tracking is disabled, and no data is being collected. To turn it on, edit the configuration.'>
|
||||
<p><LinkTo @route="vault.cluster.metrics.config">Go to configuration</LinkTo></p>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{#if (eq model.config.enabled 'Off')}}
|
||||
<AlertBanner
|
||||
data-test-tracking-disabled
|
||||
@type="warning"
|
||||
@title="Tracking is disabled"
|
||||
>
|
||||
This feature is currently disabled and data is not being collected. <LinkTo @route="vault.cluster.metrics.edit">Edit the configuration</LinkTo> to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/if}}
|
||||
<p class="has-bottom-margin-s">The active clients metric contributes to billing. It is collected at the end of each month alongside unique entities and direct active tokens. The data below includes the current namespace and all its children.</p>
|
||||
<PricingMetricsDates
|
||||
@queryStart={{model.queryStart}}
|
||||
@queryEnd={{model.queryEnd}}
|
||||
@resultStart={{model.activity.startTime}}
|
||||
@resultEnd={{model.activity.endTime}}
|
||||
@defaultSpan={{or model.config.defaultReportMonths 12}}
|
||||
@retentionMonths={{model.config.retentionMonths}}
|
||||
/>
|
||||
{{#unless model.activity.total}}
|
||||
<EmptyState @title="No data found" @message="No data exists for that query period. Try searching again." />
|
||||
{{else}}
|
||||
<div class="selectable-card-container">
|
||||
<SelectableCard
|
||||
@cardTitle="Active clients"
|
||||
@total={{model.activity.total.clients}}
|
||||
@subText="Current namespace"
|
||||
/>
|
||||
<SelectableCard
|
||||
@cardTitle="Unique entities"
|
||||
@total={{model.activity.total.distinct_entities}}
|
||||
@subText="Current namespace"
|
||||
/>
|
||||
<SelectableCard
|
||||
@cardTitle="Active direct tokens"
|
||||
@total={{model.activity.total.non_entity_tokens}}
|
||||
@subText="Current namespace"
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -94,7 +94,7 @@ class BarChartComponent extends Component {
|
|||
container
|
||||
.append('div')
|
||||
.attr('class', 'chart-tooltip')
|
||||
.attr('style', 'position: fixed; opacity: 0;')
|
||||
.attr('style', 'position: absolute; opacity: 0;')
|
||||
.style('color', 'white')
|
||||
.style('background', `${TOOLTIP_BACKGROUND}`)
|
||||
.style('font-size', '.929rem')
|
||||
|
@ -214,11 +214,11 @@ class BarChartComponent extends Component {
|
|||
select('.chart-tooltip')
|
||||
.style('opacity', 1)
|
||||
.style('max-width', '200px')
|
||||
.style('left', `${event.pageX - 90}px`)
|
||||
.style('top', `${event.pageY - 90}px`)
|
||||
.style('left', `${event.pageX - 95}px`)
|
||||
.style('top', `${event.pageY - 155}px`)
|
||||
.text(
|
||||
`${Math.round((chartData.total * 100) / totalCount)}% of total client counts:
|
||||
${chartData.distinct_entities} unique entities, ${chartData.non_entity_tokens} active tokens.
|
||||
${chartData.non_entity_tokens} active tokens, ${chartData.distinct_entities} unique entities.
|
||||
`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@ export default function() {
|
|||
|
||||
this.get('sys/internal/counters/activity', function(db) {
|
||||
let data = {};
|
||||
const firstRecord = db['metrics/activities'].first();
|
||||
const firstRecord = db['clients/activities'].first();
|
||||
if (firstRecord) {
|
||||
data = firstRecord;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export default function() {
|
|||
this.get('sys/internal/counters/config', function(db) {
|
||||
return {
|
||||
request_id: '00001',
|
||||
data: db['metrics/configs'].first(),
|
||||
data: db['clients/configs'].first(),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function(server) {
|
||||
server.create('metrics/config');
|
||||
server.create('clients/config');
|
||||
server.create('feature', { feature_flags: ['SOME_FLAG', 'VAULT_CLOUD_ADMIN_NAMESPACE'] });
|
||||
}
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, findAll } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import logout from 'vault/tests/pages/logout';
|
||||
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
|
||||
const consoleComponent = create(consoleClass);
|
||||
|
||||
const tokenWithPolicy = async function(name, policy) {
|
||||
await consoleComponent.runCommands([
|
||||
`write sys/policies/acl/${name} policy=${btoa(policy)}`,
|
||||
`write -field=client_token auth/token/create policies=${name}`,
|
||||
]);
|
||||
|
||||
return consoleComponent.lastLogOutput;
|
||||
};
|
||||
|
||||
module('Acceptance | usage metrics', function(hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
return authPage.login();
|
||||
});
|
||||
|
||||
hooks.afterEach(function() {
|
||||
return logout.visit();
|
||||
});
|
||||
|
||||
test('it shows empty state when disabled and no data available', async function(assert) {
|
||||
server.create('metrics/config', { enabled: 'disable' });
|
||||
await visit('/vault/metrics');
|
||||
|
||||
assert.equal(currentURL(), '/vault/metrics');
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data is being received');
|
||||
});
|
||||
|
||||
test('it shows empty state when enabled and no data available', async function(assert) {
|
||||
server.create('metrics/config', { enabled: 'enable' });
|
||||
await visit('/vault/metrics');
|
||||
|
||||
assert.equal(currentURL(), '/vault/metrics');
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data is being received');
|
||||
});
|
||||
|
||||
test('it shows empty state when data available but not returned', async function(assert) {
|
||||
server.create('metrics/config', { queries_available: true });
|
||||
await visit('/vault/metrics');
|
||||
|
||||
assert.equal(currentURL(), '/vault/metrics');
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Pricing metrics date form exists');
|
||||
assert.dom('[data-test-pricing-result-dates]').doesNotExist('Pricing metric result dates are not shown');
|
||||
assert.dom('[data-test-component="empty-state"]').exists('Empty state exists');
|
||||
assert.dom('[data-test-empty-state-title]').hasText('No data found');
|
||||
});
|
||||
|
||||
test('it shows warning when disabled and data available', async function(assert) {
|
||||
server.create('metrics/config', { queries_available: true, enabled: 'disable' });
|
||||
server.create('metrics/activity');
|
||||
await visit('/vault/metrics');
|
||||
|
||||
assert.equal(currentURL(), '/vault/metrics');
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Pricing metrics date form 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) {
|
||||
server.create('metrics/config', { queries_available: true });
|
||||
server.create('metrics/activity');
|
||||
await visit('/vault/metrics');
|
||||
|
||||
assert.equal(currentURL(), '/vault/metrics');
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Pricing metrics date form exists');
|
||||
assert.dom('[data-test-configuration-tab]').exists('Metrics config tab exists');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.ok(findAll('.selectable-card').length === 3, 'renders the counts');
|
||||
});
|
||||
|
||||
test('it shows metrics even if config endpoint not allowed', async function(assert) {
|
||||
server.create('metrics/activity');
|
||||
const deny_config_policy = `
|
||||
path "sys/internal/counters/config" {
|
||||
capabilities = ["deny"]
|
||||
},
|
||||
`;
|
||||
|
||||
const userToken = await tokenWithPolicy('no-metrics-config', deny_config_policy);
|
||||
await logout.visit();
|
||||
await authPage.login(userToken);
|
||||
|
||||
await visit('/vault/metrics');
|
||||
|
||||
assert.equal(currentURL(), '/vault/metrics');
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Pricing metrics date form exists');
|
||||
assert.dom('[data-test-configuration-tab]').doesNotExist('Metrics config tab does not exist');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.ok(findAll('.selectable-card').length === 3, 'renders the counts');
|
||||
});
|
||||
});
|
|
@ -15,7 +15,7 @@ const routerService = Service.extend({
|
|||
},
|
||||
});
|
||||
|
||||
module('Integration | Component | pricing-metrics-config', function(hooks) {
|
||||
module('Integration | Component | client count config', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
const createAttr = (name, type, options) => {
|
||||
|
@ -50,7 +50,7 @@ module('Integration | Component | pricing-metrics-config', function(hooks) {
|
|||
});
|
||||
|
||||
test('it shows the table with the correct rows by default', async function(assert) {
|
||||
await render(hbs`<PricingMetricsConfig @model={{model}} />`);
|
||||
await render(hbs`<Clients::Config @model={{model}} />`);
|
||||
|
||||
assert.dom('[data-test-pricing-metrics-config-table]').exists('Pricing metrics config table exists');
|
||||
const rows = document.querySelectorAll('.info-table-row');
|
||||
|
@ -72,7 +72,7 @@ module('Integration | Component | pricing-metrics-config', function(hooks) {
|
|||
test('TODO: it shows the config edit form when mode = edit', async function(assert) {
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<PricingMetricsConfig @model={{model}} @mode="edit" />
|
||||
<Clients::Config @model={{model}} @mode="edit" />
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-pricing-metrics-config-form]').exists('Pricing metrics config form exists');
|
||||
|
@ -89,7 +89,7 @@ module('Integration | Component | pricing-metrics-config', function(hooks) {
|
|||
this.set('model', simModel);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<PricingMetricsConfig @model={{model}} @mode="edit" />
|
||||
<Clients::Config @model={{model}} @mode="edit" />
|
||||
`);
|
||||
|
||||
await click('[data-test-edit-metrics-config-save]');
|
||||
|
@ -110,7 +110,7 @@ module('Integration | Component | pricing-metrics-config', function(hooks) {
|
|||
this.set('model', simModel);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<PricingMetricsConfig @model={{model}} @mode="edit" />
|
||||
<Clients::Config @model={{model}} @mode="edit" />
|
||||
`);
|
||||
|
||||
await click('[data-test-edit-metrics-config-save]');
|
||||
|
@ -131,7 +131,7 @@ module('Integration | Component | pricing-metrics-config', function(hooks) {
|
|||
this.set('model', simModel);
|
||||
await render(hbs`
|
||||
<div id="modal-wormhole"></div>
|
||||
<PricingMetricsConfig @model={{model}} @mode="edit" />
|
||||
<Clients::Config @model={{model}} @mode="edit" />
|
||||
`);
|
||||
|
||||
await click('[data-test-edit-metrics-config-save]');
|
|
@ -0,0 +1,55 @@
|
|||
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: {},
|
||||
activity: {},
|
||||
});
|
||||
this.model = model;
|
||||
this.tab = 'current';
|
||||
});
|
||||
|
||||
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`<Clients::History @tab={{tab}} @model={{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`<Clients::History @tab={{tab}} @model={{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 data available but not returned', async function(assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, enabled: 'On' });
|
||||
await render(hbs`<Clients::History @tab={{tab}} @model={{model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').doesNotExist('Date range component should not 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 data when available from query', async function(assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true, configPath: { canRead: true } });
|
||||
Object.assign(this.model.activity, {
|
||||
clients: 1234,
|
||||
distinct_entities: 234,
|
||||
non_entity_tokens: 232,
|
||||
});
|
||||
|
||||
await render(hbs`<Clients::History @tab={{tab}} @model={{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-client-count-stats]').exists('Client count data exists');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
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) {
|
||||
setupRenderingTest(hooks);
|
||||
hooks.beforeEach(function() {
|
||||
let model = EmberObject.create({
|
||||
config: {},
|
||||
activity: {},
|
||||
});
|
||||
this.model = model;
|
||||
this.tab = 'history';
|
||||
});
|
||||
|
||||
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`<Clients::History @tab={{tab}} @model={{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`<Clients::History @tab={{tab}} @model={{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 data available but not returned', async function(assert) {
|
||||
Object.assign(this.model.config, { queriesAvailable: true });
|
||||
await render(hbs`<Clients::History @tab={{tab}} @model={{model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component exists');
|
||||
assert.dom('[data-test-pricing-result-dates]').doesNotExist('Date range form result dates are not shown');
|
||||
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`<Clients::History @tab={{tab}} @model={{model}} />`);
|
||||
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component 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.activity, {
|
||||
total: {
|
||||
clients: 1234,
|
||||
distinct_entities: 234,
|
||||
non_entity_tokens: 232,
|
||||
},
|
||||
});
|
||||
|
||||
await render(hbs`<Clients::History @tab={{tab}} @model={{model}} />`);
|
||||
assert.dom('[data-test-pricing-metrics-form]').exists('Date range form component exists');
|
||||
assert.dom('[data-test-tracking-disabled]').doesNotExist('Flash message does not exists');
|
||||
assert.dom('[data-test-client-count-stats]').exists('Client count data exists');
|
||||
});
|
||||
});
|
|
@ -55,34 +55,6 @@ module('Integration | Component | pricing-metrics-dates', function(hooks) {
|
|||
assert.dom('[data-test-results-date-warning]').doesNotExist('Does not show result states warning');
|
||||
});
|
||||
|
||||
test('If result and query start dates are > 1 day apart, warning is shown', async function(assert) {
|
||||
this.set('resultStart', new Date(2020, 1, 20));
|
||||
this.set('resultEnd', new Date(2020, 9, 31));
|
||||
await render(hbs`
|
||||
<PricingMetricsDates
|
||||
@queryStart="2-2020"
|
||||
@queryEnd="10-2020"
|
||||
@resultStart={{resultStart}}
|
||||
@resultEnd={{resultEnd}}
|
||||
/>
|
||||
`);
|
||||
assert.dom('[data-test-results-date-warning]').exists('shows states warning');
|
||||
});
|
||||
|
||||
test('If result and query end dates are > 1 day apart, warning is shown', async function(assert) {
|
||||
this.set('resultStart', new Date(2020, 1, 1));
|
||||
this.set('resultEnd', new Date(2020, 9, 15));
|
||||
await render(hbs`
|
||||
<PricingMetricsDates
|
||||
@queryStart="2-2020"
|
||||
@queryEnd="10-2020"
|
||||
@resultStart={{resultStart}}
|
||||
@resultEnd={{resultEnd}}
|
||||
/>
|
||||
`);
|
||||
assert.dom('[data-test-results-date-warning]').exists('shows states warning');
|
||||
});
|
||||
|
||||
test('it shows appropriate errors on input form', async function(assert) {
|
||||
const lastAvailable = endOfMonth(subMonths(startOfToday(), 1));
|
||||
const firstAvailable = subMonths(lastAvailable, 12);
|
||||
|
|
Loading…
Reference in New Issue