Commit a46cca2e authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'environment-performance-improvements' into 'master'

Improved Environments Metrics UX

Closes #29227

See merge request !9946
parents 10c76853 3069a2c9
/* eslint-disable import/prefer-default-export */
/**
* Function that allows a number with an X amount of decimals
* to be formatted in the following fashion:
* * For 1 digit to the left of the decimal point and X digits to the right of it
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
*/
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
let formattedNumber = '';
if (!isNaN(Number(number))) {
digitsLeft = number.split('.')[0];
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
break;
case 2:
relevantDigits = 2;
break;
case 3:
relevantDigits = 1;
break;
default:
relevantDigits = 4;
break;
}
formattedNumber = Number(number).toFixed(relevantDigits);
}
return formattedNumber;
}
/* eslint-disable no-new*/ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import d3 from 'd3'; import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import '../lib/utils/common_utils'; import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash'; import '../flash';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
...@@ -21,19 +21,19 @@ class PrometheusGraph { ...@@ -21,19 +21,19 @@ class PrometheusGraph {
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent; extraAddedWidthParent;
this.originalWidth = parentContainerWidth; this.originalWidth = parentContainerWidth;
this.originalHeight = 400; this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right; this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = 400 - this.margin.top - this.margin.bottom; this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0; this.backOffRequestCounter = 0;
this.configureGraph(); this.configureGraph();
this.init(); this.init();
} }
createGraph() { createGraph() {
Object.keys(this.data).forEach((key) => { Object.keys(this.graphSpecificProperties).forEach((key) => {
const value = this.data[key]; const value = this.graphSpecificProperties[key];
if (value.length > 0) { if (value.data.length > 0) {
this.plotValues(value, key); this.plotValues(key);
} }
}); });
} }
...@@ -49,53 +49,56 @@ class PrometheusGraph { ...@@ -49,53 +49,56 @@ class PrometheusGraph {
}); });
} }
plotValues(valuesToPlot, key) { plotValues(key) {
const graphSpecifics = this.graphSpecificProperties[key];
const x = d3.time.scale() const x = d3.time.scale()
.range([0, this.width]); .range([0, this.width]);
const y = d3.scale.linear() const y = d3.scale.linear()
.range([this.height, 0]); .range([this.height, 0]);
const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; graphSpecifics.xScale = x;
graphSpecifics.yScale = y;
const graphSpecifics = this.graphSpecificProperties[key]; const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const chart = d3.select(prometheusGraphContainer) const chart = d3.select(prometheusGraphContainer)
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top) .attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g') .append('g')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`); .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer) const axisLabelContainer = d3.select(prometheusGraphContainer)
.attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) .attr('width', this.originalWidth)
.attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) .attr('height', this.originalHeight)
.append('g') .append('g')
.attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
x.domain(d3.extent(valuesToPlot, d => d.time)); x.domain(d3.extent(graphSpecifics.data, d => d.time));
y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]);
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(x) .scale(x)
.ticks(this.commonGraphProperties.axis_no_ticks) .ticks(this.commonGraphProperties.axis_no_ticks)
.orient('bottom'); .orient('bottom');
const yAxis = d3.svg.axis() const yAxis = d3.svg.axis()
.scale(y) .scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks) .ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width) .tickSize(-this.width)
.orient('left'); .orient('left');
this.createAxisLabelContainers(axisLabelContainer, key); this.createAxisLabelContainers(axisLabelContainer, key);
chart.append('g') chart.append('g')
.attr('class', 'x-axis') .attr('class', 'x-axis')
.attr('transform', `translate(0,${this.height})`) .attr('transform', `translate(0,${this.height})`)
.call(xAxis); .call(xAxis);
chart.append('g') chart.append('g')
.attr('class', 'y-axis') .attr('class', 'y-axis')
.call(yAxis); .call(yAxis);
const area = d3.svg.area() const area = d3.svg.area()
.x(d => x(d.time)) .x(d => x(d.time))
...@@ -108,13 +111,13 @@ class PrometheusGraph { ...@@ -108,13 +111,13 @@ class PrometheusGraph {
.y(d => y(d.value)); .y(d => y(d.value));
chart.append('path') chart.append('path')
.datum(valuesToPlot) .datum(graphSpecifics.data)
.attr('d', area) .attr('d', area)
.attr('class', 'metric-area') .attr('class', 'metric-area')
.attr('fill', graphSpecifics.area_fill_color); .attr('fill', graphSpecifics.area_fill_color);
chart.append('path') chart.append('path')
.datum(valuesToPlot) .datum(graphSpecifics.data)
.attr('class', 'metric-line') .attr('class', 'metric-line')
.attr('stroke', graphSpecifics.line_color) .attr('stroke', graphSpecifics.line_color)
.attr('fill', 'none') .attr('fill', 'none')
...@@ -126,7 +129,7 @@ class PrometheusGraph { ...@@ -126,7 +129,7 @@ class PrometheusGraph {
.attr('class', 'prometheus-graph-overlay') .attr('class', 'prometheus-graph-overlay')
.attr('width', this.width) .attr('width', this.width)
.attr('height', this.height) .attr('height', this.height)
.on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key)); .on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer));
} }
// The legends from the metric // The legends from the metric
...@@ -134,128 +137,150 @@ class PrometheusGraph { ...@@ -134,128 +137,150 @@ class PrometheusGraph {
const graphSpecifics = this.graphSpecificProperties[key]; const graphSpecifics = this.graphSpecificProperties[key];
axisLabelContainer.append('line') axisLabelContainer.append('line')
.attr('class', 'label-x-axis-line') .attr('class', 'label-x-axis-line')
.attr('stroke', '#000000') .attr('stroke', '#000000')
.attr('stroke-width', '1') .attr('stroke-width', '1')
.attr({ .attr({
x1: 0, x1: 10,
y1: this.originalHeight - this.marginLabelContainer.top, y1: this.originalHeight - this.margin.top,
x2: this.originalWidth - this.margin.right, x2: (this.originalWidth - this.margin.right) + 10,
y2: this.originalHeight - this.marginLabelContainer.top, y2: this.originalHeight - this.margin.top,
}); });
axisLabelContainer.append('line') axisLabelContainer.append('line')
.attr('class', 'label-y-axis-line') .attr('class', 'label-y-axis-line')
.attr('stroke', '#000000') .attr('stroke', '#000000')
.attr('stroke-width', '1') .attr('stroke-width', '1')
.attr({ .attr({
x1: 0, x1: 10,
y1: 0, y1: 0,
x2: 0, x2: 10,
y2: this.originalHeight - this.marginLabelContainer.top, y2: this.originalHeight - this.margin.top,
}); });
axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text')
.attr('x', 0)
.attr('y', 50)
.attr('width', 30)
.attr('height', 150);
axisLabelContainer.append('text') axisLabelContainer.append('text')
.attr('class', 'label-axis-text') .attr('class', 'label-axis-text')
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`) .attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`)
.text(graphSpecifics.graph_legend_title); .text(graphSpecifics.graph_legend_title);
axisLabelContainer.append('rect') axisLabelContainer.append('rect')
.attr('class', 'rect-axis-text') .attr('class', 'rect-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right) .attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - this.marginLabelContainer.top - 20) .attr('y', this.originalHeight - 100)
.attr('width', 30) .attr('width', 30)
.attr('height', 80); .attr('height', 80);
axisLabelContainer.append('text') axisLabelContainer.append('text')
.attr('class', 'label-axis-text') .attr('class', 'label-axis-text')
.attr('x', (this.originalWidth / 2) - this.margin.right) .attr('x', (this.originalWidth / 2) - this.margin.right)
.attr('y', this.originalHeight - this.marginLabelContainer.top) .attr('y', this.originalHeight - this.margin.top)
.attr('dy', '.35em') .attr('dy', '.35em')
.text('Time'); .text('Time');
// Legends // Legends
// Metric Usage // Metric Usage
axisLabelContainer.append('rect') axisLabelContainer.append('rect')
.attr('x', this.originalWidth - 170) .attr('x', this.originalWidth - 170)
.attr('y', (this.originalHeight / 2) - 60) .attr('y', (this.originalHeight / 2) - 60)
.style('fill', graphSpecifics.area_fill_color) .style('fill', graphSpecifics.area_fill_color)
.attr('width', 20) .attr('width', 20)
.attr('height', 35); .attr('height', 35);
axisLabelContainer.append('text') axisLabelContainer.append('text')
.attr('class', 'label-axis-text') .attr('class', 'text-metric-title')
.attr('x', this.originalWidth - 140) .attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 50) .attr('y', (this.originalHeight / 2) - 50)
.text('Average'); .text('Average');
axisLabelContainer.append('text') axisLabelContainer.append('text')
.attr('class', 'text-metric-usage') .attr('class', 'text-metric-usage')
.attr('x', this.originalWidth - 140) .attr('x', this.originalWidth - 140)
.attr('y', (this.originalHeight / 2) - 25); .attr('y', (this.originalHeight / 2) - 25);
} }
handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { handleMouseOverGraph(prometheusGraphContainer) {
const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); const currentXCoordinate = d3.mouse(rectOverlay)[0];
const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
const d0 = valuesToPlot[timeValueIndex - 1]; Object.keys(this.graphSpecificProperties).forEach((key) => {
const d1 = valuesToPlot[timeValueIndex]; const currentGraphProps = this.graphSpecificProperties[key];
const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0; const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate);
const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value))); const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1);
const currentTimeCoordinate = x(currentData.time); const d0 = currentGraphProps.data[overlayIndex - 1];
const graphSpecifics = this.graphSpecificProperties[key]; const d1 = currentGraphProps.data[overlayIndex];
// Remove the current selectors const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove(); const currentData = evalTime ? d1 : d0;
d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove(); const currentTimeCoordinate = currentGraphProps.xScale(currentData.time);
d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove(); const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove(); const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
chart.append('line')
.attr('class', 'selected-metric-line') // Clear up all the pieces of the flag
.attr({ d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
x1: currentTimeCoordinate, d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
y1: y(0), d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove();
x2: currentTimeCoordinate, d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
y2: maxValueMetric,
}); const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({
x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
y2: maxMetricValue,
});
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
// The little box with text
const rectTextMetric = currentChart.append('g')
.attr('class', 'rect-text-metric')
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
rectTextMetric.append('rect')
.attr('class', 'rect-metric')
.attr('x', currentTimeCoordinate + 10)
.attr('y', maxMetricValue)
.attr('width', this.commonGraphProperties.rect_text_width)
.attr('height', this.commonGraphProperties.rect_text_height);
rectTextMetric.append('text')
.attr('class', 'text-metric')
.attr('x', currentTimeCoordinate + 35)
.attr('y', maxMetricValue + 35)
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr('class', 'text-metric-date')
.attr('x', currentTimeCoordinate + 15)
.attr('y', maxMetricValue + 15)
.text(dayFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
currentMetricValue = `${currentMetricValue}%`;
} else {
currentMetricValue = `${currentMetricValue} MB`;
}
chart.append('circle') d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`)
.attr('class', 'circle-metric') .text(currentMetricValue);
.attr('fill', graphSpecifics.line_color) });
.attr('cx', currentTimeCoordinate)
.attr('cy', y(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
// The little box with text
const rectTextMetric = chart.append('g')
.attr('class', 'rect-text-metric')
.attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
rectTextMetric.append('rect')
.attr('class', 'rect-metric')
.attr('x', currentTimeCoordinate + 10)
.attr('y', maxValueMetric)
.attr('width', this.commonGraphProperties.rect_text_width)
.attr('height', this.commonGraphProperties.rect_text_height);
rectTextMetric.append('text')
.attr('class', 'text-metric')
.attr('x', currentTimeCoordinate + 35)
.attr('y', maxValueMetric + 35)
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
.attr('class', 'text-metric-date')
.attr('x', currentTimeCoordinate + 15)
.attr('y', maxValueMetric + 15)
.text(dayFormat(currentData.time));
// Update the text
d3.select(`${prometheusGraphContainer} .text-metric-usage`)
.text(currentData.value.substring(0, 8));
} }
configureGraph() { configureGraph() {
...@@ -263,12 +288,18 @@ class PrometheusGraph { ...@@ -263,12 +288,18 @@ class PrometheusGraph {
cpu_values: { cpu_values: {
area_fill_color: '#edf3fc', area_fill_color: '#edf3fc',
line_color: '#5b99f7', line_color: '#5b99f7',
graph_legend_title: 'CPU utilization (%)', graph_legend_title: 'CPU Usage (Cores)',
data: [],
xScale: {},
yScale: {},
}, },
memory_values: { memory_values: {
area_fill_color: '#fca326', area_fill_color: '#fca326',
line_color: '#fc6d26', line_color: '#fc6d26',
graph_legend_title: 'Memory usage (MB)', graph_legend_title: 'Memory Usage (MB)',
data: [],
xScale: {},
yScale: {},
}, },
}; };
...@@ -318,17 +349,17 @@ class PrometheusGraph { ...@@ -318,17 +349,17 @@ class PrometheusGraph {
} }
transformData(metricsResponse) { transformData(metricsResponse) {
const metricTypes = {};
Object.keys(metricsResponse.metrics).forEach((key) => { Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') { if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0]; const metricValues = (metricsResponse.metrics[key])[0];
metricTypes[key] = metricValues.values.map(metric => ({ if (typeof metricValues !== 'undefined') {
time: new Date(metric[0] * 1000), this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
value: metric[1], time: new Date(metric[0] * 1000),
})); value: metric[1],
}));
}
} }
}); });
this.data = metricTypes;
} }
} }
......
...@@ -159,6 +159,16 @@ ...@@ -159,6 +159,16 @@
text { text {
fill: $stat-graph-axis-fill; fill: $stat-graph-axis-fill;
} }
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
}
.legend-axis-text {
fill: $black;
}
} }
.x-axis path, .x-axis path,
......
- if environment.external_url && can?(current_user, :read_environment, environment) - if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link') = icon('external-link')
View deployment
...@@ -4,3 +4,4 @@ ...@@ -4,3 +4,4 @@
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart') = icon('area-chart')
Monitoring
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.col-sm-6 .col-sm-6
%h3.page-title %h3.page-title
Environment: Environment:
= @environment.name = link_to @environment.name, environment_path(@environment)
.col-sm-6 .col-sm-6
.nav-controls .nav-controls
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.col-md-9 .col-md-7
%h3.page-title= @environment.name %h3.page-title= @environment.name
.col-md-3 .col-md-5
.nav-controls .nav-controls
= render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment
......
---
title: Improved UX for the environments metrics view
merge_request: 9946
author:
import { formatRelevantDigits } from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
it('returns an empty string when the number is NaN', () => {
expect(formatRelevantDigits('fail')).toBe('');
});
it('returns 4 decimals when there is 4 plus digits to the left', () => {
const formattedNumber = formatRelevantDigits('1000.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(4);
expect(leftFromDecimal.length).toBe(4);
});
it('returns 3 decimals when there is 1 digit to the left', () => {
const formattedNumber = formatRelevantDigits('0.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(3);
expect(leftFromDecimal.length).toBe(1);
});
it('returns 2 decimals when there is 2 digits to the left', () => {
const formattedNumber = formatRelevantDigits('10.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(2);
expect(leftFromDecimal.length).toBe(2);
});
it('returns 1 decimal when there is 3 digits to the left', () => {
const formattedNumber = formatRelevantDigits('100.1234567');
const rightFromDecimal = formattedNumber.split('.')[1];
const leftFromDecimal = formattedNumber.split('.')[0];
expect(rightFromDecimal.length).toBe(1);
expect(leftFromDecimal.length).toBe(3);
});
});
});
...@@ -37,9 +37,11 @@ describe('PrometheusGraph', () => { ...@@ -37,9 +37,11 @@ describe('PrometheusGraph', () => {
it('transforms the data', () => { it('transforms the data', () => {
this.prometheusGraph.init(prometheusMockData.metrics); this.prometheusGraph.init(prometheusMockData.metrics);
expect(this.prometheusGraph.data).toBeDefined(); Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => {
expect(this.prometheusGraph.data.cpu_values.length).toBe(121); const graphProps = this.prometheusGraph.graphSpecificProperties[key];
expect(this.prometheusGraph.data.memory_values.length).toBe(121); expect(graphProps.data).toBeDefined();
expect(graphProps.data.length).toBe(121);
});
}); });
it('creates two graphs', () => { it('creates two graphs', () => {
...@@ -68,7 +70,7 @@ describe('PrometheusGraph', () => { ...@@ -68,7 +70,7 @@ describe('PrometheusGraph', () => {
expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined(); expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined(); expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined(); expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
expect($axisLabelContainer.find('rect').length).toBe(2); expect($axisLabelContainer.find('rect').length).toBe(3);
expect($axisLabelContainer.find('text').length).toBe(4); expect($axisLabelContainer.find('text').length).toBe(4);
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment