Handle window and container resize events

Resizes metrics graph on window and sidebard width changes
This commit is contained in:
Adriel Santiago 2019-01-23 18:06:40 -05:00
parent 2b0f4df021
commit c974f4a82e
No known key found for this signature in database
GPG key ID: 3728DA7C3CC3EE76
5 changed files with 86 additions and 30 deletions

View file

@ -220,6 +220,22 @@ export const scrollToElement = element => {
);
};
/**
* Returns a function that can only be invoked once between
* each browser screen repaint.
* @param {Function} fn
*/
export const debounceByAnimationFrame = fn => {
let requestId;
return function debounced(...args) {
if (requestId) {
window.cancelAnimationFrame(requestId);
}
requestId = window.requestAnimationFrame(() => fn.apply(this, args));
};
};
/**
this will take in the `name` of the param you want to parse in the url
if the name does not exist this function will return `null`

View file

@ -1,6 +1,9 @@
<script>
import { GlAreaChart } from '@gitlab/ui';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
let debouncedResize;
export default {
components: {
@ -26,12 +29,22 @@ export default {
);
},
},
containerWidth: {
type: Number,
required: true,
},
alertData: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
width: 0,
height: 0,
};
},
computed: {
chartData() {
return this.graphData.queries.reduce((accumulator, query) => {
@ -76,11 +89,26 @@ export default {
return `${this.graphData.y_label} (${query.unit})`;
},
},
watch: {
containerWidth: 'onResize',
},
beforeDestroy() {
window.removeEventListener('resize', debouncedResize);
},
created() {
debouncedResize = debounceByAnimationFrame(this.onResize);
window.addEventListener('resize', debouncedResize);
},
methods: {
formatTooltipText(params) {
const [date, value] = params;
return [dateFormat(date, 'dd mmm yyyy, h:MMtt'), value.toFixed(3)];
},
onResize() {
const { width, height } = this.$refs.areaChart.$el.getBoundingClientRect();
this.width = width;
this.height = height;
},
},
};
</script>
@ -92,11 +120,14 @@ export default {
<div class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
ref="areaChart"
v-bind="$attrs"
:data="chartData"
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:thresholds="alertData"
:width="width"
:height="height"
/>
</div>
</template>

View file

@ -1,5 +1,4 @@
<script>
import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
@ -9,6 +8,9 @@ import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
const sidebarAnimationDuration = 150;
let sidebarMutationObserver;
export default {
components: {
MonitorAreaChart,
@ -89,39 +91,29 @@ export default {
elWidth: 0,
};
},
computed: {
forceRedraw() {
return this.elWidth;
},
},
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
this.mutationObserverConfig = {
attributes: true,
childList: false,
subtree: false,
};
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
this.sidebarMutationObserver.disconnect();
if (sidebarMutationObserver) {
sidebarMutationObserver.disconnect();
}
},
mounted() {
this.resizeThrottled = _.debounce(this.resize, 100);
if (!this.hasMetrics) {
this.state = 'gettingStarted';
} else {
this.getGraphsData();
window.addEventListener('resize', this.resizeThrottled, false);
const sidebarEl = document.querySelector('.nav-sidebar');
// The sidebar listener
this.sidebarMutationObserver = new MutationObserver(this.resizeThrottled);
this.sidebarMutationObserver.observe(sidebarEl, this.mutationObserverConfig);
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true,
childList: false,
subtree: false,
});
}
},
methods: {
@ -149,20 +141,21 @@ export default {
this.showEmptyState = false;
})
.then(this.resize)
.catch(() => {
this.state = 'unableToConnect';
});
},
resize() {
this.elWidth = this.$el.clientWidth;
onSidebarMutation() {
setTimeout(() => {
this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration);
},
},
};
</script>
<template>
<div v-if="!showEmptyState" :key="forceRedraw" class="prometheus-graphs prepend-top-default">
<div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default">
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
@ -198,6 +191,7 @@ export default {
:key="graphIndex"
:graph-data="graphData"
:alert-data="getGraphAlerts(graphData.id)"
:container-width="elWidth"
group-id="monitor-area-chart"
/>
</graph-group>

View file

@ -232,6 +232,21 @@ describe('common_utils', () => {
});
});
describe('debounceByAnimationFrame', () => {
it('debounces a function to allow a maximum of one call per animation frame', done => {
const spy = jasmine.createSpy('spy');
const debouncedSpy = commonUtils.debounceByAnimationFrame(spy);
window.requestAnimationFrame(() => {
debouncedSpy();
debouncedSpy();
window.requestAnimationFrame(() => {
expect(spy).toHaveBeenCalledTimes(1);
done();
});
});
});
});
describe('getParameterByName', () => {
beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2');

View file

@ -29,7 +29,7 @@ describe('Dashboard', () => {
beforeEach(() => {
setFixtures(`
<div class="prometheus-graphs"></div>
<div class="nav-sidebar"></div>
<div class="layout-page"></div>
`);
DashboardComponent = Vue.extend(Dashboard);
});
@ -164,16 +164,16 @@ describe('Dashboard', () => {
jasmine.clock().uninstall();
});
it('rerenders the dashboard when the sidebar is resized', done => {
it('sets elWidth to page width when the sidebar is resized', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showPanels: false },
});
expect(component.forceRedraw).toEqual(0);
expect(component.elWidth).toEqual(0);
const navSidebarEl = document.querySelector('.nav-sidebar');
navSidebarEl.classList.add('nav-sidebar-collapsed');
const pageLayoutEl = document.querySelector('.layout-page');
pageLayoutEl.classList.add('page-with-icon-sidebar');
Vue.nextTick()
.then(() => {
@ -181,7 +181,7 @@ describe('Dashboard', () => {
return Vue.nextTick();
})
.then(() => {
expect(component.forceRedraw).toEqual(component.elWidth);
expect(component.elWidth).toEqual(pageLayoutEl.clientWidth);
done();
})
.catch(done.fail);