Commit 50c50a8f authored by Miguel Rincon's avatar Miguel Rincon

Add time picker to logs page

Add the datepicker component to the logs page and
connect it to the Vuex store.

Fixes a few style issues in the component, so it can adapt to
more layouts.
parent 3c21e5e2
......@@ -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