From dfc45f4dcd5bb9f37040fc050a8b22606aa74f2e Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:26:18 -0700 Subject: [PATCH 1/9] Gauge chart component --- ui/app/components/gauge-chart.js | 86 +++++++++++++++++++++ ui/app/templates/components/gauge-chart.hbs | 19 +++++ 2 files changed, 105 insertions(+) create mode 100644 ui/app/components/gauge-chart.js create mode 100644 ui/app/templates/components/gauge-chart.hbs diff --git a/ui/app/components/gauge-chart.js b/ui/app/components/gauge-chart.js new file mode 100644 index 00000000000..2f30a2f3d19 --- /dev/null +++ b/ui/app/components/gauge-chart.js @@ -0,0 +1,86 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { assert } from '@ember/debug'; +import { guidFor } from '@ember/object/internals'; +import { run } from '@ember/runloop'; +import d3Shape from 'd3-shape'; +import WindowResizable from 'nomad-ui/mixins/window-resizable'; + +export default Component.extend(WindowResizable, { + classNames: ['chart', 'gauge-chart'], + + value: null, + complement: null, + total: null, + chartClass: 'is-info', + + width: 0, + height: 0, + + percent: computed('value', 'complement', 'total', function() { + assert( + 'Provide complement OR total to GaugeChart, not both.', + this.complement != null || this.total != null + ); + + if (this.complement != null) { + return this.value / (this.value + this.complement); + } + + return this.value / this.total; + }), + + fillId: computed(function() { + return `gauge-chart-fill-${guidFor(this)}`; + }), + + maskId: computed(function() { + return `gauge-chart-mask-${guidFor(this)}`; + }), + + radius: computed('width', function() { + return this.width / 2; + }), + + weight: 4, + + backgroundArc: computed('radius', 'weight', function() { + const { radius, weight } = this; + const arc = d3Shape + .arc() + .outerRadius(radius) + .innerRadius(radius - weight) + .cornerRadius(weight) + .startAngle(-Math.PI / 2) + .endAngle(Math.PI / 2); + return arc(); + }), + + valueArc: computed('radius', 'weight', 'percent', function() { + const { radius, weight, percent } = this; + + const arc = d3Shape + .arc() + .outerRadius(radius) + .innerRadius(radius - weight) + .cornerRadius(weight) + .startAngle(-Math.PI / 2) + .endAngle(-Math.PI / 2 + Math.PI * percent); + return arc(); + }), + + didInsertElement() { + this.updateDimensions(); + }, + + updateDimensions() { + const $svg = this.$('svg'); + const width = $svg.width(); + + this.setProperties({ width, height: width / 2 }); + }, + + windowResizeHandler() { + run.once(this, this.updateDimensions); + }, +}); diff --git a/ui/app/templates/components/gauge-chart.hbs b/ui/app/templates/components/gauge-chart.hbs new file mode 100644 index 00000000000..fcd94f26643 --- /dev/null +++ b/ui/app/templates/components/gauge-chart.hbs @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + +
+

{{label}}

+

{{format-percentage value total=total complement=complement}}

