Commit 29be3894 authored by Miguel Rincon's avatar Miguel Rincon Committed by Nicolò Maria Mezzopera

Add refresh rate options to dashboard header

With this change, users will not only be able to refresh their
dashboards manually, they be able to set the dashboard to refresh
automatically.
parent 18ec84cd
......@@ -22,6 +22,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
import RefreshButton from './refresh_button.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
......@@ -44,6 +45,7 @@ export default {
DateTimePicker,
DashboardsDropdown,
RefreshButton,
},
directives: {
GlModal: GlModalDirective,
......@@ -129,11 +131,7 @@ export default {
},
},
methods: {
...mapActions('monitoringDashboard', [
'filterEnvironments',
'fetchDashboardData',
'toggleStarredValue',
]),
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
selectDashboard(dashboard) {
const params = {
dashboard: encodeURIComponent(dashboard.path),
......@@ -149,9 +147,6 @@ export default {
onDateTimePickerInvalid() {
this.$emit('dateTimePickerInvalid');
},
refreshDashboard() {
this.fetchDashboardData();
},
toggleRearrangingPanels() {
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
......@@ -252,16 +247,7 @@ export default {
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
<gl-deprecated-button
ref="refreshDashboardBtn"
v-gl-tooltip
class="flex-grow-1"
variant="default"
:title="s__('Metrics|Refresh dashboard')"
@click="refreshDashboard"
>
<icon name="retry" />
</gl-deprecated-button>
<refresh-button />
</div>
<div class="flex-grow-1"></div>
......
<script>
import { n__, __ } from '~/locale';
import { mapActions } from 'vuex';
import {
GlButtonGroup,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
GlNewDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
const makeInterval = (length = 0, unit = 's') => {
const shortLabel = `${length}${unit}`;
switch (unit) {
case 'd':
return {
interval: length * 24 * 60 * 60 * 1000,
shortLabel,
label: n__('%d day', '%d days', length),
};
case 'h':
return {
interval: length * 60 * 60 * 1000,
shortLabel,
label: n__('%d hour', '%d hours', length),
};
case 'm':
return {
interval: length * 60 * 1000,
shortLabel,
label: n__('%d minute', '%d minutes', length),
};
case 's':
default:
return {
interval: length * 1000,
shortLabel,
label: n__('%d second', '%d seconds', length),
};
}
};
export default {
components: {
GlButtonGroup,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
GlNewDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
},
data() {
return {
refreshInterval: null,
timeoutId: null,
};
},
computed: {
dropdownText() {
return this.refreshInterval?.shortLabel ?? __('Off');
},
},
watch: {
refreshInterval() {
if (this.refreshInterval !== null) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
},
},
destroyed() {
this.stopAutoRefresh();
},
methods: {
...mapActions('monitoringDashboard', ['fetchDashboardData']),
refresh() {
this.fetchDashboardData();
},
startAutoRefresh() {
const schedule = () => {
if (this.refreshInterval) {
this.timeoutId = setTimeout(this.startAutoRefresh, this.refreshInterval.interval);
}
};
this.stopAutoRefresh();
if (document.hidden) {
// Inactive tab? Skip fetch and schedule again
schedule();
} else {
// Active tab! Fetch data and then schedule when settled
// eslint-disable-next-line promise/catch-or-return
this.fetchDashboardData().finally(schedule);
}
},
stopAutoRefresh() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
},
setRefreshInterval(option) {
this.refreshInterval = option;
},
removeRefreshInterval() {
this.refreshInterval = null;
},
isChecked(option) {
if (this.refreshInterval) {
return option.interval === this.refreshInterval.interval;
}
return false;
},
},
refreshIntervals: [
makeInterval(5),
makeInterval(10),
makeInterval(30),
makeInterval(5, 'm'),
makeInterval(30, 'm'),
makeInterval(1, 'h'),
makeInterval(2, 'h'),
makeInterval(12, 'h'),
makeInterval(1, 'd'),
],
};
</script>
<template>
<gl-button-group>
<gl-button
v-gl-tooltip
class="gl-flex-grow-1"
variant="default"
:title="s__('Metrics|Refresh dashboard')"
icon="retry"
@click="refresh"
/>
<gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
<gl-new-dropdown-item
:is-check-item="true"
:is-checked="refreshInterval === null"
@click="removeRefreshInterval()"
>{{ __('Off') }}</gl-new-dropdown-item
>
<gl-new-dropdown-divider />
<gl-new-dropdown-item
v-for="(option, i) in $options.refreshIntervals"
:key="i"
:is-check-item="true"
:is-checked="isChecked(option)"
@click="setRefreshInterval(option)"
>{{ option.label }}</gl-new-dropdown-item
>
</gl-new-dropdown>
</gl-button-group>
</template>
---
title: Add refresh rate options to dashboard header
merge_request: 35238
author:
type: added
......@@ -167,6 +167,11 @@ msgid_plural "%d groups selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d hour"
msgid_plural "%d hours"
msgstr[0] ""
msgstr[1] ""
msgid "%d inaccessible merge request"
msgid_plural "%d inaccessible merge requests"
msgstr[0] ""
......@@ -14505,6 +14510,9 @@ msgstr ""
msgid "Metrics|Select a value"
msgstr ""
msgid "Metrics|Set refresh rate"
msgstr ""
msgid "Metrics|Star dashboard"
msgstr ""
......@@ -15715,6 +15723,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
msgid "Off"
msgstr ""
msgid "Oh no!"
msgstr ""
......
......@@ -84,17 +84,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
<gl-deprecated-button-stub
class="flex-grow-1"
size="md"
title="Refresh dashboard"
variant="default"
>
<icon-stub
name="retry"
size="16"
/>
</gl-deprecated-button-stub>
<refresh-button-stub />
</div>
<div
......
......@@ -10,6 +10,7 @@ import { metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
......@@ -592,10 +593,9 @@ describe('Dashboard', () => {
setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
const refreshBtn = wrapper.find(DashboardHeader).findAll({ ref: 'refreshDashboardBtn' });
const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton);
expect(refreshBtn).toHaveLength(1);
expect(refreshBtn.is(GlDeprecatedButton)).toBe(true);
expect(refreshBtn.exists()).toBe(true);
});
});
......
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
describe('RefreshButton', () => {
let wrapper;
let store;
let dispatch;
let documentHidden;
const createWrapper = () => {
wrapper = shallowMount(RefreshButton, { store });
};
const findRefreshBtn = () => wrapper.find(GlButton);
const findDropdown = () => wrapper.find(GlNewDropdown);
const findOptions = () => findDropdown().findAll(GlNewDropdownItem);
const findOptionAt = index => findOptions().at(index);
const expectFetchDataToHaveBeenCalledTimes = times => {
const refreshCalls = dispatch.mock.calls.filter(([action, payload]) => {
return action === 'monitoringDashboard/fetchDashboardData' && payload === undefined;
});
expect(refreshCalls).toHaveLength(times);
};
beforeEach(() => {
store = createStore();
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
// Document can be mock hidden by overriding the `hidden` property
documentHidden = false;
Object.defineProperty(document, 'hidden', {
configurable: true,
get() {
return documentHidden;
},
});
createWrapper();
});
afterEach(() => {
dispatch.mockReset();
wrapper.destroy();
});
it('refreshes data when "refresh" is clicked', () => {
findRefreshBtn().vm.$emit('click');
expectFetchDataToHaveBeenCalledTimes(1);
});
it('refresh rate is "Off" in the dropdown', () => {
expect(findDropdown().props('text')).toBe('Off');
});
describe('refresh rate options', () => {
it('presents multiple options', () => {
expect(findOptions().length).toBeGreaterThan(1);
});
it('presents an "Off" option as the default option', () => {
expect(findOptionAt(0).text()).toBe('Off');
expect(findOptionAt(0).props('isChecked')).toBe(true);
});
});
describe('when a refresh rate is chosen', () => {
const optIndex = 2; // Other option than "Off"
beforeEach(() => {
findOptionAt(optIndex).vm.$emit('click');
return wrapper.vm.$nextTick;
});
it('refresh rate appears in the dropdown', () => {
expect(findDropdown().props('text')).toBe('10s');
});
it('refresh rate option is checked', () => {
expect(findOptionAt(0).props('isChecked')).toBe(false);
expect(findOptionAt(optIndex).props('isChecked')).toBe(true);
});
it('refreshes data when a new refresh rate is chosen', () => {
expectFetchDataToHaveBeenCalledTimes(1);
});
it('refreshes data after two intervals of time have passed', async () => {
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(2);
await wrapper.vm.$nextTick();
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(3);
});
it('does not refresh data if the document is hidden', async () => {
documentHidden = true;
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick();
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
});
it('data is not refreshed anymore after component is destroyed', () => {
expect(jest.getTimerCount()).toBe(1);
wrapper.destroy();
expect(jest.getTimerCount()).toBe(0);
});
describe('when "Off" refresh rate is chosen', () => {
beforeEach(() => {
findOptionAt(0).vm.$emit('click');
return wrapper.vm.$nextTick;
});
it('refresh rate is "Off" in the dropdown', () => {
expect(findDropdown().props('text')).toBe('Off');
});
it('refresh rate option is appears selected', () => {
expect(findOptionAt(0).props('isChecked')).toBe(true);
expect(findOptionAt(optIndex).props('isChecked')).toBe(false);
});
it('stops refreshing data', () => {
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(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