Commit 5c1256a3 authored by Mike Greiling's avatar Mike Greiling

Merge branch '214882-render-single-panel-prep' into 'master'

Extend panel with "go back" slot & height

Closes #214882

See merge request gitlab-org/gitlab!30252
parents 6e557832 77fd3f3b
...@@ -64,10 +64,10 @@ export default { ...@@ -64,10 +64,10 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
singleEmbed: { height: {
type: Boolean, type: Number,
required: false, required: false,
default: false, default: chartHeight,
}, },
thresholds: { thresholds: {
type: Array, type: Array,
...@@ -100,7 +100,6 @@ export default { ...@@ -100,7 +100,6 @@ export default {
sha: '', sha: '',
}, },
width: 0, width: 0,
height: chartHeight,
svgs: {}, svgs: {},
primaryColor: null, primaryColor: null,
throttledDatazoom: null, throttledDatazoom: null,
......
...@@ -31,16 +31,12 @@ import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from ' ...@@ -31,16 +31,12 @@ import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '
const events = { const events = {
timeRangeZoom: 'timerangezoom', timeRangeZoom: 'timerangezoom',
expand: 'expand',
}; };
export default { export default {
components: { components: {
MonitorEmptyChart, MonitorEmptyChart,
MonitorSingleStatChart,
MonitorHeatmapChart,
MonitorColumnChart,
MonitorBarChart,
MonitorStackedColumnChart,
AlertWidget, AlertWidget,
GlIcon, GlIcon,
GlLoadingIcon, GlLoadingIcon,
...@@ -65,11 +61,6 @@ export default { ...@@ -65,11 +61,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
index: {
type: String,
required: false,
default: '',
},
groupId: { groupId: {
type: String, type: String,
required: false, required: false,
...@@ -96,6 +87,7 @@ export default { ...@@ -96,6 +87,7 @@ export default {
showTitleTooltip: false, showTitleTooltip: false,
zoomedTimeRange: null, zoomedTimeRange: null,
allAlerts: {}, allAlerts: {},
expandBtnAvailable: Boolean(this.$listeners[events.expand]),
}; };
}, },
computed: { computed: {
...@@ -156,20 +148,55 @@ export default { ...@@ -156,20 +148,55 @@ export default {
const data = new Blob([this.csvText], { type: 'text/plain' }); const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data); return window.URL.createObjectURL(data);
}, },
timeChartComponent() {
/**
* A chart is "basic" if it doesn't support
* the same features as the TimeSeries based components
* such as "annotations".
*
* @returns Vue Component wrapping a basic visualization
*/
basicChartComponent() {
if (this.isPanelType(panelTypes.SINGLE_STAT)) {
return MonitorSingleStatChart;
}
if (this.isPanelType(panelTypes.HEATMAP)) {
return MonitorHeatmapChart;
}
if (this.isPanelType(panelTypes.BAR)) {
return MonitorBarChart;
}
if (this.isPanelType(panelTypes.COLUMN)) {
return MonitorColumnChart;
}
if (this.isPanelType(panelTypes.STACKED_COLUMN)) {
return MonitorStackedColumnChart;
}
if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
return MonitorAnomalyChart;
}
return null;
},
/**
* In monitoring, Time Series charts typically support
* a larger feature set like "annotations", "deployment
* data", alert "thresholds" and "datazoom".
*
* This is intentional as Time Series are more frequently
* used.
*
* @returns Vue Component wrapping a time series visualization,
* Area Charts are rendered by default.
*/
timeSeriesChartComponent() {
if (this.isPanelType(panelTypes.ANOMALY_CHART)) { if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
return MonitorAnomalyChart; return MonitorAnomalyChart;
} }
return MonitorTimeSeriesChart; return MonitorTimeSeriesChart;
}, },
isContextualMenuShown() { isContextualMenuShown() {
return ( return Boolean(this.graphDataHasResult && !this.basicChartComponent);
this.graphDataHasResult &&
!this.isPanelType(panelTypes.SINGLE_STAT) &&
!this.isPanelType(panelTypes.HEATMAP) &&
!this.isPanelType(panelTypes.COLUMN) &&
!this.isPanelType(panelTypes.STACKED_COLUMN)
);
}, },
editCustomMetricLink() { editCustomMetricLink() {
return this.graphData?.metrics[0].edit_path; return this.graphData?.metrics[0].edit_path;
...@@ -224,6 +251,9 @@ export default { ...@@ -224,6 +251,9 @@ export default {
this.zoomedTimeRange = { start, end }; this.zoomedTimeRange = { start, end };
this.$emit(events.timeRangeZoom, { start, end }); this.$emit(events.timeRangeZoom, { start, end });
}, },
onExpand() {
this.$emit(events.expand);
},
setAlerts(alertPath, alertAttributes) { setAlerts(alertPath, alertAttributes) {
if (alertAttributes) { if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes); this.$set(this.allAlerts, alertPath, alertAttributes);
...@@ -238,6 +268,7 @@ export default { ...@@ -238,6 +268,7 @@ export default {
<template> <template>
<div v-gl-resize-observer="onResize" class="prometheus-graph"> <div v-gl-resize-observer="onResize" class="prometheus-graph">
<div class="d-flex align-items-center mr-3"> <div class="d-flex align-items-center mr-3">
<slot name="topLeft"></slot>
<h5 <h5
ref="graphTitle" ref="graphTitle"
class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8" class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8"
...@@ -250,7 +281,7 @@ export default { ...@@ -250,7 +281,7 @@ export default {
<alert-widget <alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable" v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1" class="mx-1"
:modal-id="`alert-modal-${index}`" :modal-id="`alert-modal-${graphData.id}`"
:alerts-endpoint="alertsEndpoint" :alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics" :relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)" :alerts-to-manage="getGraphAlerts(graphData.metrics)"
...@@ -277,6 +308,9 @@ export default { ...@@ -277,6 +308,9 @@ export default {
<template slot="button-content"> <template slot="button-content">
<gl-icon name="ellipsis_v" class="text-secondary" /> <gl-icon name="ellipsis_v" class="text-secondary" />
</template> </template>
<gl-dropdown-item v-if="expandBtnAvailable" ref="expandBtn" @click="onExpand">
{{ s__('Metrics|Expand panel') }}
</gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-if="editCustomMetricLink" v-if="editCustomMetricLink"
ref="editMetricLink" ref="editMetricLink"
...@@ -312,7 +346,7 @@ export default { ...@@ -312,7 +346,7 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-if="alertWidgetAvailable" v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}`" v-gl-modal="`alert-modal-${graphData.id}`"
data-qa-selector="alert_widget_menu_item" data-qa-selector="alert_widget_menu_item"
> >
{{ __('Alerts') }} {{ __('Alerts') }}
...@@ -322,38 +356,27 @@ export default { ...@@ -322,38 +356,27 @@ export default {
</div> </div>
</div> </div>
<monitor-single-stat-chart <monitor-empty-chart v-if="!graphDataHasResult" />
v-if="isPanelType($options.panelTypes.SINGLE_STAT) && graphDataHasResult" <component
:graph-data="graphData" :is="basicChartComponent"
/> v-else-if="basicChartComponent"
<monitor-heatmap-chart
v-else-if="isPanelType($options.panelTypes.HEATMAP) && graphDataHasResult"
:graph-data="graphData"
/>
<monitor-bar-chart
v-else-if="isPanelType($options.panelTypes.BAR) && graphDataHasResult"
:graph-data="graphData"
/>
<monitor-column-chart
v-else-if="isPanelType($options.panelTypes.COLUMN) && graphDataHasResult"
:graph-data="graphData"
/>
<monitor-stacked-column-chart
v-else-if="isPanelType($options.panelTypes.STACKED_COLUMN) && graphDataHasResult"
:graph-data="graphData" :graph-data="graphData"
v-bind="$attrs"
v-on="$listeners"
/> />
<component <component
:is="timeChartComponent" :is="timeSeriesChartComponent"
v-else-if="graphDataHasResult" v-else
ref="timeChart" ref="timeSeriesChart"
:graph-data="graphData" :graph-data="graphData"
:deployment-data="deploymentData" :deployment-data="deploymentData"
:annotations="annotations" :annotations="annotations"
:project-path="projectPath" :project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)" :thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId" :group-id="groupId"
v-bind="$attrs"
v-on="$listeners"
@datazoom="onDatazoom" @datazoom="onDatazoom"
/> />
<monitor-empty-chart v-else v-bind="$attrs" v-on="$listeners" />
</div> </div>
</template> </template>
...@@ -144,6 +144,7 @@ const mapYAxisToViewModel = ({ ...@@ -144,6 +144,7 @@ const mapYAxisToViewModel = ({
* @returns {Object} * @returns {Object}
*/ */
const mapPanelToViewModel = ({ const mapPanelToViewModel = ({
id = null,
title = '', title = '',
type, type,
x_axis = {}, x_axis = {},
...@@ -162,6 +163,7 @@ const mapPanelToViewModel = ({ ...@@ -162,6 +163,7 @@ const mapPanelToViewModel = ({
const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase
return { return {
id,
title, title,
type, type,
xLabel: xAxis.name, xLabel: xAxis.name,
......
...@@ -13057,6 +13057,9 @@ msgid_plural "Metrics|Edit metrics" ...@@ -13057,6 +13057,9 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics" msgid "Metrics|For grouping similar metrics"
msgstr "" msgstr ""
......
import { mount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout'; import { setTestTimeout } from 'helpers/timeout';
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'jest/helpers/test_constants'; import { TEST_HOST } from 'jest/helpers/test_constants';
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import { panelTypes } from '~/monitoring/constants'; import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue'; import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data'; import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
...@@ -40,10 +40,10 @@ describe('Time series component', () => { ...@@ -40,10 +40,10 @@ describe('Time series component', () => {
let mockGraphData; let mockGraphData;
let store; let store;
const makeTimeSeriesChart = (graphData, type) => const createWrapper = (graphData = mockGraphData, mountingMethod = shallowMount) =>
mount(TimeSeries, { mountingMethod(TimeSeries, {
propsData: { propsData: {
graphData: { ...graphData, type }, graphData,
deploymentData: store.state.monitoringDashboard.deploymentData, deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations, annotations: store.state.monitoringDashboard.annotations,
projectPath: `${TEST_HOST}${mockProjectDir}`, projectPath: `${TEST_HOST}${mockProjectDir}`,
...@@ -80,9 +80,9 @@ describe('Time series component', () => { ...@@ -80,9 +80,9 @@ describe('Time series component', () => {
const findChart = () => timeSeriesChart.find({ ref: 'chart' }); const findChart = () => timeSeriesChart.find({ ref: 'chart' });
beforeEach(done => { beforeEach(() => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); timeSeriesChart = createWrapper(mockGraphData, mount);
timeSeriesChart.vm.$nextTick(done); return timeSeriesChart.vm.$nextTick();
}); });
it('allows user to override max value label text using prop', () => { it('allows user to override max value label text using prop', () => {
...@@ -101,6 +101,21 @@ describe('Time series component', () => { ...@@ -101,6 +101,21 @@ describe('Time series component', () => {
}); });
}); });
it('chart sets a default height', () => {
const wrapper = createWrapper();
expect(wrapper.props('height')).toBe(chartHeight);
});
it('chart has a configurable height', () => {
const mockHeight = 599;
const wrapper = createWrapper();
wrapper.setProps({ height: mockHeight });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.props('height')).toBe(mockHeight);
});
});
describe('events', () => { describe('events', () => {
describe('datazoom', () => { describe('datazoom', () => {
let eChartMock; let eChartMock;
...@@ -126,7 +141,7 @@ describe('Time series component', () => { ...@@ -126,7 +141,7 @@ describe('Time series component', () => {
}), }),
}; };
timeSeriesChart = makeTimeSeriesChart(mockGraphData); timeSeriesChart = createWrapper(mockGraphData, mount);
timeSeriesChart.vm.$nextTick(() => { timeSeriesChart.vm.$nextTick(() => {
findChart().vm.$emit('created', eChartMock); findChart().vm.$emit('created', eChartMock);
done(); done();
...@@ -551,7 +566,10 @@ describe('Time series component', () => { ...@@ -551,7 +566,10 @@ describe('Time series component', () => {
const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component); const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
beforeEach(done => { beforeEach(done => {
timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType); timeSeriesAreaChart = createWrapper(
{ ...mockGraphData, type: dynamicComponent.chartType },
mount,
);
timeSeriesAreaChart.vm.$nextTick(done); timeSeriesAreaChart.vm.$nextTick(done);
}); });
...@@ -633,7 +651,7 @@ describe('Time series component', () => { ...@@ -633,7 +651,7 @@ describe('Time series component', () => {
Object.assign(metric, { result: metricResultStatus.result }), Object.assign(metric, { result: metricResultStatus.result }),
); );
timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart'); timeSeriesChart = createWrapper({ ...graphData, type: 'area-chart' }, mount);
timeSeriesChart.vm.$nextTick(done); timeSeriesChart.vm.$nextTick(done);
}); });
......
...@@ -52,11 +52,11 @@ describe('Dashboard Panel', () => { ...@@ -52,11 +52,11 @@ describe('Dashboard Panel', () => {
const exampleText = 'example_text'; const exampleText = 'example_text';
const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' }); const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
const findTimeChart = () => wrapper.find({ ref: 'timeChart' }); const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' });
const findTitle = () => wrapper.find({ ref: 'graphTitle' }); const findTitle = () => wrapper.find({ ref: 'graphTitle' });
const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' }); const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' });
const createWrapper = props => { const createWrapper = (props, options = {}) => {
wrapper = shallowMount(DashboardPanel, { wrapper = shallowMount(DashboardPanel, {
propsData: { propsData: {
graphData, graphData,
...@@ -64,6 +64,7 @@ describe('Dashboard Panel', () => { ...@@ -64,6 +64,7 @@ describe('Dashboard Panel', () => {
}, },
store, store,
mocks, mocks,
...options,
}); });
}; };
...@@ -80,6 +81,22 @@ describe('Dashboard Panel', () => { ...@@ -80,6 +81,22 @@ describe('Dashboard Panel', () => {
axiosMock.reset(); axiosMock.reset();
}); });
describe('Renders slots', () => {
it('renders "topLeft" slot', () => {
createWrapper(
{},
{
slots: {
topLeft: `<div class="top-left-content">OK</div>`,
},
},
);
expect(wrapper.find('.top-left-content').exists()).toBe(true);
expect(wrapper.find('.top-left-content').text()).toBe('OK');
});
});
describe('When no graphData is available', () => { describe('When no graphData is available', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ createWrapper({
...@@ -111,7 +128,7 @@ describe('Dashboard Panel', () => { ...@@ -111,7 +128,7 @@ describe('Dashboard Panel', () => {
}); });
}); });
describe('when graph data is available', () => { describe('When graphData is available', () => {
beforeEach(() => { beforeEach(() => {
createWrapper(); createWrapper();
}); });
...@@ -182,10 +199,13 @@ describe('Dashboard Panel', () => { ...@@ -182,10 +199,13 @@ describe('Dashboard Panel', () => {
${singleStatMetricsResult} | ${MonitorSingleStatChart} ${singleStatMetricsResult} | ${MonitorSingleStatChart}
${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart} ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart}
${barMockData} | ${MonitorBarChart} ${barMockData} | ${MonitorBarChart}
`('type $data.type renders the expected component', ({ data, component }) => { `('wrapps a $data.type component binding attributes', ({ data, component }) => {
createWrapper({ graphData: data }); const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
createWrapper({ graphData: data }, { attrs });
expect(wrapper.find(component).exists()).toBe(true); expect(wrapper.find(component).exists()).toBe(true);
expect(wrapper.find(component).isVueInstance()).toBe(true); expect(wrapper.find(component).isVueInstance()).toBe(true);
expect(wrapper.find(component).attributes()).toMatchObject(attrs);
}); });
}); });
}); });
...@@ -436,6 +456,32 @@ describe('Dashboard Panel', () => { ...@@ -436,6 +456,32 @@ describe('Dashboard Panel', () => {
}); });
}); });
describe('Expand to full screen', () => {
const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' });
describe('when there is no @expand listener', () => {
it('does not show `View full screen` option', () => {
createWrapper();
expect(findExpandBtn().exists()).toBe(false);
});
});
describe('when there is an @expand listener', () => {
beforeEach(() => {
createWrapper({}, { listeners: { expand: () => {} } });
});
it('shows the `expand` option', () => {
expect(findExpandBtn().exists()).toBe(true);
});
it('emits the `expand` event', () => {
findExpandBtn().vm.$emit('click');
expect(wrapper.emitted('expand')).toHaveLength(1);
});
});
});
describe('panel alerts', () => { describe('panel alerts', () => {
const setMetricsSavedToDb = val => const setMetricsSavedToDb = val =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val); monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
......
...@@ -27,6 +27,7 @@ describe('mapToDashboardViewModel', () => { ...@@ -27,6 +27,7 @@ describe('mapToDashboardViewModel', () => {
group: 'Group 1', group: 'Group 1',
panels: [ panels: [
{ {
id: 'ID_ABC',
title: 'Title A', title: 'Title A',
xLabel: '', xLabel: '',
xAxis: { xAxis: {
...@@ -49,6 +50,7 @@ describe('mapToDashboardViewModel', () => { ...@@ -49,6 +50,7 @@ describe('mapToDashboardViewModel', () => {
key: 'group-1-0', key: 'group-1-0',
panels: [ panels: [
{ {
id: 'ID_ABC',
title: 'Title A', title: 'Title A',
type: 'chart-type', type: 'chart-type',
xLabel: '', xLabel: '',
...@@ -127,11 +129,13 @@ describe('mapToDashboardViewModel', () => { ...@@ -127,11 +129,13 @@ describe('mapToDashboardViewModel', () => {
it('panel with x_label', () => { it('panel with x_label', () => {
setupWithPanel({ setupWithPanel({
id: 'ID_123',
title: panelTitle, title: panelTitle,
x_label: 'x label', x_label: 'x label',
}); });
expect(getMappedPanel()).toEqual({ expect(getMappedPanel()).toEqual({
id: 'ID_123',
title: panelTitle, title: panelTitle,
xLabel: 'x label', xLabel: 'x label',
xAxis: { xAxis: {
...@@ -149,10 +153,12 @@ describe('mapToDashboardViewModel', () => { ...@@ -149,10 +153,12 @@ describe('mapToDashboardViewModel', () => {
it('group y_axis defaults', () => { it('group y_axis defaults', () => {
setupWithPanel({ setupWithPanel({
id: 'ID_456',
title: panelTitle, title: panelTitle,
}); });
expect(getMappedPanel()).toEqual({ expect(getMappedPanel()).toEqual({
id: 'ID_456',
title: panelTitle, title: panelTitle,
xLabel: '', xLabel: '',
y_label: '', y_label: '',
......
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