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 {
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