Commit 1d1bebb3 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'dbodicherla-add-timerange-for-monitoring-panel-builder' into 'master'

Add time picker to monitoring preview panel

See merge request gitlab-org/gitlab!38837
parents 0a8d2390 ef753a5f
...@@ -9,6 +9,8 @@ import { ...@@ -9,6 +9,8 @@ import {
GlSprintf, GlSprintf,
GlAlert, GlAlert,
} from '@gitlab/ui'; } from '@gitlab/ui';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { timeRanges } from '~/vue_shared/constants';
import DashboardPanel from './dashboard_panel.vue'; import DashboardPanel from './dashboard_panel.vue';
const initialYml = `title: Go heap size const initialYml = `title: Go heap size
...@@ -30,6 +32,7 @@ export default { ...@@ -30,6 +32,7 @@ export default {
GlSprintf, GlSprintf,
GlAlert, GlAlert,
DashboardPanel, DashboardPanel,
DateTimePicker,
}, },
data() { data() {
return { return {
...@@ -41,20 +44,35 @@ export default { ...@@ -41,20 +44,35 @@ export default {
'panelPreviewIsLoading', 'panelPreviewIsLoading',
'panelPreviewError', 'panelPreviewError',
'panelPreviewGraphData', 'panelPreviewGraphData',
'panelPreviewTimeRange',
'panelPreviewIsShown',
'projectPath', 'projectPath',
'addDashboardDocumentationPath', 'addDashboardDocumentationPath',
]), ]),
}, },
methods: { methods: {
...mapActions('monitoringDashboard', ['fetchPanelPreview']), ...mapActions('monitoringDashboard', [
'fetchPanelPreview',
'fetchPanelPreviewMetrics',
'setPanelPreviewTimeRange',
]),
onSubmit() { onSubmit() {
this.fetchPanelPreview(this.yml); this.fetchPanelPreview(this.yml);
}, },
onDateTimePickerInput(timeRange) {
this.setPanelPreviewTimeRange(timeRange);
// refetch data only if preview has been clicked
// and there are no errors
if (this.panelPreviewIsShown && !this.panelPreviewError) {
this.fetchPanelPreviewMetrics();
}
},
}, },
timeRanges,
}; };
</script> </script>
<template> <template>
<div> <div class="prometheus-panel-builder">
<div class="gl-xs-flex-direction-column gl-display-flex gl-mx-n3"> <div class="gl-xs-flex-direction-column gl-display-flex gl-mx-n3">
<gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3"> <gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3">
<template #header> <template #header>
...@@ -151,7 +169,13 @@ export default { ...@@ -151,7 +169,13 @@ export default {
<gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false"> <gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
{{ panelPreviewError }} {{ panelPreviewError }}
</gl-alert> </gl-alert>
<date-time-picker
ref="dateTimePicker"
class="gl-flex-grow-1 preview-date-time-picker"
:value="panelPreviewTimeRange"
:options="$options.timeRanges"
@input="onDateTimePickerInput"
/>
<dashboard-panel :graph-data="panelPreviewGraphData" /> <dashboard-panel :graph-data="panelPreviewGraphData" />
</div> </div>
</template> </template>
...@@ -3,7 +3,7 @@ import statusCodes from '~/lib/utils/http_status'; ...@@ -3,7 +3,7 @@ import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils'; import { backOff } from '~/lib/utils/common_utils';
import { PROMETHEUS_TIMEOUT } from '../constants'; import { PROMETHEUS_TIMEOUT } from '../constants';
const backOffRequest = makeRequestCallback => const cancellableBackOffRequest = makeRequestCallback =>
backOff((next, stop) => { backOff((next, stop) => {
makeRequestCallback() makeRequestCallback()
.then(resp => { .then(resp => {
...@@ -13,16 +13,19 @@ const backOffRequest = makeRequestCallback => ...@@ -13,16 +13,19 @@ const backOffRequest = makeRequestCallback =>
stop(resp); stop(resp);
} }
}) })
.catch(stop); // If the request is cancelled by axios
// then consider it as noop so that its not
// caught by subsequent catches
.catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown)));
}, PROMETHEUS_TIMEOUT); }, PROMETHEUS_TIMEOUT);
export const getDashboard = (dashboardEndpoint, params) => export const getDashboard = (dashboardEndpoint, params) =>
backOffRequest(() => axios.get(dashboardEndpoint, { params })).then( cancellableBackOffRequest(() => axios.get(dashboardEndpoint, { params })).then(
axiosResponse => axiosResponse.data, axiosResponse => axiosResponse.data,
); );
export const getPrometheusQueryData = (prometheusEndpoint, params) => export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
backOffRequest(() => axios.get(prometheusEndpoint, { params })) cancellableBackOffRequest(() => axios.get(prometheusEndpoint, { params, ...opts }))
.then(axiosResponse => axiosResponse.data) .then(axiosResponse => axiosResponse.data)
.then(prometheusResponse => prometheusResponse.data) .then(prometheusResponse => prometheusResponse.data)
.catch(error => { .catch(error => {
......
...@@ -16,10 +16,12 @@ import getDashboardValidationWarnings from '../queries/getDashboardValidationWar ...@@ -16,10 +16,12 @@ import getDashboardValidationWarnings from '../queries/getDashboardValidationWar
import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import { getDashboard, getPrometheusQueryData } from '../requests'; import { getDashboard, getPrometheusQueryData } from '../requests';
import { defaultTimeRange } from '~/vue_shared/constants';
import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants'; import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
const axiosCancelToken = axios.CancelToken;
let cancelTokenSource;
function prometheusMetricQueryParams(timeRange) { function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange); const { start, end } = convertToFixedRange(timeRange);
...@@ -491,12 +493,18 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery ...@@ -491,12 +493,18 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
// Panel Builder // Panel Builder
export const setPanelPreviewTimeRange = ({ commit }, timeRange) => {
commit(types.SET_PANEL_PREVIEW_TIME_RANGE, timeRange);
};
export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => { export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => {
if (!panelPreviewYml) { if (!panelPreviewYml) {
return null; return null;
} }
commit(types.SET_PANEL_PREVIEW_IS_SHOWN, true);
commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml); commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml);
return axios return axios
.post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml }) .post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml })
.then(({ data }) => { .then(({ data }) => {
...@@ -510,7 +518,12 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) ...@@ -510,7 +518,12 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml)
}; };
export const fetchPanelPreviewMetrics = ({ state, commit }) => { export const fetchPanelPreviewMetrics = ({ state, commit }) => {
const defaultQueryParams = prometheusMetricQueryParams(defaultTimeRange); if (cancelTokenSource) {
cancelTokenSource.cancel();
}
cancelTokenSource = axiosCancelToken.source();
const defaultQueryParams = prometheusMetricQueryParams(state.panelPreviewTimeRange);
state.panelPreviewGraphData.metrics.forEach((metric, index) => { state.panelPreviewGraphData.metrics.forEach((metric, index) => {
commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index }); commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index });
...@@ -519,7 +532,9 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => { ...@@ -519,7 +532,9 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => {
if (metric.step) { if (metric.step) {
params.step = metric.step; params.step = metric.step;
} }
return getPrometheusQueryData(metric.prometheusEndpointPath, params) return getPrometheusQueryData(metric.prometheusEndpointPath, params, {
cancelToken: cancelTokenSource.token,
})
.then(data => { .then(data => {
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data }); commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data });
}) })
......
...@@ -57,3 +57,6 @@ export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS = ...@@ -57,3 +57,6 @@ export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS =
'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS'; 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS';
export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE = export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE =
'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE'; 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE';
export const SET_PANEL_PREVIEW_TIME_RANGE = 'SET_PANEL_PREVIEW_TIME_RANGE';
export const SET_PANEL_PREVIEW_IS_SHOWN = 'SET_PANEL_PREVIEW_IS_SHOWN';
...@@ -264,4 +264,10 @@ export default { ...@@ -264,4 +264,10 @@ export default {
metric.state = emptyStateFromError(error); metric.state = emptyStateFromError(error);
metric.result = null; metric.result = null;
}, },
[types.SET_PANEL_PREVIEW_TIME_RANGE](state, timeRange) {
state.panelPreviewTimeRange = timeRange;
},
[types.SET_PANEL_PREVIEW_IS_SHOWN](state, isPreviewShown) {
state.panelPreviewIsShown = isPreviewShown;
},
}; };
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { timezones } from '../format_date'; import { timezones } from '../format_date';
import { dashboardEmptyStates } from '../constants'; import { dashboardEmptyStates } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
export default () => ({ export default () => ({
// API endpoints // API endpoints
...@@ -66,6 +67,8 @@ export default () => ({ ...@@ -66,6 +67,8 @@ export default () => ({
panelPreviewIsLoading: false, panelPreviewIsLoading: false,
panelPreviewGraphData: null, panelPreviewGraphData: null,
panelPreviewError: null, panelPreviewError: null,
panelPreviewTimeRange: defaultTimeRange,
panelPreviewIsShown: false,
// Other project data // Other project data
dashboardTimezone: timezones.LOCAL, dashboardTimezone: timezones.LOCAL,
......
...@@ -340,3 +340,11 @@ ...@@ -340,3 +340,11 @@
opacity: 0; opacity: 0;
pointer-events: all; pointer-events: all;
} }
.prometheus-panel-builder {
.preview-date-time-picker {
// same as in .dropdown-menu-toggle
// see app/assets/stylesheets/framework/dropdowns.scss
width: 160px;
}
}
...@@ -4,8 +4,10 @@ import { createStore } from '~/monitoring/stores'; ...@@ -4,8 +4,10 @@ import { createStore } from '~/monitoring/stores';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { metricsDashboardResponse } from '../fixture_data'; import { metricsDashboardResponse } from '../fixture_data';
import { mockTimeRange } from '../mock_data';
import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue'; import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0]; const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0];
...@@ -37,6 +39,7 @@ describe('dashboard invalid url parameters', () => { ...@@ -37,6 +39,7 @@ describe('dashboard invalid url parameters', () => {
const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' }); const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' }); const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
const findPanel = () => wrapper.find(DashboardPanel); const findPanel = () => wrapper.find(DashboardPanel);
const findTimeRangePicker = () => wrapper.find(DateTimePicker);
beforeEach(() => { beforeEach(() => {
mockShowToast = jest.fn(); mockShowToast = jest.fn();
...@@ -110,6 +113,31 @@ describe('dashboard invalid url parameters', () => { ...@@ -110,6 +113,31 @@ describe('dashboard invalid url parameters', () => {
}); });
}); });
describe('time range picker', () => {
it('is visible by default', () => {
expect(findTimeRangePicker().exists()).toBe(true);
});
it('when changed does not trigger data fetch unless preview panel button is clicked', () => {
// mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
return wrapper.vm.$nextTick(() => {
expect(store.dispatch).not.toHaveBeenCalled();
});
});
it('when changed triggers data fetch if preview panel button is clicked', () => {
findForm().vm.$emit('submit', new Event('submit'));
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
return wrapper.vm.$nextTick(() => {
expect(store.dispatch).toHaveBeenCalled();
});
});
});
describe('instructions card', () => { describe('instructions card', () => {
const mockDocsPath = '/docs-path'; const mockDocsPath = '/docs-path';
const mockProjectPath = '/project-path'; const mockProjectPath = '/project-path';
...@@ -146,6 +174,14 @@ describe('dashboard invalid url parameters', () => { ...@@ -146,6 +174,14 @@ describe('dashboard invalid url parameters', () => {
it('displays an empty dashboard panel', () => { it('displays an empty dashboard panel', () => {
expect(findPanel().props('graphData')).toBe(null); expect(findPanel().props('graphData')).toBe(null);
}); });
it('changing time range should not refetch data', () => {
store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
return wrapper.vm.$nextTick(() => {
expect(store.dispatch).not.toHaveBeenCalled();
});
});
}); });
describe('when panel data is available', () => { describe('when panel data is available', () => {
......
...@@ -1183,6 +1183,7 @@ describe('Monitoring store actions', () => { ...@@ -1183,6 +1183,7 @@ describe('Monitoring store actions', () => {
mockYmlContent, mockYmlContent,
state, state,
[ [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent }, { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel }, { type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel },
], ],
...@@ -1200,6 +1201,7 @@ describe('Monitoring store actions', () => { ...@@ -1200,6 +1201,7 @@ describe('Monitoring store actions', () => {
}); });
testAction(fetchPanelPreview, mockYmlContent, state, [ testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent }, { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg }, { type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg },
]); ]);
...@@ -1209,6 +1211,7 @@ describe('Monitoring store actions', () => { ...@@ -1209,6 +1211,7 @@ describe('Monitoring store actions', () => {
mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500); mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500);
testAction(fetchPanelPreview, mockYmlContent, state, [ testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent }, { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ {
type: types.RECEIVE_PANEL_PREVIEW_FAILURE, type: types.RECEIVE_PANEL_PREVIEW_FAILURE,
......
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