Commit ef753a5f authored by Dhiraj Bodicherla's avatar Dhiraj Bodicherla

Add time picker to monitoring preview panel

This MR adds time range picker to the
preview panel UI in monitoring
parent e1724883
...@@ -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