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> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlDropdown, GlDropdownItem, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { import { scrollDown } from '~/lib/utils/scroll_utils';
canScroll, import LogControlButtons from './log_control_buttons.vue';
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlFormGroup, GlFormGroup,
GlButton, LogControlButtons,
Icon,
},
directives: {
GlTooltip: GlTooltipDirective,
}, },
props: { props: {
environmentId: { environmentId: {
...@@ -46,12 +36,6 @@ export default { ...@@ -46,12 +36,6 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
scrollToTopEnabled: false,
scrollToBottomEnabled: false,
};
},
computed: { computed: {
...mapState('environmentLogs', ['environments', 'logs', 'pods']), ...mapState('environmentLogs', ['environments', 'logs', 'pods']),
...mapGetters('environmentLogs', ['trace']), ...mapGetters('environmentLogs', ['trace']),
...@@ -63,16 +47,12 @@ export default { ...@@ -63,16 +47,12 @@ export default {
trace(val) { trace(val) {
this.$nextTick(() => { this.$nextTick(() => {
if (val) { if (val) {
this.scrollDown(); scrollDown();
} else {
this.updateScrollState();
} }
this.$refs.scrollButtons.update();
}); });
}, },
}, },
created() {
window.addEventListener('scroll', this.updateScrollState);
},
mounted() { mounted() {
this.setInitData({ this.setInitData({
projectPath: this.projectFullPath, projectPath: this.projectFullPath,
...@@ -82,17 +62,8 @@ export default { ...@@ -82,17 +62,8 @@ export default {
this.fetchEnvironments(this.environmentsPath); this.fetchEnvironments(this.environmentsPath);
}, },
destroyed() {
window.removeEventListener('scroll', this.updateScrollState);
},
methods: { methods: {
...mapActions('environmentLogs', ['setInitData', 'showPodLogs', 'fetchEnvironments']), ...mapActions('environmentLogs', ['setInitData', 'showPodLogs', 'fetchEnvironments']),
updateScrollState() {
this.scrollToTopEnabled = canScroll() && !isScrolledToTop();
this.scrollToBottomEnabled = canScroll() && !isScrolledToBottom();
},
scrollUp,
scrollDown,
}, },
}; };
</script> </script>
...@@ -147,48 +118,12 @@ export default { ...@@ -147,48 +118,12 @@ export default {
</gl-dropdown> </gl-dropdown>
</gl-form-group> </gl-form-group>
</div> </div>
<div class="controllers align-self-end">
<div <log-control-buttons
v-gl-tooltip ref="scrollButtons"
class="controllers-buttons" class="controllers align-self-end"
:title="__('Scroll to top')" @refresh="showPodLogs(pods.current)"
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>
</div> </div>
<pre class="build-trace js-log-trace"><code class="bash">{{trace}} <pre class="build-trace js-log-trace"><code class="bash">{{trace}}
<div v-if="showLoader" class="build-loader-animation js-build-loader-animation"> <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 Vue from 'vue';
import { GlDropdown, GlButton, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { import { scrollDown } from '~/lib/utils/scroll_utils';
canScroll,
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
import EnvironmentLogs from 'ee/logs/components/environment_logs.vue'; import EnvironmentLogs from 'ee/logs/components/environment_logs.vue';
import { createStore } from 'ee/logs/stores'; import { createStore } from 'ee/logs/stores';
import { import {
mockProjectPath, mockProjectPath,
...@@ -30,8 +25,8 @@ describe('EnvironmentLogs', () => { ...@@ -30,8 +25,8 @@ describe('EnvironmentLogs', () => {
let state; let state;
const propsData = { const propsData = {
environmentId: mockEnvId,
projectFullPath: mockProjectPath, projectFullPath: mockProjectPath,
environmentId: mockEnvId,
currentEnvironmentName: mockEnvName, currentEnvironmentName: mockEnvName,
environmentsPath: mockEnvironmentsEndpoint, environmentsPath: mockEnvironmentsEndpoint,
}; };
...@@ -42,11 +37,11 @@ describe('EnvironmentLogs', () => { ...@@ -42,11 +37,11 @@ describe('EnvironmentLogs', () => {
fetchEnvironments: jest.fn(), fetchEnvironments: jest.fn(),
}; };
const updateControlBtnsMock = jest.fn();
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown'); const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
const findPodsDropdown = () => wrapper.find('.js-pods-dropdown'); const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
const findScrollToTop = () => wrapper.find('.js-scroll-to-top'); const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
const findScrollToBottom = () => wrapper.find('.js-scroll-to-bottom');
const findRefreshLog = () => wrapper.find('.js-refresh-log');
const findLogTrace = () => wrapper.find('.js-log-trace'); const findLogTrace = () => wrapper.find('.js-log-trace');
const initWrapper = () => { const initWrapper = () => {
...@@ -55,6 +50,15 @@ describe('EnvironmentLogs', () => { ...@@ -55,6 +50,15 @@ describe('EnvironmentLogs', () => {
sync: false, sync: false,
propsData, propsData,
store, store,
stubs: {
LogControlButtons: {
name: 'log-control-buttons-stub',
template: '<div/>',
methods: {
update: updateControlBtnsMock,
},
},
},
methods: { methods: {
...actionMocks, ...actionMocks,
}, },
...@@ -78,15 +82,12 @@ describe('EnvironmentLogs', () => { ...@@ -78,15 +82,12 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true); expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false); expect(wrapper.isEmpty()).toBe(false);
expect(findLogTrace().isEmpty()).toBe(false);
expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true); expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
expect(findPodsDropdown().is(GlDropdown)).toBe(true); expect(findPodsDropdown().is(GlDropdown)).toBe(true);
expect(findScrollToTop().is(GlButton)).toBe(true); expect(findLogControlButtons().exists()).toBe(true);
expect(findScrollToBottom().is(GlButton)).toBe(true);
expect(findRefreshLog().is(GlButton)).toBe(true);
expect(findLogTrace().isEmpty()).toBe(false);
}); });
it('mounted inits data', () => { it('mounted inits data', () => {
...@@ -129,6 +130,10 @@ describe('EnvironmentLogs', () => { ...@@ -129,6 +130,10 @@ describe('EnvironmentLogs', () => {
expect(findPodsDropdown().findAll(GlDropdownItem).length).toBe(0); expect(findPodsDropdown().findAll(GlDropdownItem).length).toBe(0);
}); });
it('does not update buttons state', () => {
expect(updateControlBtnsMock).not.toHaveBeenCalled();
});
it('shows a logs trace', () => { it('shows a logs trace', () => {
expect(findLogTrace().text()).toBe(''); expect(findLogTrace().text()).toBe('');
expect( expect(
...@@ -164,7 +169,7 @@ describe('EnvironmentLogs', () => { ...@@ -164,7 +169,7 @@ describe('EnvironmentLogs', () => {
afterEach(() => { afterEach(() => {
scrollDown.mockReset(); scrollDown.mockReset();
scrollUp.mockReset(); updateControlBtnsMock.mockReset();
actionMocks.setInitData.mockReset(); actionMocks.setInitData.mockReset();
actionMocks.showPodLogs.mockReset(); actionMocks.showPodLogs.mockReset();
...@@ -201,6 +206,10 @@ describe('EnvironmentLogs', () => { ...@@ -201,6 +206,10 @@ describe('EnvironmentLogs', () => {
expect(trace.text().split('\n')).toEqual(mockLines); expect(trace.text().split('\n')).toEqual(mockLines);
}); });
it('update control buttons state', () => {
expect(updateControlBtnsMock).toHaveBeenCalledTimes(1);
});
it('scrolls to bottom when loaded', () => { it('scrolls to bottom when loaded', () => {
expect(scrollDown).toHaveBeenCalledTimes(1); expect(scrollDown).toHaveBeenCalledTimes(1);
}); });
...@@ -221,62 +230,11 @@ describe('EnvironmentLogs', () => { ...@@ -221,62 +230,11 @@ describe('EnvironmentLogs', () => {
it('refresh button, trace is refreshed', () => { it('refresh button, trace is refreshed', () => {
expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(0); expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(0);
findRefreshLog().vm.$emit('click'); findLogControlButtons().vm.$emit('refresh');
expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(1); expect(actionMocks.showPodLogs).toHaveBeenCalledTimes(1);
expect(actionMocks.showPodLogs).toHaveBeenLastCalledWith(mockPodName); 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