Add HTTP Request Volume page (#6925)
* Add http request volume table (#6765) * init http metrics page * remove flex-table-column * add http requests table * calculate percent change between each counter * start percent change tests * style request table * show percent more/less glyph * add percent more less tests * add inline alert about recorded metrics * make arrows diagonal * remove conditional inside countersWithChange * add better error msg * use tagName and wrapping element a la glimmer components * extend ClusterRouteBase so auth and seal checks happen * make table accessible * remove curlies * add HttpRequestsTable to storybook * make table accessible * use qunit dom for better assertions * remove EmptyState since we will never have 0 requests * ensure counters is set in test context * Http request volume/add barchart (#6814) * Add http request volume table (#6765) * init http metrics page * remove flex-table-column * add http requests table * calculate percent change between each counter * start percent change tests * style request table * show percent more/less glyph * add percent more less tests * add inline alert about recorded metrics * make arrows diagonal * remove conditional inside countersWithChange * add better error msg * use tagName and wrapping element a la glimmer components * extend ClusterRouteBase so auth and seal checks happen * make table accessible * remove curlies * add HttpRequestsTable to storybook * make table accessible * use qunit dom for better assertions * remove EmptyState since we will never have 0 requests * ensure counters is set in test context * add http-requests-bar-chart * add HttpRequestsBarChart tests * add HttpRequestsBarChart to Storybook * format total number of requests according to locale * do not show extra minus sign when percent change is negative * add link to request metrics in status bar menu * only show bar chart if we have data for more than 1 month * make ticks lighter * ensure charts show data for correct month * make example counters response look like the adapter response instead of the raw api response * ensure ui shows the same utc date as the api response * add format-utc tests * downgrade to d3 v4 to support ie11 * add gridlines * move dasharray to css * use scheduleOnce instead of debounce to prevent multiple re-renders * add key function to bars * add exit case when data is no longer in parsedCounters * fix timestamp in table test * fix timestamps * use utcParse and fallback to isoParse for non-UTC dates * fix bar chart tests
This commit is contained in:
parent
5309609758
commit
4fd783d3f4
|
@ -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/requests';
|
||||
},
|
||||
});
|
|
@ -1,7 +0,0 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: 'column',
|
||||
header: null,
|
||||
content: null,
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
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, debounce } from '@ember/runloop';
|
||||
import { task, waitForEvent } from 'ember-concurrency';
|
||||
|
||||
/**
|
||||
* @module HttpRequestsBarChart
|
||||
* HttpRequestsBarChart components are used to render a bar chart with the total number of HTTP Requests to a Vault server per month.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <HttpRequestsBarChart @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` endpoint which looks like:
|
||||
* COUNTERS = [
|
||||
* {
|
||||
* "start_time": "2019-05-01T00:00:00Z",
|
||||
* "total": 50
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
|
||||
const HEIGHT = 240;
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['http-requests-bar-chart-container'],
|
||||
counters: null,
|
||||
margin: { 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 and ticks display properly
|
||||
const { counters } = this;
|
||||
return counters.map(counter => {
|
||||
return assign({}, counter, { start_time: d3TimeFormat.isoParse(counter.start_time) });
|
||||
});
|
||||
}),
|
||||
|
||||
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();
|
||||
});
|
||||
},
|
||||
|
||||
renderBarChart() {
|
||||
const { margin, width, xScale, yScale, parsedCounters } = this;
|
||||
const height = this.height();
|
||||
const barChartSVG = d3.select('.http-requests-bar-chart');
|
||||
const barsContainer = d3.select('#bars-container');
|
||||
|
||||
// render the chart
|
||||
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}`);
|
||||
|
||||
// scale and render the axes
|
||||
const yAxis = d3Axis
|
||||
.axisRight(yScale)
|
||||
.ticks(3, '.0s')
|
||||
.tickSizeOuter(0);
|
||||
const xAxis = d3Axis
|
||||
.axisBottom(xScale)
|
||||
.tickFormat(d3TimeFormat.utcFormat('%b %Y'))
|
||||
.tickSizeOuter(0);
|
||||
|
||||
barChartSVG
|
||||
.select('g.x-axis')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(xAxis);
|
||||
|
||||
barChartSVG
|
||||
.select('g.y-axis')
|
||||
.attr('transform', `translate(${width - margin.left - margin.right}, 0)`)
|
||||
.call(yAxis);
|
||||
|
||||
// render the gridlines
|
||||
const gridlines = d3Axis
|
||||
.axisRight(yScale)
|
||||
.ticks(3)
|
||||
.tickFormat('')
|
||||
.tickSize(width - margin.left - margin.right);
|
||||
|
||||
barChartSVG.select('.gridlines').call(gridlines);
|
||||
|
||||
// render the bars
|
||||
const bars = barsContainer.selectAll('.bar').data(parsedCounters, c => +c.start_time);
|
||||
|
||||
bars.exit().remove();
|
||||
|
||||
const barsEnter = bars
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('class', 'bar');
|
||||
|
||||
bars
|
||||
.merge(barsEnter)
|
||||
.attr('width', xScale.bandwidth())
|
||||
.attr('height', counter => height - yScale(counter.total))
|
||||
// the offset between each bar
|
||||
.attr('x', counter => xScale(counter.start_time))
|
||||
.attr('y', counter => yScale(counter.total));
|
||||
},
|
||||
|
||||
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(),
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { assign } from '@ember/polyfills';
|
||||
|
||||
/**
|
||||
* @module HttpRequestsTable
|
||||
* `HttpRequestsTable` components render a table with the total number of HTTP Requests to a Vault server per month.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <HttpRequestsTable @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` endpoint which looks like:
|
||||
* COUNTERS = [
|
||||
* {
|
||||
* "start_time": "2019-05-01T00:00:00Z",
|
||||
* "total": 50
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
counters: null,
|
||||
countersWithChange: computed('counters', function() {
|
||||
let counters = this.counters || [];
|
||||
let countersWithPercentChange = [];
|
||||
let previousMonthVal;
|
||||
|
||||
counters.forEach(month => {
|
||||
if (previousMonthVal) {
|
||||
let percentChange = (((month.total - previousMonthVal) / month.total) * 100).toFixed(1);
|
||||
let glyph;
|
||||
if (percentChange > 0) {
|
||||
glyph = 'arrow-up';
|
||||
} else if (percentChange < 0) {
|
||||
glyph = 'arrow-down';
|
||||
}
|
||||
percentChange = Math.abs(percentChange);
|
||||
const newCounter = assign({ percentChange, glyph }, month);
|
||||
countersWithPercentChange.push(newCounter);
|
||||
} else {
|
||||
// we're looking at the first counter in the list, so there is no % change value.
|
||||
countersWithPercentChange.push(month);
|
||||
previousMonthVal = month.total;
|
||||
}
|
||||
});
|
||||
return countersWithPercentChange;
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function formatNumber([number]) {
|
||||
// formats a number according to the locale
|
||||
return new Intl.NumberFormat().format(number);
|
||||
}
|
||||
|
||||
export default helper(formatNumber);
|
|
@ -0,0 +1,15 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import d3 from 'd3-time-format';
|
||||
|
||||
export function formatUtc([date, specifier]) {
|
||||
// given a date, format and display it as UTC.
|
||||
const format = d3.utcFormat(specifier);
|
||||
const parse = d3.utcParse('%Y-%m-%dT%H:%M:%SZ');
|
||||
|
||||
// if a date isn't already in UTC, fallback to isoParse to convert it to UTC
|
||||
const parsedDate = parse(date) || d3.isoParse(date);
|
||||
|
||||
return format(parsedDate);
|
||||
}
|
||||
|
||||
export default helper(formatUtc);
|
|
@ -0,0 +1,32 @@
|
|||
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'),
|
||||
});
|
|
@ -14,6 +14,7 @@ Router.map(function() {
|
|||
this.route('init');
|
||||
this.route('logout');
|
||||
this.route('license');
|
||||
this.route('requests', { path: '/metrics/requests' });
|
||||
this.route('settings', function() {
|
||||
this.route('index', { path: '/' });
|
||||
this.route('seal');
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import ClusterRouteBase from './cluster-route-base';
|
||||
|
||||
export default ClusterRouteBase.extend({
|
||||
model() {
|
||||
return this.store.queryRecord('requests', {});
|
||||
},
|
||||
});
|
|
@ -28,6 +28,9 @@ const API_PATHS = {
|
|||
license: 'sys/license',
|
||||
seal: 'sys/seal',
|
||||
},
|
||||
metrics: {
|
||||
requests: 'sys/internal/counters/requests',
|
||||
},
|
||||
};
|
||||
|
||||
const API_PATHS_TO_ROUTE_PARAMS = {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
.http-requests-bar-chart-container {
|
||||
margin-top: $spacing-s;
|
||||
margin-bottom: $spacing-m;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.http-requests-bar-chart {
|
||||
margin: auto;
|
||||
overflow: inherit;
|
||||
|
||||
.tick {
|
||||
line {
|
||||
stroke: $light-grey;
|
||||
}
|
||||
|
||||
text {
|
||||
fill: $grey;
|
||||
font-size: $size-8;
|
||||
}
|
||||
}
|
||||
|
||||
.gridlines {
|
||||
.domain {
|
||||
stroke: unset;
|
||||
}
|
||||
|
||||
line {
|
||||
stroke-dasharray: 5 5;
|
||||
}
|
||||
}
|
||||
|
||||
.x-axis,
|
||||
.y-axis {
|
||||
.domain,
|
||||
line {
|
||||
stroke: $grey-light;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
.http-requests-table {
|
||||
& .is-collapsed {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
& th,
|
||||
td {
|
||||
padding: $spacing-s;
|
||||
}
|
||||
|
||||
& th {
|
||||
color: $grey-dark;
|
||||
font-weight: 500;
|
||||
font-size: $size-8;
|
||||
}
|
||||
|
||||
& tbody th {
|
||||
font-size: $size-7;
|
||||
}
|
||||
|
||||
& tr {
|
||||
border-bottom: 1px solid $grey-light;
|
||||
}
|
||||
|
||||
& td {
|
||||
color: $grey-darkest;
|
||||
}
|
||||
|
||||
& .percent-change {
|
||||
font-weight: 500;
|
||||
font-size: $size-7;
|
||||
}
|
||||
|
||||
& .arrow-up {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
& .arrow-down {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
|
@ -55,6 +55,8 @@
|
|||
@import './components/form-section';
|
||||
@import './components/global-flash';
|
||||
@import './components/hover-copy-button';
|
||||
@import './components/http-requests-table';
|
||||
@import './components/http-requests-bar-chart';
|
||||
@import './components/init-illustration';
|
||||
@import './components/info-table-row';
|
||||
@import './components/input-hint';
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<div class="thead">
|
||||
<div class="th">
|
||||
{{header}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column">
|
||||
<div class="td">
|
||||
<code>
|
||||
{{content}}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,18 @@
|
|||
{{! template-lint-disable no-inline-styles}}
|
||||
|
||||
<svg class="http-requests-bar-chart">
|
||||
<defs>
|
||||
<linearGradient id="bg-gradient" gradientTransform="rotate(90)">
|
||||
<stop stop-color="#5b92ff" stop-opacity="1" offset="0%"></stop>
|
||||
<stop stop-color="#5b92ff" stop-opacity="0.5" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g class="gridlines"></g>
|
||||
<g class="x-axis"></g>
|
||||
<g class="y-axis"></g>
|
||||
<g clip-path="url(#bars-container)">
|
||||
<rect x="0" y="0" width="100%" height="100%" style="fill: url(#bg-gradient)"></rect>
|
||||
<clipPath id="bars-container">
|
||||
</clipPath>
|
||||
</g>
|
||||
</svg>
|
|
@ -0,0 +1,32 @@
|
|||
<div class="http-requests-table">
|
||||
<table class="is-fullwidth">
|
||||
<caption class="is-collapsed">HTTP Request Volume</caption>
|
||||
<thead class="has-text-weight-semibold">
|
||||
<tr>
|
||||
<th scope="col">Month</th>
|
||||
<th scope="col">Requests</th>
|
||||
{{#if (gt counters.length 1)}}
|
||||
<th scope="col">Change</th>
|
||||
{{/if}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each (reverse countersWithChange) as |c|}}
|
||||
<tr>
|
||||
<th scope="row" class="has-text-black">{{ format-utc c.start_time '%B %Y' }}</th>
|
||||
<td>{{format-number c.total}}</td>
|
||||
{{#if (gt counters.length 1)}}
|
||||
<td class="has-text-grey-dark percent-change" data-test-change="{{c.percentChange}}">
|
||||
{{#if c.percentChange}}
|
||||
<Icon @glyph={{c.glyph}} class={{c.glyph}}/>
|
||||
{{c.percentChange}}%
|
||||
{{else}}
|
||||
<Icon @glyph="minus-plain" />
|
||||
{{/if}}
|
||||
</td>
|
||||
{{/if}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -116,5 +116,25 @@
|
|||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{#if (has-permission 'metrics' routeParams='requests')}}
|
||||
<hr />
|
||||
<nav class="menu">
|
||||
<div class="menu-label">
|
||||
Metrics
|
||||
</div>
|
||||
<ul class="menu-list">
|
||||
<li class="action">
|
||||
{{#if activeCluster.unsealed}}
|
||||
{{#link-to "vault.cluster.requests"}}
|
||||
<div class="level is-mobile">
|
||||
<span class="level-left">HTTP Requests</span>
|
||||
<Chevron class="has-text-grey-light level-right" />
|
||||
</div>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
HTTP Request Volume
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (gt model.counters.length 1) }}
|
||||
<HttpRequestsBarChart @counters={{model.counters}}/>
|
||||
{{/if}}
|
||||
<AlertInline @type="info" @message="Metrics are recorded only for interactions that produce or use a Vault token." />
|
||||
<HttpRequestsTable @counters={{model.counters}} />
|
|
@ -54,6 +54,10 @@
|
|||
"codemirror": "5.15.2",
|
||||
"columnify": "^1.5.4",
|
||||
"cool-checkboxes-for-bulma.io": "^1.1.0",
|
||||
"d3-axis": "^1.0.8",
|
||||
"d3-scale": "^1.0.7",
|
||||
"d3-selection": "^1.3.0",
|
||||
"d3-time-format": "^2.1.1",
|
||||
"date-fns": "^1.29.0",
|
||||
"deepmerge": "^2.1.1",
|
||||
"doctoc": "^1.4.0",
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/http-requests-bar-chart.js. To make changes, first edit that file and run "yarn gen-story-md http-requests-bar-chart" to re-generate the content.-->
|
||||
|
||||
## HttpRequestsBarChart
|
||||
`HttpRequestsBarChart` components are used to render a bar chart with the total number of HTTP Requests to a Vault server per month.
|
||||
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| counters | <code>Array</code> | <code></code> | A list of objects containing the total number of HTTP Requests for each month. `counters` should be the response from the `/internal/counters/requests` endpoint. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
<HttpRequestsBarChart @counters={{counters}} />
|
||||
```
|
||||
|
||||
**See**
|
||||
|
||||
- [Uses of HttpRequestsBarChart](https://github.com/hashicorp/vault/search?l=Handlebars&q=HttpRequestsBarChart+OR+http-requests-bar-chart)
|
||||
- [HttpRequestsBarChart Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/http-requests-bar-chart.js)
|
||||
|
||||
---
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable import/extensions */
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { storiesOf } from '@storybook/ember';
|
||||
import { withKnobs, object } from '@storybook/addon-knobs';
|
||||
import notes from './http-requests-bar-chart.md';
|
||||
|
||||
const COUNTERS = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
storiesOf('HttpRequests/BarChart/', module)
|
||||
.addParameters({ options: { showPanel: true } })
|
||||
.addDecorator(
|
||||
withKnobs()
|
||||
)
|
||||
.add(`HttpRequestsBarChart`, () => ({
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Http Requests Bar Chart</h5>
|
||||
<HttpRequestsBarChart @counters={{counters}}/>
|
||||
`,
|
||||
context: {
|
||||
counters: object('counters', COUNTERS)
|
||||
},
|
||||
}),
|
||||
{notes}
|
||||
);
|
|
@ -0,0 +1,22 @@
|
|||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in app/components/http-requests-table.js. To make changes, first edit that file and run "yarn gen-story-md http-requests-table" to re-generate the content.-->
|
||||
|
||||
## HttpRequestsTable
|
||||
`HttpRequestsTable` components render a table with the total number of HTTP Requests to a Vault server per month.
|
||||
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| counters | <code>Array</code> | <code></code> | A list of objects containing the total number of HTTP Requests for each month. `counters` should be the response from the `/internal/counters/requests` endpoint. |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
<HttpRequestsTable @counters={{counters}} />
|
||||
```
|
||||
|
||||
**See**
|
||||
|
||||
- [Uses of HttpRequestsTable](https://github.com/hashicorp/vault/search?l=Handlebars&q=HttpRequestsTable+OR+http-requests-table)
|
||||
- [HttpRequestsTable Source Code](https://github.com/hashicorp/vault/blob/master/ui/app/components/http-requests-table.js)
|
||||
|
||||
---
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable import/extensions */
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { storiesOf } from '@storybook/ember';
|
||||
import { withKnobs, object } from '@storybook/addon-knobs';
|
||||
import notes from './http-requests-table.md';
|
||||
|
||||
const COUNTERS = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
storiesOf('HttpRequests/Table/', module)
|
||||
.addParameters({ options: { showPanel: true } })
|
||||
.addDecorator(
|
||||
withKnobs()
|
||||
)
|
||||
.add(`HttpRequestsTable`, () => ({
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Http Requests Table</h5>
|
||||
<HttpRequestsTable @counters={{counters}}/>
|
||||
`,
|
||||
context: {
|
||||
counters: object('counters', COUNTERS),
|
||||
}
|
||||
}),
|
||||
{ notes }
|
||||
);
|
|
@ -0,0 +1,48 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const COUNTERS = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
module('Integration | Component | http-requests-bar-chart', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
this.set('counters', COUNTERS);
|
||||
});
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
await render(hbs`<HttpRequestsBarChart @counters={{counters}}/>`);
|
||||
|
||||
assert.dom('.http-requests-bar-chart').exists();
|
||||
});
|
||||
|
||||
test('it renders the correct number of bars, ticks, and gridlines', async function(assert) {
|
||||
await render(hbs`<HttpRequestsBarChart @counters={{counters}}/>`);
|
||||
|
||||
assert.equal(this.element.querySelectorAll('.bar').length, 3);
|
||||
assert.equal(this.element.querySelectorAll('.tick').length, 9), 'it renders the ticks and gridlines';
|
||||
});
|
||||
|
||||
test('it formats the ticks', async function(assert) {
|
||||
await render(hbs`<HttpRequestsBarChart @counters={{counters}}/>`);
|
||||
|
||||
debugger;
|
||||
|
||||
assert.equal(
|
||||
this.element.querySelector('.x-axis>.tick').textContent,
|
||||
'Apr 2019',
|
||||
'x axis ticks should should show the month and year'
|
||||
);
|
||||
assert.equal(
|
||||
this.element.querySelectorAll('.y-axis>.tick')[1].textContent,
|
||||
'2k',
|
||||
'y axis ticks should round to the nearest thousand'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
const COUNTERS = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
module('Integration | Component | http-requests-table', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
this.set('counters', COUNTERS);
|
||||
});
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
await render(hbs`<HttpRequestsTable @counters={{counters}}/>`);
|
||||
|
||||
assert.dom('.http-requests-table').exists();
|
||||
});
|
||||
|
||||
test('it does not show Change column with less than one month of data', async function(assert) {
|
||||
const one_month_counter = [
|
||||
{
|
||||
start_time: '2019-05-01T00:00:00Z',
|
||||
total: 50,
|
||||
},
|
||||
];
|
||||
this.set('one_month_counter', one_month_counter);
|
||||
|
||||
await render(hbs`<HttpRequestsTable @counters={{one_month_counter}}/>`);
|
||||
|
||||
assert.dom('.http-requests-table').exists();
|
||||
assert.dom('[data-test-change]').doesNotExist();
|
||||
});
|
||||
|
||||
test('it shows Change column for more than one month of data', async function(assert) {
|
||||
await render(hbs`<HttpRequestsTable @counters={{counters}}/>`);
|
||||
|
||||
assert.dom('[data-test-change]').exists();
|
||||
});
|
||||
|
||||
test('it shows the percent change between each time window', async function(assert) {
|
||||
await render(hbs`<HttpRequestsTable @counters={{counters}}/>`);
|
||||
const expected = Math.abs(
|
||||
(((COUNTERS[1].total - COUNTERS[0].total) / COUNTERS[1].total) * 100).toFixed(1)
|
||||
);
|
||||
|
||||
assert.ok(this.element.textContent.includes(expected));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { formatUtc } from '../../../helpers/format-utc';
|
||||
|
||||
module('Integration | Helper | format-utc', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it formats a UTC date string and maintains the timezone', function(assert) {
|
||||
let expected = 'Apr 01 2019, 00:00';
|
||||
let dateTime = '2019-04-01T00:00:00Z';
|
||||
let result = formatUtc([dateTime, '%b %d %Y, %H:%M']);
|
||||
assert.equal(result, expected, 'it displays the date in UTC');
|
||||
});
|
||||
});
|
62
ui/yarn.lock
62
ui/yarn.lock
|
@ -6401,6 +6401,68 @@ cyclist@~0.2.2:
|
|||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
|
||||
|
||||
d3-array@^1.2.0:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
||||
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
|
||||
|
||||
d3-axis@^1.0.8:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
|
||||
integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
|
||||
|
||||
d3-collection@1:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
|
||||
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
|
||||
|
||||
d3-color@1:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.3.tgz#6c67bb2af6df3cc8d79efcc4d3a3e83e28c8048f"
|
||||
integrity sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw==
|
||||
|
||||
d3-format@1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562"
|
||||
integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==
|
||||
|
||||
d3-interpolate@1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.3.2.tgz#417d3ebdeb4bc4efcc8fd4361c55e4040211fd68"
|
||||
integrity sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==
|
||||
dependencies:
|
||||
d3-color "1"
|
||||
|
||||
d3-scale@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
|
||||
integrity sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==
|
||||
dependencies:
|
||||
d3-array "^1.2.0"
|
||||
d3-collection "1"
|
||||
d3-color "1"
|
||||
d3-format "1"
|
||||
d3-interpolate "1"
|
||||
d3-time "1"
|
||||
d3-time-format "2"
|
||||
|
||||
d3-selection@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.0.tgz#ab9ac1e664cf967ebf1b479cc07e28ce9908c474"
|
||||
integrity sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg==
|
||||
|
||||
d3-time-format@2, d3-time-format@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b"
|
||||
integrity sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==
|
||||
dependencies:
|
||||
d3-time "1"
|
||||
|
||||
d3-time@1:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.11.tgz#1d831a3e25cd189eb256c17770a666368762bbce"
|
||||
integrity sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw==
|
||||
|
||||
dag-map@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68"
|
||||
|
|
Loading…
Reference in New Issue