Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Task group scaling timeline #8593

Merged
merged 19 commits into from
Aug 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7360adb
Make the default time series date format for line chart more useful
DingoEatingFuzz Jul 31, 2020
9465fc6
Add annotations to the line chart component
DingoEatingFuzz Jul 31, 2020
51a5209
Story for line chart annotations
DingoEatingFuzz Jul 31, 2020
f04b646
Test coverage for line chart annotations
DingoEatingFuzz Aug 4, 2020
2868d3e
Add curve options to line chart
DingoEatingFuzz Aug 4, 2020
4936c3f
Stagger line chart annotations when they are too close
DingoEatingFuzz Aug 4, 2020
c431218
Add activeAnnotation property to line-chart
DingoEatingFuzz Aug 5, 2020
e343f23
New ScaleEventsChart component
DingoEatingFuzz Aug 5, 2020
2a88541
Force mock error scale events to be annotations
DingoEatingFuzz Aug 5, 2020
8f61830
Integration tests for the ScaleEventsChart component
DingoEatingFuzz Aug 5, 2020
f9cb2ff
Unit test coverage for the ScaleEventsChart data domain logic
DingoEatingFuzz Aug 5, 2020
eb7a4f2
Conditionally show the scaling timeline or accordion
DingoEatingFuzz Aug 5, 2020
fe115db
Safestr the annotation style property
DingoEatingFuzz Aug 5, 2020
73e7322
Use the correct gray for the info details
DingoEatingFuzz Aug 5, 2020
208eb0d
Compare scale events by their UID instead of reference equality
DingoEatingFuzz Aug 5, 2020
339bccb
Add missing word "two" to test name
DingoEatingFuzz Aug 6, 2020
4040196
Add integration test for line-chart annotation staggering
DingoEatingFuzz Aug 7, 2020
7dde9ab
Key the annotations each loop by annotationKey for stable dom nodes
DingoEatingFuzz Aug 7, 2020
950c2bd
Make eq-by helper resilient to a lack of prop since handlebars doesn'…
DingoEatingFuzz Aug 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions ui/app/components/line-chart.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable ember/no-observers */
import Component from '@ember/component';
import { computed } from '@ember/object';
import { assert } from '@ember/debug';
import { observes } from '@ember-decorators/object';
import { computed as overridable } from 'ember-overridable-computed';
import { guidFor } from '@ember/object/internals';
Expand All @@ -14,7 +15,7 @@ import d3Format from 'd3-format';
import d3TimeFormat from 'd3-time-format';
import WindowResizable from 'nomad-ui/mixins/window-resizable';
import styleStringProperty from 'nomad-ui/utils/properties/style-string';
import { classNames } from '@ember-decorators/component';
import { classNames, classNameBindings } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';

