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