Commit d64ea312 authored by Miguel Rincon's avatar Miguel Rincon

Add new component for log buttons

Separate the component that handles scrolling from the component
that displays logs. Put component is log viewer.

Add new related specs.
parent dac1e700
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlDropdown, GlDropdownItem, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
import {
canScroll,
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { scrollDown } from '~/lib/utils/scroll_utils';
import LogControlButtons from './log_control_buttons.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlFormGroup,
GlButton,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
LogControlButtons,
},
props: {
environmentId: {
......@@ -46,12 +36,6 @@ export default {
default: '',
},
},
data() {
return {
scrollToTopEnabled: false,
scrollToBottomEnabled: false,
};
},
computed: {
...mapState('environmentLogs', ['environments', 'logs', 'pods']),
...mapGetters('environmentLogs', ['trace']),
......@@ -63,16 +47,12 @@ export default {
trace(val) {
this.$nextTick(() => {
if (val) {
this.scrollDown();
} else {
this.updateScrollState();
scrollDown();
}
this.$refs.scrollButtons.update();
});
},
},
created() {
window.addEventListener('scroll', this.updateScrollState);
},
mounted() {
this.setInitData({
projectPath: this.projectFullPath,
......@@ -82,17 +62,8 @@ export default {
this.fetchEnvironments(this.environmentsPath);
},
destroyed() {
window.removeEventListener('scroll', this.updateScrollState);
},
methods: {
...mapActions('environmentLogs', ['setInitData', 'showPodLogs', 'fetchEnvironments']),
updateScrollState() {
this.scrollToTopEnabled = canScroll() && !isScrolledToTop();
this.scrollToBottomEnabled = canScroll() && !isScrolledToBottom();
},
scrollUp,
scrollDown,
},
};
</script>
......@@ -147,48 +118,12 @@ export default {
</gl-dropdown>
</gl-form-group>
</div>
<div class="controllers align-self-end">
<div
v-gl-tooltip
class="controllers-buttons"
:title="__('Scroll to top')"
aria-labelledby="scroll-to-top"
>
<gl-button
id="scroll-to-top"
class="btn-blank js-scroll-to-top"
:aria-label="__('Scroll to top')"
:disabled="!scrollToTopEnabled"
@click="scrollUp()"
><icon name="scroll_up"
/></gl-button>
</div>
<div
v-gl-tooltip
class="controllers-buttons"
:title="__('Scroll to bottom')"
aria-labelledby="scroll-to-bottom"
>
<gl-button
id="scroll-to-bottom"
class="btn-blank js-scroll-to-bottom"
:aria-label="__('Scroll to bottom')"
:disabled="!scrollToBottomEnabled"
@click="scrollDown()"
><icon name="scroll_down"
/></gl-button>
</div>
<gl-button
id="refresh-log"
v-gl-tooltip
class="ml-1 px-2 js-refresh-log"
:title="__('Refresh')"
:aria-label="__('Refresh')"
@click="showPodLogs(pods.current)"
>
<icon name="retry" />
</gl-button>
</div>
<log-control-buttons
ref="scrollButtons"
class="controllers align-self-end"
@refresh="showPodLogs(pods.current)"
/>
</div>
<pre class="build-trace js-log-trace"><code class="bash">{{trace}}
<div v-if="showLoader" class="build-loader-animation js-build-loader-animation">
......
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import {
canScroll,
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
data() {
return {
scrollToTopEnabled: false,
scrollToBottomEnabled: false,
};
},
created() {
window.addEventListener('scroll', this.update);
},
destroyed() {
window.removeEventListener('scroll', this.update);
},
methods: {
/**
* Checks if page can be scrolled and updates
* enabled/disabled state of buttons accordingly
*/
update() {
this.scrollToTopEnabled = canScroll() && !isScrolledToTop();
this.scrollToBottomEnabled = canScroll() && !isScrolledToBottom();
},
handleRefreshClick() {
this.$emit('refresh');
},
scrollUp,
scrollDown,
},
};
</script>
<template>
<div>
<div
v-gl-tooltip
class="controllers-buttons"
:title="__('Scroll to top')"
aria-labelledby="scroll-to-top"
>
<gl-button
id="scroll-to-top"
class="btn-blank js-scroll-to-top"
:aria-label="__('Scroll to top')"
:disabled="!scrollToTopEnabled"
@click="scrollUp()"
><icon name="scroll_up"
/></gl-button>
</div>
<div
v-gl-tooltip
class="controllers-buttons"
:title="__('Scroll to bottom')"
aria-labelledby="scroll-to-bottom"
>
<gl-button
id="scroll-to-bottom"
class="btn-blank js-scroll-to-bottom"
:aria-label="__('Scroll to bottom')"
:disabled="!scrollToBottomEnabled"
@click="scrollDown()"
><icon name="scroll_down"
/></gl-button>
</div>
<gl-button
id="refresh-log"
v-gl-tooltip
class="ml-1 px-2 js-refresh-log"
:title="__('Refresh')"
:aria-label="__('Refresh')"
@click="handleRefreshClick"
>
<icon name="retry" />
</gl-button>
</div>
</template>
import Vue from 'vue';
import { GlDropdown, GlButton, GlDropdownItem } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import {
canScroll,
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
import { scrollDown } from '~/lib/utils/scroll_utils';
import EnvironmentLogs from 'ee/logs/components/environment_logs.vue';
import { createStore } from 'ee/logs/stores';
import {
mockProjectPath,
......@@ -30,8 +25,8 @@ describe('EnvironmentLogs', () => {
let state;
const propsData = {
environmentId: mockEnvId,
projectFullPath: mockProjectPath,
environmentId: mockEnvId,
currentEnvironmentName: mockEnvName,
environmentsPath: mockEnvironmentsEndpoint,
};
......@@ -42,11 +37,11 @@ describe('EnvironmentLogs', () => {
fetchEnvironments: jest.fn(),
};
const updateControlBtnsMock = jest.fn();
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
const findScrollToTop = () => wrapper.find('.js-scroll-to-top');
const findScrollToBottom = () => wrapper.find('.js-scroll-to-bottom');
const findRefreshLog = () => wrapper.find('.js-refresh-log');
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
const findLogTrace = () => wrapper.find('.js-log-trace');
const initWrapper = () => {
......@@ -55,6 +50,15 @@ describe('EnvironmentLogs', () => {
sync: false,
propsData,
store,
stubs: {
LogControlButtons: {
name: 'log-control-buttons-stub',
template: '<div/>',
methods: {
update: updateControlBtnsMock,
},
},
},
methods: {
...actionMocks,
},
......@@ -78,15 +82,12 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
expect(findLogTrace().isEmpty()).toBe(false);
expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
expect(findPodsDropdown().is(GlDropdown)).toBe(true);
expect(findScrollToTop().is(GlButton)).toBe(true);
expect(findScrollToBottom().is(GlButton)).toBe(true);
expect(findRefreshLog().is(GlButton)).toBe(true);
expect(findLogTrace().isEmpty()).toBe(false);
expect(findLogControlButtons().exists()).toBe(true);
});
it('mounted inits data', () => {
......@@ -129,6 +130,10 @@ describe('EnvironmentLogs', () => {
expect(findPodsDropdown().findAll(GlDropdownItem).length).toBe(0);
});
it('does not update buttons state', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled();
});
it('shows a logs trace', () => {
expect(findLogTrace().text()).toBe('');
expect(
......@@ -164,7 +169,7 @@ describe('EnvironmentLogs', () => {
afterEach(() => {
scrollDown.mockReset();
scrollUp.mockReset();
updateControlBtnsMock.mockReset();
actionMocks.setInitData.mockReset();
actionMocks.showPodLogs.mockReset();
......@@ -201,6 +206,10 @@ describe('EnvironmentLogs', () => {
expect(trace.text().split('\n')).toEqual(mockLines);
});
it('update control buttons state', () => {
expect(updateControlBtnsMock).toHaveBeenCalledTimes(1);
});
it('scrolls to bottom when loaded', () => {
expect(scrollDown).toHaveBeenCalledTimes(1);
});
......@@ -221,62 +230,11 @@ describe('EnvironmentLogs', () => {
it('refresh button, trace is refreshed', () => {
expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(0);
findRefreshLog().vm.$emit('click');
findLogControlButtons().vm.$emit('refresh');
expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(1);
expect(actionMocks.showPodLogs).toHaveBeenLastCalledWith(mockPodName);
});
describe('when scrolling actions are enabled', () => {
beforeEach(done => {
// mock scrolled to the middle of a long page
canScroll.mockReturnValue(true);
isScrolledToBottom.mockReturnValue(false);
isScrolledToTop.mockReturnValue(false);
initWrapper();
wrapper.vm.updateScrollState();
wrapper.vm.$nextTick(done);
});
afterEach(() => {
canScroll.mockReset();
isScrolledToTop.mockReset();
isScrolledToBottom.mockReset();
});
it('click on "scroll to top" scrolls up', () => {
expect(findScrollToTop().is('[disabled]')).toBe(false);
findScrollToTop().vm.$emit('click');
expect(scrollUp).toHaveBeenCalledTimes(1);
});
it('click on "scroll to bottom" scrolls down', () => {
expect(findScrollToBottom().is('[disabled]')).toBe(false);
findScrollToBottom().vm.$emit('click');
expect(scrollDown).toHaveBeenCalledTimes(2); // plus one time when trace was loaded
});
});
describe('when scrolling actions are disabled', () => {
beforeEach(() => {
// mock a short page without a scrollbar
canScroll.mockReturnValue(false);
isScrolledToBottom.mockReturnValue(true);
isScrolledToTop.mockReturnValue(true);
initWrapper();
});
it('buttons are disabled', done => {
wrapper.vm.updateScrollState();
wrapper.vm.$nextTick(() => {
expect(findScrollToTop().is('[disabled]')).toBe(true);
expect(findScrollToBottom().is('[disabled]')).toBe(true);
done();
});
});
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import {
canScroll,
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
import LogControlButtons from 'ee/logs/components/log_control_buttons.vue';
jest.mock('~/lib/utils/scroll_utils');
describe('LogControlButtons', () => {
let wrapper;
const findScrollToTop = () => wrapper.find('.js-scroll-to-top');
const findScrollToBottom = () => wrapper.find('.js-scroll-to-bottom');
const findRefreshBtn = () => wrapper.find('.js-refresh-log');
const initWrapper = () => {
wrapper = shallowMount(LogControlButtons, {
attachToDocument: true,
sync: false,
});
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
it('displays UI elements', () => {
initWrapper();
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
expect(findScrollToTop().is(GlButton)).toBe(true);
expect(findScrollToBottom().is(GlButton)).toBe(true);
expect(findRefreshBtn().is(GlButton)).toBe(true);
});
it('emits a `refresh` event on click on `refersh` button', () => {
initWrapper();
expect(wrapper.emitted('refresh')).toHaveLength(0);
findRefreshBtn().vm.$emit('click');
expect(wrapper.emitted('refresh')).toHaveLength(1);
});
describe('when scrolling actions are enabled', () => {
beforeEach(() => {
// mock scrolled to the middle of a long page
canScroll.mockReturnValue(true);
isScrolledToBottom.mockReturnValue(false);
isScrolledToTop.mockReturnValue(false);
initWrapper();
wrapper.vm.update();
return wrapper.vm.$nextTick();
});
afterEach(() => {
canScroll.mockReset();
isScrolledToTop.mockReset();
isScrolledToBottom.mockReset();
});
it('click on "scroll to top" scrolls up', () => {
expect(findScrollToTop().is('[disabled]')).toBe(false);
findScrollToTop().vm.$emit('click');
expect(scrollUp).toHaveBeenCalledTimes(1);
});
it('click on "scroll to bottom" scrolls down', () => {
expect(findScrollToBottom().is('[disabled]')).toBe(false);
findScrollToBottom().vm.$emit('click');
expect(scrollDown).toHaveBeenCalledTimes(1); // plus one time when trace was loaded
});
});
describe('when scrolling actions are disabled', () => {
beforeEach(() => {
// mock a short page without a scrollbar
canScroll.mockReturnValue(false);
isScrolledToBottom.mockReturnValue(true);
isScrolledToTop.mockReturnValue(true);
initWrapper();
});
it('buttons are disabled', () => {
wrapper.vm.update();
return wrapper.vm.$nextTick(() => {
expect(findScrollToTop().is('[disabled]')).toBe(true);
expect(findScrollToBottom().is('[disabled]')).toBe(true);
});
});
});
});
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