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 {
GlSprintf,
GlAlert,
} 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';
const initialYml = `title: Go heap size
......@@ -30,6 +32,7 @@ export default {
GlSprintf,
GlAlert,
DashboardPanel,
DateTimePicker,
},
data() {
return {
......@@ -41,20 +44,35 @@ export default {
'panelPreviewIsLoading',
'panelPreviewError',
'panelPreviewGraphData',
'panelPreviewTimeRange',
'panelPreviewIsShown',
'projectPath',
'addDashboardDocumentationPath',
]),
},
methods: {
...mapActions('monitoringDashboard', ['fetchPanelPreview']),
...mapActions('monitoringDashboard', [
'fetchPanelPreview',
'fetchPanelPreviewMetrics',
'setPanelPreviewTimeRange',
]),
onSubmit() {
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>
<template>
<div>
<div class="prometheus-panel-builder">
<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">
<template #header>
......@@ -151,7 +169,13 @@ export default {
<gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
{{ panelPreviewError }}
</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" />
</div>
</template>
......@@ -3,7 +3,7 @@ import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
import { PROMETHEUS_TIMEOUT } from '../constants';
const backOffRequest = makeRequestCallback =>
const cancellableBackOffRequest = makeRequestCallback =>
backOff((next, stop) => {
makeRequestCallback()
.then(resp => {
......@@ -13,16 +13,19 @@ const backOffRequest = makeRequestCallback =>
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);
export const getDashboard = (dashboardEndpoint, params) =>
backOffRequest(() => axios.get(dashboardEndpoint, { params })).then(
cancellableBackOffRequest(() => axios.get(dashboardEndpoint, { params })).then(
axiosResponse => axiosResponse.data,
);
export const getPrometheusQueryData = (prometheusEndpoint, params) =>
backOffRequest(() => axios.get(prometheusEndpoint, { params }))
export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
cancellableBackOffRequest(() => axios.get(prometheusEndpoint, { params, ...opts }))
.then(axiosResponse => axiosResponse.data)
.then(prometheusResponse => prometheusResponse.data)
.catch(error => {
......
......@@ -16,10 +16,12 @@ import getDashboardValidationWarnings from '../queries/getDashboardValidationWar
import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { getDashboard, getPrometheusQueryData } from '../requests';
import { defaultTimeRange } from '~/vue_shared/constants';
import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
const axiosCancelToken = axios.CancelToken;
let cancelTokenSource;
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
......@@ -491,12 +493,18 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
// Panel Builder
export const setPanelPreviewTimeRange = ({ commit }, timeRange) => {
commit(types.SET_PANEL_PREVIEW_TIME_RANGE, timeRange);
};
export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => {
if (!panelPreviewYml) {
return null;
}
commit(types.SET_PANEL_PREVIEW_IS_SHOWN, true);
commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml);
return axios
.post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml })
.then(({ data }) => {
......@@ -510,7 +518,12 @@ export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml)
};
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) => {
commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index });
......@@ -519,7 +532,9 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => {
if (metric.step) {
params.step = metric.step;
}
return getPrometheusQueryData(metric.prometheusEndpointPath, params)
return getPrometheusQueryData(metric.prometheusEndpointPath, params, {
cancelToken: cancelTokenSource.token,
})
.then(data => {
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data });
})
......
......@@ -57,3 +57,6 @@ export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS =
'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS';
export const 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 {
metric.state = emptyStateFromError(error);
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 { timezones } from '../format_date';
import { dashboardEmptyStates } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
export default () => ({
// API endpoints
......@@ -66,6 +67,8 @@ export default () => ({
panelPreviewIsLoading: false,
panelPreviewGraphData: null,
panelPreviewError: null,
panelPreviewTimeRange: defaultTimeRange,
panelPreviewIsShown: false,
// Other project data
dashboardTimezone: timezones.LOCAL,
......
......@@ -340,3 +340,11 @@
opacity: 0;
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';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { metricsDashboardResponse } from '../fixture_data';
import { mockTimeRange } from '../mock_data';
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];
......@@ -37,6 +39,7 @@ describe('dashboard invalid url parameters', () => {
const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
const findPanel = () => wrapper.find(DashboardPanel);
const findTimeRangePicker = () => wrapper.find(DateTimePicker);
beforeEach(() => {
mockShowToast = jest.fn();
......@@ -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', () => {
const mockDocsPath = '/docs-path';
const mockProjectPath = '/project-path';
......@@ -146,6 +174,14 @@ describe('dashboard invalid url parameters', () => {
it('displays an empty dashboard panel', () => {
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', () => {
......
......@@ -1183,6 +1183,7 @@ describe('Monitoring store actions', () => {
mockYmlContent,
state,
[
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel },
],
......@@ -1200,6 +1201,7 @@ describe('Monitoring store actions', () => {
});
testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{ type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg },
]);
......@@ -1209,6 +1211,7 @@ describe('Monitoring store actions', () => {
mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500);
testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
{ type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
{
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