Commit 4d8ebc79 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '197879-add-time-picker-in-logs-time' into 'master'

Add time picker to logs page

Closes #197879

See merge request gitlab-org/gitlab!23837
parents ea067014 50c50a8f
......@@ -149,7 +149,13 @@ export default {
};
</script>
<template>
<gl-dropdown :text="timeWindowText" class="date-time-picker" menu-class="date-time-picker-menu">
<gl-dropdown
:text="timeWindowText"
class="date-time-picker"
menu-class="date-time-picker-menu"
v-bind="$attrs"
toggle-class="w-100 text-truncate"
>
<div class="d-flex justify-content-between gl-p-2">
<gl-form-group
:label="__('Custom range')"
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlDropdown, GlDropdownItem, GlFormGroup, GlSearchBoxByClick, GlAlert } 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';
export default {
components: {
GlAlert,
......@@ -11,6 +14,7 @@ export default {
GlDropdownItem,
GlFormGroup,
GlSearchBoxByClick,
DateTimePicker,
LogControlButtons,
},
props: {
......@@ -37,12 +41,24 @@ export default {
data() {
return {
searchQuery: '',
selectedTimeRange: defaultTimeRange,
timeRanges,
isElasticStackCalloutDismissed: false,
};
},
computed: {
...mapState('environmentLogs', ['environments', 'timeWindow', 'logs', 'pods']),
...mapState('environmentLogs', ['environments', 'timeRange', 'logs', 'pods']),
...mapGetters('environmentLogs', ['trace']),
timeRangeModel: {
get() {
return this.timeRange.current;
},
set(val) {
this.setTimeRange(val);
},
},
showLoader() {
return this.logs.isLoading || !this.logs.isComplete;
},
......@@ -85,7 +101,7 @@ export default {
...mapActions('environmentLogs', [
'setInitData',
'setSearch',
'setTimeWindow',
'setTimeRange',
'showPodLogs',
'showEnvironment',
'fetchEnvironments',
......@@ -166,22 +182,13 @@ export default {
label-for="time-window-dropdown"
class="col-3 px-1"
>
<gl-dropdown
id="time-window-dropdown"
ref="time-window-dropdown"
<date-time-picker
ref="dateTimePicker"
v-model="timeRangeModel"
class="w-100 gl-h-32"
:disabled="disableAdvancedControls"
:text="timeWindow.options[timeWindow.current].label"
class="d-flex gl-h-32"
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>
:options="timeRanges"
/>
</gl-form-group>
<gl-form-group
id="search-fg"
......
import { __ } from '~/locale';
export const defaultTimeWindow = 'oneHour';
export const timeWindows = {
oneHour: {
label: __('1 hour'),
seconds: 60 * 60,
},
fourHours: {
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,
},
twoWeeks: {
label: __('2 weeks'),
seconds: 60 * 60 * 24 * 15,
},
};
......@@ -4,10 +4,17 @@ import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { s__ } from '~/locale';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import * as types from './mutation_types';
import { getTimeRange } from '../utils';
import { timeWindows } from '../constants';
const flashTimeRangeWarning = () => {
flash(s__('Metrics|Invalid time range, please verify.'), 'warning');
};
const flashLogsError = () => {
flash(s__('Metrics|There was an error fetching the logs, please try again'));
};
const requestLogsUntilData = params =>
backOff((next, stop) => {
......@@ -39,8 +46,8 @@ export const setSearch = ({ dispatch, commit }, searchQuery) => {
dispatch('fetchLogs');
};
export const setTimeWindow = ({ dispatch, commit }, timeWindowKey) => {
commit(types.SET_TIME_WINDOW, timeWindowKey);
export const setTimeRange = ({ dispatch, commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
dispatch('fetchLogs');
};
......@@ -72,12 +79,14 @@ export const fetchLogs = ({ commit, state }) => {
search: state.search,
};
if (state.timeWindow.current) {
const { current } = state.timeWindow;
const { start, end } = getTimeRange(timeWindows[current].seconds);
params.start = start;
params.end = end;
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);
......@@ -94,7 +103,7 @@ export const fetchLogs = ({ commit, state }) => {
.catch(() => {
commit(types.RECEIVE_PODS_DATA_ERROR);
commit(types.RECEIVE_LOGS_DATA_ERROR);
flash(s__('Metrics|There was an error fetching the logs, please try again'));
flashLogsError();
});
};
......
export const SET_PROJECT_ENVIRONMENT = 'SET_PROJECT_ENVIRONMENT';
export const SET_SEARCH = 'SET_SEARCH';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export const SET_CURRENT_POD_NAME = 'SET_CURRENT_POD_NAME';
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
......@@ -10,7 +11,6 @@ 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 SET_CURRENT_POD_NAME = 'SET_CURRENT_POD_NAME';
export const REQUEST_PODS_DATA = 'REQUEST_PODS_DATA';
export const RECEIVE_PODS_DATA_SUCCESS = 'RECEIVE_PODS_DATA_SUCCESS';
export const RECEIVE_PODS_DATA_ERROR = 'RECEIVE_PODS_DATA_ERROR';
......@@ -7,8 +7,8 @@ export default {
},
/** Time Range data */
[types.SET_TIME_WINDOW](state, timeWindowKey) {
state.timeWindow.current = timeWindowKey;
[types.SET_TIME_RANGE](state, timeRange) {
state.timeRange.current = timeRange;
},
/** Environments data */
......
import { defaultTimeWindow, timeWindows } from '../constants';
import { timeRanges, defaultTimeRange } from '~/monitoring/constants';
export default () => ({
/**
......@@ -9,9 +9,9 @@ export default () => ({
/**
* Time range (Show last)
*/
timeWindow: {
options: { ...timeWindows },
current: defaultTimeWindow,
timeRange: {
options: timeRanges,
current: defaultTimeRange,
},
/**
......
......@@ -18,10 +18,6 @@
}
}
.dropdown-menu {
width: 300px;
}
.controllers {
@include build-controllers(16px, flex-end, true, 2);
}
......
---
title: Add time picker to logs page
merge_request: 23837
author:
type: added
import Vue from 'vue';
import { GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import EnvironmentLogs from 'ee/logs/components/environment_logs.vue';
import { createStore } from 'ee/logs/stores';
......@@ -42,7 +43,7 @@ describe('EnvironmentLogs', () => {
const findEnvironmentsDropdown = () => wrapper.find('.js-environments-dropdown');
const findPodsDropdown = () => wrapper.find('.js-pods-dropdown');
const findSearchBar = () => wrapper.find('.js-logs-search');
const findTimeWindowDropdown = () => wrapper.find({ ref: 'time-window-dropdown' });
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const findLogControlButtons = () => wrapper.find({ name: 'log-control-buttons-stub' });
const findLogTrace = () => wrapper.find('.js-log-trace');
......@@ -116,8 +117,8 @@ describe('EnvironmentLogs', () => {
expect(findSearchBar().exists()).toBe(true);
expect(findSearchBar().is(GlSearchBoxByClick)).toBe(true);
expect(findTimeWindowDropdown().exists()).toBe(true);
expect(findTimeWindowDropdown().is(GlDropdown)).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true);
expect(findTimeRangePicker().is(DateTimePicker)).toBe(true);
// log trace
expect(findLogTrace().isEmpty()).toBe(false);
......@@ -169,7 +170,7 @@ describe('EnvironmentLogs', () => {
});
it('displays a disabled time window dropdown', () => {
expect(findTimeWindowDropdown().attributes('disabled')).toBe('true');
expect(findTimeRangePicker().attributes('disabled')).toBe('true');
});
it('does not update buttons state', () => {
......@@ -207,7 +208,7 @@ describe('EnvironmentLogs', () => {
it('displays a disabled search bar and time window dropdown', () => {
expect(findSearchBar().exists()).toBe(true);
expect(findSearchBar().attributes('disabled')).toBe('true');
expect(findTimeWindowDropdown().attributes('disabled')).toBe('true');
expect(findTimeRangePicker().attributes('disabled')).toBe('true');
});
});
......@@ -234,7 +235,7 @@ describe('EnvironmentLogs', () => {
});
it('displays an enabled time window dropdown', () => {
expect(findTimeWindowDropdown().attributes('disabled')).toBeFalsy();
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
});
it('populates environments dropdown', () => {
......
......@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as types from 'ee/logs/stores/mutation_types';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import logsPageState from 'ee/logs/stores/state';
import {
setInitData,
......@@ -10,8 +11,8 @@ import {
fetchEnvironments,
fetchLogs,
} from 'ee/logs/stores/actions';
import { getTimeRange } from 'ee/logs/utils';
import { timeWindows } from 'ee/logs/constants';
import { defaultTimeRange } from '~/monitoring/constants';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
......@@ -28,21 +29,44 @@ import {
} from '../mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/datetime_range');
jest.mock('ee/logs/utils');
const mockDefaultRange = {
start: '2020-01-10T18:00:00.000Z',
end: '2020-01-10T10:00:00.000Z',
};
const mockFixedRange = {
start: '2020-01-09T18:06:20.000Z',
end: '2020-01-09T18:36:20.000Z',
};
const mockRollingRange = {
duration: 120,
};
const mockRollingRangeAsFixed = {
start: '2020-01-10T18:00:00.000Z',
end: '2020-01-10T17:58:00.000Z',
};
describe('Logs Store actions', () => {
let state;
let mock;
const mockThirtyMinutesSeconds = 3600;
const mockThirtyMinutes = {
start: '2020-01-09T18:06:20.000Z',
end: '2020-01-09T18:36:20.000Z',
};
convertToFixedRange.mockImplementation(range => {
if (range === defaultTimeRange) {
return { ...mockDefaultRange };
}
if (range === mockFixedRange) {
return { ...mockFixedRange };
}
if (range === mockRollingRange) {
return { ...mockRollingRangeAsFixed };
}
throw new Error('Invalid time range');
});
beforeEach(() => {
state = logsPageState();
getTimeRange.mockReturnValue(mockThirtyMinutes);
});
afterEach(() => {
......@@ -50,45 +74,33 @@ describe('Logs Store actions', () => {
});
describe('setInitData', () => {
it('should commit environment and pod name mutation', done => {
testAction(
setInitData,
{ environmentName: mockEnvName, podName: mockPodName },
state,
[
{ type: types.SET_PROJECT_ENVIRONMENT, payload: mockEnvName },
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
],
[],
done,
);
});
it('should commit environment and pod name mutation', () =>
testAction(setInitData, { environmentName: mockEnvName, podName: mockPodName }, state, [
{ type: types.SET_PROJECT_ENVIRONMENT, payload: mockEnvName },
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
]));
});
describe('setSearch', () => {
it('should commit search mutation', done => {
it('should commit search mutation', () =>
testAction(
setSearch,
mockSearch,
state,
[{ type: types.SET_SEARCH, payload: mockSearch }],
[{ type: 'fetchLogs' }],
done,
);
});
));
});
describe('showPodLogs', () => {
it('should commit pod name', done => {
it('should commit pod name', () =>
testAction(
showPodLogs,
mockPodName,
state,
[{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName }],
[{ type: 'fetchLogs' }],
done,
);
});
));
});
describe('fetchEnvironments', () => {
......@@ -96,9 +108,9 @@ describe('Logs Store actions', () => {
mock = new MockAdapter(axios);
});
it('should commit RECEIVE_ENVIRONMENTS_DATA_SUCCESS mutation on correct data', done => {
it('should commit RECEIVE_ENVIRONMENTS_DATA_SUCCESS mutation on correct data', () => {
mock.onGet(mockEnvironmentsEndpoint).replyOnce(200, { environments: mockEnvironments });
testAction(
return testAction(
fetchEnvironments,
mockEnvironmentsEndpoint,
state,
......@@ -107,13 +119,12 @@ describe('Logs Store actions', () => {
{ type: types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, payload: mockEnvironments },
],
[{ type: 'fetchLogs' }],
done,
);
});
it('should commit RECEIVE_ENVIRONMENTS_DATA_ERROR on wrong data', done => {
it('should commit RECEIVE_ENVIRONMENTS_DATA_ERROR on wrong data', () => {
mock.onGet(mockEnvironmentsEndpoint).replyOnce(500);
testAction(
return testAction(
fetchEnvironments,
mockEnvironmentsEndpoint,
state,
......@@ -124,7 +135,6 @@ describe('Logs Store actions', () => {
[],
() => {
expect(flash).toHaveBeenCalledTimes(1);
done();
},
);
});
......@@ -139,7 +149,7 @@ describe('Logs Store actions', () => {
mock.reset();
});
it('should commit logs and pod data when there is pod name defined', done => {
it('should commit logs and pod data when there is pod name defined', () => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
state.pods.current = mockPodName;
......@@ -148,7 +158,11 @@ describe('Logs Store actions', () => {
mock
.onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, ...mockThirtyMinutes },
params: {
environment_name: mockEnvName,
pod_name: mockPodName,
...mockDefaultRange,
},
})
.reply(200, {
pod_name: mockPodName,
......@@ -158,7 +172,7 @@ describe('Logs Store actions', () => {
mock.onGet(endpoint).replyOnce(202); // mock reactive cache
testAction(
return testAction(
fetchLogs,
null,
state,
......@@ -170,33 +184,26 @@ describe('Logs Store actions', () => {
{ type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult },
],
[],
() => {
expect(getTimeRange).toHaveBeenCalledWith(mockThirtyMinutesSeconds);
done();
},
);
});
it('should commit logs and pod data when there is pod name defined and a non-default date range', done => {
const mockOneDaySeconds = timeWindows.oneDay.seconds;
const mockOneDay = {
start: '2020-01-08T18:41:39.000Z',
end: '2020-01-09T18:41:39.000Z',
};
getTimeRange.mockReturnValueOnce(mockOneDay);
it('should commit logs and pod data when there is pod name defined and a non-default date range', () => {
state.projectPath = mockProjectPath;
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
state.pods.current = mockPodName;
state.timeWindow.current = 'oneDay';
state.timeRange.current = mockFixedRange;
const endpoint = `/${mockProjectPath}/-/logs/elasticsearch.json`;
mock
.onGet(endpoint, {
params: { environment_name: mockEnvName, pod_name: mockPodName, ...mockOneDay },
params: {
environment_name: mockEnvName,
pod_name: mockPodName,
start: mockFixedRange.start,
end: mockFixedRange.end,
},
})
.reply(200, {
pod_name: mockPodName,
......@@ -204,7 +211,7 @@ describe('Logs Store actions', () => {
logs: mockLogsResult,
});
testAction(
return testAction(
fetchLogs,
null,
state,
......@@ -216,18 +223,15 @@ describe('Logs Store actions', () => {
{ type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult },
],
[],
() => {
expect(getTimeRange).toHaveBeenCalledWith(mockOneDaySeconds);
done();
},
);
});
it('should commit logs and pod data when there is pod name and search', done => {
it('should commit logs and pod data when there is pod name and search and a faulty date range', () => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
state.pods.current = mockPodName;
state.search = mockSearch;
state.timeRange.current = 'INVALID_TIME_RANGE';
const endpoint = `/${mockProjectPath}/-/logs/elasticsearch.json`;
......@@ -237,7 +241,6 @@ describe('Logs Store actions', () => {
environment_name: mockEnvName,
pod_name: mockPodName,
search: mockSearch,
...mockThirtyMinutes,
},
})
.reply(200, {
......@@ -248,7 +251,7 @@ describe('Logs Store actions', () => {
mock.onGet(endpoint).replyOnce(202); // mock reactive cache
testAction(
return testAction(
fetchLogs,
null,
state,
......@@ -260,7 +263,11 @@ describe('Logs Store actions', () => {
{ type: types.RECEIVE_LOGS_DATA_SUCCESS, payload: mockLogsResult },
],
[],
done,
() => {
// Warning about time ranges was issued
expect(flash).toHaveBeenCalledTimes(1);
expect(flash).toHaveBeenCalledWith(expect.any(String), 'warning');
},
);
});
......@@ -271,7 +278,7 @@ describe('Logs Store actions', () => {
const endpoint = `/${mockProjectPath}/-/logs/elasticsearch.json`;
mock
.onGet(endpoint, { params: { environment_name: mockEnvName, ...mockThirtyMinutes } })
.onGet(endpoint, { params: { environment_name: mockEnvName, ...mockDefaultRange } })
.reply(200, {
pod_name: mockPodName,
pods: mockPods,
......@@ -295,14 +302,14 @@ describe('Logs Store actions', () => {
);
});
it('should commit logs and pod errors when backend fails', done => {
it('should commit logs and pod errors when backend fails', () => {
state.environments.options = mockEnvironments;
state.environments.current = mockEnvName;
const endpoint = `/${mockProjectPath}/-/logs/elasticsearch.json?environment_name=${mockEnvName}`;
mock.onGet(endpoint).replyOnce(500);
testAction(
return testAction(
fetchLogs,
null,
state,
......@@ -315,7 +322,6 @@ describe('Logs Store actions', () => {
[],
() => {
expect(flash).toHaveBeenCalledTimes(1);
done();
},
);
});
......
......@@ -119,12 +119,19 @@ describe('Logs Store Mutations', () => {
});
});
describe('SET_TIME_WINDOW', () => {
it('sets a time window Key', () => {
const mockKey = 'fourHours';
mutations[types.SET_TIME_WINDOW](state, mockKey);
describe('SET_TIME_RANGE', () => {
it('sets a default range', () => {
expect(state.timeRange.current).toEqual(expect.any(Object));
});
it('sets a time range', () => {
const mockRange = {
start: '2020-01-10T18:00:00.000Z',
end: '2020-01-10T10:00:00.000Z',
};
mutations[types.SET_TIME_RANGE](state, mockRange);
expect(state.timeWindow.current).toEqual(mockKey);
expect(state.timeRange.current).toEqual(mockRange);
});
});
......
......@@ -649,9 +649,6 @@ msgstr ""
msgid "1st contribution!"
msgstr ""
msgid "2 days"
msgstr ""
msgid "2 weeks"
msgstr ""
......@@ -682,9 +679,6 @@ msgstr ""
msgid "30+ contributions"
msgstr ""
msgid "4 hours"
msgstr ""
msgid "403|Please contact your GitLab administrator to get permission."
msgstr ""
......@@ -11991,6 +11985,9 @@ msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
msgid "Metrics|Invalid time range, please verify."
msgstr ""
msgid "Metrics|Label of the y-axis (usually the unit). The x-axis always represents time."
msgstr ""
......@@ -13362,9 +13359,6 @@ msgstr ""
msgid "Past due"
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