Commit e4c70595 authored by Miguel Rincon's avatar Miguel Rincon

Add refresh data button to dashboard

Refresh only dashboard data, the data in each chart independendtly from
the rest of the dashboard structure.
parent c3c3b472
...@@ -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
...@@ -12923,7 +12923,7 @@ msgstr "" ...@@ -12923,7 +12923,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