Core Usage Metrics (#8347)

* Core usage metrics v1 (merge to side-branch) (#8238)

* restructure menu layout per designs

* setup new routing that will set the stage for a metrics landing page

* fix formatting

* Revert "fix formatting"

This reverts commit e77cdec5e58cdcea49aa1b97f80238433c4f7d1e.

* fix formatting

* small styling changes

* change request routing to metrics

* rename route js file

* Core usage metrics v2 (#8263)

* restructure menu layout per designs

* setup new routing that will set the stage for a metrics landing page

* fix formatting

* Revert "fix formatting"

This reverts commit e77cdec5e58cdcea49aa1b97f80238433c4f7d1e.

* fix formatting

* small styling changes

* change request routing to metrics

* rename route js file

* setup selectable card component and api request

* add token and http request models to route and template

* add entities to route and template

* clean up

* add breadcrumbs and some clean up work

* remove unused selectable-card component

* refactor to a serializer

* move adapters, serializers, and models into metrics folder

* remove unused file

* address pr comments

* address pr comments

* Core Usage Metrics V3 (#8316)

* restructure menu layout per designs

* setup new routing that will set the stage for a metrics landing page

* fix formatting

* Revert "fix formatting"

This reverts commit e77cdec5e58cdcea49aa1b97f80238433c4f7d1e.

* fix formatting

* small styling changes

* change request routing to metrics

* rename route js file

* setup selectable card component and api request

* add token and http request models to route and template

* add entities to route and template

* clean up

* add breadcrumbs and some clean up work

* remove unused selectable-card component

* setup smaller http request bar chart

* refactor to a serializer

* move adapters, serializers, and models into metrics folder

* remove unused file

* setup change part of component

* fix broken model

* add conditional class

* setting up computed properties in new component

* small fixes

* setup components

* minor fixes

* rename

* clean up

* firefox fix

* remove shadow bars

* move out of metrics folders

* modify permissions to show difference between token entities and requests

* make tests

* fix class names and associated tests

* clean up

* fix text overflow in non-chrome browsers

* address pr comments, specifically class names and tests

* move into one component

* clean up component descriptions in comments

* small wording changes

* fix for accessibility

* address pr comments around component examples for storybook

* fix test

* fix failing test

* fix test
This commit is contained in:
Angel Garbarino 2020-02-13 12:44:57 -07:00 committed by GitHub
parent 0937a58ad7
commit 2a52c1a82b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 803 additions and 64 deletions

View File

@ -0,0 +1,14 @@
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';
},
});

View File

@ -1,4 +1,4 @@
import Application from './application';
import Application from '../application';
export default Application.extend({
queryRecord() {

View File

@ -0,0 +1,14 @@
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';
},
});

View File

@ -0,0 +1,153 @@
import Component from '@ember/component';
import d3 from 'd3-selection';
import d3Scale from 'd3-scale';
import d3Axis from 'd3-axis';
import d3TimeFormat from 'd3-time-format';
import { assign } from '@ember/polyfills';
import { computed } from '@ember/object';
import { run } from '@ember/runloop';
import { task, waitForEvent } from 'ember-concurrency';
/**
* @module HttpRequestsBarChartSmall
* The HttpRequestsBarChartSmall is a simplified version of the HttpRequestsBarChart component.
*
*
* @example
* ```js
* <HttpRequestsBarChartSmall @counters={counters}/>
* ```
*
* @param counters=null {Array} - A list of objects containing the total number of HTTP Requests for each month. `counters` should be the response from the `/internal/counters/requests`.
* The response is then filtered showing only the 12 most recent months of data. This property is called filteredHttpsRequests, like:
* const FILTERED_HTTPS_REQUESTS = [
* { start_time: '2018-11-01T00:00:00Z', total: 5500 },
* { start_time: '2018-12-01T00:00:00Z', total: 4500 },
* { start_time: '2019-01-01T00:00:00Z', total: 5000 },
* { start_time: '2019-02-01T00:00:00Z', total: 5000 },
* ];
*/
const HEIGHT = 125;
const UI_GRAY_300 = '#bac1cc';
const UI_GRAY_100 = '#ebeef2';
export default Component.extend({
classNames: ['http-requests-bar-chart-small'],
counters: null,
margin: Object.freeze({ top: 24, right: 16, bottom: 24, left: 16 }),
padding: 0.04,
width: 0,
height() {
const { margin } = this;
return HEIGHT - margin.top - margin.bottom;
},
parsedCounters: computed('counters', function() {
// parse the start times so bars display properly
const { counters } = this;
counters.reverse();
return counters.map((counter, index) => {
return assign({}, counter, {
start_time: d3TimeFormat.isoParse(counter.start_time),
fill_color: index === counters.length - 1 ? UI_GRAY_300 : UI_GRAY_100,
});
});
}),
yScale: computed('parsedCounters', 'height', function() {
const { parsedCounters } = this;
const height = this.height();
const counterTotals = parsedCounters.map(c => c.total);
return d3Scale
.scaleLinear()
.domain([0, Math.max(...counterTotals)])
.range([height, 0]);
}),
xScale: computed('parsedCounters', 'width', function() {
const { parsedCounters, width, margin, padding } = this;
return d3Scale
.scaleBand()
.domain(parsedCounters.map(c => c.start_time))
.rangeRound([0, width - margin.left - margin.right], 0.05)
.paddingInner(padding)
.paddingOuter(padding);
}),
didInsertElement() {
this._super(...arguments);
const { margin } = this;
// set the width after the element has been rendered because the chart axes depend on it.
// this helps us avoid an arbitrary hardcoded width which causes alignment & resizing problems.
run.schedule('afterRender', this, () => {
this.set('width', this.element.clientWidth - margin.left - margin.right);
this.renderBarChart();
});
},
didUpdateAttrs() {
this.renderBarChart();
},
renderBarChart() {
const { margin, width, xScale, yScale, parsedCounters, elementId } = this;
const height = this.height();
const barChartSVG = d3.select('.http-requests-bar-chart-small');
const barsContainer = d3.select(`#bars-container-${elementId}`);
d3.select('.http-requests-bar-chart')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr('viewBox', `0 0 ${width} ${height}`);
const xAxis = d3Axis
.axisBottom(xScale)
.tickFormat('')
.tickValues([])
.tickSizeOuter(0);
barChartSVG
.select('g.x-axis')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
const bars = barsContainer.selectAll('.bar').data(parsedCounters, c => +c.start_time);
const barsEnter = bars
.enter()
.append('rect')
.attr('class', 'bar');
bars
.merge(barsEnter)
.attr('x', counter => xScale(counter.start_time))
.attr('y', () => yScale(0))
.attr('width', xScale.bandwidth())
.attr('height', counter => height - yScale(counter.total) - 5) // subtract 5 to provide the gap between the xAxis and the bars
.attr('y', counter => yScale(counter.total))
.attr('fill', counter => counter.fill_color)
.attr('stroke', counter => counter.fill_color);
bars.exit().remove();
},
updateDimensions() {
const newWidth = this.element.clientWidth;
const { margin } = this;
this.set('width', newWidth - margin.left - margin.right);
this.renderBarChart();
},
waitForResize: task(function*() {
while (true) {
yield waitForEvent(window, 'resize');
run.scheduleOnce('afterRender', this, 'updateDimensions');
}
})
.on('didInsertElement')
.cancelOn('willDestroyElement')
.drop(),
});

View File

@ -0,0 +1,54 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
/**
* @module SelectableCardContainer
* SelectableCardContainer components are used to hold SelectableCard components. They act as a CSS grid container, and change grid configurations based on the boolean of @gridContainer.
*
* @example
* ```js
* <SelectableCardContainer @counters={{model}} @gridContainer="true" />
* ```
* @param counters=null {Object} - Counters is an object that returns total entities, tokens, and an array of objects with the total https request per month.
* @param gridContainer=false {Boolean} - gridContainer is optional. If true, it's telling the container it will have a nested CSS grid.
*
* const MODEL = {
* totalEntities: 0,
* httpsRequests: [{ start_time: '2019-04-01T00:00:00Z', total: 5500 }],
* totalTokens: 1,
* };
*/
export default Component.extend({
classNameBindings: ['isGridContainer'],
counters: null,
gridContainer: false,
isGridContainer: computed('counters', function() {
return this.counters.httpsRequests.length > 1
? 'selectable-card-container has-grid'
: 'selectable-card-container';
}),
totalHttpRequests: computed('counters', function() {
let httpsRequestsArray = this.counters.httpsRequests || [];
return httpsRequestsArray.firstObject.total;
}),
// Limit number of months returned to the most recent 12
filteredHttpsRequests: computed('counters', function() {
let httpsRequestsArray = this.counters.httpsRequests || [];
if (httpsRequestsArray.length > 12) {
httpsRequestsArray = httpsRequestsArray.slice(0, 12);
}
return httpsRequestsArray;
}),
percentChange: computed('counters', function() {
let httpsRequestsArray = this.counters.httpsRequests || [];
let lastTwoMonthsArray = httpsRequestsArray.slice(0, 2);
let previousMonthVal = lastTwoMonthsArray.lastObject.total;
let thisMonthVal = lastTwoMonthsArray.firstObject.total;
let percentChange = (((previousMonthVal - thisMonthVal) / previousMonthVal) * 100).toFixed(1);
// a negative value indicates a percentage increase, so we swap the value
percentChange = -percentChange;
return percentChange;
}),
});

View File

@ -0,0 +1,35 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
/**
* @module SelectableCard
* SelectableCard components are card-like components that display a title, total, subtotal, and anything after they yield.
* They are designed to be used in containers that act as flexbox or css grid containers.
*
* @example
* ```js
* <SelectableCard @cardTitle="Tokens" @total={{totalHttpRequests}} @subText="Total" @gridContainer={{gridContainer}}/>
* ```
* @param cardTitle='' {String} - cardTitle displays the card title
* @param total=0 {Number} - the Total number displays like a title, it's the largest text in the component
* @param subText='' {String} - subText describes the total
* @param gridContainer=false {Boolean} - Optional parameter used to display CSS grid item class.
*/
export default Component.extend({
cardTitle: '',
total: 0,
subText: '',
gridContainer: false,
tagName: '', // do not wrap component with div
formattedCardTitle: computed('total', function() {
const { cardTitle, total } = this;
if (cardTitle === 'Tokens') {
return total !== 1 ? 'Tokens' : 'Token';
} else if (cardTitle === 'Entities') {
return total !== 1 ? 'Entities' : 'Entity';
}
return cardTitle;
}),
});

View File

@ -0,0 +1,27 @@
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'),
});

