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'; | ||||
|  | ||||
| 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, | ||||
|       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,7 +152,7 @@ | ||||
|             {{/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' | ||||
|   | ||||
| @@ -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,19 +37,36 @@ | ||||
|   </nav> | ||||
| </div> | ||||
|  | ||||
| <div class="box is-sideless is-fullwidth is-marginless is-bottomless"> | ||||
|   {{#unless (eq model.config.enabled 'On')}} | ||||
| {{#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> | ||||
|   {{/unless}} | ||||
|     {{/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> | ||||
|   <h2 class="title is-4"> | ||||
|     {{date-format model.activity.startTime "MMM DD, YYYY"}} through {{date-format model.activity.endTime "MMM DD, YYYY"}} | ||||
|   </h2> | ||||
|  | ||||
|     <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" | ||||
| @@ -67,4 +84,6 @@ | ||||
|           @subText="Current namespace" | ||||
|         /> | ||||
|       </div> | ||||
| </div> | ||||
|     {{/unless}} | ||||
|   </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
	 Chelsea Shaw
					Chelsea Shaw