4fd783d3f4
* 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
162 lines
4.7 KiB
JavaScript
162 lines
4.7 KiB
JavaScript
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(),
|
|
});
|