View File

@ -0,0 +1,27 @@
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'),
});

View File

@ -15,7 +15,10 @@ Router.map(function() {
this.route('logout');
this.mount('open-api-explorer', { path: '/api-explorer' });
this.route('license');
this.route('requests', { path: '/metrics/requests' });
this.route('metrics', function() {
this.route('index', { path: '/' });
this.route('http-requests');
});
this.route('storage', { path: '/storage/raft' });
this.route('storage-restore', { path: '/storage/raft/restore' });
this.route('settings', function() {

View File

@ -0,0 +1,8 @@
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';
export default Route.extend(ClusterRoute, {
model() {
return {};
},
});

View File

@ -0,0 +1,7 @@
import ClusterRouteBase from '../cluster-route-base';
export default ClusterRouteBase.extend({
model() {
return this.store.queryRecord('metrics/http-requests', {});
},
});

View File

@ -0,0 +1,26 @@
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';
import { hash } from 'rsvp';
export default Route.extend(ClusterRoute, {
model() {
let totalEntities = this.store.queryRecord('metrics/entity', {}).then(response => {
return response.entities.total;
});
let httpsRequests = this.store.queryRecord('metrics/http-requests', {}).then(response => {
let reverseArray = response.counters.reverse();
return reverseArray;
});
let totalTokens = this.store.queryRecord('metrics/token', {}).then(response => {
return response.service_tokens.total;
});
return hash({
totalEntities,
httpsRequests,
totalTokens,
});
},
});

View File

@ -1,7 +0,0 @@
import ClusterRouteBase from './cluster-route-base';
export default ClusterRouteBase.extend({
model() {
return this.store.queryRecord('requests', {});
},
});

View File

@ -0,0 +1,11 @@
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);
},
});

