Commit 71f1c7e5 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '204920-add-refresh-rate-btn' into 'master'

Add refresh rate options to dashboard header

Closes #204920

See merge request gitlab-org/gitlab!35238
parents 1b4d6d6f 29be3894
......@@ -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] ""
......@@ -14517,6 +14522,9 @@ msgstr ""
msgid "Metrics|Select a value"
msgstr ""
msgid "Metrics|Set refresh rate"
msgstr ""
msgid "Metrics|Star dashboard"
msgstr ""
......@@ -15727,6 +15735,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