// Returns a new array with the specified number of points linearly
Expand All @@ -31,14 +32,29 @@ const lerp = ([low, high], numPoints) => {
// Round a number or an array of numbers
const nice = val => (val instanceof Array ? val.map(nice) : Math.round(val));

const iconFor = {
error: 'cancel-circle-fill',
info: 'info-circle-fill',
};

const iconClassFor = {
error: 'is-danger',
info: '',
};

@classic
@classNames('chart', 'line-chart')
@classNameBindings('annotations.length:with-annotations')
export default class LineChart extends Component.extend(WindowResizable) {
// Public API

data = null;
annotations = null;
activeAnnotation = null;
onAnnotationClick() {}
xProp = null;
yProp = null;
curve = 'linear';
timeseries = false;
chartClass = 'is-primary';

Expand Down Expand Up @@ -88,9 +104,19 @@ export default class LineChart extends Component.extend(WindowResizable) {
return this.yFormat()(y);
}

@computed('curve')
get curveMethod() {
const mappings = {
linear: 'curveLinear',
stepAfter: 'curveStepAfter',
};
assert(`Provided curve "${this.curve}" is not an allowed curve type`, mappings[this.curve]);
return mappings[this.curve];
}

// Overridable functions that retrurn formatter functions
xFormat(timeseries) {
return timeseries ? d3TimeFormat.timeFormat('%b') : d3Format.format(',');
return timeseries ? d3TimeFormat.timeFormat('%b %d, %H:%M') : d3Format.format(',');
}

yFormat() {
Expand All @@ -100,6 +126,14 @@ export default class LineChart extends Component.extend(WindowResizable) {
tooltipPosition = null;
@styleStringProperty('tooltipPosition') tooltipStyle;

@computed('xAxisOffset')
get chartAnnotationBounds() {
return {
height: this.xAxisOffset,
};
}
@styleStringProperty('chartAnnotationBounds') chartAnnotationsStyle;

@computed('data.[]', 'xProp', 'timeseries', 'yAxisOffset')
get xScale() {
const xProp = this.xProp;
Expand Down Expand Up @@ -217,25 +251,27 @@ export default class LineChart extends Component.extend(WindowResizable) {
return this.width - this.yAxisWidth;
}

@computed('data.[]', 'xScale', 'yScale')
@computed('data.[]', 'xScale', 'yScale', 'curveMethod')
get line() {
const { xScale, yScale, xProp, yProp } = this;
const { xScale, yScale, xProp, yProp, curveMethod } = this;

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

return line(this.data);
}

@computed('data.[]', 'xScale', 'yScale')
@computed('data.[]', 'xScale', 'yScale', 'curveMethod')
get area() {
const { xScale, yScale, xProp, yProp } = this;
const { xScale, yScale, xProp, yProp, curveMethod } = this;

const area = d3Shape
.area()
.curve(d3Shape[curveMethod])
.defined(d => d[yProp] != null)
.x(d => xScale(d[xProp]))
.y0(yScale(0))
Expand All @@ -244,6 +280,41 @@ export default class LineChart extends Component.extend(WindowResizable) {
return area(this.data);
}

@computed('annotations.[]', 'xScale', 'xProp', 'timeseries')
get processedAnnotations() {
const { xScale, xProp, annotations, timeseries } = this;

if (!annotations || !annotations.length) return null;

let sortedAnnotations = annotations.sortBy(xProp);
if (timeseries) {
sortedAnnotations = sortedAnnotations.reverse();
backspace marked this conversation as resolved.
Show resolved Hide resolved
}

let prevX = 0;
let prevHigh = false;
return sortedAnnotations.map(annotation => {
const x = xScale(annotation[xProp]);
if (prevX && !prevHigh && Math.abs(x - prevX) < 30) {
prevHigh = true;
} else if (prevHigh) {
prevHigh = false;
}
const y = prevHigh ? -15 : 0;
const formattedX = this.xFormat(timeseries)(annotation[xProp]);

prevX = x;
return {
annotation,
style: `transform:translate(${x}px,${y}px)`.htmlSafe(),
icon: iconFor[annotation.type],
iconClass: iconClassFor[annotation.type],
staggerClass: prevHigh ? 'is-staggered' : '',
label: `${annotation.type} event at ${formattedX}`,
};
});
}

didInsertElement() {
this.updateDimensions();

Expand Down Expand Up @@ -344,6 +415,10 @@ export default class LineChart extends Component.extend(WindowResizable) {
}
}

annotationClick(annotation) {
this.onAnnotationClick(annotation);
}

windowResizeHandler() {
run.once(this, this.updateDimensions);
}
Expand Down
56 changes: 56 additions & 0 deletions ui/app/components/scale-events-chart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Component from '@ember/component';
import { copy } from 'ember-copy';
import { computed, get } from '@ember/object';
import { tagName } from '@ember-decorators/component';
import classic from 'ember-classic-decorator';

@classic
@tagName('')
export default class ScaleEventsChart extends Component {
events = [];

activeEvent = null;

@computed('events.[]')
get data() {
const data = this.events.filterBy('hasCount').sortBy('time');

// Extend the domain of the chart to the current time
data.push({
time: new Date(),
count: data.lastObject.count,
});

// Make sure the domain of the chart includes the first annotation
const firstAnnotation = this.annotations.sortBy('time')[0];
if (firstAnnotation && firstAnnotation.time < data[0].time) {
data.unshift({
time: firstAnnotation.time,
count: data[0].count,
});
}

return data;
}

@computed('events.[]')
get annotations() {
return this.events.rejectBy('hasCount').map(ev => ({
type: ev.error ? 'error' : 'info',
time: ev.time,
event: copy(ev),
}));
}

toggleEvent(ev) {
if (this.activeEvent && get(this.activeEvent, 'event.uid') === get(ev, 'event.uid')) {
this.closeEventDetails();
} else {
this.set('activeEvent', ev);
}
}

closeEventDetails() {
this.set('activeEvent', null);
}
}
6 changes: 6 additions & 0 deletions ui/app/controllers/jobs/job/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ export default class TaskGroupController extends Controller.extend(
})
sortedScaleEvents;

@computed('sortedScaleEvents.@each.{hasCount}', function() {
const countEventsCount = this.sortedScaleEvents.filterBy('hasCount').length;
return countEventsCount > 1 && countEventsCount >= this.sortedScaleEvents.length / 2;
})
shouldShowScaleEventTimeline;

@computed('model.job.runningDeployment')
get tooltipText() {
if (this.can.cannot('scale job')) return "You aren't allowed to scale task groups";
Expand Down
16 changes: 16 additions & 0 deletions ui/app/helpers/eq-by.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { get } from '@ember/object';
import { helper } from '@ember/component/helper';

/**
* Eq By
*
* Usage: {{eq-by "prop" obj1 obj2}}
*
* Returns true when obj1 and obj2 have the same value for property "prop"
*/
export function eqBy([prop, obj1, obj2]) {
if (!prop || !obj1 || !obj2) return false;
return get(obj1, prop) === get(obj2, prop);
}

export default helper(eqBy);
6 changes: 6 additions & 0 deletions ui/app/models/scale-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,10 @@ export default class ScaleEvent extends Fragment {
return Object.keys(this.meta).length > 0;
})
hasMeta;

// Since scale events don't have proper IDs, this UID is a compromise
@computed('time', 'timeNanos', 'message', function() {
return `${+this.time}${this.timeNanos}_${this.message}`;
})
uid;
}
1 change: 1 addition & 0 deletions ui/app/styles/charts.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import './charts/line-chart';
@import './charts/tooltip';
@import './charts/colors';
@import './charts/chart-annotation.scss';

.inline-chart {
height: 1.5rem;
Expand Down
55 changes: 55 additions & 0 deletions ui/app/styles/charts/chart-annotation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.chart-annotation {
position: absolute;
height: 100%;

&.is-staggered {
height: calc(100% + 15px);
}

.indicator {
color: $grey;
display: block;
width: 20px;
height: 20px;
padding: 0;
border: none;
border-radius: 100%;
background: transparent;
margin-left: -10px;
margin-top: -10px;
cursor: pointer;
pointer-events: auto;

&.is-active {
box-shadow: inset 0 0 0 2px $blue;
}

.icon {
width: 100%;
height: 100%;
}
}

@each $name, $pair in $colors {
$color: nth($pair, 1);

&.is-#{$name} .indicator {
color: $color;

&:hover,
&.is-hovered {
color: darken($color, 2.5%);
}
}
}

.line {
position: absolute;
left: 0;
top: 8px;
width: 1px;
height: calc(100% - 8px);
background: $grey;
z-index: -1;
}
}
16 changes: 15 additions & 1 deletion ui/app/styles/charts/line-chart.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
.chart.line-chart {
display: block;
height: 100%;
position: relative;

svg {
&.with-annotations {
margin-top: 2em;
}

& > svg {
display: block;
height: 100%;
width: 100%;
Expand Down Expand Up @@ -43,6 +48,15 @@
}
}

.line-chart-annotations {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
pointer-events: none;
}

@each $name, $pair in $colors {
$color: nth($pair, 1);

Expand Down
5 changes: 5 additions & 0 deletions ui/app/styles/charts/tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
}

.label {
white-space: nowrap;
font-weight: $weight-bold;
color: $black;
margin: 0;
Expand All @@ -80,6 +81,10 @@
color: rgba($grey, 0.6);
}
}

.value {
padding-left: 1em;
}
}

ol > li {
Expand Down
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@import './components/ember-power-select';
@import './components/empty-message';
@import './components/error-container';
@import './components/event';
@import './components/exec-button';
@import './components/exec-window';
@import './components/fs-explorer';
Expand Down
Loading