View File

@ -0,0 +1,3 @@
import MetricsSerializer from '../metrics';
export default MetricsSerializer.extend();

View File

@ -0,0 +1,3 @@
import MetricsSerializer from '../metrics';
export default MetricsSerializer.extend();

View File

@ -30,6 +30,7 @@ const API_PATHS = {
raft: 'sys/storage/raft/configuration',
},
metrics: {
dashboard: 'sys/internal/counters',
requests: 'sys/internal/counters/requests',
},
};

View File

@ -0,0 +1,31 @@
.selectable-card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
grid-template-rows: 1fr;
grid-gap: 2rem;
}
.selectable-card-container.has-grid {
display: grid;
grid-template-columns: 2fr 1fr;
grid-template-rows: repeat(2, 1fr);
grid-gap: 2rem;
@include until($mobile) {
grid-template-columns: 2fr;
}
.grid-item-http {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 3;
}
.selectable-card.is-grid-container {
display: grid;
grid-template-columns: 2fr 0.5fr;
grid-template-rows: 1fr 2fr 0.5fr;
padding: $spacing-l 0 14px $spacing-l; // modify bottom spacing to better align with other cards
}
}

View File

@ -0,0 +1,82 @@
.selectable-card {
box-shadow: 0 0 0 1px rgba($grey-dark, 0.3);
display: flex;
justify-content: space-between;
padding: $spacing-l 0 $spacing-l $spacing-l;
line-height: 0;
&:hover {
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle;
}
> a {
text-decoration: none;
}
.card-details {
grid-column-start: 2;
grid-row-start: 3;
align-self: center;
justify-self: right;
padding-right: $spacing-l;
}
.http-requests-bar-chart-small {
grid-column: 1 / span 2;
grid-row-start: 2;
align-self: end;
min-width: 100%; // necessary for Firefox
}
.change-metric {
justify-self: right;
padding-right: $spacing-l;
display: grid;
grid-template-columns: 1fr 2fr;
grid-template-rows: 1fr 1fr;
.hs-icon {
color: $grey-light;
align-self: center;
justify-self: right;
}
.amount-change {
align-self: center;
justify-self: center;
font-weight: 500;
}
.item-c {
grid-column: 1 / span 2;
align-self: start;
justify-self: end;
font-weight: $font-weight-semibold;
white-space: nowrap;
@include until($mobile) {
overflow: hidden;
}
}
}
.title-number {
color: $black;
font-size: 36px;
font-weight: 500;
line-height: 1.33;
}
}
.selectable-card.is-rounded {
border-radius: $radius;
}
.change-metric-icon.is-decrease {
transform: rotate(135deg);
}
.change-metric-icon.is-increase {
transform: rotate(45deg);
}

