Commit 23df40f9 authored by Clement Ho's avatar Clement Ho

Merge branch '31368-support-different-time-windows-for-performance-dashboard' into 'master'

Resolve "Support different time windows for performance dashboard"

Closes #31368

See merge request gitlab-org/gitlab-ce!26047
parents 7ffd7941 976f1feb
...@@ -10,6 +10,8 @@ import MonitorAreaChart from './charts/area.vue'; ...@@ -10,6 +10,8 @@ import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store'; import MonitoringStore from '../stores/monitoring_store';
import { timeWindows } from '../constants';
import { getTimeDiff } from '../utils';
const sidebarAnimationDuration = 150; const sidebarAnimationDuration = 150;
let sidebarMutationObserver; let sidebarMutationObserver;
...@@ -88,6 +90,10 @@ export default { ...@@ -88,6 +90,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
showTimeWindowDropdown: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -95,6 +101,7 @@ export default { ...@@ -95,6 +101,7 @@ export default {
state: 'gettingStarted', state: 'gettingStarted',
showEmptyState: true, showEmptyState: true,
elWidth: 0, elWidth: 0,
selectedTimeWindow: '',
}; };
}, },
created() { created() {
...@@ -103,6 +110,9 @@ export default { ...@@ -103,6 +110,9 @@ export default {
deploymentEndpoint: this.deploymentEndpoint, deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint, environmentsEndpoint: this.environmentsEndpoint,
}); });
this.timeWindows = timeWindows;
this.selectedTimeWindow = this.timeWindows.eightHours;
}, },
beforeDestroy() { beforeDestroy() {
if (sidebarMutationObserver) { if (sidebarMutationObserver) {
...@@ -166,18 +176,41 @@ export default { ...@@ -166,18 +176,41 @@ export default {
this.state = 'unableToConnect'; this.state = 'unableToConnect';
}); });
}, },
getGraphsDataWithTime(timeFrame) {
this.state = 'loading';
this.showEmptyState = true;
this.service
.getGraphsData(getTimeDiff(this.timeWindows[timeFrame]))
.then(data => {
this.store.storeMetrics(data);
this.selectedTimeWindow = this.timeWindows[timeFrame];
})
.catch(() => {
Flash(s__('Metrics|Not enough data to display'));
})
.finally(() => {
this.showEmptyState = false;
});
},
onSidebarMutation() { onSidebarMutation() {
setTimeout(() => { setTimeout(() => {
this.elWidth = this.$el.clientWidth; this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration); }, sidebarAnimationDuration);
}, },
activeTimeWindow(key) {
return this.timeWindows[key] === this.selectedTimeWindow;
},
}, },
}; };
</script> </script>
<template> <template>
<div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default">
<div v-if="environmentsEndpoint" class="environments d-flex align-items-center"> <div
v-if="environmentsEndpoint"
class="dropdowns d-flex align-items-center justify-content-between"
>
<div class="d-flex align-items-center">
<strong>{{ s__('Metrics|Environment') }}</strong> <strong>{{ s__('Metrics|Environment') }}</strong>
<gl-dropdown <gl-dropdown
class="prepend-left-10 js-environments-dropdown" class="prepend-left-10 js-environments-dropdown"
...@@ -194,6 +227,23 @@ export default { ...@@ -194,6 +227,23 @@ export default {
> >
</gl-dropdown> </gl-dropdown>
</div> </div>
<div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
toggle-class="dropdown-menu-toggle"
:text="selectedTimeWindow"
>
<gl-dropdown-item
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
@click="getGraphsDataWithTime(key)"
>{{ value }}</gl-dropdown-item
>
</gl-dropdown>
</div>
</div>
<graph-group <graph-group
v-for="(groupData, index) in store.groups" v-for="(groupData, index) in store.groups"
:key="index" :key="index"
......
import { __ } from '~/locale';
export const chartHeight = 300; export const chartHeight = 300;
export const graphTypes = { export const graphTypes = {
...@@ -7,3 +9,14 @@ export const graphTypes = { ...@@ -7,3 +9,14 @@ export const graphTypes = {
export const lineTypes = { export const lineTypes = {
default: 'solid', default: 'solid',
}; };
export const timeWindows = {
thirtyMinutes: __('30 minutes'),
threeHours: __('3 hours'),
eightHours: __('8 hours'),
oneDay: __('1 day'),
threeDays: __('3 days'),
oneWeek: __('1 week'),
};
export const msPerMinute = 60000;
...@@ -14,6 +14,7 @@ export default () => { ...@@ -14,6 +14,7 @@ export default () => {
props: { props: {
...el.dataset, ...el.dataset,
hasMetrics: parseBoolean(el.dataset.hasMetrics), hasMetrics: parseBoolean(el.dataset.hasMetrics),
showTimeWindowDropdown: gon.features.metricsTimeWindow,
}, },
}); });
}, },
......
...@@ -32,11 +32,11 @@ export default class MonitoringService { ...@@ -32,11 +32,11 @@ export default class MonitoringService {
this.environmentsEndpoint = environmentsEndpoint; this.environmentsEndpoint = environmentsEndpoint;
} }
getGraphsData() { getGraphsData(params = {}) {
return backOffRequest(() => axios.get(this.metricsEndpoint)) return backOffRequest(() => axios.get(this.metricsEndpoint, { params }))
.then(resp => resp.data) .then(resp => resp.data)
.then(response => { .then(response => {
if (!response || !response.data) { if (!response || !response.data || !response.success) {
throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint')); throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
} }
return response.data; return response.data;
......
import { timeWindows, msPerMinute } from './constants';
/**
* method that converts a predetermined time window to minutes
* defaults to 8 hours as the default option
* @param {String} timeWindow - The time window to convert to minutes
* @returns {number} The time window in minutes
*/
const getTimeDifferenceMinutes = timeWindow => {
switch (timeWindow) {
case timeWindows.thirtyMinutes:
return 30;
case timeWindows.threeHours:
return 60 * 3;
case timeWindows.oneDay:
return 60 * 24 * 1;
case timeWindows.threeDays:
return 60 * 24 * 3;
case timeWindows.oneWeek:
return 60 * 24 * 7 * 1;
default:
return 60 * 8;
}
};
export const getTimeDiff = selectedTimeWindow => {
const end = Date.now();
const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow);
const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime();
return { start, end };
};
export default {};
...@@ -204,7 +204,7 @@ ...@@ -204,7 +204,7 @@
} }
.prometheus-graphs { .prometheus-graphs {
.environments { .dropdowns {
.dropdown-menu-toggle { .dropdown-menu-toggle {
svg { svg {
position: absolute; position: absolute;
......
...@@ -238,6 +238,9 @@ msgid_plural "%d closed merge requests" ...@@ -238,6 +238,9 @@ msgid_plural "%d closed merge requests"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "1 day"
msgstr ""
msgid "1 merged merge request" msgid "1 merged merge request"
msgid_plural "%d merged merge requests" msgid_plural "%d merged merge requests"
msgstr[0] "" msgstr[0] ""
...@@ -258,6 +261,9 @@ msgid_plural "%d pipelines" ...@@ -258,6 +261,9 @@ msgid_plural "%d pipelines"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "1 week"
msgstr ""
msgid "1st contribution!" msgid "1st contribution!"
msgstr "" msgstr ""
...@@ -267,6 +273,15 @@ msgstr "" ...@@ -267,6 +273,15 @@ msgstr ""
msgid "2FA enabled" msgid "2FA enabled"
msgstr "" msgstr ""
msgid "3 days"
msgstr ""
msgid "3 hours"
msgstr ""
msgid "30 minutes"
msgstr ""
msgid "403|Please contact your GitLab administrator to get permission." msgid "403|Please contact your GitLab administrator to get permission."
msgstr "" msgstr ""
...@@ -282,6 +297,9 @@ msgstr "" ...@@ -282,6 +297,9 @@ msgstr ""
msgid "404|Please contact your GitLab administrator if you think this is a mistake." msgid "404|Please contact your GitLab administrator if you think this is a mistake."
msgstr "" msgstr ""
msgid "8 hours"
msgstr ""
msgid "<code>\"johnsmith@example.com\": \"@johnsmith\"</code> will add \"By <a href=\"#\">@johnsmith</a>\" to all issues and comments originally created by johnsmith@example.com, and will set <a href=\"#\">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com." msgid "<code>\"johnsmith@example.com\": \"@johnsmith\"</code> will add \"By <a href=\"#\">@johnsmith</a>\" to all issues and comments originally created by johnsmith@example.com, and will set <a href=\"#\">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com."
msgstr "" msgstr ""
...@@ -5109,6 +5127,12 @@ msgstr "" ...@@ -5109,6 +5127,12 @@ msgstr ""
msgid "Metrics|No deployed environments" msgid "Metrics|No deployed environments"
msgstr "" msgstr ""
msgid "Metrics|Not enough data to display"
msgstr ""
msgid "Metrics|Show last"
msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again" msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr "" msgstr ""
......
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import { timeWindows } from '~/monitoring/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data'; import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data';
...@@ -50,7 +51,7 @@ describe('Dashboard', () => { ...@@ -50,7 +51,7 @@ describe('Dashboard', () => {
it('shows a getting started empty state when no metrics are present', () => { it('shows a getting started empty state when no metrics are present', () => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData, propsData: { ...propsData, showTimeWindowDropdown: false },
}); });
expect(component.$el.querySelector('.prometheus-graphs')).toBe(null); expect(component.$el.querySelector('.prometheus-graphs')).toBe(null);
...@@ -66,7 +67,7 @@ describe('Dashboard', () => { ...@@ -66,7 +67,7 @@ describe('Dashboard', () => {
it('shows up a loading state', done => { it('shows up a loading state', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true }, propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: false },
}); });
Vue.nextTick(() => { Vue.nextTick(() => {
...@@ -78,7 +79,12 @@ describe('Dashboard', () => { ...@@ -78,7 +79,12 @@ describe('Dashboard', () => {
it('hides the legend when showLegend is false', done => { it('hides the legend when showLegend is false', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showLegend: false }, propsData: {
...propsData,
hasMetrics: true,
showLegend: false,
showTimeWindowDropdown: false,
},
}); });
setTimeout(() => { setTimeout(() => {
...@@ -92,7 +98,12 @@ describe('Dashboard', () => { ...@@ -92,7 +98,12 @@ describe('Dashboard', () => {
it('hides the group panels when showPanels is false', done => { it('hides the group panels when showPanels is false', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false }, propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
},
}); });
setTimeout(() => { setTimeout(() => {
...@@ -106,7 +117,12 @@ describe('Dashboard', () => { ...@@ -106,7 +117,12 @@ describe('Dashboard', () => {
it('renders the environments dropdown with a number of environments', done => { it('renders the environments dropdown with a number of environments', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false }, propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
},
}); });
component.store.storeEnvironmentsData(environmentData); component.store.storeEnvironmentsData(environmentData);
...@@ -124,7 +140,12 @@ describe('Dashboard', () => { ...@@ -124,7 +140,12 @@ describe('Dashboard', () => {
it('hides the environments dropdown list when there is no environments', done => { it('hides the environments dropdown list when there is no environments', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false }, propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
},
}); });
component.store.storeEnvironmentsData([]); component.store.storeEnvironmentsData([]);
...@@ -142,7 +163,12 @@ describe('Dashboard', () => { ...@@ -142,7 +163,12 @@ describe('Dashboard', () => {
it('renders the environments dropdown with a single is-active element', done => { it('renders the environments dropdown with a single is-active element', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false }, propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
},
}); });
component.store.storeEnvironmentsData(environmentData); component.store.storeEnvironmentsData(environmentData);
...@@ -166,6 +192,7 @@ describe('Dashboard', () => { ...@@ -166,6 +192,7 @@ describe('Dashboard', () => {
hasMetrics: true, hasMetrics: true,
showPanels: false, showPanels: false,
environmentsEndpoint: '', environmentsEndpoint: '',
showTimeWindowDropdown: false,
}, },
}); });
...@@ -176,6 +203,51 @@ describe('Dashboard', () => { ...@@ -176,6 +203,51 @@ describe('Dashboard', () => {
done(); done();
}); });
}); });
it('does not show the time window dropdown when the feature flag is not set', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
},
});
setTimeout(() => {
const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
expect(timeWindowDropdown).toBeNull();
done();
});
});
it('renders the time window dropdown with a set of options', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: true,
},
});
const numberOfTimeWindows = Object.keys(timeWindows).length;
setTimeout(() => {
const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
const timeWindowDropdownEls = component.$el.querySelectorAll(
'.js-time-window-dropdown .dropdown-item',
);
expect(timeWindowDropdown).not.toBeNull();
expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
done();
});
});
}); });
describe('when the window resizes', () => { describe('when the window resizes', () => {
...@@ -191,7 +263,12 @@ describe('Dashboard', () => { ...@@ -191,7 +263,12 @@ describe('Dashboard', () => {
it('sets elWidth to page width when the sidebar is resized', done => { it('sets elWidth to page width when the sidebar is resized', done => {
const component = new DashboardComponent({ const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'), el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false }, propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: false,
},
}); });
expect(component.elWidth).toEqual(0); expect(component.elWidth).toEqual(0);
......
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