From e7bb9b95e8b7dc29d6cf8417d74f249fc89d23b3 Mon Sep 17 00:00:00 2001 From: Adriel Santiago Date: Fri, 1 Feb 2019 08:37:48 +0000 Subject: [PATCH] Remove d3 metrics graph --- .../monitoring/components/dashboard.vue | 36 +- .../monitoring/components/graph.vue | 329 ------------------ .../monitoring/components/graph/axis.vue | 118 ------- .../components/graph/deployment.vue | 48 --- .../monitoring/components/graph/flag.vue | 151 -------- .../monitoring/components/graph/legend.vue | 62 ---- .../monitoring/components/graph/path.vue | 65 ---- .../components/graph/track_info.vue | 28 -- .../components/graph/track_line.vue | 33 -- .../javascripts/monitoring/event_hub.js | 3 - .../monitoring/mixins/monitoring_mixins.js | 86 ----- .../monitoring/utils/date_time_formatters.js | 42 --- .../monitoring/utils/measurements.js | 44 --- .../monitoring/utils/multiple_time_series.js | 223 ------------ locale/gitlab.pot | 6 - .../javascripts/monitoring/graph/axis_spec.js | 65 ---- .../monitoring/graph/deployment_spec.js | 53 --- .../javascripts/monitoring/graph/flag_spec.js | 133 ------- .../monitoring/graph/legend_spec.js | 44 --- .../monitoring/graph/track_info_spec.js | 44 --- .../monitoring/graph/track_line_spec.js | 52 --- .../javascripts/monitoring/graph_path_spec.js | 56 --- spec/javascripts/monitoring/graph_spec.js | 127 ------- .../utils/multiple_time_series_spec.js | 22 -- 24 files changed, 2 insertions(+), 1868 deletions(-) delete mode 100644 app/assets/javascripts/monitoring/components/graph.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/axis.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/deployment.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/flag.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/legend.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/path.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/track_info.vue delete mode 100644 app/assets/javascripts/monitoring/components/graph/track_line.vue delete mode 100644 app/assets/javascripts/monitoring/event_hub.js delete mode 100644 app/assets/javascripts/monitoring/mixins/monitoring_mixins.js delete mode 100644 app/assets/javascripts/monitoring/utils/date_time_formatters.js delete mode 100644 app/assets/javascripts/monitoring/utils/measurements.js delete mode 100644 app/assets/javascripts/monitoring/utils/multiple_time_series.js delete mode 100644 spec/javascripts/monitoring/graph/axis_spec.js delete mode 100644 spec/javascripts/monitoring/graph/deployment_spec.js delete mode 100644 spec/javascripts/monitoring/graph/flag_spec.js delete mode 100644 spec/javascripts/monitoring/graph/legend_spec.js delete mode 100644 spec/javascripts/monitoring/graph/track_info_spec.js delete mode 100644 spec/javascripts/monitoring/graph/track_line_spec.js delete mode 100644 spec/javascripts/monitoring/graph_path_spec.js delete mode 100644 spec/javascripts/monitoring/graph_spec.js delete mode 100644 spec/javascripts/monitoring/utils/multiple_time_series_spec.js diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 973fc8e10c9..0b4bb9cc686 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -6,15 +6,12 @@ import Flash from '../../flash'; import MonitoringService from '../services/monitoring_service'; import MonitorAreaChart from './charts/area.vue'; import GraphGroup from './graph_group.vue'; -import Graph from './graph.vue'; import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; -import eventHub from '../event_hub'; export default { components: { MonitorAreaChart, - Graph, GraphGroup, EmptyState, Icon, @@ -25,21 +22,11 @@ export default { required: false, default: true, }, - showLegend: { - type: Boolean, - required: false, - default: true, - }, showPanels: { type: Boolean, required: false, default: true, }, - forceSmallGraph: { - type: Boolean, - required: false, - default: false, - }, documentationPath: { type: String, required: true, @@ -99,14 +86,10 @@ export default { store: new MonitoringStore(), state: 'gettingStarted', showEmptyState: true, - hoverData: {}, elWidth: 0, }; }, computed: { - graphComponent() { - return gon.features && gon.features.areaChart ? MonitorAreaChart : Graph; - }, forceRedraw() { return this.elWidth; }, @@ -122,10 +105,8 @@ export default { childList: false, subtree: false, }; - eventHub.$on('hoverChanged', this.hoverChanged); }, beforeDestroy() { - eventHub.$off('hoverChanged', this.hoverChanged); window.removeEventListener('resize', this.resizeThrottled, false); this.sidebarMutationObserver.disconnect(); }, @@ -176,9 +157,6 @@ export default { resize() { this.elWidth = this.$el.clientWidth; }, - hoverChanged(data) { - this.hoverData = data; - }, }, }; @@ -215,23 +193,13 @@ export default { :name="groupData.group" :show-panels="showPanels" > - - - {{ null }} - + /> -import { scaleLinear, scaleTime } from 'd3-scale'; -import { axisLeft, axisBottom } from 'd3-axis'; -import _ from 'underscore'; -import { max, extent } from 'd3-array'; -import { select } from 'd3-selection'; -import GraphAxis from './graph/axis.vue'; -import GraphLegend from './graph/legend.vue'; -import GraphFlag from './graph/flag.vue'; -import GraphDeployment from './graph/deployment.vue'; -import GraphPath from './graph/path.vue'; -import MonitoringMixin from '../mixins/monitoring_mixins'; -import eventHub from '../event_hub'; -import measurements from '../utils/measurements'; -import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters'; -import createTimeSeries from '../utils/multiple_time_series'; -import bp from '../../breakpoints'; - -const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; - -export default { - components: { - GraphAxis, - GraphFlag, - GraphDeployment, - GraphPath, - GraphLegend, - }, - mixins: [MonitoringMixin], - props: { - graphData: { - type: Object, - required: true, - }, - deploymentData: { - type: Array, - required: true, - }, - hoverData: { - type: Object, - required: false, - default: () => ({}), - }, - projectPath: { - type: String, - required: true, - }, - tagsPath: { - type: String, - required: true, - }, - showLegend: { - type: Boolean, - required: false, - default: true, - }, - smallGraph: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - baseGraphHeight: 450, - baseGraphWidth: 600, - graphHeight: 450, - graphWidth: 600, - graphHeightOffset: 120, - margin: {}, - unitOfDisplay: '', - yAxisLabel: '', - legendTitle: '', - reducedDeploymentData: [], - measurements: measurements.large, - currentData: { - time: new Date(), - value: 0, - }, - currentXCoordinate: 0, - currentCoordinates: {}, - showFlag: false, - showFlagContent: false, - timeSeries: [], - graphDrawData: {}, - realPixelRatio: 1, - seriesUnderMouse: [], - }; - }, - computed: { - outerViewBox() { - return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; - }, - innerViewBox() { - return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; - }, - axisTransform() { - return `translate(70, ${this.graphHeight - 100})`; - }, - paddingBottomRootSvg() { - return { - paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`, - }; - }, - deploymentFlagData() { - return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); - }, - shouldRenderData() { - return this.graphData.queries.filter(s => s.result.length > 0).length > 0; - }, - }, - watch: { - hoverData() { - this.positionFlag(); - }, - }, - mounted() { - this.draw(); - }, - methods: { - showDot(path) { - return this.showFlagContent && this.seriesUnderMouse.includes(path); - }, - draw() { - const breakpointSize = bp.getBreakpointSize(); - const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; - - this.margin = measurements.large.margin; - - if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { - this.graphHeight = 300; - this.margin = measurements.small.margin; - this.measurements = measurements.small; - } - - this.yAxisLabel = this.graphData.y_label || 'Values'; - this.graphWidth = svgWidth - this.margin.left - this.margin.right; - this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; - this.baseGraphHeight = this.graphHeight - 50; - this.baseGraphWidth = this.graphWidth; - - // pixel offsets inside the svg and outside are not 1:1 - this.realPixelRatio = svgWidth / this.baseGraphWidth; - - // set the legends on the axes - const [query] = this.graphData.queries; - this.legendTitle = query ? query.label : 'Average'; - this.unitOfDisplay = query ? query.unit : ''; - - if (this.shouldRenderData) { - this.renderAxesPaths(); - this.formatDeployments(); - } - }, - handleMouseOverGraph(e) { - let point = this.$refs.graphData.createSVGPoint(); - point.x = e.clientX; - point.y = e.clientY; - point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); - point.x += 7; - - this.seriesUnderMouse = this.timeSeries.filter(series => { - const mouseX = series.timeSeriesScaleX.invert(point.x); - let minDistance = Infinity; - - const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => { - const distance = Math.abs(Number(new Date(x)) - Number(mouseX)); - if (distance < minDistance) { - minDistance = distance; - return x; - } - return closest; - }); - - return series.values.find(v => v.time.toString() === closestTickMark); - }); - - const firstTimeSeries = this.seriesUnderMouse[0]; - const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); - const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); - const d0 = firstTimeSeries.values[overlayIndex - 1]; - const d1 = firstTimeSeries.values[overlayIndex]; - if (d0 === undefined || d1 === undefined) return; - const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; - const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1; - const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time; - const currentDeployXPos = this.mouseOverDeployInfo(point.x); - - eventHub.$emit('hoverChanged', { - hoveredDate, - currentDeployXPos, - }); - }, - renderAxesPaths() { - ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries( - this.graphData.queries, - this.graphWidth, - this.graphHeight, - this.graphHeightOffset, - )); - - if (_.findWhere(this.timeSeries, { renderCanary: true })) { - this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true })); - } - - const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]); - const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]); - - const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []); - axisXScale.domain(d3.extent(allValues, d => d.time)); - axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); - - this.allXAxisValues = this.timeSeries.reduce((obj, series) => { - const seriesKeys = {}; - series.values.forEach(v => { - seriesKeys[v.time] = true; - }); - return { - ...obj, - ...seriesKeys, - }; - }, {}); - - const xAxis = d3 - .axisBottom() - .scale(axisXScale) - .ticks(this.graphWidth / 120) - .tickFormat(timeScaleFormat); - - const yAxis = d3 - .axisLeft() - .scale(axisYScale) - .ticks(measurements.yTicks); - - d3.select(this.$refs.baseSvg) - .select('.x-axis') - .call(xAxis); - - const width = this.graphWidth; - d3.select(this.$refs.baseSvg) - .select('.y-axis') - .call(yAxis) - .selectAll('.tick') - .each(function createTickLines(d, i) { - if (i > 0) { - d3.select(this) - .select('line') - .attr('x2', width) - .attr('class', 'axis-tick'); - } // Avoid adding the class to the first tick, to prevent coloring - }); // This will select all of the ticks once they're rendered - }, - }, -}; - - - diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue deleted file mode 100644 index 8f046857a20..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/axis.vue +++ /dev/null @@ -1,118 +0,0 @@ - - diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue deleted file mode 100644 index bee9784692c..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ /dev/null @@ -1,48 +0,0 @@ - - diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue deleted file mode 100644 index 9d6d1caef80..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue deleted file mode 100644 index b5211c306a3..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ /dev/null @@ -1,62 +0,0 @@ - - diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue deleted file mode 100644 index f2c237ec391..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ /dev/null @@ -1,65 +0,0 @@ - - diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue deleted file mode 100644 index 3464067834f..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/track_info.vue +++ /dev/null @@ -1,28 +0,0 @@ - - diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue deleted file mode 100644 index d2ed1ba113e..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/track_line.vue +++ /dev/null @@ -1,33 +0,0 @@ - - diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js deleted file mode 100644 index 0948c2e5352..00000000000 --- a/app/assets/javascripts/monitoring/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -export default new Vue(); diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js deleted file mode 100644 index 87c3d969de4..00000000000 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ /dev/null @@ -1,86 +0,0 @@ -import { bisectDate } from '../utils/date_time_formatters'; - -const mixins = { - methods: { - mouseOverDeployInfo(mouseXPos) { - if (!this.reducedDeploymentData) return false; - - let dataFound = false; - this.reducedDeploymentData = this.reducedDeploymentData.map(d => { - const deployment = d; - if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { - dataFound = d.xPos + 1; - - deployment.showDeploymentFlag = true; - } else { - deployment.showDeploymentFlag = false; - } - return deployment; - }); - - return dataFound; - }, - - formatDeployments() { - this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { - const time = new Date(deployment.created_at); - const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time)); - - time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); - - if (xPos >= 0) { - const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1); - - deploymentDataArray.push({ - id: deployment.id, - time, - sha: deployment.sha, - commitUrl: `${this.projectPath}/commit/${deployment.sha}`, - tag: deployment.tag, - tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null, - ref: deployment.ref.name, - xPos, - seriesIndex, - showDeploymentFlag: false, - }); - } - - return deploymentDataArray; - }, []); - }, - - positionFlag() { - const timeSeries = this.seriesUnderMouse[0]; - if (!timeSeries) { - return; - } - const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate); - - this.currentData = timeSeries.values[hoveredDataIndex]; - this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); - - this.currentCoordinates = {}; - - this.seriesUnderMouse.forEach(series => { - const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate); - const currentData = series.values[currentDataIndex]; - const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); - const currentY = Math.floor(series.timeSeriesScaleY(currentData.value)); - - this.currentCoordinates[series.metricTag] = { - currentX, - currentY, - currentDataIndex, - }; - }); - - if (this.hoverData.currentDeployXPos) { - this.showFlag = false; - } else { - this.showFlag = true; - } - }, - }, -}; - -export default mixins; diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js deleted file mode 100644 index d88c13609dc..00000000000 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ /dev/null @@ -1,42 +0,0 @@ -import { timeFormat as time } from 'd3-time-format'; -import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time'; -import { bisector } from 'd3-array'; - -const d3 = { - time, - bisector, - timeSecond, - timeMinute, - timeHour, - timeDay, - timeWeek, - timeMonth, - timeYear, -}; - -export const dateFormat = d3.time('%d %b %Y, '); -export const timeFormat = d3.time('%-I:%M%p'); -export const dateFormatWithName = d3.time('%a, %b %-d'); -export const bisectDate = d3.bisector(d => d.time).left; - -export function timeScaleFormat(date) { - let formatFunction; - if (d3.timeSecond(date) < date) { - formatFunction = d3.time('.%L'); - } else if (d3.timeMinute(date) < date) { - formatFunction = d3.time(':%S'); - } else if (d3.timeHour(date) < date) { - formatFunction = d3.time('%-I:%M'); - } else if (d3.timeDay(date) < date) { - formatFunction = d3.time('%-I %p'); - } else if (d3.timeWeek(date) < date) { - formatFunction = d3.time('%a %d'); - } else if (d3.timeMonth(date) < date) { - formatFunction = d3.time('%b %d'); - } else if (d3.timeYear(date) < date) { - formatFunction = d3.time('%B'); - } else { - formatFunction = d3.time('%Y'); - } - return formatFunction(date); -} diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js deleted file mode 100644 index 7c771f43eee..00000000000 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ /dev/null @@ -1,44 +0,0 @@ -export default { - small: { - // Covers both xs and sm screen sizes - margin: { - top: 40, - right: 40, - bottom: 50, - left: 40, - }, - legends: { - width: 15, - height: 3, - offsetX: 20, - offsetY: 32, - }, - backgroundLegend: { - width: 30, - height: 50, - }, - axisLabelLineOffset: -20, - }, - large: { - // This covers both md and lg screen sizes - margin: { - top: 80, - right: 80, - bottom: 100, - left: 80, - }, - legends: { - width: 15, - height: 3, - offsetX: 20, - offsetY: 34, - }, - backgroundLegend: { - width: 30, - height: 150, - }, - axisLabelLineOffset: 20, - }, - xTicks: 8, - yTicks: 3, -}; diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js deleted file mode 100644 index 50ba14dfb2e..00000000000 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ /dev/null @@ -1,223 +0,0 @@ -import _ from 'underscore'; -import { scaleLinear, scaleTime } from 'd3-scale'; -import { line, area, curveLinear } from 'd3-shape'; -import { extent, max, sum } from 'd3-array'; -import { timeMinute, timeSecond } from 'd3-time'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; - -const d3 = { - scaleLinear, - scaleTime, - line, - area, - curveLinear, - extent, - max, - timeMinute, - timeSecond, - sum, -}; - -const defaultColorPalette = { - blue: ['#1f78d1', '#8fbce8'], - orange: ['#fc9403', '#feca81'], - red: ['#db3b21', '#ed9d90'], - green: ['#1aaa55', '#8dd5aa'], - purple: ['#6666c4', '#d1d1f0'], -}; - -const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; - -const defaultStyleOrder = ['solid', 'dashed', 'dotted']; - -function queryTimeSeries(query, graphDrawData, lineStyle) { - let usedColors = []; - let renderCanary = false; - const timeSeriesParsed = []; - - function pickColor(name) { - let pick; - if (name && defaultColorPalette[name]) { - pick = name; - } else { - const unusedColors = _.difference(defaultColorOrder, usedColors); - if (unusedColors.length > 0) { - [pick] = unusedColors; - } else { - usedColors = []; - [pick] = defaultColorOrder; - } - } - usedColors.push(pick); - return defaultColorPalette[pick]; - } - - function findByDate(series, time) { - const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60); - if (val) { - return val.value; - } - return NaN; - } - - // The timeseries data may have gaps in it - // but we need a regularly-spaced set of time/value pairs - // this gives us a complete range of one minute intervals - // offset the same amount as the original data - const [minX, maxX] = graphDrawData.xDom; - const offset = d3.timeMinute(minX) - Number(minX); - const datesWithoutGaps = d3.timeSecond - .every(60) - .range(d3.timeMinute.offset(minX, -1), maxX) - .map(d => d - offset); - - query.result.forEach((timeSeries, timeSeriesNumber) => { - let metricTag = ''; - let lineColor = ''; - let areaColor = ''; - let shouldRenderLegend = true; - const timeSeriesValues = timeSeries.values.map(d => d.value); - const maximumValue = d3.max(timeSeriesValues); - const accum = d3.sum(timeSeriesValues); - const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable'); - - if (trackName === 'Canary') { - renderCanary = true; - } - - const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; - const seriesCustomizationData = - query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); - - if (seriesCustomizationData) { - metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; - [lineColor, areaColor] = pickColor(seriesCustomizationData.color); - if (timeSeriesParsed.length > 0) { - shouldRenderLegend = false; - } else { - shouldRenderLegend = true; - } - } else { - metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`; - [lineColor, areaColor] = pickColor(); - if (timeSeriesParsed.length > 1) { - shouldRenderLegend = false; - } - } - - const values = datesWithoutGaps.map(time => ({ - time, - value: findByDate(timeSeries.values, time), - })); - - timeSeriesParsed.push({ - linePath: graphDrawData.lineFunction(values), - areaPath: graphDrawData.areaBelowLine(values), - timeSeriesScaleX: graphDrawData.timeSeriesScaleX, - timeSeriesScaleY: graphDrawData.timeSeriesScaleY, - values: timeSeries.values, - max: maximumValue, - average: accum / timeSeries.values.length, - lineStyle, - lineColor, - areaColor, - metricTag, - trackName, - shouldRenderLegend, - renderCanary, - }); - - if (!shouldRenderLegend) { - if (!timeSeriesParsed[0].tracksLegend) { - timeSeriesParsed[0].tracksLegend = []; - } - timeSeriesParsed[0].tracksLegend.push({ - max: maximumValue, - average: accum / timeSeries.values.length, - lineStyle, - lineColor, - metricTag, - }); - } - }); - - return timeSeriesParsed; -} - -function xyDomain(queries) { - const allValues = queries.reduce( - (allQueryResults, query) => - allQueryResults.concat( - query.result.reduce((allResults, result) => allResults.concat(result.values), []), - ), - [], - ); - - const xDom = d3.extent(allValues, d => d.time); - const yDom = [0, d3.max(allValues.map(d => d.value))]; - - return { - xDom, - yDom, - }; -} - -export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) { - const { xDom, yDom } = xyDomain(queries); - - const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]); - const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]); - - timeSeriesScaleX.domain(xDom); - timeSeriesScaleX.ticks(d3.timeMinute, 60); - timeSeriesScaleY.domain(yDom); - - const defined = d => !Number.isNaN(d.value) && d.value != null; - - const lineFunction = d3 - .line() - .defined(defined) - .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate - .x(d => timeSeriesScaleX(d.time)) - .y(d => timeSeriesScaleY(d.value)); - - const areaBelowLine = d3 - .area() - .defined(defined) - .curve(d3.curveLinear) - .x(d => timeSeriesScaleX(d.time)) - .y0(graphHeight - graphHeightOffset) - .y1(d => timeSeriesScaleY(d.value)); - - const areaAboveLine = d3 - .area() - .defined(defined) - .curve(d3.curveLinear) - .x(d => timeSeriesScaleX(d.time)) - .y0(0) - .y1(d => timeSeriesScaleY(d.value)); - - return { - lineFunction, - areaBelowLine, - areaAboveLine, - xDom, - yDom, - timeSeriesScaleX, - timeSeriesScaleY, - }; -} - -export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { - const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset); - - const timeSeries = queries.reduce((series, query, index) => { - const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length]; - return series.concat(queryTimeSeries(query, graphDrawData, lineStyle)); - }, []); - - return { - timeSeries, - graphDrawData, - }; -} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bb98fc06ed6..8caa876e6b0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4447,9 +4447,6 @@ msgstr "" msgid "Metrics|Learn about environments" msgstr "" -msgid "Metrics|No data to display" -msgstr "" - msgid "Metrics|No deployed environments" msgstr "" @@ -5711,9 +5708,6 @@ msgstr "" msgid "ProjectsDropdown|This feature requires browser localStorage support" msgstr "" -msgid "PrometheusDashboard|Time" -msgstr "" - msgid "PrometheusService|%{exporters} with %{metrics} were found" msgstr "" diff --git a/spec/javascripts/monitoring/graph/axis_spec.js b/spec/javascripts/monitoring/graph/axis_spec.js deleted file mode 100644 index c7adba00637..00000000000 --- a/spec/javascripts/monitoring/graph/axis_spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import Vue from 'vue'; -import GraphAxis from '~/monitoring/components/graph/axis.vue'; -import measurements from '~/monitoring/utils/measurements'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphAxis); - - return new Component({ - propsData, - }).$mount(); -}; - -const defaultValuesComponent = { - graphWidth: 500, - graphHeight: 300, - graphHeightOffset: 120, - margin: measurements.large.margin, - measurements: measurements.large, - yAxisLabel: 'Values', - unitOfDisplay: 'MB', -}; - -function getTextFromNode(component, selector) { - return component.$el.querySelector(selector).firstChild.nodeValue.trim(); -} - -describe('Axis', () => { - describe('Computed props', () => { - it('textTransform', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); - }); - - it('xPosition', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.xPosition).toEqual(180); - }); - - it('yPosition', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.yPosition).toEqual(240); - }); - - it('rectTransform', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); - }); - }); - - it('has 2 rect-axis-text rect svg elements', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); - }); - - it('contains text to signal the usage, title and time with multiple time series', () => { - const component = createComponent(defaultValuesComponent); - - expect(getTextFromNode(component, '.y-label-text')).toEqual('Values (MB)'); - }); -}); diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js deleted file mode 100644 index 7d39c4345d2..00000000000 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import Vue from 'vue'; -import GraphDeployment from '~/monitoring/components/graph/deployment.vue'; -import { deploymentData } from '../mock_data'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphDeployment); - - return new Component({ - propsData, - }).$mount(); -}; - -describe('MonitoringDeployment', () => { - describe('Methods', () => { - it('should contain a hidden gradient', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); - }); - - it('transformDeploymentGroup translates an available deployment', () => { - const component = createComponent({ - showDeployInfo: false, - deploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.transformDeploymentGroup({ xPos: 16 })).toContain('translate(11, 20)'); - }); - - describe('Computed props', () => { - it('calculatedHeight', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.calculatedHeight).toEqual(180); - }); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js deleted file mode 100644 index 038bfffd44f..00000000000 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import Vue from 'vue'; -import GraphFlag from '~/monitoring/components/graph/flag.vue'; -import { deploymentData } from '../mock_data'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphFlag); - - return new Component({ - propsData, - }).$mount(); -}; - -const defaultValuesComponent = { - currentXCoordinate: 200, - currentYCoordinate: 100, - currentFlagPosition: 100, - currentData: { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - graphHeight: 300, - graphHeightOffset: 120, - showFlagContent: true, - realPixelRatio: 1, - timeSeries: [ - { - values: [ - { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - ], - }, - ], - unitOfDisplay: 'ms', - currentDataIndex: 0, - legendTitle: 'Average', - currentCoordinates: {}, -}; - -const deploymentFlagData = { - ...deploymentData[0], - ref: deploymentData[0].ref.name, - xPos: 10, - time: new Date(deploymentData[0].created_at), -}; - -describe('GraphFlag', () => { - let component; - - it('has a line at the currentXCoordinate', () => { - component = createComponent(defaultValuesComponent); - - expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`); - }); - - describe('Deployment flag', () => { - it('shows a deployment flag when deployment data provided', () => { - const deploymentFlagComponent = createComponent({ - ...defaultValuesComponent, - deploymentFlagData, - }); - - expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed'); - }); - - it('contains the ref when a tag is available', () => { - const deploymentFlagComponent = createComponent({ - ...defaultValuesComponent, - deploymentFlagData: { - ...deploymentFlagData, - sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', - tag: true, - ref: '1.0', - }, - }); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( - 'f5bcd1d9', - ); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( - '1.0', - ); - }); - - it('does not contain the ref when a tag is unavailable', () => { - const deploymentFlagComponent = createComponent({ - ...defaultValuesComponent, - deploymentFlagData: { - ...deploymentFlagData, - sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', - tag: false, - ref: '1.0', - }, - }); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( - 'f5bcd1d9', - ); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText( - '1.0', - ); - }); - }); - - describe('Computed props', () => { - beforeEach(() => { - component = createComponent(defaultValuesComponent); - }); - - it('formatTime', () => { - expect(component.formatTime).toMatch(/\d:17PM/); - }); - - it('formatDate', () => { - expect(component.formatDate).toEqual('04 Jun 2017, '); - }); - - it('cursorStyle', () => { - expect(component.cursorStyle).toEqual({ - top: '20px', - left: '270px', - height: '180px', - }); - }); - - it('flagOrientation', () => { - expect(component.flagOrientation).toEqual('left'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js deleted file mode 100644 index 9209e77dcf4..00000000000 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Vue from 'vue'; -import GraphLegend from '~/monitoring/components/graph/legend.vue'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); - -const defaultValuesComponent = {}; - -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); - -defaultValuesComponent.timeSeries = timeSeries; - -describe('Legend Component', () => { - let vm; - let Legend; - - beforeEach(() => { - Legend = Vue.extend(GraphLegend); - }); - - describe('View', () => { - beforeEach(() => { - vm = mountComponent(Legend, { - legendTitle: 'legend', - timeSeries, - currentDataIndex: 0, - unitOfDisplay: 'Req/Sec', - }); - }); - - it('should render the usage, title and time with multiple time series', () => { - const titles = vm.$el.querySelectorAll('.legend-metric-title'); - - expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); - expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); - }); - - it('should container the same number of rows in the table as time series', () => { - expect(vm.$el.querySelectorAll('.prometheus-table tr').length).toEqual(vm.timeSeries.length); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/track_info_spec.js b/spec/javascripts/monitoring/graph/track_info_spec.js deleted file mode 100644 index ce93ae28842..00000000000 --- a/spec/javascripts/monitoring/graph/track_info_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Vue from 'vue'; -import TrackInfo from '~/monitoring/components/graph/track_info.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); - -describe('TrackInfo component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(TrackInfo); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Computed props', () => { - beforeEach(() => { - vm = mountComponent(Component, { track: timeSeries[0] }); - }); - - it('summaryMetrics', () => { - expect(vm.summaryMetrics).toEqual('Avg: 0.000 · Max: 0.000'); - }); - }); - - describe('Rendered output', () => { - beforeEach(() => { - vm = mountComponent(Component, { track: timeSeries[0] }); - }); - - it('contains metric tag and the summary metrics', () => { - const metricTag = vm.$el.querySelector('strong'); - - expect(metricTag.textContent.trim()).toEqual(vm.track.metricTag); - expect(vm.$el.textContent).toContain('Avg: 0.000 · Max: 0.000'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/track_line_spec.js b/spec/javascripts/monitoring/graph/track_line_spec.js deleted file mode 100644 index 2a4f89ddf6e..00000000000 --- a/spec/javascripts/monitoring/graph/track_line_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import Vue from 'vue'; -import TrackLine from '~/monitoring/components/graph/track_line.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); - -describe('TrackLine component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(TrackLine); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Computed props', () => { - it('stylizedLine for dashed lineStyles', () => { - vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dashed' } }); - - expect(vm.stylizedLine).toEqual('6, 3'); - }); - - it('stylizedLine for dotted lineStyles', () => { - vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dotted' } }); - - expect(vm.stylizedLine).toEqual('3, 3'); - }); - }); - - describe('Rendered output', () => { - it('has an svg with a line', () => { - vm = mountComponent(Component, { track: { ...timeSeries[0] } }); - const svgEl = vm.$el.querySelector('svg'); - const lineEl = vm.$el.querySelector('svg line'); - - expect(svgEl.getAttribute('width')).toEqual('16'); - expect(svgEl.getAttribute('height')).toEqual('8'); - - expect(lineEl.getAttribute('stroke-width')).toEqual('4'); - expect(lineEl.getAttribute('x1')).toEqual('0'); - expect(lineEl.getAttribute('x2')).toEqual('16'); - expect(lineEl.getAttribute('y1')).toEqual('4'); - expect(lineEl.getAttribute('y2')).toEqual('4'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js deleted file mode 100644 index fd167b83d51..00000000000 --- a/spec/javascripts/monitoring/graph_path_spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import Vue from 'vue'; -import GraphPath from '~/monitoring/components/graph/path.vue'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphPath); - - return new Component({ - propsData, - }).$mount(); -}; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); - -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120); -const firstTimeSeries = timeSeries[0]; - -describe('Monitoring Paths', () => { - it('renders two paths to represent a line and the area underneath it', () => { - const component = createComponent({ - generatedLinePath: firstTimeSeries.linePath, - generatedAreaPath: firstTimeSeries.areaPath, - lineColor: firstTimeSeries.lineColor, - areaColor: firstTimeSeries.areaColor, - showDot: false, - }); - const metricArea = component.$el.querySelector('.metric-area'); - const metricLine = component.$el.querySelector('.metric-line'); - - expect(metricArea.getAttribute('fill')).toBe('#8fbce8'); - expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath); - expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); - expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); - }); - - describe('Computed properties', () => { - it('strokeDashArray', () => { - const component = createComponent({ - generatedLinePath: firstTimeSeries.linePath, - generatedAreaPath: firstTimeSeries.areaPath, - lineColor: firstTimeSeries.lineColor, - areaColor: firstTimeSeries.areaColor, - showDot: false, - }); - - component.lineStyle = 'dashed'; - - expect(component.strokeDashArray).toBe('3, 1'); - - component.lineStyle = 'dotted'; - - expect(component.strokeDashArray).toBe('1, 1'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js deleted file mode 100644 index 59d6d4f3a7f..00000000000 --- a/spec/javascripts/monitoring/graph_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import Vue from 'vue'; -import Graph from '~/monitoring/components/graph.vue'; -import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; -import { - deploymentData, - convertDatesMultipleSeries, - singleRowMetricsMultipleSeries, - queryWithoutData, -} from './mock_data'; - -const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; -const projectPath = 'http://test.host/frontend-fixtures/environments-project'; -const createComponent = propsData => { - const Component = Vue.extend(Graph); - - return new Component({ - propsData, - }).$mount(); -}; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); - -describe('Graph', () => { - beforeEach(() => { - spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); - }); - - it('has a title', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - expect(component.$el.querySelector('.prometheus-graph-title').innerText.trim()).toBe( - component.graphData.title, - ); - }); - - describe('Computed props', () => { - it('axisTransform translates an element Y position depending of its height', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - const transformedHeight = `${component.graphHeight - 100}`; - - expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual(-1); - }); - - it('outerViewBox gets a width and height property based on the DOM size of the element', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - const viewBoxArray = component.outerViewBox.split(' '); - - expect(typeof component.outerViewBox).toEqual('string'); - expect(viewBoxArray[2]).toEqual(component.graphWidth.toString()); - expect(viewBoxArray[3]).toEqual((component.graphHeight - 50).toString()); - }); - }); - - it('has a title for the y-axis and the chart legend that comes from the backend', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - expect(component.yAxisLabel).toEqual(component.graphData.y_label); - expect(component.legendTitle).toEqual(component.graphData.queries[0].label); - }); - - it('sets the currentData object based on the hovered data index', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - graphIdentifier: 0, - hoverData: { - hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'), - currentDeployXPos: null, - }, - tagsPath, - projectPath, - }); - - // simulate moving mouse over data series - component.seriesUnderMouse = component.timeSeries; - - component.positionFlag(); - - expect(component.currentData).toBe(component.timeSeries[0].values[10]); - }); - - describe('Without data to display', () => { - it('shows a "no data to display" empty state on a graph', done => { - const component = createComponent({ - graphData: queryWithoutData, - deploymentData, - tagsPath, - projectPath, - }); - - Vue.nextTick(() => { - expect( - component.$el.querySelector('.js-no-data-to-display text').textContent.trim(), - ).toEqual('No data to display'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js deleted file mode 100644 index 8937b7d9680..00000000000 --- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js +++ /dev/null @@ -1,22 +0,0 @@ -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120); -const firstTimeSeries = timeSeries[0]; - -describe('Multiple time series', () => { - it('createTimeSeries returned array contains an object for each element', () => { - expect(typeof firstTimeSeries.linePath).toEqual('string'); - expect(typeof firstTimeSeries.areaPath).toEqual('string'); - expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function'); - expect(typeof firstTimeSeries.areaColor).toEqual('string'); - expect(typeof firstTimeSeries.lineColor).toEqual('string'); - expect(firstTimeSeries.values instanceof Array).toEqual(true); - }); - - it('createTimeSeries returns an array', () => { - expect(timeSeries instanceof Array).toEqual(true); - expect(timeSeries.length).toEqual(2); - }); -}); -- GitLab