Skip to content

Commit

Permalink
Core Usage Metrics (#8347)
Browse files Browse the repository at this point in the history
* 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 e77cdec.

* 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 e77cdec.

* 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 e77cdec.

* 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
  • Loading branch information
Monkeychip committed Feb 13, 2020
1 parent 0937a58 commit 2a52c1a
Show file tree
Hide file tree
Showing 31 changed files with 803 additions and 64 deletions.
14 changes: 14 additions & 0 deletions ui/app/adapters/metrics/entity.js
Original file line number Diff line number Diff line change
@@ -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';
},
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Application from './application';
import Application from '../application';

export default Application.extend({
queryRecord() {
Expand Down
14 changes: 14 additions & 0 deletions ui/app/adapters/metrics/token.js
Original file line number Diff line number Diff line change
@@ -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';
},
});
153 changes: 153 additions & 0 deletions ui/app/components/http-requests-bar-chart-small.js
Original file line number Diff line number Diff line change
@@ -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(),
});
54 changes: 54 additions & 0 deletions ui/app/components/selectable-card-container.js
Original file line number Diff line number Diff line change
@@ -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;
}),
});
35 changes: 35 additions & 0 deletions ui/app/components/selectable-card.js
Original file line number Diff line number Diff line change
@@ -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;
}),
});
27 changes: 27 additions & 0 deletions ui/app/models/metrics/entity.js
Original file line number Diff line number Diff line change
@@ -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'),
});
File renamed without changes.
27 changes: 27 additions & 0 deletions ui/app/models/metrics/token.js
Original file line number Diff line number Diff line change
@@ -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'),
});
5 changes: 4 additions & 1 deletion ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
8 changes: 8 additions & 0 deletions ui/app/routes/vault/cluster/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';

export default Route.extend(ClusterRoute, {
model() {
return {};
},
});
7 changes: 7 additions & 0 deletions ui/app/routes/vault/cluster/metrics/http-requests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ClusterRouteBase from '../cluster-route-base';

export default ClusterRouteBase.extend({
model() {
return this.store.queryRecord('metrics/http-requests', {});
},
});
26 changes: 26 additions & 0 deletions ui/app/routes/vault/cluster/metrics/index.js
Original file line number Diff line number Diff line change
@@ -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,
});
},
});
Loading

0 comments on commit 2a52c1a

Please sign in to comment.