Commit a0bea201 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '207549-add-refresh-dashboard-button-second-iteration' into 'master'

Dashboard refresh button refreshes data dynamically

See merge request gitlab-org/gitlab!28756
parents d1bba971 e4c70595
...@@ -19,12 +19,7 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; ...@@ -19,12 +19,7 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import createFlash from '~/flash'; import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility';
mergeUrlParams,
redirectTo,
refreshCurrentPage,
updateHistory,
} from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
...@@ -273,6 +268,7 @@ export default { ...@@ -273,6 +268,7 @@ export default {
...mapActions('monitoringDashboard', [ ...mapActions('monitoringDashboard', [
'setTimeRange', 'setTimeRange',
'fetchData', 'fetchData',
'fetchDashboardData',
'setGettingStartedEmptyState', 'setGettingStartedEmptyState',
'setInitialState', 'setInitialState',
'setPanelGroupMetrics', 'setPanelGroupMetrics',
...@@ -360,7 +356,7 @@ export default { ...@@ -360,7 +356,7 @@ export default {
}, },
refreshDashboard() { refreshDashboard() {
refreshCurrentPage(); this.fetchDashboardData();
}, },
onTimeRangeZoom({ start, end }) { onTimeRangeZoom({ start, end }) {
...@@ -475,7 +471,7 @@ export default { ...@@ -475,7 +471,7 @@ export default {
ref="refreshDashboardBtn" ref="refreshDashboardBtn"
v-gl-tooltip v-gl-tooltip
variant="default" variant="default"
:title="s__('Metrics|Reload this page')" :title="s__('Metrics|Refresh dashboard')"
@click="refreshDashboard" @click="refreshDashboard"
> >
<icon name="retry" /> <icon name="retry" />
......
...@@ -4,6 +4,7 @@ import { pickBy } from 'lodash'; ...@@ -4,6 +4,7 @@ import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { import {
GlResizeObserverDirective, GlResizeObserverDirective,
GlLoadingIcon,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlModal, GlModal,
...@@ -37,6 +38,7 @@ export default { ...@@ -37,6 +38,7 @@ export default {
MonitorStackedColumnChart, MonitorStackedColumnChart,
MonitorEmptyChart, MonitorEmptyChart,
Icon, Icon,
GlLoadingIcon,
GlTooltip, GlTooltip,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
...@@ -104,13 +106,17 @@ export default { ...@@ -104,13 +106,17 @@ export default {
// This method is extended by ee functionality // This method is extended by ee functionality
return false; return false;
}, },
graphDataHasMetrics() { graphDataHasResult() {
return ( return (
this.graphData.metrics && this.graphData.metrics &&
this.graphData.metrics[0].result && this.graphData.metrics[0].result &&
this.graphData.metrics[0].result.length > 0 this.graphData.metrics[0].result.length > 0
); );
}, },
graphDataIsLoading() {
const { metrics = [] } = this.graphData;
return metrics.some(({ loading }) => loading);
},
logsPathWithTimeRange() { logsPathWithTimeRange() {
const timeRange = this.zoomedTimeRange || this.timeRange; const timeRange = this.zoomedTimeRange || this.timeRange;
...@@ -140,7 +146,7 @@ export default { ...@@ -140,7 +146,7 @@ export default {
}, },
isContextualMenuShown() { isContextualMenuShown() {
return ( return (
this.graphDataHasMetrics && this.graphDataHasResult &&
!this.isPanelType('single-stat') && !this.isPanelType('single-stat') &&
!this.isPanelType('heatmap') && !this.isPanelType('heatmap') &&
!this.isPanelType('column') && !this.isPanelType('column') &&
...@@ -193,7 +199,7 @@ export default { ...@@ -193,7 +199,7 @@ export default {
</script> </script>
<template> <template>
<div v-gl-resize-observer="onResize" class="prometheus-graph"> <div v-gl-resize-observer="onResize" class="prometheus-graph">
<div class="prometheus-graph-header"> <div class="d-flex align-items-center mr-3">
<h5 <h5
ref="graphTitle" ref="graphTitle"
class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8" class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8"
...@@ -203,23 +209,27 @@ export default { ...@@ -203,23 +209,27 @@ export default {
<gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip"> <gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ title }} {{ title }}
</gl-tooltip> </gl-tooltip>
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<div class="flex-grow-1"></div>
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon />
</div>
<div <div
v-if="isContextualMenuShown" v-if="isContextualMenuShown"
class="prometheus-graph-widgets js-graph-widgets flex-fill" class="js-graph-widgets"
data-qa-selector="prometheus_graph_widgets" data-qa-selector="prometheus_graph_widgets"
> >
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<alert-widget
v-if="alertWidgetAvailable"
:modal-id="`alert-modal-${index}`"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<gl-dropdown <gl-dropdown
v-gl-tooltip v-gl-tooltip
class="ml-auto mx-3"
toggle-class="btn btn-transparent border-0" toggle-class="btn btn-transparent border-0"
data-qa-selector="prometheus_widgets_dropdown" data-qa-selector="prometheus_widgets_dropdown"
right right
...@@ -275,28 +285,28 @@ export default { ...@@ -275,28 +285,28 @@ export default {
</div> </div>
<monitor-single-stat-chart <monitor-single-stat-chart
v-if="isPanelType('single-stat') && graphDataHasMetrics" v-if="isPanelType('single-stat') && graphDataHasResult"
:graph-data="graphData" :graph-data="graphData"
/> />
<monitor-heatmap-chart <monitor-heatmap-chart
v-else-if="isPanelType('heatmap') && graphDataHasMetrics" v-else-if="isPanelType('heatmap') && graphDataHasResult"
:graph-data="graphData" :graph-data="graphData"
/> />
<monitor-bar-chart <monitor-bar-chart
v-else-if="isPanelType('bar') && graphDataHasMetrics" v-else-if="isPanelType('bar') && graphDataHasResult"
:graph-data="graphData" :graph-data="graphData"
/> />
<monitor-column-chart <monitor-column-chart
v-else-if="isPanelType('column') && graphDataHasMetrics" v-else-if="isPanelType('column') && graphDataHasResult"
:graph-data="graphData" :graph-data="graphData"
/> />
<monitor-stacked-column-chart <monitor-stacked-column-chart
v-else-if="isPanelType('stacked-column') && graphDataHasMetrics" v-else-if="isPanelType('stacked-column') && graphDataHasResult"
:graph-data="graphData" :graph-data="graphData"
/> />
<component <component
:is="timeChartComponent" :is="timeChartComponent"
v-else-if="graphDataHasMetrics" v-else-if="graphDataHasResult"
ref="timeChart" ref="timeChart"
:graph-data="graphData" :graph-data="graphData"
:deployment-data="deploymentData" :deployment-data="deploymentData"
......
...@@ -10,7 +10,10 @@ export const metricStates = { ...@@ -10,7 +10,10 @@ export const metricStates = {
OK: 'OK', OK: 'OK',
/** /**
* Metric data is being fetched * Metric data is being fetched for the first time.
*
* Not used during data refresh, if data is available in
* the metric, the recommneded state is OK.
*/ */
LOADING: 'LOADING', LOADING: 'LOADING',
......
...@@ -128,7 +128,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response ...@@ -128,7 +128,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response
commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard); commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard);
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data)); commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data));
return dispatch('fetchPrometheusMetrics'); return dispatch('fetchDashboardData');
}; };
export const receiveMetricsDashboardFailure = ({ commit }, error) => { export const receiveMetricsDashboardFailure = ({ commit }, error) => {
commit(types.RECEIVE_METRICS_DASHBOARD_FAILURE, error); commit(types.RECEIVE_METRICS_DASHBOARD_FAILURE, error);
...@@ -140,7 +140,7 @@ export const receiveMetricsDashboardFailure = ({ commit }, error) => { ...@@ -140,7 +140,7 @@ export const receiveMetricsDashboardFailure = ({ commit }, error) => {
* Loads timeseries data: Prometheus data points and deployment data from the project * Loads timeseries data: Prometheus data points and deployment data from the project
* @param {Object} Vuex store * @param {Object} Vuex store
*/ */
export const fetchPrometheusMetrics = ({ state, dispatch, getters }) => { export const fetchDashboardData = ({ state, dispatch, getters }) => {
dispatch('fetchDeploymentsData'); dispatch('fetchDeploymentsData');
if (!state.timeRange) { if (!state.timeRange) {
......
import Vue from 'vue';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils'; import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
...@@ -26,24 +25,6 @@ const findMetricInDashboard = (metricId, dashboard) => { ...@@ -26,24 +25,6 @@ const findMetricInDashboard = (metricId, dashboard) => {
return res; return res;
}; };
/**
* Set a new state for a metric.
*
* Initally metric data is not populated, so `Vue.set` is
* used to add new properties to the metric.
*
* @param {Object} metric - Metric object as defined in the dashboard
* @param {Object} state - New state
* @param {Array|null} state.result - Array of results
* @param {String} state.error - Error code from metricStates
* @param {Boolean} state.loading - True if the metric is loading
*/
const setMetricState = (metric, { result = null, loading = false, state = null }) => {
Vue.set(metric, 'result', result);
Vue.set(metric, 'loading', loading);
Vue.set(metric, 'state', state);
};
/** /**
* Maps a backened error state to a `metricStates` constant * Maps a backened error state to a `metricStates` constant
* @param {Object} error - Error from backend response * @param {Object} error - Error from backend response
...@@ -116,39 +97,32 @@ export default { ...@@ -116,39 +97,32 @@ export default {
*/ */
[types.REQUEST_METRIC_RESULT](state, { metricId }) { [types.REQUEST_METRIC_RESULT](state, { metricId }) {
const metric = findMetricInDashboard(metricId, state.dashboard); const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, { metric.loading = true;
loading: true, if (!metric.result) {
state: metricStates.LOADING, metric.state = metricStates.LOADING;
}); }
}, },
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) { [types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
if (!metricId) { const metric = findMetricInDashboard(metricId, state.dashboard);
return; metric.loading = false;
}
state.showEmptyState = false; state.showEmptyState = false;
const metric = findMetricInDashboard(metricId, state.dashboard);
if (!result || result.length === 0) { if (!result || result.length === 0) {
setMetricState(metric, { metric.state = metricStates.NO_DATA;
state: metricStates.NO_DATA, metric.result = null;
});
} else { } else {
const normalizedResults = result.map(normalizeQueryResult); const normalizedResults = result.map(normalizeQueryResult);
setMetricState(metric, {
result: Object.freeze(normalizedResults), metric.state = metricStates.OK;
state: metricStates.OK, metric.result = Object.freeze(normalizedResults);
});
} }
}, },
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) { [types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
if (!metricId) {
return;
}
const metric = findMetricInDashboard(metricId, state.dashboard); const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
state: emptyStateFromError(error), metric.state = emptyStateFromError(error);
}); metric.loading = false;
metric.result = null;
}, },
[types.SET_INITIAL_STATE](state, initialState = {}) { [types.SET_INITIAL_STATE](state, initialState = {}) {
Object.assign(state, pick(initialState, initialStateKeys)); Object.assign(state, pick(initialState, initialStateKeys));
......
...@@ -76,6 +76,12 @@ const mapToMetricsViewModel = metrics => ...@@ -76,6 +76,12 @@ const mapToMetricsViewModel = metrics =>
queryRange: query_range, queryRange: query_range,
prometheusEndpointPath: prometheus_endpoint_path, prometheusEndpointPath: prometheus_endpoint_path,
metricId: uniqMetricsId({ metric_id, id }), metricId: uniqMetricsId({ metric_id, id }),
// metric data
loading: false,
result: null,
state: null,
...metric, ...metric,
})); }));
......
...@@ -84,13 +84,6 @@ ...@@ -84,13 +84,6 @@
border-radius: $border-radius-default; border-radius: $border-radius-default;
} }
.prometheus-graph-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $gl-padding-8;
}
.alert-current-setting { .alert-current-setting {
max-width: 240px; max-width: 240px;
......
---
title: Refresh metrics dashboard data without reloading the page
merge_request: 28756
author:
type: added
...@@ -12929,7 +12929,7 @@ msgstr "" ...@@ -12929,7 +12929,7 @@ msgstr ""
msgid "Metrics|Prometheus Query Documentation" msgid "Metrics|Prometheus Query Documentation"
msgstr "" msgstr ""
msgid "Metrics|Reload this page" msgid "Metrics|Refresh dashboard"
msgstr "" msgstr ""
msgid "Metrics|Show last" msgid "Metrics|Show last"
......
...@@ -92,7 +92,7 @@ exports[`Dashboard template matches the default snapshot 1`] = ` ...@@ -92,7 +92,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
> >
<gl-deprecated-button-stub <gl-deprecated-button-stub
size="md" size="md"
title="Reload this page" title="Refresh dashboard"
variant="default" variant="default"
> >
<icon-stub <icon-stub
......
...@@ -15,7 +15,7 @@ import { ...@@ -15,7 +15,7 @@ import {
receiveMetricsDashboardSuccess, receiveMetricsDashboardSuccess,
fetchDeploymentsData, fetchDeploymentsData,
fetchEnvironmentsData, fetchEnvironmentsData,
fetchPrometheusMetrics, fetchDashboardData,
fetchPrometheusMetric, fetchPrometheusMetric,
setInitialState, setInitialState,
filterEnvironments, filterEnvironments,
...@@ -375,7 +375,7 @@ describe('Monitoring store actions', () => { ...@@ -375,7 +375,7 @@ describe('Monitoring store actions', () => {
metricsDashboardResponse.dashboard, metricsDashboardResponse.dashboard,
); );
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics'); expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
}); });
it('sets the dashboards loaded from the repository', () => { it('sets the dashboards loaded from the repository', () => {
const params = {}; const params = {};
...@@ -395,7 +395,7 @@ describe('Monitoring store actions', () => { ...@@ -395,7 +395,7 @@ describe('Monitoring store actions', () => {
expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse); expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
}); });
}); });
describe('fetchPrometheusMetrics', () => { describe('fetchDashboardData', () => {
let commit; let commit;
let dispatch; let dispatch;
let state; let state;
...@@ -413,7 +413,7 @@ describe('Monitoring store actions', () => { ...@@ -413,7 +413,7 @@ describe('Monitoring store actions', () => {
const getters = { const getters = {
metricsWithData: () => [], metricsWithData: () => [],
}; };
fetchPrometheusMetrics({ state, commit, dispatch, getters }) fetchDashboardData({ state, commit, dispatch, getters })
.then(() => { .then(() => {
expect(Tracking.event).toHaveBeenCalledWith( expect(Tracking.event).toHaveBeenCalledWith(
document.body.dataset.page, document.body.dataset.page,
...@@ -442,7 +442,7 @@ describe('Monitoring store actions', () => { ...@@ -442,7 +442,7 @@ describe('Monitoring store actions', () => {
metricsWithData: () => [metric.id], metricsWithData: () => [metric.id],
}; };
fetchPrometheusMetrics({ state, commit, dispatch, getters }) fetchDashboardData({ state, commit, dispatch, getters })
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
metric, metric,
...@@ -478,7 +478,7 @@ describe('Monitoring store actions', () => { ...@@ -478,7 +478,7 @@ describe('Monitoring store actions', () => {
dispatch.mockRejectedValueOnce(new Error('Error fetching this metric')); dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
dispatch.mockResolvedValue(); dispatch.mockResolvedValue();
fetchPrometheusMetrics({ state, commit, dispatch }) fetchDashboardData({ state, commit, dispatch })
.then(() => { .then(() => {
expect(dispatch).toHaveBeenCalledTimes(10); // one per metric plus 1 for deployments expect(dispatch).toHaveBeenCalledTimes(10); // one per metric plus 1 for deployments
expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
......
...@@ -202,15 +202,12 @@ describe('Monitoring mutations', () => { ...@@ -202,15 +202,12 @@ describe('Monitoring mutations', () => {
mutations[types.REQUEST_METRIC_RESULT](stateCopy, { mutations[types.REQUEST_METRIC_RESULT](stateCopy, {
metricId, metricId,
result,
}); });
expect(stateCopy.showEmptyState).toBe(true); expect(stateCopy.showEmptyState).toBe(true);
expect(getMetric()).toEqual( expect(getMetric()).toEqual(
expect.objectContaining({ expect.objectContaining({
loading: true, loading: true,
result: null,
state: metricStates.LOADING,
}), }),
); );
}); });
...@@ -232,7 +229,7 @@ describe('Monitoring mutations', () => { ...@@ -232,7 +229,7 @@ describe('Monitoring mutations', () => {
}); });
it('adds results to the store', () => { it('adds results to the store', () => {
expect(getMetric().result).toBe(undefined); expect(getMetric().result).toBe(null);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
metricId, metricId,
......
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
removeLeadingSlash, removeLeadingSlash,
mapToDashboardViewModel, mapToDashboardViewModel,
} from '~/monitoring/stores/utils'; } from '~/monitoring/stores/utils';
import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
const projectPath = 'gitlab-org/gitlab-test'; const projectPath = 'gitlab-org/gitlab-test';
...@@ -256,6 +257,9 @@ describe('mapToDashboardViewModel', () => { ...@@ -256,6 +257,9 @@ describe('mapToDashboardViewModel', () => {
expect(getMappedMetric(dashboard)).toEqual({ expect(getMappedMetric(dashboard)).toEqual({
label: expect.any(String), label: expect.any(String),
metricId: expect.any(String), metricId: expect.any(String),
loading: false,
result: null,
state: null,
}); });
}); });
...@@ -307,7 +311,7 @@ describe('mapToDashboardViewModel', () => { ...@@ -307,7 +311,7 @@ describe('mapToDashboardViewModel', () => {
describe('uniqMetricsId', () => { describe('uniqMetricsId', () => {
[ [
{ input: { id: 1 }, expected: 'NO_DB_1' }, { input: { id: 1 }, expected: `${NOT_IN_DB_PREFIX}_1` },
{ input: { metric_id: 2 }, expected: '2_undefined' }, { input: { metric_id: 2 }, expected: '2_undefined' },
{ input: { metric_id: 2, id: 21 }, expected: '2_21' }, { input: { metric_id: 2, id: 21 }, expected: '2_21' },
{ input: { metric_id: 22, id: 1 }, expected: '22_1' }, { input: { metric_id: 22, id: 1 }, expected: '22_1' },
......
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