+
From 7e93f9033ddea92ca07e3cc7e1423fa0dcab2139 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:27:12 -0700 Subject: [PATCH 2/9] Refactor metrics styles to allow for standalone metrics --- ui/app/styles/components/metrics.scss | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/ui/app/styles/components/metrics.scss b/ui/app/styles/components/metrics.scss index 0f94e3fa1c7..66f9bf2866f 100644 --- a/ui/app/styles/components/metrics.scss +++ b/ui/app/styles/components/metrics.scss @@ -6,7 +6,6 @@ .metric { padding: 0.75em 1em; border: 1px solid $grey-blue; - text-align: center; display: flex; flex-direction: column; min-width: 120px; @@ -50,15 +49,25 @@ } } - .label { - font-size: 1.1em; - font-weight: $weight-semibold; - margin-bottom: 0; + &.is-hollow { + border-color: transparent; + background: transparent; } + } +} - .value { - font-size: 2em; - margin-bottom: 0; - } +.metric { + text-align: center; + + .label { + font-size: 1.1em; + font-weight: $weight-semibold; + margin-bottom: 0; + } + + .value { + font-size: 2em; + margin-bottom: 0; + line-height: 1; } } From 72a928c5ec26e1fdcd682e78667fa6ff1661e496 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:27:54 -0700 Subject: [PATCH 3/9] Treat null and undefined equally --- ui/app/helpers/format-percentage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/helpers/format-percentage.js b/ui/app/helpers/format-percentage.js index cfd409c13f4..9bd2b6f0dfd 100644 --- a/ui/app/helpers/format-percentage.js +++ b/ui/app/helpers/format-percentage.js @@ -15,9 +15,9 @@ export function formatPercentage(params, options = {}) { let ratio; let total = options.total; - if (total !== undefined) { + if (total != undefined) { total = safeNumber(total); - } else if (complement !== undefined) { + } else if (complement != undefined) { total = value + safeNumber(complement); } else { // Ensures that ratio is between 0 and 1 when neither total or complement are defined From fe26e904bb7640abde57ba59c60c1b31fb4bc286 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:29:07 -0700 Subject: [PATCH 4/9] Style the gauge chart component --- ui/app/styles/charts.scss | 1 + ui/app/styles/charts/gauge-chart.scss | 52 +++++++++++++++++++++++ ui/app/styles/utils/structure-colors.scss | 1 + 3 files changed, 54 insertions(+) create mode 100644 ui/app/styles/charts/gauge-chart.scss diff --git a/ui/app/styles/charts.scss b/ui/app/styles/charts.scss index 571535ca5ea..9d7d4a47ed3 100644 --- a/ui/app/styles/charts.scss +++ b/ui/app/styles/charts.scss @@ -1,4 +1,5 @@ @import './charts/distribution-bar'; +@import './charts/gauge-chart'; @import './charts/line-chart'; @import './charts/tooltip'; @import './charts/colors'; diff --git a/ui/app/styles/charts/gauge-chart.scss b/ui/app/styles/charts/gauge-chart.scss new file mode 100644 index 00000000000..f82b82c0e91 --- /dev/null +++ b/ui/app/styles/charts/gauge-chart.scss @@ -0,0 +1,52 @@ +.gauge-chart { + position: relative; + display: block; + width: auto; + + svg { + display: block; + margin: auto; + width: 100%; + max-width: 200px; + height: 100%; + } + + .background, + .fill { + transform: translate(50%, 100%); + } + + .background { + fill: $ui-gray-100; + } + + @each $name, $pair in $colors { + $color: nth($pair, 1); + + .canvas.is-#{$name} { + .line { + stroke: $color; + } + } + + linearGradient { + &.is-#{$name} { + > .start { + stop-color: $color; + stop-opacity: 0.2; + } + + > .end { + stop-color: $color; + stop-opacity: 1; + } + } + } + } + + .metric { + position: absolute; + bottom: 0; + width: 100%; + } +} diff --git a/ui/app/styles/utils/structure-colors.scss b/ui/app/styles/utils/structure-colors.scss index 6ef72ebdab3..70d320a888b 100644 --- a/ui/app/styles/utils/structure-colors.scss +++ b/ui/app/styles/utils/structure-colors.scss @@ -1,3 +1,4 @@ +$ui-gray-100: #ebeef2; $ui-gray-200: #dce0e6; $ui-gray-300: #bac1cc; $ui-gray-400: #8e96a3; From 4e7354117a6331a031b174937b063476fc394c98 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:29:37 -0700 Subject: [PATCH 5/9] Add gauge chart stories --- ui/app/styles/storybook.scss | 28 +++++- ui/stories/charts/gauge-chart.stories.js | 115 +++++++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 ui/stories/charts/gauge-chart.stories.js diff --git a/ui/app/styles/storybook.scss b/ui/app/styles/storybook.scss index d357a833c99..d5a0157f7f5 100644 --- a/ui/app/styles/storybook.scss +++ b/ui/app/styles/storybook.scss @@ -66,7 +66,7 @@ } .description { - font-size: .8rem; + font-size: 0.8rem; padding-bottom: 5px; } @@ -116,4 +116,30 @@ margin: 0; } } + + .multiples { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + } + + .chart-container { + width: 150px; + padding: 15px; + border: 1px solid $ui-gray-200; + display: inline-block; + + &.is-small { + width: 100px; + } + + &.is-large { + width: 200px; + } + + &.is-xlarge { + width: 300px; + } + } } diff --git a/ui/stories/charts/gauge-chart.stories.js b/ui/stories/charts/gauge-chart.stories.js new file mode 100644 index 00000000000..b902b3ba570 --- /dev/null +++ b/ui/stories/charts/gauge-chart.stories.js @@ -0,0 +1,115 @@ +import hbs from 'htmlbars-inline-precompile'; +import DelayedArray from '../utils/delayed-array'; +import DelayedTruth from '../utils/delayed-truth'; + +export default { + title: 'Charts|Gauge Chart', +}; + +let totalVariations = [ + { value: 0, total: 10 }, + { value: 1, total: 10 }, + { value: 2, total: 10 }, + { value: 3, total: 10 }, + { value: 4, total: 10 }, + { value: 5, total: 10 }, + { value: 6, total: 10 }, + { value: 7, total: 10 }, + { value: 8, total: 10 }, + { value: 9, total: 10 }, + { value: 10, total: 10 }, +]; + +let complementVariations = [ + { value: 0, complement: 10 }, + { value: 1, complement: 9 }, + { value: 2, complement: 8 }, + { value: 3, complement: 7 }, + { value: 4, complement: 6 }, + { value: 5, complement: 5 }, + { value: 6, complement: 4 }, + { value: 7, complement: 3 }, + { value: 8, complement: 2 }, + { value: 9, complement: 1 }, + { value: 10, complement: 0 }, +]; + +let colorVariations = ['is-info', 'is-warning', 'is-success', 'is-danger']; + +export let Total = () => { + return { + template: hbs` +
+ {{#each variations as |v|}} +
+ {{gauge-chart value=v.value total=v.total label="Total" chartClass="is-info"}} +
+ {{/each}} +
+ `, + context: { + variations: DelayedArray.create(totalVariations), + }, + }; +}; + +export let Complement = () => { + return { + template: hbs` +
+ {{#each variations as |v|}} +
+ {{gauge-chart value=v.value complement=v.complement label="Complement" chartClass="is-info"}} +
+ {{/each}} +
+ `, + context: { + variations: DelayedArray.create(complementVariations), + }, + }; +}; + +export let Colors = () => { + return { + template: hbs` +
+ {{#each variations as |color|}} +
+ {{gauge-chart value=7 total=10 label=color chartClass=color}} +
+ {{/each}} +
+ `, + context: { + variations: DelayedArray.create(colorVariations), + }, + }; +}; + +export let Sizing = () => { + return { + template: hbs` + {{#if delayedTruth.complete}} +
+
+ {{gauge-chart value=7 total=10 label="Small"}} +
+
+ {{gauge-chart value=7 total=10 label="Regular"}} +
+
+ {{gauge-chart value=7 total=10 label="Large"}} +
+
+ {{gauge-chart value=7 total=10 label="X-Large"}} +
+
+ {{/if}} +

GaugeCharts fill the width of their container and have a dynamic height according to the height of the arc. However, the text within a gauge chart is fixed. This can create unsightly overlap or whitespace, so be careful about responsiveness when using this chart type.

+ `, + context: { + delayedTruth: DelayedTruth.create(), + }, + }; +}; From df3c24f9684eac497b5b8d99d512f984bb3a35c4 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:30:02 -0700 Subject: [PATCH 6/9] Bottom aligned columns variant --- ui/app/styles/core/columns.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/app/styles/core/columns.scss b/ui/app/styles/core/columns.scss index 9105950476f..d68425e9a88 100644 --- a/ui/app/styles/core/columns.scss +++ b/ui/app/styles/core/columns.scss @@ -10,4 +10,8 @@ flex-grow: 0; } } + + &.is-bottom-aligned { + align-items: flex-end; + } } From 83cd585682981e4240e0abe31b6aefa6c03b3251 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:30:36 -0700 Subject: [PATCH 7/9] Add gauge charts to the plugin detail page to measure availability --- ui/app/templates/csi/plugins/plugin.hbs | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/ui/app/templates/csi/plugins/plugin.hbs b/ui/app/templates/csi/plugins/plugin.hbs index e777a11e983..c189e8d4e18 100644 --- a/ui/app/templates/csi/plugins/plugin.hbs +++ b/ui/app/templates/csi/plugins/plugin.hbs @@ -22,6 +22,63 @@ +
+
+
+
Controller Health
+
+
+
+ {{gauge-chart + label="Availability" + value=model.controllersHealthy + total=model.controllersExpected}} +
+
+
+

Available

+

{{model.controllersHealthy}}

+
+
+
+
+

Expected

+

{{model.controllersExpected}}

+
+
+
+
+
+
+
+
+
Node Health
+
+
+
+ {{gauge-chart + label="Availability" + value=model.nodesHealthy + total=model.nodesExpected}} +
+
+
+

Available

+

{{model.nodesHealthy}}

+
+
+
+
+

Expected

+

{{model.nodesExpected}}

+
+
+
+
+
+
+
+
Controller Allocations From b3475add53633af45dd8e0fef2003b9d8d6ccb38 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 8 May 2020 17:56:35 -0700 Subject: [PATCH 8/9] Adjust gauge chart stories --- ui/app/styles/storybook.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/app/styles/storybook.scss b/ui/app/styles/storybook.scss index d5a0157f7f5..bc37fe4265f 100644 --- a/ui/app/styles/storybook.scss +++ b/ui/app/styles/storybook.scss @@ -125,17 +125,17 @@ } .chart-container { - width: 150px; + width: 200px; padding: 15px; border: 1px solid $ui-gray-200; display: inline-block; &.is-small { - width: 100px; + width: 150px; } &.is-large { - width: 200px; + width: 250px; } &.is-xlarge { From 0a258b1a9f69b61c74b482e1d37cc0a3a1f8a073 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 11 May 2020 16:58:17 -0700 Subject: [PATCH 9/9] Test coverage for the gauge chart --- ui/app/templates/components/gauge-chart.hbs | 6 +-- ui/tests/integration/gauge-chart-test.js | 53 ++++++++++++++++++++ ui/tests/pages/components/gauge-chart.js | 9 ++++ ui/tests/unit/components/gauge-chart-test.js | 27 ++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 ui/tests/integration/gauge-chart-test.js create mode 100644 ui/tests/pages/components/gauge-chart.js create mode 100644 ui/tests/unit/components/gauge-chart-test.js diff --git a/ui/app/templates/components/gauge-chart.hbs b/ui/app/templates/components/gauge-chart.hbs index fcd94f26643..38bb1913762 100644 --- a/ui/app/templates/components/gauge-chart.hbs +++ b/ui/app/templates/components/gauge-chart.hbs @@ -1,4 +1,4 @@ - + @@ -14,6 +14,6 @@
-

{{label}}

-

{{format-percentage value total=total complement=complement}}

+

{{label}}

+

{{format-percentage value total=total complement=complement}}

diff --git a/ui/tests/integration/gauge-chart-test.js b/ui/tests/integration/gauge-chart-test.js new file mode 100644 index 00000000000..aa3ddd06a82 --- /dev/null +++ b/ui/tests/integration/gauge-chart-test.js @@ -0,0 +1,53 @@ +import { find, render } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import { create } from 'ember-cli-page-object'; +import gaugeChart from 'nomad-ui/tests/pages/components/gauge-chart'; + +const GaugeChart = create(gaugeChart()); + +module('Integration | Component | gauge chart', function(hooks) { + setupRenderingTest(hooks); + + const commonProperties = () => ({ + value: 5, + total: 10, + label: 'Gauge', + }); + + test('presents as an svg, a formatted percentage, and a label', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + + await render(hbs` + {{gauge-chart + value=value + total=total + label=label}} + `); + + assert.equal(GaugeChart.label, props.label); + assert.equal(GaugeChart.percentage, '50%'); + assert.ok(GaugeChart.svgIsPresent); + }); + + test('the width of the chart is determined based on the container and the height is a function of the width', async function(assert) { + const props = commonProperties(); + this.setProperties(props); + + await render(hbs` +
+ {{gauge-chart + value=value + total=total + label=label}} +
+ `); + + const svg = find('[data-test-gauge-svg]'); + + assert.equal(window.getComputedStyle(svg).width, '100px'); + assert.equal(svg.getAttribute('height'), 50); + }); +}); diff --git a/ui/tests/pages/components/gauge-chart.js b/ui/tests/pages/components/gauge-chart.js new file mode 100644 index 00000000000..04b59287c18 --- /dev/null +++ b/ui/tests/pages/components/gauge-chart.js @@ -0,0 +1,9 @@ +import { isPresent, text } from 'ember-cli-page-object'; + +export default scope => ({ + scope, + + svgIsPresent: isPresent('[data-test-gauge-svg]'), + label: text('[data-test-label]'), + percentage: text('[data-test-percentage]'), +}); diff --git a/ui/tests/unit/components/gauge-chart-test.js b/ui/tests/unit/components/gauge-chart-test.js new file mode 100644 index 00000000000..771da46a114 --- /dev/null +++ b/ui/tests/unit/components/gauge-chart-test.js @@ -0,0 +1,27 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Component | gauge-chart', function(hooks) { + setupTest(hooks); + + hooks.beforeEach(function() { + this.subject = this.owner.factoryFor('component:gauge-chart'); + }); + + test('percent is a function of value and total OR complement', function(assert) { + const chart = this.subject.create(); + chart.setProperties({ + value: 5, + total: 10, + }); + + assert.equal(chart.percent, 0.5); + + chart.setProperties({ + total: null, + complement: 15, + }); + + assert.equal(chart.percent, 0.25); + }); +});