diff options
9 files changed, 711 insertions, 10 deletions
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 90c764587a3..78d97a3c122 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -12,6 +12,9 @@ import { graphDataValidatorForValues } from '../../utils'; let debouncedResize; +// TODO: Remove this component in favor of the more general time_series.vue +// Please port all changes here to time_series.vue as well. + export default { components: { GlAreaChart, diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue new file mode 100644 index 00000000000..2fdc75f63ca --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -0,0 +1,334 @@ +<script> +import { __ } from '~/locale'; +import { mapState } from 'vuex'; +import { GlLink, GlButton } from '@gitlab/ui'; +import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; +import dateFormat from 'dateformat'; +import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; +import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; +import Icon from '~/vue_shared/components/icon.vue'; +import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants'; +import { makeDataSeries } from '~/helpers/monitor_helper'; +import { graphDataValidatorForValues } from '../../utils'; + +let debouncedResize; + +export default { + components: { + GlAreaChart, + GlLineChart, + GlButton, + GlChartSeriesLabel, + GlLink, + Icon, + }, + inheritAttrs: false, + props: { + graphData: { + type: Object, + required: true, + validator: graphDataValidatorForValues.bind(null, false), + }, + containerWidth: { + type: Number, + required: true, + }, + deploymentData: { + type: Array, + required: false, + default: () => [], + }, + projectPath: { + type: String, + required: false, + default: '', + }, + showBorder: { + type: Boolean, + required: false, + default: false, + }, + thresholds: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + return { + tooltip: { + title: '', + content: [], + commitUrl: '', + isDeployment: false, + sha: '', + }, + width: 0, + height: chartHeight, + svgs: {}, + primaryColor: null, + }; + }, + computed: { + ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), + chartData() { + // Transforms & supplements query data to render appropriate labels & styles + // Input: [{ queryAttributes1 }, { queryAttributes2 }] + // Output: [{ seriesAttributes1 }, { seriesAttributes2 }] + return this.graphData.queries.reduce((acc, query) => { + const { appearance } = query; + const lineType = + appearance && appearance.line && appearance.line.type + ? appearance.line.type + : lineTypes.default; + const lineWidth = + appearance && appearance.line && appearance.line.width + ? appearance.line.width + : undefined; + const areaStyle = { + opacity: + appearance && appearance.area && typeof appearance.area.opacity === 'number' + ? appearance.area.opacity + : undefined, + }; + + const series = makeDataSeries(query.result, { + name: this.formatLegendLabel(query), + lineStyle: { + type: lineType, + width: lineWidth, + }, + showSymbol: false, + areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined, + }); + + return acc.concat(series); + }, []); + }, + chartOptions() { + return { + xAxis: { + name: __('Time'), + type: 'time', + axisLabel: { + formatter: date => dateFormat(date, dateFormats.timeOfDay), + }, + axisPointer: { + snap: true, + }, + }, + yAxis: { + name: this.yAxisLabel, + axisLabel: { + formatter: num => roundOffFloat(num, 3).toString(), + }, + }, + series: this.scatterSeries, + dataZoom: this.dataZoomConfig, + }; + }, + dataZoomConfig() { + const handleIcon = this.svgs['scroll-handle']; + + return handleIcon ? { handleIcon } : {}; + }, + earliestDatapoint() { + return this.chartData.reduce((acc, series) => { + const { data } = series; + const { length } = data; + if (!length) { + return acc; + } + + const [first] = data[0]; + const [last] = data[length - 1]; + const seriesEarliest = first < last ? first : last; + + return seriesEarliest < acc || acc === null ? seriesEarliest : acc; + }, null); + }, + glChartComponent() { + const chartTypes = { + 'area-chart': GlAreaChart, + 'line-chart': GlLineChart, + }; + return chartTypes[this.graphData.type] || GlAreaChart; + }, + isMultiSeries() { + return this.tooltip.content.length > 1; + }, + recentDeployments() { + return this.deploymentData.reduce((acc, deployment) => { + if (deployment.created_at >= this.earliestDatapoint) { + const { id, created_at, sha, ref, tag } = deployment; + acc.push({ + id, + createdAt: created_at, + sha, + commitUrl: `${this.projectPath}/commit/${sha}`, + tag, + tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null, + ref: ref.name, + showDeploymentFlag: false, + }); + } + + return acc; + }, []); + }, + scatterSeries() { + return { + type: graphTypes.deploymentData, + data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]), + symbol: this.svgs.rocket, + symbolSize: symbolSizes.default, + itemStyle: { + color: this.primaryColor, + }, + }; + }, + yAxisLabel() { + return `${this.graphData.y_label}`; + }, + csvText() { + const chartData = this.chartData[0].data; + const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadLink() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, + }, + watch: { + containerWidth: 'onResize', + }, + beforeDestroy() { + window.removeEventListener('resize', debouncedResize); + }, + created() { + debouncedResize = debounceByAnimationFrame(this.onResize); + window.addEventListener('resize', debouncedResize); + this.setSvg('rocket'); + this.setSvg('scroll-handle'); + }, + methods: { + formatLegendLabel(query) { + return `${query.label}`; + }, + formatTooltipText(params) { + this.tooltip.title = dateFormat(params.value, dateFormats.default); + this.tooltip.content = []; + params.seriesData.forEach(dataPoint => { + const [xVal, yVal] = dataPoint.value; + this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData; + if (this.tooltip.isDeployment) { + const [deploy] = this.recentDeployments.filter( + deployment => deployment.createdAt === xVal, + ); + this.tooltip.sha = deploy.sha.substring(0, 8); + this.tooltip.commitUrl = deploy.commitUrl; + } else { + const { seriesName, color } = dataPoint; + const value = yVal.toFixed(3); + this.tooltip.content.push({ + name: seriesName, + value, + color, + }); + } + }); + }, + setSvg(name) { + getSvgIconPathContent(name) + .then(path => { + if (path) { + this.$set(this.svgs, name, `path://${path}`); + } + }) + .catch(e => { + // eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings + console.error('SVG could not be rendered correctly: ', e); + }); + }, + onChartUpdated(chart) { + [this.primaryColor] = chart.getOption().color; + }, + onResize() { + if (!this.$refs.chart) return; + const { width } = this.$refs.chart.$el.getBoundingClientRect(); + this.width = width; + }, + }, +}; +</script> + +<template> + <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5> + <gl-button + v-if="exportMetricsToCsvEnabled" + :href="downloadLink" + :title="__('Download CSV')" + :aria-label="__('Download CSV')" + style="margin-left: 200px;" + download="chart_metrics.csv" + > + {{ __('Download CSV') }} + </gl-button> + <div class="prometheus-graph-widgets js-graph-widgets"> + <slot></slot> + </div> + </div> + + <component + :is="glChartComponent" + ref="chart" + v-bind="$attrs" + :data="chartData" + :option="chartOptions" + :format-tooltip-text="formatTooltipText" + :thresholds="thresholds" + :width="width" + :height="height" + @updated="onChartUpdated" + > + <template v-if="tooltip.isDeployment"> + <template slot="tooltipTitle"> + {{ __('Deployed') }} + </template> + <div slot="tooltipContent" class="d-flex align-items-center"> + <icon name="commit" class="mr-2" /> + <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> + </div> + </template> + <template v-else> + <template slot="tooltipTitle"> + <div class="text-nowrap"> + {{ tooltip.title }} + </div> + </template> + <template slot="tooltipContent"> + <div + v-for="(content, key) in tooltip.content" + :key="key" + class="d-flex justify-content-between" + > + <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> + {{ content.name }} + </gl-chart-series-label> + <div class="prepend-left-32"> + {{ content.value }} + </div> + </div> + </template> + </template> + </component> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index ebd610af7b6..d330ceb836c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -15,7 +15,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; -import MonitorAreaChart from './charts/area.vue'; +import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; @@ -26,7 +26,7 @@ let sidebarMutationObserver; export default { components: { - MonitorAreaChart, + MonitorTimeSeriesChart, MonitorSingleStatChart, PanelType, GraphGroup, @@ -465,7 +465,7 @@ export default { /> </template> <template v-else> - <monitor-area-chart + <monitor-time-series-chart v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)" :key="graphIndex" :graph-data="graphData" @@ -473,7 +473,7 @@ export default { :thresholds="getGraphAlertValues(graphData.queries)" :container-width="elWidth" :project-path="projectPath" - group-id="monitor-area-chart" + group-id="monitor-time-series-chart" > <div class="d-flex align-items-center"> <alert-widget @@ -515,7 +515,7 @@ export default { </gl-dropdown-item> </gl-dropdown> </div> - </monitor-area-chart> + </monitor-time-series-chart> </template> </graph-group> </div> diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index d7d89522732..13aba3d9f44 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -8,6 +8,10 @@ export const graphTypes = { deploymentData: 'scatter', }; +export const symbolSizes = { + default: 14, +}; + export const lineTypes = { default: 'solid', }; @@ -21,6 +25,11 @@ export const timeWindows = { oneWeek: __('1 week'), }; +export const dateFormats = { + timeOfDay: 'h:MM TT', + default: 'dd mmm yyyy, h:MMTT', +}; + export const secondsIn = { thirtyMinutes: 60 * 30, threeHours: 60 * 60 * 3, diff --git a/changelogs/unreleased/65412-add-support-for-line-charts.yml b/changelogs/unreleased/65412-add-support-for-line-charts.yml new file mode 100644 index 00000000000..cb9043596b7 --- /dev/null +++ b/changelogs/unreleased/65412-add-support-for-line-charts.yml @@ -0,0 +1,5 @@ +--- +title: Create component to display area and line charts in monitor dashboards +merge_request: 31639 +author: +type: added diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml index 32475ef8380..08504d6f7d5 100644 --- a/config/prometheus/common_metrics.yml +++ b/config/prometheus/common_metrics.yml @@ -166,7 +166,7 @@ panel_groups: label: Total (cores) unit: "cores" - title: "Memory Usage (Pod average)" - type: "area-chart" + type: "line-chart" y_label: "Memory Used per Pod (MB)" weight: 2 metrics: @@ -175,7 +175,7 @@ panel_groups: label: Pod average (MB) unit: MB - title: "Canary: Memory Usage (Pod Average)" - type: "area-chart" + type: "line-chart" y_label: "Memory Used per Pod (MB)" weight: 2 metrics: @@ -185,7 +185,7 @@ panel_groups: unit: MB track: canary - title: "Core Usage (Pod Average)" - type: "area-chart" + type: "line-chart" y_label: "Cores per Pod" weight: 1 metrics: @@ -194,7 +194,7 @@ panel_groups: label: Pod average (cores) unit: "cores" - title: "Canary: Core Usage (Pod Average)" - type: "area-chart" + type: "line-chart" y_label: "Cores per Pod" weight: 1 metrics: diff --git a/db/migrate/20190814205640_import_common_metrics_line_charts.rb b/db/migrate/20190814205640_import_common_metrics_line_charts.rb new file mode 100644 index 00000000000..1c28d686a42 --- /dev/null +++ b/db/migrate/20190814205640_import_common_metrics_line_charts.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ImportCommonMetricsLineCharts < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute + end + + def down + # no-op + end +end diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js new file mode 100644 index 00000000000..d145a64e8d0 --- /dev/null +++ b/spec/javascripts/monitoring/charts/time_series_spec.js @@ -0,0 +1,335 @@ +import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/monitoring/stores'; +import { GlLink } from '@gitlab/ui'; +import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; +import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; +import TimeSeries from '~/monitoring/components/charts/time_series.vue'; +import * as types from '~/monitoring/stores/mutation_types'; +import { TEST_HOST } from 'spec/test_constants'; +import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data'; + +describe('Time series component', () => { + const mockSha = 'mockSha'; + const mockWidgets = 'mockWidgets'; + const mockSvgPathContent = 'mockSvgPathContent'; + const projectPath = `${TEST_HOST}${mockProjectPath}`; + const commitUrl = `${projectPath}/commit/${mockSha}`; + let mockGraphData; + let makeTimeSeriesChart; + let spriteSpy; + let store; + + beforeEach(() => { + store = createStore(); + store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); + store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); + [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; + + makeTimeSeriesChart = (graphData, type) => + shallowMount(TimeSeries, { + propsData: { + graphData: { ...graphData, type }, + containerWidth: 0, + deploymentData: store.state.monitoringDashboard.deploymentData, + projectPath, + }, + slots: { + default: mockWidgets, + }, + sync: false, + store, + }); + + spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake( + () => new Promise(resolve => resolve(mockSvgPathContent)), + ); + }); + + describe('general functions', () => { + let timeSeriesChart; + + beforeEach(() => { + timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); + }); + + it('renders chart title', () => { + expect(timeSeriesChart.find('.js-graph-title').text()).toBe(mockGraphData.title); + }); + + it('contains graph widgets from slot', () => { + expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets); + }); + + describe('when exportMetricsToCsvEnabled is disabled', () => { + beforeEach(() => { + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); + }); + + it('does not render the Download CSV button', done => { + timeSeriesChart.vm.$nextTick(() => { + expect(timeSeriesChart.contains('glbutton-stub')).toBe(false); + done(); + }); + }); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + const mockDate = deploymentData[0].created_at; + const mockCommitUrl = deploymentData[0].commitUrl; + const generateSeriesData = type => ({ + seriesData: [ + { + seriesName: timeSeriesChart.vm.chartData[0].name, + componentSubType: type, + value: [mockDate, 5.55555], + seriesIndex: 0, + }, + ], + value: mockDate, + }); + + describe('when series is of line type', () => { + beforeEach(done => { + timeSeriesChart.vm.formatTooltipText(generateSeriesData('line')); + timeSeriesChart.vm.$nextTick(done); + }); + + it('formats tooltip title', () => { + expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip content', () => { + const name = 'Core Usage'; + const value = '5.556'; + const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); + + expect(seriesLabel.vm.color).toBe(''); + expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true); + expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]); + expect( + shallowWrapperContainsSlotText( + timeSeriesChart.find(GlAreaChart), + 'tooltipContent', + value, + ), + ).toBe(true); + }); + }); + + describe('when series is of scatter type', () => { + beforeEach(() => { + timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter')); + }); + + it('formats tooltip title', () => { + expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip sha', () => { + expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9'); + }); + + it('formats tooltip commit url', () => { + expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl); + }); + }); + }); + + describe('setSvg', () => { + const mockSvgName = 'mockSvgName'; + + beforeEach(done => { + timeSeriesChart.vm.setSvg(mockSvgName); + timeSeriesChart.vm.$nextTick(done); + }); + + it('gets svg path content', () => { + expect(spriteSpy).toHaveBeenCalledWith(mockSvgName); + }); + + it('sets svg path content', () => { + timeSeriesChart.vm.$nextTick(() => { + expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`); + }); + }); + }); + + describe('onResize', () => { + const mockWidth = 233; + + beforeEach(() => { + spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({ + width: mockWidth, + })); + timeSeriesChart.vm.onResize(); + }); + + it('sets area chart width', () => { + expect(timeSeriesChart.vm.width).toBe(mockWidth); + }); + }); + }); + + describe('computed', () => { + describe('chartData', () => { + let chartData; + const seriesData = () => chartData[0]; + + beforeEach(() => { + ({ chartData } = timeSeriesChart.vm); + }); + + it('utilizes all data points', () => { + const { values } = mockGraphData.queries[0].result[0]; + + expect(chartData.length).toBe(1); + expect(seriesData().data.length).toBe(values.length); + }); + + it('creates valid data', () => { + const { data } = seriesData(); + + expect( + data.filter( + ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number', + ).length, + ).toBe(data.length); + }); + + it('formats line width correctly', () => { + expect(chartData[0].lineStyle.width).toBe(2); + }); + }); + + describe('chartOptions', () => { + describe('yAxis formatter', () => { + let format; + + beforeEach(() => { + format = timeSeriesChart.vm.chartOptions.yAxis.axisLabel.formatter; + }); + + it('rounds to 3 decimal places', () => { + expect(format(0.88888)).toBe('0.889'); + }); + }); + }); + + describe('scatterSeries', () => { + it('utilizes deployment data', () => { + expect(timeSeriesChart.vm.scatterSeries.data).toEqual([ + ['2017-05-31T21:23:37.881Z', 0], + ['2017-05-30T20:08:04.629Z', 0], + ['2017-05-30T17:42:38.409Z', 0], + ]); + + expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14); + }); + }); + + describe('yAxisLabel', () => { + it('constructs a label for the chart y-axis', () => { + expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU'); + }); + }); + + describe('csvText', () => { + it('converts data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(timeSeriesChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadLink', () => { + it('produces a link to download metrics as csv', () => { + const link = timeSeriesChart.vm.downloadLink; + + expect(link).toContain('blob:'); + }); + }); + }); + + afterEach(() => { + timeSeriesChart.destroy(); + }); + }); + + describe('wrapped components', () => { + const glChartComponents = [ + { + chartType: 'area-chart', + component: GlAreaChart, + }, + { + chartType: 'line-chart', + component: GlLineChart, + }, + ]; + + glChartComponents.forEach(dynamicComponent => { + describe(`GitLab UI: ${dynamicComponent.chartType}`, () => { + let timeSeriesAreaChart; + let glChart; + + beforeEach(done => { + timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType); + glChart = timeSeriesAreaChart.find(dynamicComponent.component); + timeSeriesAreaChart.vm.$nextTick(done); + }); + + it('is a Vue instance', () => { + expect(glChart.exists()).toBe(true); + expect(glChart.isVueInstance()).toBe(true); + }); + + it('receives data properties needed for proper chart render', () => { + const props = glChart.props(); + + expect(props.data).toBe(timeSeriesAreaChart.vm.chartData); + expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions); + expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText); + expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds); + }); + + it('recieves a tooltip title', done => { + const mockTitle = 'mockTitle'; + timeSeriesAreaChart.vm.tooltip.title = mockTitle; + + timeSeriesAreaChart.vm.$nextTick(() => { + expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', mockTitle)).toBe(true); + done(); + }); + }); + + describe('when tooltip is showing deployment data', () => { + beforeEach(done => { + timeSeriesAreaChart.vm.tooltip.isDeployment = true; + timeSeriesAreaChart.vm.$nextTick(done); + }); + + it('uses deployment title', () => { + expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', 'Deployed')).toBe(true); + }); + + it('renders clickable commit sha in tooltip content', done => { + timeSeriesAreaChart.vm.tooltip.sha = mockSha; + timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl; + + timeSeriesAreaChart.vm.$nextTick(() => { + const commitLink = timeSeriesAreaChart.find(GlLink); + + expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true); + expect(commitLink.attributes('href')).toEqual(commitUrl); + done(); + }); + }); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 85e660d3925..17e7314e214 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -1,5 +1,7 @@ export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`; +export const mockProjectPath = '/frontend-fixtures/environments-project'; + export const metricsGroupsAPIResponse = { success: true, data: [ @@ -902,7 +904,7 @@ export const metricsDashboardResponse = { }, { title: 'Memory Usage (Pod average)', - type: 'area-chart', + type: 'line-chart', y_label: 'Memory Used per Pod', weight: 2, metrics: [ |