mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 18:48:08 +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'; | import Application from '../application'; | ||||||
|  |  | ||||||
| export default Application.extend({ | export default Application.extend({ | ||||||
|   queryRecord() { |   pathForType() { | ||||||
|     return this.ajax(this.urlForQuery(), 'GET').then(resp => { |     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; |       resp.id = resp.request_id; | ||||||
|       return resp; |       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 Route from '@ember/routing/route'; | ||||||
| import ClusterRoute from 'vault/mixins/cluster-route'; | import ClusterRoute from 'vault/mixins/cluster-route'; | ||||||
| import { hash } from 'rsvp'; | 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, { | export default Route.extend(ClusterRoute, { | ||||||
|   model() { |   queryParams: { | ||||||
|     let config = this.store.queryRecord('metrics/config', {}); |     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({ |     return hash({ | ||||||
|  |       queryStart: params.start, | ||||||
|  |       queryEnd: params.end, | ||||||
|       activity, |       activity, | ||||||
|       config, |       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) { |   normalizeResponse(store, primaryModelClass, payload, id, requestType) { | ||||||
|     const normalizedPayload = { |     const normalizedPayload = { | ||||||
|       id: payload.id, |       id: payload.id, | ||||||
|       ...payload.data, |       data: { | ||||||
|       enabled: payload.data.enabled.includes('enabled') ? 'On' : 'Off', |         ...payload.data, | ||||||
|  |         enabled: payload.data.enabled.includes('enabled') ? 'On' : 'Off', | ||||||
|  |       }, | ||||||
|     }; |     }; | ||||||
|     return this._super(store, primaryModelClass, normalizedPayload, id, requestType); |     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', |     raft: 'sys/storage/raft/configuration', | ||||||
|   }, |   }, | ||||||
|   metrics: { |   metrics: { | ||||||
|     dashboard: 'sys/internal/counters', |     activity: 'sys/internal/counters/activity', | ||||||
|     requests: 'sys/internal/counters/requests', |     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/navigate-input'; | ||||||
| @import './components/page-header'; | @import './components/page-header'; | ||||||
| @import './components/popup-menu'; | @import './components/popup-menu'; | ||||||
|  | @import './components/pricing-metrics-dates'; | ||||||
| @import './components/radio-card'; | @import './components/radio-card'; | ||||||
| @import './components/radial-progress'; | @import './components/radial-progress'; | ||||||
| @import './components/raft-join'; | @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}} |             {{/if}} | ||||||
|           </ul> |           </ul> | ||||||
|         {{/if}} |         {{/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"> |           <ul class="menu-list"> | ||||||
|             <li class="action"> |             <li class="action"> | ||||||
|               {{#link-to 'vault.cluster.metrics'  |               {{#link-to 'vault.cluster.metrics' | ||||||
|                 invokeAction=(action (queue (action onLinkClick) (action d.actions.close))) |                 invokeAction=(action (queue (action onLinkClick) (action d.actions.close))) | ||||||
|               }} |               }} | ||||||
|                 <div class="level is-mobile"> |                 <div class="level is-mobile"> | ||||||
|   | |||||||
| @@ -72,6 +72,18 @@ | |||||||
|           <StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} /> |           <StatusMenu @label="Status" @onLinkClick={{action Nav.closeDrawer}} /> | ||||||
|         </div> |         </div> | ||||||
|         <div class="navbar-separator is-hidden-mobile"></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}} |       {{/if}} | ||||||
|       <div class="navbar-item"> |       <div class="navbar-item"> | ||||||
|         <button type="button" class="button is-transparent nav-console-button{{if consoleOpen " popup-open"}}" |         <button type="button" class="button is-transparent nav-console-button{{if consoleOpen " popup-open"}}" | ||||||
|   | |||||||
| @@ -37,34 +37,53 @@ | |||||||
|   </nav> |   </nav> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div class="box is-sideless is-fullwidth is-marginless is-bottomless"> | {{#if (eq model.config.queriesAvailable false)}} | ||||||
|   {{#unless (eq model.config.enabled 'On')}} |   {{#if (eq model.config.enabled "On")}} | ||||||
|     <AlertBanner |     <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.' /> | ||||||
|       @type="warning" |   {{else}} | ||||||
|       @title="Tracking is disabled" |     <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> | ||||||
|       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. |     </EmptyState> | ||||||
|     </AlertBanner> |   {{/if}} | ||||||
|   {{/unless}} | {{else}} | ||||||
|   <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> |   <div class="box is-sideless is-fullwidth is-marginless is-bottomless"> | ||||||
|   <h2 class="title is-4"> |     {{#if (eq model.config.enabled 'Off')}} | ||||||
|     {{date-format model.activity.startTime "MMM DD, YYYY"}} through {{date-format model.activity.endTime "MMM DD, YYYY"}} |       <AlertBanner | ||||||
|   </h2> |         @type="warning" | ||||||
|   <div class="selectable-card-container"> |         @title="Tracking is disabled" | ||||||
|     <SelectableCard |       > | ||||||
|       @cardTitle="Active clients" |         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. | ||||||
|       @total={{model.activity.total.clients}} |       </AlertBanner> | ||||||
|       @subText="Current namespace" |     {{/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> | ||||||
|     <SelectableCard |  | ||||||
|       @cardTitle="Unique entities" |     <PricingMetricsDates | ||||||
|       @total={{model.activity.total.distinct_entities}} |       @queryStart={{model.queryStart}} | ||||||
|       @subText="Current namespace" |       @queryEnd={{model.queryEnd}} | ||||||
|     /> |       @resultStart={{model.activity.startTime}} | ||||||
|     <SelectableCard |       @resultEnd={{model.activity.endTime}} | ||||||
|       @cardTitle="Active direct tokens" |  | ||||||
|       @total={{model.activity.total.non_entity_tokens}} |  | ||||||
|       @subText="Current namespace" |  | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|  |     {{#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> | ||||||
| </div> | {{/if}} | ||||||
|   | |||||||
| @@ -25,7 +25,12 @@ module.exports = function(environment) { | |||||||
|       // endpoints that UI uses to determine the cluster state |       // endpoints that UI uses to determine the cluster state | ||||||
|       // calls to these endpoints will always go to the root namespace |       // calls to these endpoints will always go to the root namespace | ||||||
|       // these also need to be updated in the open-api-explorer engine |       // 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 |       // number of records to show on a single page by default - this is used by the client-side pagination | ||||||
|       DEFAULT_PAGE_SIZE: 100, |       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
	 Chelsea Shaw
					Chelsea Shaw