Commit f0b119c0 authored by Miguel Rincon's avatar Miguel Rincon

Add time window filter to logs

- Add new dropdown for logs screen
- Add new param start & end to requests to backend
- Use logs specific constants for time ranges
- Update translation pot file
parent f347c4bd
---
title: Add time filters to environments log view
merge_request: 22638
author:
type: added
......@@ -88,13 +88,15 @@ export default {
* Returns pods logs for an environment with an optional pod and container
*
* @param {Object} params
* @param {string} param.projectFullPath - Path of the project, in format `/<namespace>/<project-key>`
* @param {number} param.environmentId - Id of the environment
* @param {string} params.projectFullPath - Path of the project, in format `/<namespace>/<project-key>`
* @param {number} params.environmentId - Id of the environment
* @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({ projectPath, environmentName, podName, containerName, search }) {
getPodLogs({ projectPath, environmentName, podName, containerName, search, start, end }) {
const url = this.buildUrl(this.podLogsPath).replace(':project_full_path', projectPath);
const params = {
......@@ -110,6 +112,12 @@ export default {
if (search) {
params.search = search;
}
if (start) {
params.start = start;
}
if (end) {
params.end = end;
}
return axios.get(url, { params });
},
......
......@@ -45,7 +45,13 @@ export default {
};
},
computed: {
...mapState('environmentLogs', ['environments', 'logs', 'pods', 'enableAdvancedQuerying']),
...mapState('environmentLogs', [
'environments',
'timeWindow',
'logs',
'pods',
'enableAdvancedQuerying',
]),
...mapGetters('environmentLogs', ['trace']),
showLoader() {
return this.logs.isLoading || !this.logs.isComplete;
......@@ -87,6 +93,7 @@ export default {
...mapActions('environmentLogs', [
'setInitData',
'setSearch',
'setTimeWindow',
'showPodLogs',
'showEnvironment',
'fetchEnvironments',
......@@ -119,7 +126,7 @@ export default {
:label="s__('Environments|Environment')"
label-size="sm"
label-for="environments-dropdown"
:class="featureElasticEnabled ? 'col-4' : 'col-6'"
:class="featureElasticEnabled ? 'col-3' : 'col-6'"
>
<gl-dropdown
id="environments-dropdown"
......@@ -142,7 +149,7 @@ export default {
:label="s__('Environments|Pod logs from')"
label-size="sm"
label-for="pods-dropdown"
:class="featureElasticEnabled ? 'col-4' : 'col-6'"
:class="featureElasticEnabled ? 'col-3' : 'col-6'"
>
<gl-dropdown
id="pods-dropdown"
......@@ -166,7 +173,7 @@ export default {
:label="s__('Environments|Search')"
label-size="sm"
label-for="search"
class="col-4"
class="col-3"
>
<gl-search-box-by-click
v-model.trim="searchQuery"
......@@ -178,6 +185,29 @@ export default {
@submit="setSearch(searchQuery)"
/>
</gl-form-group>
<gl-form-group
v-if="featureElasticEnabled"
id="dates-fg"
:label="s__('Environments|Show last')"
label-size="sm"
label-for="time-window-dropdown"
class="col-3"
>
<gl-dropdown
id="time-window-dropdown"
:text="timeWindow.options[timeWindow.current].label"
class="d-flex"
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-item
v-for="(option, key) in timeWindow.options"
:key="key"
@click="setTimeWindow(key)"
>
{{ option.label }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</div>
<log-control-buttons
......
import { __ } from '~/locale';
export const defaultTimeWindow = 'thirtyMinutes';
export const timeWindows = {
thirtyMinutes: {
label: __('1 hour'),
seconds: 60 * 60,
},
threeHours: {
label: __('4 hours'),
seconds: 60 * 60 * 4,
},
oneDay: {
label: __('1 day'),
seconds: 60 * 60 * 24,
},
twoDays: {
label: __('2 days'),
seconds: 60 * 60 * 24 * 3,
},
pastWeek: {
label: __('Past week'),
seconds: 60 * 60 * 24 * 7,
},
pastMonth: {
label: __('Past month'),
seconds: 60 * 60 * 24 * 30,
},
};
......@@ -6,6 +6,8 @@ import flash from '~/flash';
import { s__ } from '~/locale';
import * as types from './mutation_types';
import { getTimeRange } from '../utils';
const requestLogsUntilData = params =>
backOff((next, stop) => {
Api.getPodLogs(params)
......@@ -38,6 +40,11 @@ export const setSearch = ({ dispatch, commit }, searchQuery) => {
dispatch('fetchLogs');
};
export const setTimeWindow = ({ dispatch, commit }, timeWindow) => {
commit(types.SET_TIME_WINDOW, timeWindow);
dispatch('fetchLogs');
};
export const showEnvironment = ({ dispatch, commit }, environmentName) => {
commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, null);
......@@ -66,6 +73,12 @@ export const fetchLogs = ({ commit, state }) => {
search: state.search,
};
if (state.timeWindow.current) {
const { start, end } = getTimeRange(state.timeWindow.current.seconds);
params.start = start;
params.end = end;
}
commit(types.REQUEST_PODS_DATA);
commit(types.REQUEST_LOGS_DATA);
......
......@@ -2,6 +2,7 @@ export const SET_PROJECT_PATH = 'SET_PROJECT_PATH';
export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT';
export const SET_SEARCH = 'SET_SEARCH';
export const ENABLE_ADVANCED_QUERYING = 'ENABLE_ADVANCED_QUERYING';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
......
......@@ -15,6 +15,10 @@ export default {
[types.ENABLE_ADVANCED_QUERYING](state, enableAdvancedQuerying) {
state.enableAdvancedQuerying = enableAdvancedQuerying;
},
/** Time Range data */
[types.SET_TIME_WINDOW](state, timeWindow) {
state.timeWindow.current = timeWindow;
},
/** Environments data */
[types.SET_PROJECT_ENVIRONMENT](state, environmentName) {
......
import { defaultTimeWindow, timeWindows } from '../constants';
export default () => ({
/**
* Current project path
......@@ -14,6 +16,14 @@ export default () => ({
*/
enableAdvancedQuerying: false,
/**
* Time range (Show last)
*/
timeWindow: {
options: { ...timeWindows },
current: defaultTimeWindow,
},
/**
* Environments list information
*/
......
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
/**
* Returns a time range (`start`, `end`) where `start` is the
* current time minus a given number of seconds and `end`
* is the current time (`now()`).
*
* @param {Number} seconds Seconds duration, defaults to 0.
* @returns {Object} range Time range
* @returns {String} range.start ISO String of current time minus given seconds
* @returns {String} range.end ISO String of current time
*/
export const getTimeRange = (seconds = 0) => {
const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds
const start = end - seconds;
return {
start: new Date(secondsToMilliseconds(start)).toISOString(),
end: new Date(secondsToMilliseconds(end)).toISOString(),
};
};
export default {};
......@@ -238,7 +238,7 @@ describe('EnvironmentLogs', () => {
const item = items.at(i);
expect(item.text()).toBe(env.name);
});
expect(wrapper.find('#environments-dropdown-fg').attributes('class')).toEqual('col-4');
expect(wrapper.find('#environments-dropdown-fg').attributes('class')).toEqual('col-3');
});
it('populates pods dropdown', () => {
......@@ -250,7 +250,7 @@ describe('EnvironmentLogs', () => {
const item = items.at(i);
expect(item.text()).toBe(pod);
});
expect(wrapper.find('#pods-dropdown-fg').attributes('class')).toEqual('col-4');
expect(wrapper.find('#pods-dropdown-fg').attributes('class')).toEqual('col-3');
});
it('populates logs trace', () => {
......@@ -262,7 +262,7 @@ describe('EnvironmentLogs', () => {
it('displays an enabled search bar', () => {
expect(findSearchBar().exists()).toEqual(true);
expect(findSearchBar().attributes('disabled')).toEqual(undefined);
expect(wrapper.find('#search-fg').attributes('class')).toEqual('col-4');
expect(wrapper.find('#search-fg').attributes('class')).toEqual('col-3');
});
it('update control buttons state', () => {
......
......@@ -10,8 +10,9 @@ import {
fetchEnvironments,
fetchLogs,
} from 'ee/logs/stores/actions';
import axios from '~/lib/utils/axios_utils';
import { getTimeRange } from 'ee/logs/utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import {
......@@ -27,13 +28,20 @@ import {
} from '../mock_data';
jest.mock('~/flash');
jest.mock('ee/logs/utils');
describe('Logs Store actions', () => {
let state;
let mock;
const mockThirtyMinutes = {
start: '2020-01-09T18:06:20.000Z',
end: '2020-01-09T18:36:20.000Z',
};
beforeEach(() => {
state = logsPageState();
getTimeRange.mockReturnValue(mockThirtyMinutes);
});
afterEach(() => {
......@@ -139,7 +147,9 @@ describe('Logs Store actions', () => {
const endpoint = `/${mockProjectPath}/-/logs/k8s.json`;
mock
.onGet(endpoint, { params: { environment_name: mockEnvName, pod_name: mockPodName } })
.onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, ...mockThirtyMinutes },
})
.reply(200, {
pod_name: mockPodName,
pods: mockPods,
......@@ -166,6 +176,42 @@ describe('Logs Store actions', () => {
);
});
it('should commit logs and pod data when there is pod name defined and a non-default date range', done => {
const mockOneDay = { start: '2020-01-08T18:41:39.000Z', end: '2020-01-09T18:41:39.000Z' };
getTimeRange.mockReturnValueOnce(mockOneDay);
state.projectPath = mockProjectPath;
state.environments.current = mockEnvName;
state.pods.current = mockPodName;
const endpoint = `/${mockProjectPath}/-/logs/k8s.json`;
mock
.onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, ...mockOneDay },
})
.reply(200, {
pod_name: mockPodName,
pods: mockPods,
logs: mockLogsResult,
});
testAction(
fetchLogs,
null,
state,
[
{ type: types.REQUEST_PODS_DATA },
{ type: types.REQUEST_LOGS_DATA },
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.RECEIVE_PODS_DATA_SUCCESS, payload: mockPods },
{ type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult },
],
[],
done,
);
});
it('should commit logs and pod data when there is pod name and search', done => {
state.projectPath = mockProjectPath;
state.environments.current = mockEnvName;
......@@ -177,7 +223,12 @@ describe('Logs Store actions', () => {
mock
.onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, search: mockSearch },
params: {
environment_name: mockEnvName,
pod_name: mockPodName,
search: mockSearch,
...mockThirtyMinutes,
},
})
.reply(200, {
pod_name: mockPodName,
......@@ -211,12 +262,14 @@ describe('Logs Store actions', () => {
const endpoint = `/${mockProjectPath}/-/logs/k8s.json`;
mock.onGet(endpoint, { params: { environment_name: mockEnvName } }).reply(200, {
pod_name: mockPodName,
pods: mockPods,
logs: mockLogsResult,
enable_advanced_querying: mockEnableAdvancedQuerying,
});
mock
.onGet(endpoint, { params: { environment_name: mockEnvName, ...mockThirtyMinutes } })
.reply(200, {
pod_name: mockPodName,
pods: mockPods,
logs: mockLogsResult,
enable_advanced_querying: mockEnableAdvancedQuerying,
});
mock.onGet(endpoint).replyOnce(202); // mock reactive cache
testAction(
......
import { getTimeRange } from 'ee/logs/utils';
describe('logs/utils', () => {
describe('getTimeRange', () => {
const nowTimestamp = 1577836800000;
const nowString = '2020-01-01T00:00:00.000Z';
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => nowTimestamp);
});
afterEach(() => {
Date.now.mockRestore();
});
it('returns the right values', () => {
expect(getTimeRange(0)).toEqual({
start: '2020-01-01T00:00:00.000Z',
end: nowString,
});
expect(getTimeRange(60 * 30)).toEqual({
start: '2019-12-31T23:30:00.000Z',
end: nowString,
});
expect(getTimeRange(60 * 60 * 24 * 7 * 1)).toEqual({
start: '2019-12-25T00:00:00.000Z',
end: nowString,
});
expect(getTimeRange(60 * 60 * 24 * 7 * 4)).toEqual({
start: '2019-12-04T00:00:00.000Z',
end: nowString,
});
});
});
});
......@@ -638,6 +638,9 @@ msgstr ""
msgid "1st contribution!"
msgstr ""
msgid "2 days"
msgstr ""
msgid "20-29 contributions"
msgstr ""
......@@ -665,6 +668,9 @@ msgstr ""
msgid "30+ contributions"
msgstr ""
msgid "4 hours"
msgstr ""
msgid "403|Please contact your GitLab administrator to get permission."
msgstr ""
......@@ -7183,6 +7189,9 @@ msgstr ""
msgid "Environments|Show all"
msgstr ""
msgid "Environments|Show last"
msgstr ""
msgid "Environments|Stop"
msgstr ""
......@@ -13082,6 +13091,12 @@ msgstr ""
msgid "Past due"
msgstr ""
msgid "Past month"
msgstr ""
msgid "Past week"
msgstr ""
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
......
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