mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
Ui/pricing metrics params (#10083)
metrics route takes start and end params and passes to the date display field, as well as the route's API call
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import Application from '../application';
|
||||
|
||||
export default Application.extend({
|
||||
queryRecord() {
|
||||
return this.ajax(this.urlForQuery(), 'GET').then(resp => {
|
||||
pathForType() {
|
||||
return 'internal/counters/activity';
|
||||
},
|
||||
queryRecord(store, type, query) {
|
||||
const url = this.urlForQuery(null, type);
|
||||
// API accepts start and end as query params
|
||||
return this.ajax(url, 'GET', { data: query }).then(resp => {
|
||||
resp.id = resp.request_id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
urlForQuery() {
|
||||
return this.buildURL() + '/internal/counters/activity';
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import Application from '../application';
|
||||
|
||||
export default Application.extend({
|
||||
queryRecord() {
|
||||
return this.ajax(this.urlForQuery(), 'GET').then(resp => {
|
||||
resp.id = resp.request_id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
urlForQuery() {
|
||||
return this.buildURL() + '/internal/counters/entities';
|
||||
},
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import Application from '../application';
|
||||
|
||||
export default Application.extend({
|
||||
queryRecord() {
|
||||
return this.ajax(this.urlForQuery(), 'GET').then(resp => {
|
||||
resp.id = resp.request_id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
urlForQuery() {
|
||||
return this.buildURL() + '/internal/counters/requests';
|
||||
},
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import Application from '../application';
|
||||
|
||||
export default Application.extend({
|
||||
queryRecord() {
|
||||
return this.ajax(this.urlForQuery(), 'GET').then(resp => {
|
||||
resp.id = resp.request_id;
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
urlForQuery() {
|
||||
return this.buildURL() + '/internal/counters/tokens';
|
||||
},
|
||||
});
|
||||
131
ui/app/components/pricing-metrics-dates.js
Normal file
131
ui/app/components/pricing-metrics-dates.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @module PricingMetricsDates
|
||||
* PricingMetricsDates components are used on the Pricing Metrics page to handle queries related to pricing metrics.
|
||||
* This component assumes that query parameters (as in, from route params) are being passed in with the format MM-YYYY,
|
||||
* while the inputs expect a format of MM/YYYY.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <PricingMetricsDates @resultStart="2020-03-01T00:00:00Z" @resultEnd="2020-08-31T23:59:59Z" @queryStart="03-2020" @queryEnd="08-2020" />
|
||||
* ```
|
||||
* @param {object} resultStart - resultStart is the start date of the metrics returned. Should be a valid date string that the built-in Date() fn can parse
|
||||
* @param {object} resultEnd - resultEnd is the end date of the metrics returned. Should be a valid date string that the built-in Date() fn can parse
|
||||
* @param {string} [queryStart] - queryStart is the route param (formatted MM-YYYY) that the result will be measured against for showing discrepancy warning
|
||||
* @param {string} [queryEnd] - queryEnd is the route param (formatted MM-YYYY) that the result will be measured against for showing discrepancy warning
|
||||
* @param {number} [defaultSpan=12] - setting for default time between start and end input dates
|
||||
* @param {number} [retentionMonths=24] - setting for the retention months, which informs valid dates to query by
|
||||
*/
|
||||
import { computed } from '@ember/object';
|
||||
import Component from '@ember/component';
|
||||
import {
|
||||
compareAsc,
|
||||
differenceInSeconds,
|
||||
isValid,
|
||||
subMonths,
|
||||
startOfToday,
|
||||
format,
|
||||
endOfMonth,
|
||||
} from 'date-fns';
|
||||
import layout from '../templates/components/pricing-metrics-dates';
|
||||
import { parseDateString } from 'vault/helpers/parse-date-string';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
queryStart: null,
|
||||
queryEnd: null,
|
||||
resultStart: null,
|
||||
resultEnd: null,
|
||||
|
||||
start: null,
|
||||
end: null,
|
||||
|
||||
defaultSpan: 12,
|
||||
retentionMonths: 24,
|
||||
|
||||
startDate: computed('start', function() {
|
||||
if (!this.start) return null;
|
||||
let date;
|
||||
try {
|
||||
date = parseDateString(this.start, '/');
|
||||
if (date) return date;
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
endDate: computed('end', function() {
|
||||
if (!this.end) return null;
|
||||
let date;
|
||||
try {
|
||||
date = parseDateString(this.end, '/');
|
||||
if (date) return endOfMonth(date);
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
|
||||
showResultsWarning: computed('resultStart', 'resultEnd', 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', 'start', function() {
|
||||
if (!this.startDate) {
|
||||
return 'Start date is invalid. Please use format MM/YYYY';
|
||||
}
|
||||
if (!this.endDate) {
|
||||
return 'End date is invalid. Please use format MM/YYYY';
|
||||
}
|
||||
if (compareAsc(this.endDate, this.startDate) < 0) {
|
||||
return 'Start date is after end date';
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
let initialEnd;
|
||||
let initialStart;
|
||||
|
||||
initialEnd = subMonths(startOfToday(), 1);
|
||||
if (this.queryEnd) {
|
||||
initialEnd = parseDateString(this.queryEnd, '-');
|
||||
} else {
|
||||
// if query isn't passed in, set it so that showResultsWarning works
|
||||
this.queryEnd = format(initialEnd, 'MM-YYYY');
|
||||
}
|
||||
initialStart = subMonths(initialEnd, this.defaultSpan);
|
||||
if (this.queryStart) {
|
||||
initialStart = parseDateString(this.queryStart, '-');
|
||||
} else {
|
||||
// if query isn't passed in, set it so that showResultsWarning works
|
||||
this.queryStart = format(initialStart, 'MM-YYYY');
|
||||
}
|
||||
|
||||
this.start = format(initialStart, 'MM/YYYY');
|
||||
this.end = format(initialEnd, 'MM/YYYY');
|
||||
},
|
||||
});
|
||||
8
ui/app/controllers/vault/cluster/metrics.js
Normal file
8
ui/app/controllers/vault/cluster/metrics.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({
|
||||
queryParams: ['start', 'end'],
|
||||
|
||||
start: null,
|
||||
end: null,
|
||||
});
|
||||
20
ui/app/helpers/parse-date-string.js
Normal file
20
ui/app/helpers/parse-date-string.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
import { isValid } from 'date-fns';
|
||||
|
||||
export function parseDateString(date, separator = '-') {
|
||||
// Expects format MM-YYYY by default: no dates
|
||||
let datePieces = date.split(separator);
|
||||
if (datePieces.length === 2) {
|
||||
if (datePieces[0] < 1 || datePieces[0] > 12) {
|
||||
throw new Error('Not a valid month value');
|
||||
}
|
||||
let firstOfMonth = new Date(datePieces[1], datePieces[0] - 1, 1);
|
||||
if (isValid(firstOfMonth)) {
|
||||
return firstOfMonth;
|
||||
}
|
||||
}
|
||||
// what to return if not valid?
|
||||
throw new Error(`Please use format MM${separator}YYYY`);
|
||||
}
|
||||
|
||||
export default helper(parseDateString);
|
||||
@@ -1,27 +0,0 @@
|
||||
import DS from 'ember-data';
|
||||
const { attr } = DS;
|
||||
|
||||
/* sample response
|
||||
|
||||
{
|
||||
"request_id": "75cbaa46-e741-3eba-2be2-325b1ba8f03f",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"counters": {
|
||||
"entities": {
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export default DS.Model.extend({
|
||||
entities: attr('object'),
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import DS from 'ember-data';
|
||||
const { attr } = DS;
|
||||
|
||||
/* sample response
|
||||
|
||||
{
|
||||
"request_id": "75cbaa46-e741-3eba-2be2-325b1ba8f03f",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"counters": [
|
||||
{
|
||||
"start_time": "2019-05-01T00:00:00Z",
|
||||
"total": 50
|
||||
},
|
||||
{
|
||||
"start_time": "2019-04-01T00:00:00Z",
|
||||
"total": 45
|
||||
}
|
||||
]
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export default DS.Model.extend({
|
||||
counters: attr('array'),
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import DS from 'ember-data';
|
||||
const { attr } = DS;
|
||||
|
||||
/* sample response
|
||||
|
||||
{
|
||||
"request_id": "75cbaa46-e741-3eba-2be2-325b1ba8f03f",
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"counters": {
|
||||
"service_tokens": {
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrap_info": null,
|
||||
"warnings": null,
|
||||
"auth": null
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
export default DS.Model.extend({
|
||||
service_tokens: attr('object'),
|
||||
});
|
||||
@@ -1,14 +1,52 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import ClusterRoute from 'vault/mixins/cluster-route';
|
||||
import { hash } from 'rsvp';
|
||||
import { endOfMonth } from 'date-fns';
|
||||
import { parseDateString } from 'vault/helpers/parse-date-string';
|
||||
|
||||
const getActivityParams = ({ start, end }) => {
|
||||
// Expects MM-YYYY format
|
||||
// TODO: minStart, maxEnd
|
||||
let params = {};
|
||||
if (start) {
|
||||
let startDate = parseDateString(start);
|
||||
if (startDate) {
|
||||
// TODO: Replace with formatRFC3339 when date-fns is updated
|
||||
params.start_time = Math.round(startDate.getTime() / 1000);
|
||||
}
|
||||
}
|
||||
if (end) {
|
||||
let endDate = parseDateString(end);
|
||||
if (endDate) {
|
||||
// TODO: Replace with formatRFC3339 when date-fns is updated
|
||||
params.end_time = Math.round(endOfMonth(endDate).getTime() / 1000);
|
||||
}
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
export default Route.extend(ClusterRoute, {
|
||||
model() {
|
||||
let config = this.store.queryRecord('metrics/config', {});
|
||||
queryParams: {
|
||||
start: {
|
||||
refreshModel: true,
|
||||
},
|
||||
end: {
|
||||
refreshModel: true,
|
||||
},
|
||||
},
|
||||
|
||||
let activity = this.store.queryRecord('metrics/activity', {});
|
||||
model(params) {
|
||||
let config = this.store.queryRecord('metrics/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);
|
||||
|
||||
return hash({
|
||||
queryStart: params.start,
|
||||
queryEnd: params.end,
|
||||
activity,
|
||||
config,
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
const normalizedPayload = {
|
||||
id: payload.id,
|
||||
data: payload.data.counters,
|
||||
};
|
||||
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
|
||||
},
|
||||
});
|
||||
@@ -4,8 +4,10 @@ export default ApplicationSerializer.extend({
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
const normalizedPayload = {
|
||||
id: payload.id,
|
||||
...payload.data,
|
||||
enabled: payload.data.enabled.includes('enabled') ? 'On' : 'Off',
|
||||
data: {
|
||||
...payload.data,
|
||||
enabled: payload.data.enabled.includes('enabled') ? 'On' : 'Off',
|
||||
},
|
||||
};
|
||||
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import MetricsSerializer from '../metrics';
|
||||
|
||||
export default MetricsSerializer.extend();
|
||||
@@ -1,3 +0,0 @@
|
||||
import MetricsSerializer from '../metrics';
|
||||
|
||||
export default MetricsSerializer.extend();
|
||||
@@ -30,8 +30,8 @@ const API_PATHS = {
|
||||
raft: 'sys/storage/raft/configuration',
|
||||
},
|
||||
metrics: {
|
||||
dashboard: 'sys/internal/counters',
|
||||
requests: 'sys/internal/counters/requests',
|
||||
activity: 'sys/internal/counters/activity',
|
||||
config: 'sys/internal/counters/config',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
4
ui/app/styles/components/pricing-metrics-dates.scss
Normal file
4
ui/app/styles/components/pricing-metrics-dates.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.pricing-metrics-date-form {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
@@ -78,6 +78,7 @@
|
||||
@import './components/navigate-input';
|
||||
@import './components/page-header';
|
||||
@import './components/popup-menu';
|
||||
@import './components/pricing-metrics-dates';
|
||||
@import './components/radio-card';
|
||||
@import './components/radial-progress';
|
||||
@import './components/raft-join';
|
||||
|
||||
52
ui/app/templates/components/pricing-metrics-dates.hbs
Normal file
52
ui/app/templates/components/pricing-metrics-dates.hbs
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="field-body pricing-metrics-date-form">
|
||||
<div class="field is-narrow">
|
||||
<label for="start" class="is-label">From</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
type="string"
|
||||
value=start
|
||||
name="start"
|
||||
class=(concat 'input' (unless startDate ' has-error'))
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-test-start-input="true"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-narrow">
|
||||
<label for="end" class="is-label">Through</label>
|
||||
<div class="control">
|
||||
{{input
|
||||
type="string"
|
||||
value=end
|
||||
name="end"
|
||||
class=(concat 'input' (unless endDate ' has-error'))
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-test-end-input="true"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{#link-to 'vault.cluster.metrics'
|
||||
(query-params start=(date-format startDate 'MM-YYYY') end=(date-format endDate 'MM-YYYY'))
|
||||
class="button"
|
||||
disabled=error
|
||||
}}
|
||||
Query
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{#if error}}
|
||||
<FormError>{{error}}</FormError>
|
||||
{{/if}}
|
||||
<div class="box is-fullwidth is-shadowless">
|
||||
<h2 class="title is-4">
|
||||
{{date-format resultStart "MMM DD, YYYY"}} through {{date-format resultEnd "MMM DD, YYYY"}}
|
||||
</h2>
|
||||
{{#if showResultsWarning}}
|
||||
<div class="access-information" data-test-results-date-warning>
|
||||
<Icon @glyph="info-circle-fill" class="has-text-info"/>
|
||||
{{!-- TODO: Add "Learn more here." link --}}
|
||||
<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. </p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@@ -152,10 +152,10 @@
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
{{#if ( and (has-permission 'metrics' routeParams='dashboard') (not cluster.dr.isSecondary) auth.currentToken)}}
|
||||
{{#if ( and (has-permission 'metrics' routeParams='activity') (not cluster.dr.isSecondary) auth.currentToken)}}
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
{{#link-to 'vault.cluster.metrics'
|
||||
{{#link-to 'vault.cluster.metrics'
|
||||
invokeAction=(action (queue (action onLinkClick) (action d.actions.close)))
|
||||
}}
|
||||
<div class="level is-mobile">
|
||||
|
||||
@@ -72,6 +72,18 @@
|
||||
<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)}}
|
||||
<div class="navbar-sections">
|
||||
<div class="{{if (is-active-route 'vault.cluster.metrics') 'is-active'}}">
|
||||
{{#link-to
|
||||
"vault.cluster.metrics"
|
||||
current-when="vault.cluster.metrics"
|
||||
data-test-navbar-item='metrics'
|
||||
}}
|
||||
Metrics
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="navbar-item">
|
||||
<button type="button" class="button is-transparent nav-console-button{{if consoleOpen " popup-open"}}"
|
||||
|
||||
@@ -37,34 +37,53 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{#unless (eq model.config.enabled 'On')}}
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@title="Tracking is disabled"
|
||||
>
|
||||
This feature is currently disabled and data is not being collected. {{#link-to 'vault.cluster.metrics-config'}}Edit the configuration{{/link-to}} to enable tracking again.
|
||||
</AlertBanner>
|
||||
{{/unless}}
|
||||
<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.</p>
|
||||
<h2 class="title is-4">
|
||||
{{date-format model.activity.startTime "MMM DD, YYYY"}} through {{date-format model.activity.endTime "MMM DD, YYYY"}}
|
||||
</h2>
|
||||
<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"
|
||||
{{#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.' />
|
||||
{{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>{{#link-to 'vault.cluster.metrics-config'}}Go to configuration{{/link-to}}</p>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
|
||||
{{#if (eq model.config.enabled 'Off')}}
|
||||
<AlertBanner
|
||||
@type="warning"
|
||||
@title="Tracking is disabled"
|
||||
>
|
||||
This feature is currently disabled and data is not being collected. {{#link-to 'vault.cluster.metrics-config'}}Edit the configuration{{/link-to}} 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.</p>
|
||||
|
||||
<PricingMetricsDates
|
||||
@queryStart={{model.queryStart}}
|
||||
@queryEnd={{model.queryEnd}}
|
||||
@resultStart={{model.activity.startTime}}
|
||||
@resultEnd={{model.activity.endTime}}
|
||||
/>
|
||||
|
||||
{{#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>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -25,7 +25,12 @@ module.exports = function(environment) {
|
||||
// endpoints that UI uses to determine the cluster state
|
||||
// calls to these endpoints will always go to the root namespace
|
||||
// these also need to be updated in the open-api-explorer engine
|
||||
NAMESPACE_ROOT_URLS: ['sys/health', 'sys/seal-status', 'sys/license/features'],
|
||||
NAMESPACE_ROOT_URLS: [
|
||||
'sys/health',
|
||||
'sys/seal-status',
|
||||
'sys/license/features',
|
||||
'sys/internal/counters/config',
|
||||
],
|
||||
// number of records to show on a single page by default - this is used by the client-side pagination
|
||||
DEFAULT_PAGE_SIZE: 100,
|
||||
},
|
||||
|
||||
18
ui/lib/core/addon/components/form-error.js
Normal file
18
ui/lib/core/addon/components/form-error.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @module FormError
|
||||
* FormError components are used to show an error on a form field that is more compact than the
|
||||
* normal MessageError component. This component adds an icon and styling to the content of the
|
||||
* component, so additionally styling (bold, italic) and links are allowed.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <FormError>Oh no <em>something bad</em>! <a href="#">Do something</a></FormError>
|
||||
* ```
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/form-error';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
});
|
||||
13
ui/lib/core/addon/templates/components/form-error.hbs
Normal file
13
ui/lib/core/addon/templates/components/form-error.hbs
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="has-top-margin-s is-flex is-flex-center">
|
||||
<div class="is-narrow message-icon">
|
||||
<Icon
|
||||
@size="s"
|
||||
class="has-text-danger"
|
||||
aria-hidden=true
|
||||
@glyph="cancel-square-fill"
|
||||
/>
|
||||
</div>
|
||||
<div class="has-text-danger">
|
||||
{{yield}}
|
||||
</div>
|
||||
</div>
|
||||
1
ui/lib/core/app/components/form-error.js
Normal file
1
ui/lib/core/app/components/form-error.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/form-error';
|
||||
26
ui/tests/integration/components/form-error-test.js
Normal file
26
ui/tests/integration/components/form-error-test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | form-error', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`{{form-error}}`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), '');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
{{#form-error}}
|
||||
template block text
|
||||
{{/form-error}}
|
||||
`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'template block text');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { subMonths, startOfToday, format } from 'date-fns';
|
||||
|
||||
module('Integration | Component | pricing-metrics-dates', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('by default it sets the start and end inputs', async function(assert) {
|
||||
const expectedEnd = subMonths(startOfToday(), 1);
|
||||
const expectedStart = subMonths(expectedEnd, 12);
|
||||
await render(hbs`
|
||||
<PricingMetricsDates />
|
||||
`);
|
||||
assert.dom('[data-test-end-input]').hasValue(format(expectedEnd, 'MM/YYYY'), 'End input is last month');
|
||||
assert
|
||||
.dom('[data-test-start-input]')
|
||||
.hasValue(format(expectedStart, 'MM/YYYY'), 'Start input is 12 months before last month');
|
||||
});
|
||||
|
||||
test('On init if end date passed, start is calculated', async function(assert) {
|
||||
const expectedStart = subMonths(new Date(2020, 8, 15), 12);
|
||||
this.set('queryEnd', '09-2020');
|
||||
await render(hbs`
|
||||
<PricingMetricsDates @queryEnd={{queryEnd}} />
|
||||
`);
|
||||
assert.dom('[data-test-end-input]').hasValue('09/2020', 'End input matches query');
|
||||
assert
|
||||
.dom('[data-test-start-input]')
|
||||
.hasValue(format(expectedStart, 'MM/YYYY'), 'Start input is 12 months before end input');
|
||||
});
|
||||
|
||||
test('On init if query start date passed, end is default', async function(assert) {
|
||||
const expectedEnd = subMonths(startOfToday(), 1);
|
||||
this.set('queryStart', '01-2020');
|
||||
await render(hbs`
|
||||
<PricingMetricsDates @queryStart={{queryStart}} />
|
||||
`);
|
||||
assert.dom('[data-test-end-input]').hasValue(format(expectedEnd, 'MM/YYYY'), 'End input is last month');
|
||||
assert.dom('[data-test-start-input]').hasValue('01/2020', 'Start input matches query');
|
||||
});
|
||||
|
||||
test('If result and query dates are within 1 day, warning is not shown', async function(assert) {
|
||||
this.set('resultStart', new Date(2020, 1, 1));
|
||||
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]').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');
|
||||
});
|
||||
});
|
||||
47
ui/tests/unit/helpers/parse-date-string-test.js
Normal file
47
ui/tests/unit/helpers/parse-date-string-test.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { parseDateString } from 'vault/helpers/parse-date-string';
|
||||
import { module, test } from 'qunit';
|
||||
import { compareAsc } from 'date-fns';
|
||||
|
||||
module('Unit | Helpers | parse-date-string', function() {
|
||||
test('it returns the first of the month when date like MM-YYYY passed in', function(assert) {
|
||||
let expected = new Date(2020, 3, 1);
|
||||
let result = parseDateString('04-2020');
|
||||
assert.equal(compareAsc(expected, result), 0);
|
||||
});
|
||||
|
||||
test('it can handle a date format like MM/YYYY', function(assert) {
|
||||
let expected = new Date(2020, 11, 1);
|
||||
let result = parseDateString('12/2020', '/');
|
||||
assert.equal(compareAsc(expected, result), 0);
|
||||
});
|
||||
|
||||
test('it throws an error with passed separator if bad format', function(assert) {
|
||||
let result;
|
||||
try {
|
||||
result = parseDateString('01-12-2020');
|
||||
} catch (e) {
|
||||
result = e.message;
|
||||
}
|
||||
assert.equal('Please use format MM-YYYY', result);
|
||||
});
|
||||
|
||||
test('it throws an error with wrong separator', function(assert) {
|
||||
let result;
|
||||
try {
|
||||
result = parseDateString('12/2020', '.');
|
||||
} catch (e) {
|
||||
result = e.message;
|
||||
}
|
||||
assert.equal('Please use format MM.YYYY', result);
|
||||
});
|
||||
|
||||
test('it throws an error if month is invalid', function(assert) {
|
||||
let result;
|
||||
try {
|
||||
result = parseDateString('13-2020');
|
||||
} catch (e) {
|
||||
result = e.message;
|
||||
}
|
||||
assert.equal('Not a valid month value', result);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user