Commit 515ecb89 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Clement Ho

Add support for time windows for the performance dashbooards

The performance dashboards will now display the data
from a set amount of time windows that are defined
on a constants file
parent df1d62c2
...@@ -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;
......
...@@ -41,6 +41,7 @@ describe('Dashboard', () => { ...@@ -41,6 +41,7 @@ describe('Dashboard', () => {
hasMetrics: true, hasMetrics: true,
prometheusAlertsAvailable: true, prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint', alertsEndpoint: '/endpoint',
showTimeWindowDropdown: false,
}, },
}); });
}); });
...@@ -62,6 +63,7 @@ describe('Dashboard', () => { ...@@ -62,6 +63,7 @@ describe('Dashboard', () => {
hasMetrics: true, hasMetrics: true,
prometheusAlertsAvailable: false, prometheusAlertsAvailable: false,
alertsEndpoint: '/endpoint', alertsEndpoint: '/endpoint',
showTimeWindowDropdown: false,
}, },
}); });
}); });
......
...@@ -309,6 +309,9 @@ msgid_plural "%d closed merge requests" ...@@ -309,6 +309,9 @@ msgid_plural "%d closed merge requests"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "1 day"
msgstr ""
msgid "1 group" msgid "1 group"
msgid_plural "%d groups" msgid_plural "%d groups"
msgstr[0] "" msgstr[0] ""
...@@ -344,6 +347,9 @@ msgid_plural "%d users" ...@@ -344,6 +347,9 @@ msgid_plural "%d users"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "1 week"
msgstr ""
msgid "1st contribution!" msgid "1st contribution!"
msgstr "" msgstr ""
...@@ -353,6 +359,15 @@ msgstr "" ...@@ -353,6 +359,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 ""
...@@ -368,6 +383,9 @@ msgstr "" ...@@ -368,6 +383,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 ""
...@@ -6884,12 +6902,18 @@ msgstr "" ...@@ -6884,12 +6902,18 @@ msgstr ""
msgid "Metrics|No deployed environments" msgid "Metrics|No deployed environments"
msgstr "" msgstr ""
msgid "Metrics|Not enough data to display"
msgstr ""
msgid "Metrics|PromQL query is valid" msgid "Metrics|PromQL query is valid"
msgstr "" msgstr ""
msgid "Metrics|Prometheus Query Documentation" msgid "Metrics|Prometheus Query Documentation"
msgstr "" 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