Skip to content

Commit

Permalink
Merge pull request #4704 from hashicorp/f-ui-applied-stat-charts
Browse files Browse the repository at this point in the history
UI: Stat charts everywhere
  • Loading branch information
DingoEatingFuzz authored Sep 26, 2018
2 parents 5e2edf8 + 47ec74e commit f5eaffe
Show file tree
Hide file tree
Showing 35 changed files with 892 additions and 162 deletions.
14 changes: 14 additions & 0 deletions ui/app/components/freestyle/sg-line-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ export default Component.extend({
];
}),

lineChartGapData: computed(() => {
return [
{ year: 2010, value: 10 },
{ year: 2011, value: 10 },
{ year: 2012, value: null },
{ year: 2013, value: 30 },
{ year: 2014, value: 50 },
{ year: 2015, value: 80 },
{ year: 2016, value: null },
{ year: 2017, value: 210 },
{ year: 2018, value: 340 },
];
}),

lineChartLive: computed(() => {
return [];
}),
Expand Down
24 changes: 18 additions & 6 deletions ui/app/components/line-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,18 @@ export default Component.extend(WindowResizable, {
xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() {
const xProp = this.get('xProp');
const scale = this.get('timeseries') ? d3Scale.scaleTime() : d3Scale.scaleLinear();
const data = this.get('data');

scale
.rangeRound([10, this.get('yAxisOffset')])
.domain(d3Array.extent(this.get('data'), d => d[xProp]));
const domain = data.length ? d3Array.extent(this.get('data'), d => d[xProp]) : [0, 1];

scale.rangeRound([10, this.get('yAxisOffset')]).domain(domain);

return scale;
}),

yScale: computed('data.[]', 'yProp', 'xAxisOffset', function() {
const yProp = this.get('yProp');
let max = d3Array.max(this.get('data'), d => d[yProp]);
let max = d3Array.max(this.get('data'), d => d[yProp]) || 1;
if (max > 1) {
max = nice(max);
}
Expand Down Expand Up @@ -182,6 +183,7 @@ export default Component.extend(WindowResizable, {

const line = d3Shape
.line()
.defined(d => d[yProp] != null)
.x(d => xScale(d[xProp]))
.y(d => yScale(d[yProp]));

Expand All @@ -198,6 +200,7 @@ export default Component.extend(WindowResizable, {

const area = d3Shape
.area()
.defined(d => d[yProp] != null)
.x(d => xScale(d[xProp]))
.y0(yScale(0))
.y1(d => yScale(d[yProp]));
Expand Down Expand Up @@ -244,6 +247,8 @@ export default Component.extend(WindowResizable, {
'data'
);

if (!data || !data.length) return;

// Map the mouse coordinate to the index in the data array
const bisector = d3Array.bisector(d => d[xProp]).left;
const x = xScale.invert(mouseX);
Expand All @@ -253,8 +258,15 @@ export default Component.extend(WindowResizable, {
const dLeft = data[index - 1];
const dRight = data[index];

// Pick the closer point
const datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft;
let datum;

// If there is only one point, it's the activeDatum
if (dLeft && !dRight) {
datum = dLeft;
} else {
// Pick the closer point
datum = x - dLeft[xProp] > dRight[xProp] - x ? dRight : dLeft;
}

this.set('activeDatum', datum);
this.set('tooltipPosition', {
Expand Down
99 changes: 99 additions & 0 deletions ui/app/components/primary-metric.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Ember from 'ember';
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { task, timeout } from 'ember-concurrency';

export default Component.extend({
token: service(),
statsTrackersRegistry: service('stats-trackers-registry'),

classNames: ['primary-metric'],

// One of Node, Allocation, or TaskState
resource: null,

// cpu or memory
metric: null,

'data-test-primary-metric': true,

// An instance of a StatsTracker. An alternative interface to resource
tracker: computed('trackedResource', 'type', function() {
const resource = this.get('trackedResource');
return this.get('statsTrackersRegistry').getTracker(resource);
}),

type: computed('resource', function() {
const resource = this.get('resource');
return resource && resource.constructor.modelName;
}),

trackedResource: computed('resource', 'type', function() {
// TaskStates use the allocation stats tracker
return this.get('type') === 'task-state'
? this.get('resource.allocation')
: this.get('resource');
}),

metricLabel: computed('metric', function() {
const metric = this.get('metric');
const mappings = {
cpu: 'CPU',
memory: 'Memory',
};
return mappings[metric] || metric;
}),

data: computed('resource', 'metric', 'type', function() {
if (!this.get('tracker')) return [];

const metric = this.get('metric');
if (this.get('type') === 'task-state') {
// handle getting the right task out of the tracker
const task = this.get('tracker.tasks').findBy('task', this.get('resource.name'));
return task && task[metric];
}

return this.get(`tracker.${metric}`);
}),

reservedAmount: computed('resource', 'metric', 'type', function() {
const metricProperty = this.get('metric') === 'cpu' ? 'reservedCPU' : 'reservedMemory';

if (this.get('type') === 'task-state') {
const task = this.get('tracker.tasks').findBy('task', this.get('resource.name'));
return task[metricProperty];
}

return this.get(`tracker.${metricProperty}`);
}),

chartClass: computed('metric', function() {
const metric = this.get('metric');
const mappings = {
cpu: 'is-info',
memory: 'is-danger',
};

return mappings[metric] || 'is-primary';
}),

poller: task(function*() {
do {
this.get('tracker.poll').perform();
yield timeout(100);
} while (!Ember.testing);
}),

didReceiveAttrs() {
if (this.get('tracker')) {
this.get('poller').perform();
}
},

willDestroy() {
this.get('poller').cancelAll();
this.get('tracker.signalPause').perform();
},
});
9 changes: 6 additions & 3 deletions ui/app/components/stats-time-series.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import LineChart from 'nomad-ui/components/line-chart';

export default LineChart.extend({
xProp: 'timestamp',
yProp: 'value',
yProp: 'percent',
timeseries: true,

xFormat() {
Expand All @@ -22,12 +22,15 @@ export default LineChart.extend({
xScale: computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset', function() {
const xProp = this.get('xProp');
const scale = this.get('timeseries') ? d3Scale.scaleTime() : d3Scale.scaleLinear();
const data = this.get('data');

const [low, high] = d3Array.extent(this.get('data'), d => d[xProp]);
const [low, high] = d3Array.extent(data, d => d[xProp]);
const minLow = moment(high)
.subtract(5, 'minutes')
.toDate();
scale.rangeRound([10, this.get('yAxisOffset')]).domain([Math.min(low, minLow), high]);

const extent = data.length ? [Math.min(low, minLow), high] : [minLow, new Date()];
scale.rangeRound([10, this.get('yAxisOffset')]).domain(extent);

return scale;
}),
Expand Down
14 changes: 0 additions & 14 deletions ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import Ember from 'ember';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { task, timeout } from 'ember-concurrency';
import Sortable from 'nomad-ui/mixins/sortable';
import { lazyClick } from 'nomad-ui/helpers/lazy-click';
import { stats } from 'nomad-ui/utils/classes/allocation-stats-tracker';

export default Controller.extend(Sortable, {
token: service(),
Expand All @@ -21,17 +18,6 @@ export default Controller.extend(Sortable, {
listToSort: alias('model.states'),
sortedStates: alias('listSorted'),

stats: stats('model', function statsFetch() {
return url => this.get('token').authorizedRequest(url);
}),

pollStats: task(function*() {
do {
yield this.get('stats').poll();
yield timeout(1000);
} while (!Ember.testing);
}),

actions: {
gotoTask(allocation, task) {
this.transitionToRoute('allocations.allocation.task', task);
Expand Down
14 changes: 0 additions & 14 deletions ui/app/controllers/clients/client.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import Ember from 'ember';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import Sortable from 'nomad-ui/mixins/sortable';
import Searchable from 'nomad-ui/mixins/searchable';
import { stats } from 'nomad-ui/utils/classes/node-stats-tracker';

export default Controller.extend(Sortable, Searchable, {
queryParams: {
Expand Down Expand Up @@ -37,17 +34,6 @@ export default Controller.extend(Sortable, Searchable, {
return this.get('model.drivers').sortBy('name');
}),

stats: stats('model', function statsFetch() {
return url => this.get('token').authorizedRequest(url);
}),

pollStats: task(function*() {
do {
yield this.get('stats').poll();
yield timeout(1000);
} while (!Ember.testing);
}),

actions: {
gotoAllocation(allocation) {
this.transitionToRoute('allocations.allocation', allocation);
Expand Down
12 changes: 0 additions & 12 deletions ui/app/routes/allocations/allocation/index.js

This file was deleted.

11 changes: 0 additions & 11 deletions ui/app/routes/clients/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,4 @@ export default Route.extend(WithWatchers, {
watchAllocations: watchRelationship('allocations'),

watchers: collect('watch', 'watchAllocations'),

setupController(controller, model) {
this._super(...arguments);
if (model) {
controller.get('pollStats').perform();
}
},

resetController(controller) {
controller.get('pollStats').cancelAll();
},
});
49 changes: 49 additions & 0 deletions ui/app/services/stats-trackers-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { computed } from '@ember/object';
import Service, { inject as service } from '@ember/service';
import { LRUMap } from 'lru_map';
import NodeStatsTracker from 'nomad-ui/utils/classes/node-stats-tracker';
import AllocationStatsTracker from 'nomad-ui/utils/classes/allocation-stats-tracker';

// An unbounded number of stat trackers is a great way to gobble up all the memory
// on a machine. This max number is unscientific, but aims to balance losing
// stat trackers a user is likely to return to with preventing gc from freeing
// memory occupied by stat trackers a user is likely to no longer care about
const MAX_STAT_TRACKERS = 10;
let registry;

export default Service.extend({
token: service(),

init() {
// The LRUMap limits the number of trackers tracked by making room for
// new entries beyond the limit by removing the least recently used entry.
registry = new LRUMap(MAX_STAT_TRACKERS);
},

// A read-only way of getting a reference to the registry.
// Since this could be overwritten by a bad actor, it isn't
// used in getTracker
registryRef: computed(() => registry),

getTracker(resource) {
if (!resource) return;

const type = resource && resource.constructor.modelName;
const key = `${type}:${resource.get('id')}`;

const cachedTracker = registry.get(key);
if (cachedTracker) return cachedTracker;

const Constructor = type === 'node' ? NodeStatsTracker : AllocationStatsTracker;
const resourceProp = type === 'node' ? 'node' : 'allocation';

const tracker = Constructor.create({
fetch: url => this.get('token').authorizedRequest(url),
[resourceProp]: resource,
});

registry.set(key, tracker);

return tracker;
},
});
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@import './components/node-status-light';
@import './components/nomad-logo';
@import './components/page-layout';
@import './components/primary-metric';
@import './components/simple-list';
@import './components/status-text';
@import './components/timeline';
Expand Down
1 change: 1 addition & 0 deletions ui/app/styles/components/boxed-section.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

& + .boxed-section-body {
border-top: none;
padding-top: 0.75em;
}
}

Expand Down
30 changes: 30 additions & 0 deletions ui/app/styles/components/primary-metric.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.primary-metric {
background: $white-bis;
border-radius: $radius;
padding: 0.75em;
color: $grey-dark;

.title {
color: $grey;
font-weight: $weight-normal;
}

.primary-graphic {
height: 150px;
}

.secondary-graphic {
padding: 0.75em;
padding-bottom: 0;
margin-bottom: 0;

> .column {
padding: 0.5rem 0.75rem;
}
}

.annotation {
padding: 0 0.75em;
margin-top: -0.75rem;
}
}
Loading

0 comments on commit f5eaffe

Please sign in to comment.