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 {
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph">
<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>
<div v-gl-resize-observer-directive="onResize">
<gl-column-chart
ref="columnChart"
v-bind="$attrs"
......
......@@ -27,10 +27,7 @@ export default {
};
</script>
<template>
<div class="prometheus-graph d-flex flex-column justify-content-center">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5>
</div>
<div class="d-flex flex-column justify-content-center">
<div
class="prepend-top-8 svg-w-100 d-flex align-items-center"
:style="svgContainerStyle"
......
......@@ -2,13 +2,11 @@
import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlHeatmap } from '@gitlab/ui/dist/charts';
import dateformat from 'dateformat';
import PrometheusHeader from '../shared/prometheus_header.vue';
import { graphDataValidatorForValues } from '../../utils';
export default {
components: {
GlHeatmap,
PrometheusHeader,
},
directives: {
GlResizeObserverDirective,
......@@ -65,8 +63,7 @@ export default {
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph col-12 col-lg-6">
<prometheus-header :graph-title="graphData.title" />
<div v-gl-resize-observer-directive="onResize" class="col-12 col-lg-6">
<gl-heatmap
ref="heatmapChart"
v-bind="$attrs"
......
......@@ -42,10 +42,7 @@ export default {
};
</script>
<template>
<div class="prometheus-graph">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphTitle }}</h5>
</div>
<div>
<gl-single-stat :value="statValue" :title="graphTitle" variant="success" />
</div>
</template>
......@@ -81,11 +81,7 @@ export default {
};
</script>
<template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph">
<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>
<div v-gl-resize-observer-directive="onResize">
<gl-stacked-column-chart
ref="chart"
v-bind="$attrs"
......
......@@ -112,7 +112,6 @@ export default {
isDeployment: false,
sha: '',
},
showTitleTooltip: false,
width: 0,
height: chartHeight,
svgs: {},
......@@ -285,12 +284,6 @@ export default {
return `${this.graphData.y_label}`;
},
},
mounted() {
const graphTitleEl = this.$refs.graphTitle;
if (graphTitleEl && graphTitleEl.scrollWidth > graphTitleEl.offsetWidth) {
this.showTitleTooltip = true;
}
},
created() {
this.setSvg('rocket');
this.setSvg('scroll-handle');
......@@ -387,24 +380,7 @@ export default {
</script>
<template>
<div v-gl-resize-observer-directive="onResize" class="prometheus-graph">
<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>
<div v-gl-resize-observer-directive="onResize">
<component
:is="glChartComponent"
ref="chart"
......
......@@ -3,10 +3,12 @@ import { mapState } from 'vuex';
import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import {
GlResizeObserverDirective,
GlDropdown,
GlDropdownItem,
GlModal,
GlModalDirective,
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
import { __ } from '~/locale';
......@@ -29,11 +31,13 @@ export default {
MonitorStackedColumnChart,
MonitorEmptyChart,
Icon,
GlTooltip,
GlDropdown,
GlDropdownItem,
GlModal,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
......@@ -61,11 +65,15 @@ export default {
},
data() {
return {
showTitleTooltip: false,
zoomedTimeRange: null,
};
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']),
title() {
return this.graphData.title || '';
},
alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
......@@ -97,12 +105,24 @@ export default {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
monitorChartComponent() {
timeChartComponent() {
if (this.isPanelType('anomaly-chart')) {
return MonitorAnomalyChart;
}
return MonitorTimeSeriesChart;
},
isContextualMenuShown() {
return (
this.graphDataHasMetrics &&
!this.isPanelType('single-stat') &&
!this.isPanelType('heatmap') &&
!this.isPanelType('column') &&
!this.isPanelType('stacked-column')
);
},
},
mounted() {
this.refreshTitleTooltip();
},
methods: {
getGraphAlerts(queries) {
......@@ -119,9 +139,18 @@ export default {
showToast() {
this.$toast.show(__('Link copied'));
},
refreshTitleTooltip() {
const { graphTitle } = this.$refs;
this.showTitleTooltip =
Boolean(graphTitle) && graphTitle.scrollWidth > graphTitle.offsetWidth;
},
downloadCSVOptions,
generateLinkToChartOptions,
onResize() {
this.refreshTitleTooltip();
},
onDatazoom({ start, end }) {
this.zoomedTimeRange = { start, end };
},
......@@ -129,88 +158,109 @@ export default {
};
</script>
<template>
<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="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')"
<div v-gl-resize-observer="onResize" class="prometheus-graph">
<div class="prometheus-graph-header">
<h5
ref="graphTitle"
class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8"
>
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
{{ title }}
</h5>
<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
v-if="logsPathWithTimeRange"
ref="viewLogsLink"
:href="logsPathWithTimeRange"
>
{{ s__('Metrics|View logs') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="logsPathWithTimeRange"
ref="viewLogsLink"
:href="logsPathWithTimeRange"
>
{{ s__('Metrics|View logs') }}
</gl-dropdown-item>
<gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="clipboardText"
ref="copyChartLink"
v-track-event="generateLinkToChartOptions(clipboardText)"
:data-clipboard-text="clipboardText"
@click="showToast(clipboardText)"
>
{{ __('Generate link to chart') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}`"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
</gl-dropdown-item>
</gl-dropdown>
<gl-dropdown-item
v-if="csvText"
ref="downloadCsvLink"
v-track-event="downloadCSVOptions(title)"
:href="downloadCsv"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="clipboardText"
ref="copyChartLink"
v-track-event="generateLinkToChartOptions(clipboardText)"
:data-clipboard-text="clipboardText"
@click="showToast(clipboardText)"
>
{{ __('Generate link to chart') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}`"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
</gl-dropdown-item>
</gl-dropdown>
</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>
<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 @@
margin-bottom: $gl-padding-8;
}
.prometheus-graph-title {
font-size: $gl-font-size-large;
}
.alert-current-setting {
max-width: 240px;
}
......
......@@ -12,11 +12,8 @@ module QA
element :prometheus_graphs
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
element :prometheus_graph_widgets
element :prometheus_widgets_dropdown
element :alert_widget_menu_item
end
......
......@@ -11,7 +11,6 @@ import {
} from '../../mock_data';
import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
const mockWidgets = 'mockWidgets';
const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent
......@@ -35,9 +34,6 @@ describe('Anomaly chart component', () => {
const setupAnomalyChart = props => {
wrapper = shallowMount(Anomaly, {
propsData: { ...props },
slots: {
default: mockWidgets,
},
});
};
const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart);
......
......@@ -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', () => {
it('sets the height for the svg container', () => {
expect(emptyChart.vm.svgContainerStyle.height).toBe('300px');
......
......@@ -16,8 +16,6 @@ import {
} from '../../mock_data';
import * as iconUtils from '~/lib/utils/icon_utils';
const mockWidgets = 'mockWidgets';
const mockSvgPathContent = 'mockSvgPathContent';
jest.mock('lodash/throttle', () =>
......@@ -65,9 +63,6 @@ describe('Time series component', () => {
deploymentData: store.state.monitoringDashboard.deploymentData,
projectPath: `${mockHost}${mockProjectDir}`,
},
slots: {
default: mockWidgets,
},
store,
});
});
......@@ -82,14 +77,6 @@ describe('Time series component', () => {
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', () => {
timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' });
......
......@@ -74,6 +74,18 @@ describe('Panel Type component', () => {
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', () => {
expect(glEmptyChart.isVueInstance()).toBe(true);
});
......@@ -97,6 +109,15 @@ describe('Panel Type component', () => {
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', () => {
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