View File

@ -78,6 +78,8 @@
@import './components/raft-join';
@import './components/role-item';
@import './components/search-select';
@import './components/selectable-card';
@import './components/selectable-card-container.scss';
@import './components/shamir-progress';
@import './components/sidebar';
@import './components/splash-page';

View File

@ -0,0 +1,4 @@
<svg class="http-requests-bar-chart">
<g class="x-axis"></g>
<g id="bars-container-{{this.elementId}}" class="bars"></g>
</svg>

After

Width:  |  Height:  |  Size: 132 B

View File

@ -0,0 +1,74 @@
{{#linked-block
"vault.cluster.metrics.http-requests"
class="grid-item-http"
}}
<SelectableCard
@cardTitle="Http Requests"
@total={{totalHttpRequests}}
@subText="This month"
@gridContainer={{gridContainer}}
>
{{#if (eq counters.httpsRequests.length 1)}}
<div class="is-paddingless is-marginless">
<div class="level-item">
<PopupMenu name="engine-menu">
<nav class="menu">
<ul class="menu-list">
<li class="action">
{{#link-to "vault.cluster.metrics.http-requests"}}
View Details
{{/link-to}}
</li>
</ul>
</nav>
</PopupMenu>
</div>
</div>
{{else}}
<div class="change-metric">
<Icon @glyph="arrow-up" @size="xlm" aria-hidden="true" class="change-metric-icon {{if (gt percentChange 0) "is-increase" "is-decrease"}}"/>
{{#if (gt percentChange 0)}}
<p class="has-text-grey is-size-4 amount-change">+{{percentChange}}%</p>
{{else}}
<p class="has-text-grey is-size-4 amount-change">{{percentChange}}%</p>
{{/if}}
<p class="has-text-grey is-size-6 item-c">Since last month</p>
</div>
<HttpRequestsBarChartSmall @counters={{filteredHttpsRequests}}/>
{{#link-to "vault.cluster.metrics.http-requests" class="card-details"}} View Details {{/link-to}}
{{/if}}
</SelectableCard>
{{/linked-block}}
<SelectableCard
@cardTitle="Entities"
@total={{counters.totalEntities}}
@subText="Total"
>
<div class="is-paddingless is-marginless">
<div class="level-item">
<PopupMenu name="engine-menu">
<nav class="menu">
<ul class="menu-list">
<li class="action">
{{#link-to "vault.cluster.access.identity" "entities" data-test-link=true }}
Manage entities
{{/link-to}}
</li>
<li class="action">
{{#link-to "vault.cluster.access.identity.create" "entities" }}
Create entity
{{/link-to}}
</li>
</ul>
</nav>
</PopupMenu>
</div>
</div>
</SelectableCard>
<SelectableCard
@cardTitle="Tokens"
@total={{counters.totalTokens}}
@subText="Total"
/>

View File

@ -0,0 +1,20 @@
{{!-- conditional to check if SelectableCard is apart of a CSS Grid, if yes return grid item class --}}
{{#if gridContainer}}
<div class="selectable-card is-rounded is-grid-container">
<div class="selectable-card-title">
<h2 class="title-number">{{format-number total}}</h2>
<h3 class="title is-5" data-test-selectable-card-title={{formattedCardTitle}}>{{formattedCardTitle}}</h3>
<p class="has-text-grey is-size-8">{{subText}}</p>
</div>
{{yield}}
</div>
{{else}}
<div class="selectable-card is-rounded">
<div>
<h2 class="title-number">{{format-number total}}</h2>
<h3 class="title is-5" data-test-selectable-card-title={{formattedCardTitle}}>{{formattedCardTitle}}</h3>
<p class="has-text-grey is-size-8">{{subText}}</p>
</div>
{{yield}}
</div>
{{/if}}

View File

@ -65,43 +65,9 @@
{{/if}}
{{/if}}
{{/unless}}
{{#if (and (or
(and version.features (has-permission 'status' routeParams='license'))
(and cluster.usingRaft (has-permission 'status' routeParams='raft'))
) (not cluster.dr.isSecondary))
}}
<nav class="menu">
<div class="menu-label">
Server
</div>
<ul class="menu-list">
{{#if (and version.features (has-permission 'status' routeParams='license') (not cluster.dr.isSecondary))}}
<li class="action">
{{#link-to "vault.cluster.license" activeCluster.name invokeAction=onLinkClick}}
<div class="level is-mobile">
<span class="level-left">License</span>
<Chevron class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
{{/if}}
{{#if (and cluster.usingRaft (has-permission 'status' routeParams='raft'))}}
<li class="action">
{{#link-to "vault.cluster.storage" activeCluster.name invokeAction=onLinkClick}}
<div class="level is-mobile">
<span class="level-left">Raft Storage</span>
<Chevron class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
{{/if}}
</ul>
</nav>
<hr/>
{{/if}}
<nav class="menu">
<div class="menu-label">
Seal status
Server
</div>
<ul class="menu-list">
<li class="action">
@ -133,24 +99,49 @@
{{/if}}
</li>
</ul>
</nav>
{{#if ( and (has-permission 'metrics' routeParams='requests') (not cluster.dr.isSecondary) auth.currentToken)}}
<hr />
<nav class="menu">
<div class="menu-label">
Metrics
</div>
{{#if (and (or
(and version.features (has-permission 'status' routeParams='license'))
(and cluster.usingRaft (has-permission 'status' routeParams='raft'))
)
(not cluster.dr.isSecondary))
}}
<ul class="menu-list">
{{#if (and version.features (has-permission 'status' routeParams='license') (not cluster.dr.isSecondary))}}
<li class="action">
{{#link-to "vault.cluster.license" activeCluster.name invokeAction=(action (queue (action onLinkClick) (action d.actions.close)))}}
<div class="level is-mobile">
<span class="level-left">License</span>
<Chevron class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
{{/if}}
{{#if (and cluster.usingRaft (has-permission 'status' routeParams='raft'))}}
<li class="action">
{{#link-to "vault.cluster.storage" activeCluster.name invokeAction=onLinkClick}}
<div class="level is-mobile">
<span class="level-left">Raft Storage</span>
<Chevron class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
{{/if}}
</ul>
{{/if}}
{{#if ( and (has-permission 'metrics' routeParams='dashboard') (not cluster.dr.isSecondary) auth.currentToken)}}
<ul class="menu-list">
<li class="action">
{{#link-to "vault.cluster.requests"}}
{{#link-to 'vault.cluster.metrics'
invokeAction=(action (queue (action onLinkClick) (action d.actions.close)))
}}
<div class="level is-mobile">
<span class="level-left">HTTP Requests</span>
<span class="level-left">Metrics</span>
<Chevron class="has-text-grey-light level-right" />
</div>
{{/link-to}}
</li>
</ul>
</nav>
{{/if}}
{{/if}}
</nav>
</div>
</div>

View File

@ -0,0 +1,21 @@
<PageHeader as |p|>
<p.top>
<nav class="key-value-header breadcrumb">
<ul>
<li>
<span class="sep">&#x0002f;</span>
{{#link-to "vault.cluster.metrics"}}
Metrics
{{/link-to}}
</li>
</ul>
</nav>
</p.top>
<p.levelLeft>
<h1 class="title is-3">
HTTP Request Volume
</h1>
</p.levelLeft>
</PageHeader>
<HttpRequestsContainer @counters={{model.counters}}/>

View File

@ -0,0 +1,15 @@
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3">
Metrics
</h1>
</p.levelLeft>
</PageHeader>
<div class="box is-sideless is-fullwidth is-marginless is-bottomless">
{{#if (gt model.httpsRequests.length 1) }}
<SelectableCardContainer @counters={{model}} @gridContainer="true"/>
{{else}}
<SelectableCardContainer @counters={{model}} />
{{/if}}
</div>

View File

@ -1,9 +0,0 @@
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3">
HTTP Request Volume
</h1>
</p.levelLeft>
</PageHeader>
<HttpRequestsContainer @counters={{model.counters}}/>

View 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';
const FILTERED_HTTPS_REQUESTS = [
{ start_time: '2018-11-01T00:00:00Z', total: 5500 },
{ start_time: '2018-12-01T00:00:00Z', total: 4500 },
{ start_time: '2019-01-01T00:00:00Z', total: 5000 },
{ start_time: '2019-02-01T00:00:00Z', total: 5000 },
];
module('Integration | Component | http-requests-bar-chart-small', function(hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function() {
this.set('filteredHttpsRequests', FILTERED_HTTPS_REQUESTS);
});
test('it renders and the correct number of bars are showing', async function(assert) {
await render(hbs`<HttpRequestsBarChartSmall @counters={{filteredHttpsRequests}}/>`);
assert.dom('rect').exists({ count: FILTERED_HTTPS_REQUESTS.length });
assert.dom('.http-requests-bar-chart-small').exists();
});
});

View File

@ -0,0 +1,73 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
const MODEL = {
totalEntities: 0,
httpsRequests: [{ start_time: '2019-04-01T00:00:00Z', total: 5500 }],
totalTokens: 1,
};
const MODEL_WITH_GRID = {
httpsRequests: [
{ start_time: '2018-11-01T00:00:00Z', total: 5500 },
{ start_time: '2018-12-01T00:00:00Z', total: 4500 },
{ start_time: '2019-01-01T00:00:00Z', total: 5000 },
{ start_time: '2019-02-01T00:00:00Z', total: 5000 },
{ start_time: '2019-03-01T00:00:00Z', total: 5000 },
{ start_time: '2019-04-01T00:00:00Z', total: 5500 },
{ start_time: '2019-05-01T00:00:00Z', total: 4500 },
{ start_time: '2019-06-01T00:00:00Z', total: 5000 },
{ start_time: '2019-07-01T00:00:00Z', total: 5000 },
{ start_time: '2019-08-01T00:00:00Z', total: 5000 },
{ start_time: '2019-09-01T00:00:00Z', total: 5000 },
{ start_time: '2019-10-01T00:00:00Z', total: 5000 },
{ start_time: '2019-11-01T00:00:00Z', total: 5000 },
{ start_time: '2019-12-01T00:00:00Z', total: 5000 },
{ start_time: '2020-01-01T00:00:00Z', total: 5000 },
{ start_time: '2020-02-01T00:00:00Z', total: 5000 },
],
totalEntities: 0,
totalTokens: 1,
};
module('Integration | Component | selectable-card-container', function(hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function() {
this.set('model', MODEL);
this.set('modelWithGrid', MODEL_WITH_GRID);
});
test('it renders', async function(assert) {
await render(hbs`<SelectableCardContainer @counters={{model}}/>`);
assert.dom('.selectable-card-container').exists();
});
test('it renders a card for each of the models and titles are returned', async function(assert) {
await render(hbs`<SelectableCardContainer @counters={{model}}/>`);
assert.dom('.selectable-card').exists({ count: 3 });
let cardTitles = ['Http Requests', 'Entities', 'Token'];
let httpRequestsTitle = this.element.querySelectorAll('[data-test-selectable-card-title]');
httpRequestsTitle.forEach(item => {
assert.notEqual(cardTitles.indexOf(item.innerText), -1);
});
});
test('it renders with more than one month of data', async function(assert) {
await render(hbs`<SelectableCardContainer @counters={{modelWithGrid}}/>`);
assert.dom('.selectable-card-container.has-grid').exists();
});
test('it renders 3 selectable cards when there is more than one month of data', async function(assert) {
await render(hbs`<SelectableCardContainer @counters={{modelWithGrid}}/>`);
assert.dom('.selectable-card').exists({ count: 3 });
});
test('it only renders a bar chart with the last 12 months of data', async function(assert) {
await render(hbs`<SelectableCardContainer @counters={{modelWithGrid}}/>`);
assert.dom('rect').exists({ count: 12 });
});
});

View File

@ -0,0 +1,30 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
const TOTAL = 15;
const CARD_TITLE = 'Tokens';
module('Integration | Component selectable-card', function(hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function() {
this.set('total', TOTAL);
this.set('cardTitle', CARD_TITLE);
});
test('it shows the card total', async function(assert) {
await render(hbs`<SelectableCard @total={{total}} @cardTitle={{cardTitle}}/>`);
let titleNumber = this.element.querySelector('.title-number').innerText;
assert.equal(titleNumber, 15);
});
test('it returns non-plural version of card title if total is 1, ', async function(assert) {
await render(hbs`<SelectableCard @total={{1}} @cardTitle={{cardTitle}}/>`);
let titleText = this.element.querySelector('.title').innerText;
assert.equal(titleText, 'Token');
});
});