Unverified Commit 2c7cadb7 authored by Chelsea Shaw's avatar Chelsea Shaw Committed by GitHub
Browse files

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
Showing with 272 additions and 160 deletions
+272 -160
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';
},
});
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';
},
});
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';
},
});
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';
},
});
/**
* @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');
},
});
import Controller from '@ember/controller';
export default Controller.extend({
queryParams: ['start', 'end'],
start: null,
end: null,
});
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);
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'),
});
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'),
});
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'),
});
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,
});
......
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);
},
......
import MetricsSerializer from '../metrics';
export default MetricsSerializer.extend();
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',
},
};
......
.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';
......
<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">
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment