Commit c6302fe8 authored by Miguel Rincon's avatar Miguel Rincon

Refactor charts header to the panel wrapper

- Reduce duplication by moving the header to the panel header instead of
each chart.
- Ensure widget contextual menu is present in all charts, instead of
only the time series chart.
- Clean some css so the heading keeps the same style when embedded.
parent 7d0fae8a
...@@ -90,11 +90,7 @@ export default { ...@@ -90,11 +90,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph"> <div v-gl-resize-observer-directive="onResize">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-column-chart <gl-column-chart
ref="columnChart" ref="columnChart"
v-bind="$attrs" v-bind="$attrs"
......
...@@ -27,10 +27,7 @@ export default { ...@@ -27,10 +27,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="prometheus-graph d-flex flex-column justify-content-center"> <div class="d-flex flex-column justify-content-center">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5>
</div>
<div <div
class="prepend-top-8 svg-w-100 d-flex align-items-center" class="prepend-top-8 svg-w-100 d-flex align-items-center"
:style="svgContainerStyle" :style="svgContainerStyle"
......
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
import { GlResizeObserverDirective } from '@gitlab/ui'; import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlHeatmap } from '@gitlab/ui/dist/charts'; import { GlHeatmap } from '@gitlab/ui/dist/charts';
import dateformat from 'dateformat'; import dateformat from 'dateformat';
import PrometheusHeader from '../shared/prometheus_header.vue';
import { graphDataValidatorForValues } from '../../utils'; import { graphDataValidatorForValues } from '../../utils';
export default { export default {
components: { components: {
GlHeatmap, GlHeatmap,
PrometheusHeader,
}, },
directives: { directives: {
GlResizeObserverDirective, GlResizeObserverDirective,
...@@ -65,8 +63,7 @@ export default { ...@@ -65,8 +63,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph col-12 col-lg-6"> <div v-gl-resize-observer-directive="onResize" class="col-12 col-lg-6">
<prometheus-header :graph-title="graphData.title" />
<gl-heatmap <gl-heatmap
ref="heatmapChart" ref="heatmapChart"
v-bind="$attrs" v-bind="$attrs"
......
...@@ -42,10 +42,7 @@ export default { ...@@ -42,10 +42,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="prometheus-graph"> <div>
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5>
</div>
<gl-single-stat :value="statValue" :title="graphTitle" variant="success" /> <gl-single-stat :value="statValue" :title="graphTitle" variant="success" />
</div> </div>
</template> </template>
...@@ -81,11 +81,7 @@ export default { ...@@ -81,11 +81,7 @@ export default {
}; };
</script> </script>
<template> <template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph"> <div v-gl-resize-observer-directive="onResize">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-stacked-column-chart <gl-stacked-column-chart
ref="chart" ref="chart"
v-bind="$attrs" v-bind="$attrs"
......
...@@ -112,7 +112,6 @@ export default { ...@@ -112,7 +112,6 @@ export default {
isDeployment: false, isDeployment: false,
sha: '', sha: '',
}, },
showTitleTooltip: false,
width: 0, width: 0,
height: chartHeight, height: chartHeight,
svgs: {}, svgs: {},
...@@ -285,12 +284,6 @@ export default { ...@@ -285,12 +284,6 @@ export default {
return `${this.graphData.y_label}`; return `${this.graphData.y_label}`;
}, },
}, },
mounted() {
const graphTitleEl = this.$refs.graphTitle;
if (graphTitleEl && graphTitleEl.scrollWidth > graphTitleEl.offsetWidth) {
this.showTitleTooltip = true;
}
},
created() { created() {
this.setSvg('rocket'); this.setSvg('rocket');
this.setSvg('scroll-handle'); this.setSvg('scroll-handle');
...@@ -387,24 +380,7 @@ export default { ...@@ -387,24 +380,7 @@ export default {
</script> </script>
<template> <template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph"> <div v-gl-resize-observer-directive="onResize">
<div class="prometheus-graph-header">
<h5
ref="graphTitle"
class="prometheus-graph-title js-graph-title text-truncate append-right-8"
>
{{ graphData.title }}
</h5>
<gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ graphData.title }}
</gl-tooltip>
<div
class="prometheus-graph-widgets js-graph-widgets flex-fill"
data-qa-selector="prometheus_graph_widgets"
>
<slot></slot>
</div>
</div>
<component <component
:is="glChartComponent" :is="glChartComponent"
ref="chart" ref="chart"
......
...@@ -3,10 +3,12 @@ import { mapState } from 'vuex'; ...@@ -3,10 +3,12 @@ import { mapState } from 'vuex';
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { import {
GlResizeObserverDirective,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlModal, GlModal,
GlModalDirective, GlModalDirective,
GlTooltip,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -29,11 +31,13 @@ export default { ...@@ -29,11 +31,13 @@ export default {
MonitorStackedColumnChart, MonitorStackedColumnChart,
MonitorEmptyChart, MonitorEmptyChart,
Icon, Icon,
GlTooltip,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlModal, GlModal,
}, },
directives: { directives: {
GlResizeObserver: GlResizeObserverDirective,
GlModal: GlModalDirective, GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective, TrackEvent: TrackEventDirective,
...@@ -61,11 +65,15 @@ export default { ...@@ -61,11 +65,15 @@ export default {
}, },
data() { data() {
return { return {
showTitleTooltip: false,
zoomedTimeRange: null, zoomedTimeRange: null,
}; };
}, },
computed: { computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']), ...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']),
title() {
return this.graphData.title || '';
},
alertWidgetAvailable() { alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData; return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
}, },
...@@ -97,12 +105,24 @@ export default { ...@@ -97,12 +105,24 @@ 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);
}, },
monitorChartComponent() { timeChartComponent() {
if (this.isPanelType('anomaly-chart')) { if (this.isPanelType('anomaly-chart')) {
return MonitorAnomalyChart; return MonitorAnomalyChart;
} }
return MonitorTimeSeriesChart; return MonitorTimeSeriesChart;
}, },
isContextualMenuShown() {
return (
this.graphDataHasMetrics &&
!this.isPanelType('single-stat') &&
!this.isPanelType('heatmap') &&
!this.isPanelType('column') &&
!this.isPanelType('stacked-column')
);
},
},
mounted() {
this.refreshTitleTooltip();
}, },
methods: { methods: {
getGraphAlerts(queries) { getGraphAlerts(queries) {
...@@ -119,9 +139,18 @@ export default { ...@@ -119,9 +139,18 @@ export default {
showToast() { showToast() {
this.$toast.show(__('Link copied')); this.$toast.show(__('Link copied'));
}, },
refreshTitleTooltip() {
const { graphTitle } = this.$refs;
this.showTitleTooltip =
Boolean(graphTitle) && graphTitle.scrollWidth > graphTitle.offsetWidth;
},
downloadCSVOptions, downloadCSVOptions,
generateLinkToChartOptions, generateLinkToChartOptions,
onResize() {
this.refreshTitleTooltip();
},
onDatazoom({ start, end }) { onDatazoom({ start, end }) {
this.zoomedTimeRange = { start, end }; this.zoomedTimeRange = { start, end };
}, },
...@@ -129,88 +158,109 @@ export default { ...@@ -129,88 +158,109 @@ export default {
}; };
</script> </script>
<template> <template>
<monitor-single-stat-chart <div v-gl-resize-observer="onResize" class="prometheus-graph">
v-if="isPanelType('single-stat') && graphDataHasMetrics" <div class="prometheus-graph-header">
:graph-data="graphData" <h5
/> ref="graphTitle"
<monitor-heatmap-chart class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8"
v-else-if="isPanelType('heatmap') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-column-chart
v-else-if="isPanelType('column') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-stacked-column-chart
v-else-if="isPanelType('stacked-column') && graphDataHasMetrics"
:graph-data="graphData"
/>
<component
:is="monitorChartComponent"
v-else-if="graphDataHasMetrics"
ref="timeChart"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
@datazoom="onDatazoom"
>
<div class="d-flex align-items-center">
<alert-widget
v-if="alertWidgetAvailable && graphData"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<gl-dropdown
v-gl-tooltip
class="ml-auto mx-3"
toggle-class="btn btn-transparent border-0"
data-qa-selector="prometheus_widgets_dropdown"
:right="true"
:no-caret="true"
:title="__('More actions')"
> >
<template slot="button-content"> {{ title }}
<icon name="ellipsis_v" class="text-secondary" /> </h5>
</template> <gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ title }}
</gl-tooltip>
<div
v-if="isContextualMenuShown"
class="prometheus-graph-widgets js-graph-widgets flex-fill"
data-qa-selector="prometheus_graph_widgets"
>
<div class="d-flex align-items-center">
<alert-widget
v-if="alertWidgetAvailable && graphData"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<gl-dropdown
v-gl-tooltip
class="ml-auto mx-3"
toggle-class="btn btn-transparent border-0"
data-qa-selector="prometheus_widgets_dropdown"
right
no-caret
:title="__('More actions')"
>
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
<gl-dropdown-item <gl-dropdown-item
v-if="logsPathWithTimeRange" v-if="logsPathWithTimeRange"
ref="viewLogsLink" ref="viewLogsLink"
:href="logsPathWithTimeRange" :href="logsPathWithTimeRange"
> >
{{ s__('Metrics|View logs') }} {{ s__('Metrics|View logs') }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)" v-if="csvText"
:href="downloadCsv" ref="downloadCsvLink"
download="chart_metrics.csv" v-track-event="downloadCSVOptions(title)"
> :href="downloadCsv"
{{ __('Download CSV') }} download="chart_metrics.csv"
</gl-dropdown-item> >
<gl-dropdown-item {{ __('Download CSV') }}
v-if="clipboardText" </gl-dropdown-item>
ref="copyChartLink" <gl-dropdown-item
v-track-event="generateLinkToChartOptions(clipboardText)" v-if="clipboardText"
:data-clipboard-text="clipboardText" ref="copyChartLink"
@click="showToast(clipboardText)" v-track-event="generateLinkToChartOptions(clipboardText)"
> :data-clipboard-text="clipboardText"
{{ __('Generate link to chart') }} @click="showToast(clipboardText)"
</gl-dropdown-item> >
<gl-dropdown-item {{ __('Generate link to chart') }}
v-if="alertWidgetAvailable" </gl-dropdown-item>
v-gl-modal="`alert-modal-${index}`" <gl-dropdown-item
data-qa-selector="alert_widget_menu_item" v-if="alertWidgetAvailable"
> v-gl-modal="`alert-modal-${index}`"
{{ __('Alerts') }} data-qa-selector="alert_widget_menu_item"
</gl-dropdown-item> >
</gl-dropdown> {{ __('Alerts') }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
</div> </div>
</component>
<monitor-empty-chart v-else :graph-title="graphData.title" /> <monitor-single-stat-chart
v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-heatmap-chart
v-else-if="isPanelType('heatmap') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-column-chart
v-else-if="isPanelType('column') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-stacked-column-chart
v-else-if="isPanelType('stacked-column') && graphDataHasMetrics"
:graph-data="graphData"
/>
<component
:is="timeChartComponent"
v-else-if="graphDataHasMetrics"
ref="timeChart"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
@datazoom="onDatazoom"
/>
<monitor-empty-chart v-else :graph-title="title" v-bind="$attrs" v-on="$listeners" />
</div>
</template> </template>
<script>
export default {
props: {
graphTitle: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="prometheus-graph-header">
<h5 ref="title" class="prometheus-graph-title">{{ graphTitle }}</h5>
</div>
</template>
...@@ -91,10 +91,6 @@ ...@@ -91,10 +91,6 @@
margin-bottom: $gl-padding-8; margin-bottom: $gl-padding-8;
} }
.prometheus-graph-title {
font-size: $gl-font-size-large;
}
.alert-current-setting { .alert-current-setting {
max-width: 240px; max-width: 240px;
} }
......
...@@ -12,11 +12,8 @@ module QA ...@@ -12,11 +12,8 @@ module QA
element :prometheus_graphs element :prometheus_graphs
end end
view 'app/assets/javascripts/monitoring/components/charts/time_series.vue' do
element :prometheus_graph_widgets
end
view 'app/assets/javascripts/monitoring/components/panel_type.vue' do view 'app/assets/javascripts/monitoring/components/panel_type.vue' do
element :prometheus_graph_widgets
element :prometheus_widgets_dropdown element :prometheus_widgets_dropdown
element :alert_widget_menu_item element :alert_widget_menu_item
end end
......
...@@ -11,7 +11,6 @@ import { ...@@ -11,7 +11,6 @@ import {
} from '../../mock_data'; } from '../../mock_data';
import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
const mockWidgets = 'mockWidgets';
const mockProjectPath = `${TEST_HOST}${mockProjectDir}`; const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent
...@@ -35,9 +34,6 @@ describe('Anomaly chart component', () => { ...@@ -35,9 +34,6 @@ describe('Anomaly chart component', () => {
const setupAnomalyChart = props => { const setupAnomalyChart = props => {
wrapper = shallowMount(Anomaly, { wrapper = shallowMount(Anomaly, {
propsData: { ...props }, propsData: { ...props },
slots: {
default: mockWidgets,
},
}); });
}; };
const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart); const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart);
......
...@@ -13,14 +13,6 @@ describe('Empty Chart component', () => { ...@@ -13,14 +13,6 @@ describe('Empty Chart component', () => {
}); });
}); });
afterEach(() => {
emptyChart.destroy();
});
it('render the chart title', () => {
expect(emptyChart.find({ ref: 'graphTitle' }).text()).toBe(graphTitle);
});
describe('Computed props', () => { describe('Computed props', () => {
it('sets the height for the svg container', () => { it('sets the height for the svg container', () => {
expect(emptyChart.vm.svgContainerStyle.height).toBe('300px'); expect(emptyChart.vm.svgContainerStyle.height).toBe('300px');
......
...@@ -16,8 +16,6 @@ import { ...@@ -16,8 +16,6 @@ import {
} from '../../mock_data'; } from '../../mock_data';
import * as iconUtils from '~/lib/utils/icon_utils'; import * as iconUtils from '~/lib/utils/icon_utils';
const mockWidgets = 'mockWidgets';
const mockSvgPathContent = 'mockSvgPathContent'; const mockSvgPathContent = 'mockSvgPathContent';
jest.mock('lodash/throttle', () => jest.mock('lodash/throttle', () =>
...@@ -65,9 +63,6 @@ describe('Time series component', () => { ...@@ -65,9 +63,6 @@ describe('Time series component', () => {
deploymentData: store.state.monitoringDashboard.deploymentData, deploymentData: store.state.monitoringDashboard.deploymentData,
projectPath: `${mockHost}${mockProjectDir}`, projectPath: `${mockHost}${mockProjectDir}`,
}, },
slots: {
default: mockWidgets,
},
store, store,
}); });
}); });
...@@ -82,14 +77,6 @@ describe('Time series component', () => { ...@@ -82,14 +77,6 @@ describe('Time series component', () => {
timeSeriesChart.vm.$nextTick(done); timeSeriesChart.vm.$nextTick(done);
}); });
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);
});
it('allows user to override max value label text using prop', () => { it('allows user to override max value label text using prop', () => {
timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' }); timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
......
...@@ -74,6 +74,18 @@ describe('Panel Type component', () => { ...@@ -74,6 +74,18 @@ describe('Panel Type component', () => {
glEmptyChart = wrapper.find(EmptyChart); glEmptyChart = wrapper.find(EmptyChart);
}); });
it('renders the chart title', () => {
expect(wrapper.find({ ref: 'graphTitle' }).text()).toBe(graphDataNoResult.title);
});
it('renders the no download csv link', () => {
expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
});
it('does not contain graph widgets', () => {
expect(wrapper.find('.js-graph-widgets').exists()).toBe(false);
});
it('is a Vue instance', () => { it('is a Vue instance', () => {
expect(glEmptyChart.isVueInstance()).toBe(true); expect(glEmptyChart.isVueInstance()).toBe(true);
}); });
...@@ -97,6 +109,15 @@ describe('Panel Type component', () => { ...@@ -97,6 +109,15 @@ describe('Panel Type component', () => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders the chart title', () => {
expect(wrapper.find({ ref: 'graphTitle' }).text()).toBe(graphDataPrometheusQueryRange.title);
});
it('contains graph widgets', () => {
expect(wrapper.find('.js-graph-widgets').exists()).toBe(true);
expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true);
});
it('sets no clipboard copy link on dropdown by default', () => { it('sets no clipboard copy link on dropdown by default', () => {
expect(findCopyLink().exists()).toBe(false); expect(findCopyLink().exists()).toBe(false);
}); });
......
import { shallowMount } from '@vue/test-utils';
import PrometheusHeader from '~/monitoring/components/shared/prometheus_header.vue';
describe('Prometheus Header component', () => {
let prometheusHeader;
beforeEach(() => {
prometheusHeader = shallowMount(PrometheusHeader, {
propsData: {
graphTitle: 'graph header',
},
});
});
afterEach(() => {
prometheusHeader.destroy();
});
describe('Prometheus header component', () => {
it('should show a title', () => {
const title = prometheusHeader.find({ ref: 'title' }).text();
expect(title).toBe('graph header');
});
});
});
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