Commit bec477eb authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '198050-frontend-pagination-in-log-explorer' into 'master'

Add infinite scrolling to logs

Closes #206936

See merge request gitlab-org/gitlab!26254
parents 5dfbaf36 198cf31f
......@@ -492,41 +492,6 @@ const Api = {
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
/**
* Returns pods logs for an environment with an optional pod and container
*
* @param {Object} params
* @param {Object} param.environment - Environment object
* @param {string=} params.podName - Pod name, if not set the backend assumes a default one
* @param {string=} params.containerName - Container name, if not set the backend assumes a default one
* @param {string=} params.start - Starting date to query the logs in ISO format
* @param {string=} params.end - Ending date to query the logs in ISO format
* @returns {Promise} Axios promise for the result of a GET request of logs
*/
getPodLogs({ environment, podName, containerName, search, start, end }) {
const url = this.buildUrl(environment.logs_api_path);
const params = {};
if (podName) {
params.pod_name = podName;
}
if (containerName) {
params.container_name = containerName;
}
if (search) {
params.search = search;
}
if (start) {
params.start = start;
}
if (end) {
params.end = end;
}
return axios.get(url, { params });
},
};
export default Api;
<script>
import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlDropdown, GlDropdownItem, GlFormGroup, GlSearchBoxByClick, GlAlert } from '@gitlab/ui';
import {
GlSprintf,
GlAlert,
GlDropdown,
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll,
} from '@gitlab/ui';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { scrollDown } from '~/lib/utils/scroll_utils';
import LogControlButtons from './log_control_buttons.vue';
import { timeRanges, defaultTimeRange } from '~/monitoring/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
import { formatDate } from '../utils';
export default {
components: {
GlSprintf,
GlAlert,
GlDropdown,
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
GlInfiniteScroll,
DateTimePicker,
LogControlButtons,
},
filters: {
formatDate,
},
props: {
environmentName: {
type: String,
......@@ -39,11 +53,13 @@ export default {
required: true,
},
},
traceHeight: 600,
data() {
return {
searchQuery: '',
timeRanges,
isElasticStackCalloutDismissed: false,
scrollDownButtonDisabled: true,
};
},
computed: {
......@@ -52,7 +68,7 @@ export default {
timeRangeModel: {
get() {
return this.timeRange.current;
return this.timeRange.selected;
},
set(val) {
this.setTimeRange(val);
......@@ -60,7 +76,7 @@ export default {
},
showLoader() {
return this.logs.isLoading || !this.logs.isComplete;
return this.logs.isLoading;
},
advancedFeaturesEnabled() {
const environment = this.environments.options.find(
......@@ -75,16 +91,6 @@ export default {
return !this.isElasticStackCalloutDismissed && this.disableAdvancedControls;
},
},
watch: {
trace(val) {
this.$nextTick(() => {
if (val) {
scrollDown();
}
this.$refs.scrollButtons.update();
});
},
},
mounted() {
this.setInitData({
timeRange: timeRangeFromUrl() || defaultTimeRange,
......@@ -102,12 +108,26 @@ export default {
'showPodLogs',
'showEnvironment',
'fetchEnvironments',
'fetchMoreLogsPrepend',
]),
topReached() {
if (!this.logs.isLoading) {
this.fetchMoreLogsPrepend();
}
},
scrollDown() {
this.$refs.infiniteScroll.scrollDown();
},
scroll: throttle(function scrollThrottled({ target = {} }) {
const { scrollTop = 0, clientHeight = 0, scrollHeight = 0 } = target;
this.scrollDownButtonDisabled = scrollTop + clientHeight === scrollHeight;
}, 200),
},
};
</script>
<template>
<div class="build-page-pod-logs mt-3">
<div class="environment-logs-viewer mt-3">
<gl-alert
v-if="shouldShowElasticStackCallout"
class="mb-3 js-elasticsearch-alert"
......@@ -209,14 +229,50 @@ export default {
<log-control-buttons
ref="scrollButtons"
class="controllers align-self-end mb-1"
:scroll-down-button-disabled="scrollDownButtonDisabled"
@refresh="showPodLogs(pods.current)"
@scrollDown="scrollDown"
/>
</div>
<pre class="build-trace js-log-trace"><code class="bash js-build-output">{{trace}}
<div v-if="showLoader" class="build-loader-animation js-build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div></code></pre>
<gl-infinite-scroll
ref="infiniteScroll"
class="log-lines"
:style="{ height: `${$options.traceHeight}px` }"
:max-list-height="$options.traceHeight"
:fetched-items="logs.lines.length"
@topReached="topReached"
@scroll="scroll"
>
<template #items>
<pre
class="build-trace js-log-trace"
><code class="bash js-build-output"><div v-if="showLoader" class="build-loader-animation js-build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>{{trace}}
</code></pre>
</template>
<template #default
><div></div
></template>
</gl-infinite-scroll>
<div ref="logFooter" class="log-footer py-2 px-3">
<gl-sprintf :message="s__('Environments|Logs from %{start} to %{end}.')">
<template #start>{{ timeRange.current.start | formatDate }}</template>
<template #end>{{ timeRange.current.end | formatDate }}</template>
</gl-sprintf>
<gl-sprintf
v-if="!logs.isComplete"
:message="s__('Environments|Currently showing %{fetched} results.')"
>
<template #fetched>{{ logs.lines.length }}</template>
</gl-sprintf>
<template v-else>
{{ s__('Environments|Currently showing all results.') }}</template
>
</div>
</div>
</template>
<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 {
......@@ -17,32 +10,34 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
scrollUpButtonDisabled: {
type: Boolean,
required: false,
default: false,
},
scrollDownButtonDisabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
scrollToTopEnabled: false,
scrollToBottomEnabled: false,
scrollUpAvailable: Boolean(this.$listeners.scrollUp),
scrollDownAvailable: Boolean(this.$listeners.scrollDown),
};
},
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,
handleScrollUp() {
this.$emit('scrollUp');
},
handleScrollDown() {
this.$emit('scrollDown');
},
},
};
</script>
......@@ -50,6 +45,7 @@ export default {
<template>
<div>
<div
v-if="scrollUpAvailable"
v-gl-tooltip
class="controllers-buttons"
:title="__('Scroll to top')"
......@@ -59,13 +55,15 @@ export default {
id="scroll-to-top"
class="btn-blank js-scroll-to-top"
:aria-label="__('Scroll to top')"
:disabled="!scrollToTopEnabled"
@click="scrollUp()"
:disabled="scrollUpButtonDisabled"
@click="handleScrollUp()"
><icon name="scroll_up"
/></gl-button>
</div>
<div
v-if="scrollDownAvailable"
v-gl-tooltip
:disabled="scrollUpButtonDisabled"
class="controllers-buttons"
:title="__('Scroll to bottom')"
aria-labelledby="scroll-to-bottom"
......@@ -74,8 +72,9 @@ export default {
id="scroll-to-bottom"
class="btn-blank js-scroll-to-bottom"
:aria-label="__('Scroll to bottom')"
:disabled="!scrollToBottomEnabled"
@click="scrollDown()"
:v-if="scrollDownAvailable"
:disabled="scrollDownButtonDisabled"
@click="handleScrollDown()"
><icon name="scroll_down"
/></gl-button>
</div>
......
import Api from '~/api';
import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
......@@ -16,9 +15,10 @@ const flashLogsError = () => {
flash(s__('Metrics|There was an error fetching the logs, please try again'));
};
const requestLogsUntilData = params =>
const requestUntilData = (url, params) =>
backOff((next, stop) => {
Api.getPodLogs(params)
axios
.get(url, { params })
.then(res => {
if (res.status === httpStatusCodes.ACCEPTED) {
next();
......@@ -31,10 +31,36 @@ const requestLogsUntilData = params =>
});
});
export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => {
if (timeRange) {
commit(types.SET_TIME_RANGE, timeRange);
const requestLogsUntilData = state => {
const params = {};
const { logs_api_path } = state.environments.options.find(
({ name }) => name === state.environments.current,
);
if (state.pods.current) {
params.pod_name = state.pods.current;
}
if (state.search) {
params.search = state.search;
}
if (state.timeRange.current) {
try {
const { start, end } = convertToFixedRange(state.timeRange.current);
params.start = start;
params.end = end;
} catch {
flashTimeRangeWarning();
}
}
if (state.logs.cursor) {
params.cursor = state.logs.cursor;
}
return requestUntilData(logs_api_path, params);
};
export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => {
commit(types.SET_TIME_RANGE, timeRange);
commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, podName);
};
......@@ -60,10 +86,15 @@ export const showEnvironment = ({ dispatch, commit }, environmentName) => {
dispatch('fetchLogs');
};
/**
* Fetch environments data and initial logs
* @param {Object} store
* @param {String} environmentsPath
*/
export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
commit(types.REQUEST_ENVIRONMENTS_DATA);
axios
return axios
.get(environmentsPath)
.then(({ data }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data.environments);
......@@ -76,32 +107,16 @@ export const fetchEnvironments = ({ commit, dispatch }, environmentsPath) => {
};
export const fetchLogs = ({ commit, state }) => {
const params = {
environment: state.environments.options.find(({ name }) => name === state.environments.current),
podName: state.pods.current,
search: state.search,
};
if (state.timeRange.current) {
try {
const { start, end } = convertToFixedRange(state.timeRange.current);
params.start = start;
params.end = end;
} catch {
flashTimeRangeWarning();
}
}
commit(types.REQUEST_PODS_DATA);
commit(types.REQUEST_LOGS_DATA);
return requestLogsUntilData(params)
return requestLogsUntilData(state)
.then(({ data }) => {
const { pod_name, pods, logs } = data;
const { pod_name, pods, logs, cursor } = data;
commit(types.SET_CURRENT_POD_NAME, pod_name);
commit(types.RECEIVE_PODS_DATA_SUCCESS, pods);
commit(types.RECEIVE_LOGS_DATA_SUCCESS, logs);
commit(types.RECEIVE_LOGS_DATA_SUCCESS, { logs, cursor });
})
.catch(() => {
commit(types.RECEIVE_PODS_DATA_ERROR);
......@@ -110,5 +125,24 @@ export const fetchLogs = ({ commit, state }) => {
});
};
export const fetchMoreLogsPrepend = ({ commit, state }) => {
if (state.logs.isComplete) {
// return when all logs are loaded
return Promise.resolve();
}
commit(types.REQUEST_LOGS_DATA_PREPEND);
return requestLogsUntilData(state)
.then(({ data }) => {
const { logs, cursor } = data;
commit(types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS, { logs, cursor });
})
.catch(() => {
commit(types.RECEIVE_LOGS_DATA_PREPEND_ERROR);
flashLogsError();
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import dateFormat from 'dateformat';
import { formatDate } from '../utils';
export const trace = state =>
state.logs.lines
.map(item => [dateFormat(item.timestamp, 'UTC:mmm dd HH:MM:ss.l"Z"'), item.message].join(' | '))
.join('\n');
const mapTrace = ({ timestamp = null, message = '' }) =>
[timestamp ? formatDate(timestamp) : '', message].join(' | ');
export const trace = state => state.logs.lines.map(mapTrace).join('\n');
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -10,6 +10,9 @@ export const RECEIVE_ENVIRONMENTS_DATA_ERROR = 'RECEIVE_ENVIRONMENTS_DATA_ERROR'
export const REQUEST_LOGS_DATA = 'REQUEST_LOGS_DATA';
export const RECEIVE_LOGS_DATA_SUCCESS = 'RECEIVE_LOGS_DATA_SUCCESS';
export const RECEIVE_LOGS_DATA_ERROR = 'RECEIVE_LOGS_DATA_ERROR';
export const REQUEST_LOGS_DATA_PREPEND = 'REQUEST_LOGS_DATA_PREPEND';
export const RECEIVE_LOGS_DATA_PREPEND_SUCCESS = 'RECEIVE_LOGS_DATA_PREPEND_SUCCESS';
export const RECEIVE_LOGS_DATA_PREPEND_ERROR = 'RECEIVE_LOGS_DATA_PREPEND_ERROR';
export const REQUEST_PODS_DATA = 'REQUEST_PODS_DATA';
export const RECEIVE_PODS_DATA_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS';
......
import * as types from './mutation_types';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
const mapLine = ({ timestamp, message }) => ({
timestamp,
message,
});
export default {
/** Search data */
// Search Data
[types.SET_SEARCH](state, searchQuery) {
state.search = searchQuery;
},
/** Time Range data */
// Time Range Data
[types.SET_TIME_RANGE](state, timeRange) {
state.timeRange.current = timeRange;
state.timeRange.selected = timeRange;
state.timeRange.current = convertToFixedRange(timeRange);
},
/** Environments data */
// Environments Data
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
state.environments.current = environmentName;
},
......@@ -28,24 +35,49 @@ export default {
state.environments.isLoading = false;
},
/** Logs data */
// Logs data
[types.REQUEST_LOGS_DATA](state) {
state.timeRange.current = convertToFixedRange(state.timeRange.selected);
state.logs.lines = [];
state.logs.isLoading = true;
// start pagination from the beginning
state.logs.cursor = null;
state.logs.isComplete = false;
},
[types.RECEIVE_LOGS_DATA_SUCCESS](state, lines) {
state.logs.lines = lines;
[types.RECEIVE_LOGS_DATA_SUCCESS](state, { logs = [], cursor }) {
state.logs.lines = logs.map(mapLine);
state.logs.isLoading = false;
state.logs.isComplete = true;
state.logs.cursor = cursor;
if (!cursor) {
state.logs.isComplete = true;
}
},
[types.RECEIVE_LOGS_DATA_ERROR](state) {
state.logs.lines = [];
state.logs.isLoading = false;
state.logs.isComplete = true;
},
/** Pods data */
[types.REQUEST_LOGS_DATA_PREPEND](state) {
state.logs.isLoading = true;
},
[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, { logs = [], cursor }) {
const lines = logs.map(mapLine);
state.logs.lines = lines.concat(state.logs.lines);
state.logs.isLoading = false;
state.logs.cursor = cursor;
if (!cursor) {
state.logs.isComplete = true;
}
},
[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state) {
state.logs.isLoading = false;
},
// Pods data
[types.SET_CURRENT_POD_NAME](state, podName) {
state.pods.current = podName;
},
......
import { timeRanges, defaultTimeRange } from '~/monitoring/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
export default () => ({
/**
......@@ -11,7 +12,10 @@ export default () => ({
*/
timeRange: {
options: timeRanges,
current: defaultTimeRange,
// Selected time range, can be fixed or relative
selected: defaultTimeRange,
// Current time range, must be fixed
current: convertToFixedRange(defaultTimeRange),
},
/**
......@@ -29,7 +33,12 @@ export default () => ({
logs: {
lines: [],
isLoading: false,
isComplete: true,
/**
* Logs `cursor` represents the current pagination position,
* Should be sent in next batch (page) of logs to be fetched
*/
cursor: null,
isComplete: false,
},
/**
......
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import dateFormat from 'dateformat';
const dateFormatMask = 'UTC:mmm dd HH:MM:ss.l"Z"';
/**
* Returns a time range (`start`, `end`) where `start` is the
......@@ -20,4 +23,6 @@ export const getTimeRange = (seconds = 0) => {
};
};
export const formatDate = timestamp => dateFormat(timestamp, dateFormatMask);
export default {};
......@@ -257,7 +257,6 @@
width: 15px;
height: 15px;
display: $svg-display;
fill: $gl-text-color;
top: $svg-top;
}
......
......@@ -358,17 +358,30 @@
}
}
.build-page-pod-logs {
.environment-logs-viewer {
.build-trace-container {
position: relative;
}
.log-lines,
.gl-infinite-scroll-container {
// makes scrollbar visible by creating contrast
background: $black;
}
.gl-infinite-scroll-legend {
margin: 0;
}
.build-trace {
@include build-trace();
margin: 0;
}
.top-bar {
@include build-trace-top-bar($gl-line-height * 5);
position: relative;
top: 0;
.dropdown-menu-toggle {
width: 200px;
......@@ -395,4 +408,9 @@
.build-loader-animation {
@include build-loader-animation;
}
.log-footer {
color: $white-normal;
background-color: $gray-900;
}
}
---
title: More logs entries are loaded when logs are scrolled to the top
merge_request: 26254
author:
type: added
......@@ -46,13 +46,15 @@ Logs can be displayed by clicking on a specific pod from [Deploy Boards](../depl
### Logs view
The logs view will contain the last 500 lines for a pod, and has control to filter through:
The logs view lets you filter the logs by:
- Pods.
- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/5769), environments.
- [From GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21656), [full text search](#full-text-search).
- [From GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/197879), dates.
Loading more than 500 log lines is possible from [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/198050) onwards.
Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/13404).
Support for historical data is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/196191).
......
......@@ -5,8 +5,6 @@ require 'spec_helper'
describe 'Environment > Pod Logs', :js do
include KubernetesHelpers
SCROLL_DISTANCE = 400
let(:pod_names) { %w(kube-pod) }
let(:pod_name) { pod_names.first }
let(:project) { create(:project, :repository) }
......@@ -62,49 +60,4 @@ describe 'Environment > Pod Logs', :js do
expect(page).to have_content("Dec 13 14:04:22.123Z | Log 1 Dec 13 14:04:23.123Z | Log 2 Dec 13 14:04:24.123Z | Log 3")
end
end
context 'with perf bar enabled' do
before do
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
end
it 'log header sticks to top' do
load_and_scroll_down
expect(log_header_top).to eq(navbar_height + perf_bar_height)
end
end
context 'with perf bar disabled' do
it 'log header sticks to top' do
load_and_scroll_down
expect(log_header_top).to eq(navbar_height)
end
end
def load_and_scroll_down
visit project_logs_path(environment.project, environment_name: environment.name, pod_name: pod_name)
wait_for_requests
scroll_down_build_log
end
def scroll_down_build_log
page.execute_script("$('.js-build-output').height('200vh')")
page.execute_script("window.scrollTo(0, #{SCROLL_DISTANCE})")
end
def perf_bar_height
page.evaluate_script("$('#js-peek').height()").to_i
end
def navbar_height
page.evaluate_script("$('.js-navbar').height()").to_i
end
def log_header_top
page.evaluate_script("$('.js-top-bar').offset().top") - SCROLL_DISTANCE
end
end
......@@ -165,77 +165,6 @@ describe('Api', () => {
});
});
describe('getPodLogs', () => {
const projectPath = '/root/test-project';
const podName = 'pod';
const containerName = 'container';
const search = 'foo +bar';
const expectedUrl = '/gitlab/dummy_api_path.json';
const environment = {
enable_advanced_logs_querying: false,
project_path: projectPath,
logs_api_path: '/dummy_api_path.json',
};
const getRequest = () => mock.history.get[0];
beforeEach(() => {
mock.onAny().reply(200);
});
afterEach(() => {
mock.reset();
});
it('calls `axios.get` with pod_name and container_name', done => {
Api.getPodLogs({ environment, podName, containerName })
.then(() => {
expect(getRequest().url).toBe(expectedUrl);
expect(getRequest().params).toEqual({
pod_name: podName,
container_name: containerName,
});
})
.then(done)
.catch(done.fail);
});
it('calls `axios.get` without pod_name and container_name', done => {
Api.getPodLogs({ environment })
.then(() => {
expect(getRequest().url).toBe(expectedUrl);
expect(getRequest().params).toEqual({});
})
.then(done)
.catch(done.fail);
});
it('calls `axios.get` with pod_name', done => {
Api.getPodLogs({ environment, podName })
.then(() => {
expect(getRequest().url).toBe(expectedUrl);
expect(getRequest().params).toEqual({
pod_name: podName,
});
})
.then(done)
.catch(done.fail);
});
it('calls `axios.get` with pod_name and search', done => {
Api.getPodLogs({ environment, podName, search })
.then(() => {
expect(getRequest().url).toBe(expectedUrl);
expect(getRequest().params).toEqual({
pod_name: podName,
search,
});
})
.then(done)
.catch(done.fail);
});
});
describe('packages', () => {
const projectId = 'project_a';
const packageId = 'package_b';
......
......@@ -7634,6 +7634,12 @@ msgstr ""
msgid "Environments|Commit"
msgstr ""
msgid "Environments|Currently showing %{fetched} results."
msgstr ""
msgid "Environments|Currently showing all results."
msgstr ""
msgid "Environments|Deploy to..."
msgstr ""
......@@ -7667,6 +7673,9 @@ msgstr ""
msgid "Environments|Logs from"
msgstr ""
msgid "Environments|Logs from %{start} to %{end}."
msgstr ""
msgid "Environments|New environment"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import LogControlButtons from '~/logs/components/log_control_buttons.vue';
import {
canScroll,
isScrolledToTop,
isScrolledToBottom,
scrollDown,
scrollUp,
} from '~/lib/utils/scroll_utils';
jest.mock('~/lib/utils/scroll_utils');
describe('LogControlButtons', () => {
let wrapper;
......@@ -18,8 +9,14 @@ describe('LogControlButtons', () => {
const findScrollToBottom = () => wrapper.find('.js-scroll-to-bottom');
const findRefreshBtn = () => wrapper.find('.js-refresh-log');
const initWrapper = () => {
wrapper = shallowMount(LogControlButtons);
const initWrapper = opts => {
wrapper = shallowMount(LogControlButtons, {
listeners: {
scrollUp: () => {},
scrollDown: () => {},
},
...opts,
});
};
afterEach(() => {
......@@ -55,27 +52,16 @@ describe('LogControlButtons', () => {
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);
expect(wrapper.emitted('scrollUp')).toHaveLength(1);
});
it('click on "scroll to bottom" scrolls down', () => {
......@@ -83,25 +69,23 @@ describe('LogControlButtons', () => {
findScrollToBottom().vm.$emit('click');
expect(scrollDown).toHaveBeenCalledTimes(1); // plus one time when trace was loaded
expect(wrapper.emitted('scrollDown')).toHaveLength(1);
});
});
describe('when scrolling actions are disabled', () => {
beforeEach(() => {
// mock a short page without a scrollbar
canScroll.mockReturnValue(false);
isScrolledToBottom.mockReturnValue(true);
isScrolledToTop.mockReturnValue(true);
initWrapper();
initWrapper({ listeners: {} });
return wrapper.vm.$nextTick();
});
it('buttons are disabled', () => {
wrapper.vm.update();
return wrapper.vm.$nextTick(() => {
expect(findScrollToTop().is('[disabled]')).toBe(true);
expect(findScrollToBottom().is('[disabled]')).toBe(true);
expect(findScrollToTop().exists()).toBe(false);
expect(findScrollToBottom().exists()).toBe(false);
// This should be enabled when gitlab-ui contains:
// https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1149
// expect(findScrollToBottom().is('[disabled]')).toBe(true);
});
});
});
......
export const mockProjectPath = 'root/autodevops-deploy';
const mockProjectPath = 'root/autodevops-deploy';
export const mockEnvName = 'production';
export const mockEnvironmentsEndpoint = `${mockProjectPath}/environments.json`;
export const mockEnvId = '99';
export const mockDocumentationPath = '/documentation.md';
export const mockLogsEndpoint = '/dummy_logs_path.json';
export const mockCursor = 'MOCK_CURSOR';
export const mockNextCursor = 'MOCK_NEXT_CURSOR';
const makeMockEnvironment = (id, name, advancedQuerying) => ({
id,
project_path: mockProjectPath,
name,
logs_api_path: '/dummy_logs_path.json',
logs_api_path: mockLogsEndpoint,
enable_advanced_logs_querying: advancedQuerying,
});
......@@ -28,58 +32,22 @@ export const mockPods = [
];
export const mockLogsResult = [
{
timestamp: '2019-12-13T13:43:18.2760123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:18.2760123Z', message: '- -> /' },
{
timestamp: '2019-12-13T13:43:26.8420123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:26.8420123Z', message: '- -> /' },
{
timestamp: '2019-12-13T13:43:28.3710123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:28.3710123Z', message: '- -> /' },
{
timestamp: '2019-12-13T13:43:36.8860123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:36.8860123Z', message: '- -> /' },
{
timestamp: '2019-12-13T13:43:38.4000123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:38.4000123Z', message: '- -> /' },
{
timestamp: '2019-12-13T13:43:46.8420123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:46.8430123Z', message: '- -> /' },
{
timestamp: '2019-12-13T13:43:48.3240123Z',
message: '10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13',
},
{ timestamp: '2019-12-13T13:43:48.3250123Z', message: '- -> /' },
{ timestamp: '2019-12-13T13:43:18.2760123Z', message: 'Log 1' },
{ timestamp: '2019-12-13T13:43:18.2760123Z', message: 'Log 2' },
{ timestamp: '2019-12-13T13:43:26.8420123Z', message: 'Log 3' },
];
export const mockTrace = [
'Dec 13 13:43:18.276Z | 10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:18.276Z | - -> /',
'Dec 13 13:43:26.842Z | 10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:26.842Z | - -> /',
'Dec 13 13:43:28.371Z | 10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:28.371Z | - -> /',
'Dec 13 13:43:36.886Z | 10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:36.886Z | - -> /',
'Dec 13 13:43:38.400Z | 10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:38.400Z | - -> /',
'Dec 13 13:43:46.842Z | 10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:46.843Z | - -> /',
'Dec 13 13:43:48.324Z | 10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13',
'Dec 13 13:43:48.325Z | - -> /',
'Dec 13 13:43:18.276Z | Log 1',
'Dec 13 13:43:18.276Z | Log 2',
'Dec 13 13:43:26.842Z | Log 3',
];
export const mockResponse = {
pod_name: mockPodName,
pods: mockPods,
logs: mockLogsResult,
cursor: mockNextCursor,
};
export const mockSearch = 'foo +bar';
This diff is collapsed.
......@@ -9,6 +9,8 @@ import {
mockPodName,
mockLogsResult,
mockSearch,
mockCursor,
mockNextCursor,
} from '../mock_data';
describe('Logs Store Mutations', () => {
......@@ -73,27 +75,47 @@ describe('Logs Store Mutations', () => {
it('starts loading for logs', () => {
mutations[types.REQUEST_LOGS_DATA](state);
expect(state.logs).toEqual(
expect.objectContaining({
lines: [],
isLoading: true,
isComplete: false,
}),
);
expect(state.timeRange.current).toEqual({
start: expect.any(String),
end: expect.any(String),
});
expect(state.logs).toEqual({
lines: [],
cursor: null,
isLoading: true,
isComplete: false,
});
});
});
describe('RECEIVE_LOGS_DATA_SUCCESS', () => {
it('receives logs lines', () => {
mutations[types.RECEIVE_LOGS_DATA_SUCCESS](state, mockLogsResult);
it('receives logs lines and cursor', () => {
mutations[types.RECEIVE_LOGS_DATA_SUCCESS](state, {
logs: mockLogsResult,
cursor: mockCursor,
});
expect(state.logs).toEqual(
expect.objectContaining({
lines: mockLogsResult,
isLoading: false,
isComplete: true,
}),
);
expect(state.logs).toEqual({
lines: mockLogsResult,
isLoading: false,
cursor: mockCursor,
isComplete: false,
});
});
it('receives logs lines and a null cursor to indicate the end', () => {
mutations[types.RECEIVE_LOGS_DATA_SUCCESS](state, {
logs: mockLogsResult,
cursor: null,
});
expect(state.logs).toEqual({
lines: mockLogsResult,
isLoading: false,
cursor: null,
isComplete: true,
});
});
});
......@@ -101,13 +123,77 @@ describe('Logs Store Mutations', () => {
it('receives log data error and stops loading', () => {
mutations[types.RECEIVE_LOGS_DATA_ERROR](state);
expect(state.logs).toEqual(
expect.objectContaining({
lines: [],
isLoading: false,
isComplete: true,
}),
);
expect(state.logs).toEqual({
lines: [],
isLoading: false,
cursor: null,
isComplete: false,
});
});
});
describe('REQUEST_LOGS_DATA_PREPEND', () => {
it('receives logs lines and cursor', () => {
mutations[types.REQUEST_LOGS_DATA_PREPEND](state);
expect(state.logs.isLoading).toBe(true);
});
});
describe('RECEIVE_LOGS_DATA_PREPEND_SUCCESS', () => {
it('receives logs lines and cursor', () => {
mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, {
logs: mockLogsResult,
cursor: mockCursor,
});
expect(state.logs).toEqual({
lines: mockLogsResult,
isLoading: false,
cursor: mockCursor,
isComplete: false,
});
});
it('receives additional logs lines and a new cursor', () => {
mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, {
logs: mockLogsResult,
cursor: mockCursor,
});
mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, {
logs: mockLogsResult,
cursor: mockNextCursor,
});
expect(state.logs).toEqual({
lines: [...mockLogsResult, ...mockLogsResult],
isLoading: false,
cursor: mockNextCursor,
isComplete: false,
});
});
it('receives logs lines and a null cursor to indicate is complete', () => {
mutations[types.RECEIVE_LOGS_DATA_PREPEND_SUCCESS](state, {
logs: mockLogsResult,
cursor: null,
});
expect(state.logs).toEqual({
lines: mockLogsResult,
isLoading: false,
cursor: null,
isComplete: true,
});
});
});
describe('RECEIVE_LOGS_DATA_PREPEND_ERROR', () => {
it('receives logs lines and cursor', () => {
mutations[types.RECEIVE_LOGS_DATA_PREPEND_ERROR](state);
expect(state.logs.isLoading).toBe(false);
});
});
......@@ -121,6 +207,7 @@ describe('Logs Store Mutations', () => {
describe('SET_TIME_RANGE', () => {
it('sets a default range', () => {
expect(state.timeRange.selected).toEqual(expect.any(Object));
expect(state.timeRange.current).toEqual(expect.any(Object));
});
......@@ -131,12 +218,13 @@ describe('Logs Store Mutations', () => {
};
mutations[types.SET_TIME_RANGE](state, mockRange);
expect(state.timeRange.selected).toEqual(mockRange);
expect(state.timeRange.current).toEqual(mockRange);
});
});
describe('REQUEST_PODS_DATA', () => {
it('receives log data error and stops loading', () => {
it('receives pods data', () => {
mutations[types.REQUEST_PODS_DATA](state);
expect(state.pods).toEqual(
......
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