Commit f85a287b authored by Miguel Rincon's avatar Miguel Rincon

Add filtered search to logs

Adds the filtered search and a first token for pods.
parent baee3dd3
...@@ -88,10 +88,9 @@ export default { ...@@ -88,10 +88,9 @@ export default {
methods: { methods: {
...mapActions('environmentLogs', [ ...mapActions('environmentLogs', [
'setInitData', 'setInitData',
'setSearch',
'showPodLogs',
'showEnvironment', 'showEnvironment',
'fetchEnvironments', 'fetchEnvironments',
'fetchLogs',
'fetchMoreLogsPrepend', 'fetchMoreLogsPrepend',
'dismissRequestEnvironmentsError', 'dismissRequestEnvironmentsError',
'dismissInvalidTimeRangeWarning', 'dismissInvalidTimeRangeWarning',
...@@ -189,13 +188,13 @@ export default { ...@@ -189,13 +188,13 @@ export default {
<log-advanced-filters <log-advanced-filters
v-if="showAdvancedFilters" v-if="showAdvancedFilters"
ref="log-advanced-filters" ref="log-advanced-filters"
class="d-md-flex flex-grow-1" class="d-md-flex flex-grow-1 min-width-0"
:disabled="environments.isLoading" :disabled="environments.isLoading"
/> />
<log-simple-filters <log-simple-filters
v-else v-else
ref="log-simple-filters" ref="log-simple-filters"
class="d-md-flex flex-grow-1" class="d-md-flex flex-grow-1 min-width-0"
:disabled="environments.isLoading" :disabled="environments.isLoading"
/> />
...@@ -203,7 +202,7 @@ export default { ...@@ -203,7 +202,7 @@ export default {
ref="scrollButtons" ref="scrollButtons"
class="flex-grow-0 pr-2 mb-2 controllers" class="flex-grow-0 pr-2 mb-2 controllers"
:scroll-down-button-disabled="scrollDownButtonDisabled" :scroll-down-button-disabled="scrollDownButtonDisabled"
@refresh="showPodLogs(pods.current)" @refresh="fetchLogs()"
@scrollDown="scrollDown" @scrollDown="scrollDown"
/> />
</div> </div>
......
<script> <script>
import { s__ } from '~/locale';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { import { GlFilteredSearch } from '@gitlab/ui';
GlIcon, import { __, s__ } from '~/locale';
GlDropdown, import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
} from '@gitlab/ui';
import { timeRanges } from '~/vue_shared/constants'; import { timeRanges } from '~/vue_shared/constants';
import { TOKEN_TYPE_POD_NAME } from '../constants';
import TokenWithLoadingState from './tokens/token_with_loading_state.vue';
export default { export default {
components: { components: {
GlIcon, GlFilteredSearch,
GlDropdown,
GlDropdownHeader,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByClick,
DateTimePicker, DateTimePicker,
}, },
props: { props: {
...@@ -32,11 +22,10 @@ export default { ...@@ -32,11 +22,10 @@ export default {
data() { data() {
return { return {
timeRanges, timeRanges,
searchQuery: '',
}; };
}, },
computed: { computed: {
...mapState('environmentLogs', ['timeRange', 'pods']), ...mapState('environmentLogs', ['timeRange', 'pods', 'logs']),
timeRangeModel: { timeRangeModel: {
get() { get() {
...@@ -46,75 +35,56 @@ export default { ...@@ -46,75 +35,56 @@ export default {
this.setTimeRange(val); this.setTimeRange(val);
}, },
}, },
/**
* Token options.
*
* Returns null when no pods are present, so suggestions are displayed in the token
*/
podOptions() {
if (this.pods.options.length) {
return this.pods.options.map(podName => ({ value: podName, title: podName }));
}
return null;
},
podDropdownText() { tokens() {
return this.pods.current || s__('Environments|All pods'); return [
{
icon: 'pod',
type: TOKEN_TYPE_POD_NAME,
title: s__('Environments|Pod name'),
token: TokenWithLoadingState,
operators: [{ value: '=', description: __('is'), default: 'true' }],
unique: true,
options: this.podOptions,
loading: this.logs.isLoading,
noOptionsText: s__('Environments|No pods to display'),
},
];
}, },
}, },
methods: { methods: {
...mapActions('environmentLogs', ['setSearch', 'showPodLogs', 'setTimeRange']), ...mapActions('environmentLogs', ['showFilteredLogs', 'setTimeRange']),
isCurrentPod(podName) {
return podName === this.pods.current; filteredSearchSubmit(filters) {
this.showFilteredLogs(filters);
}, },
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-dropdown <div class="mb-2 pr-2 flex-grow-1 min-width-0">
ref="podsDropdown" <gl-filtered-search
:text="podDropdownText" :placeholder="__('Search')"
:disabled="disabled" :clear-button-title="__('Clear')"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown" :close-button-title="__('Close')"
> class="gl-h-32"
<gl-dropdown-header class="text-center"> :disabled="disabled || logs.isLoading"
{{ s__('Environments|Filter by pod') }} :available-tokens="tokens"
</gl-dropdown-header> @submit="filteredSearchSubmit"
/>
<gl-dropdown-item v-if="!pods.options.length" disabled> </div>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
</gl-dropdown-item>
<template v-else>
<gl-dropdown-item ref="allPodsOption" key="all-pods" @click="showPodLogs(null)">
<div class="d-flex">
<gl-icon
:class="{ invisible: pods.current !== null }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ s__('Environments|All pods') }}</div>
</div>
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@click="showPodLogs(podName)"
>
<div class="d-flex">
<gl-icon
:class="{ invisible: !isCurrentPod(podName) }"
name="status_success_borderless"
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
</gl-dropdown-item>
</template>
</gl-dropdown>
<gl-search-box-by-click
ref="searchBox"
v-model.trim="searchQuery"
:disabled="disabled"
:placeholder="s__('Environments|Search')"
class="mb-2 pr-2 flex-grow-1"
type="search"
autofocus
@submit="setSearch(searchQuery)"
/>
<date-time-picker <date-time-picker
ref="dateTimePicker" ref="dateTimePicker"
......
<script>
import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlFilteredSearchToken,
GlLoadingIcon,
},
inheritAttrs: false,
props: {
config: {
type: Object,
required: true,
},
},
};
</script>
<template>
<gl-filtered-search-token :config="config" v-bind="{ ...$attrs }" v-on="$listeners">
<template #suggestions>
<div class="m-1">
<gl-loading-icon v-if="config.loading" />
<div v-else class="py-1 px-2 text-muted">
{{ config.noOptionsText }}
</div>
</div>
</template>
</gl-filtered-search-token>
</template>
export const dateFormatMask = 'UTC:mmm dd HH:MM:ss.l"Z"';
export const TOKEN_TYPE_POD_NAME = 'TOKEN_TYPE_POD_NAME';
...@@ -2,6 +2,7 @@ import { backOff } from '~/lib/utils/common_utils'; ...@@ -2,6 +2,7 @@ import { backOff } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { TOKEN_TYPE_POD_NAME } from '../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -49,19 +50,42 @@ const requestLogsUntilData = ({ commit, state }) => { ...@@ -49,19 +50,42 @@ const requestLogsUntilData = ({ commit, state }) => {
return requestUntilData(logs_api_path, params); return requestUntilData(logs_api_path, params);
}; };
/**
* Converts filters emitted by the component, e.g. a filterered-search
* to parameters to be applied to the filters of the store
* @param {Array} filters - List of strings or objects to filter by.
* @returns {Object} - An object with `search` and `podName` keys.
*/
const filtersToParams = (filters = []) => {
// Strings become part of the `search`
const search = filters
.filter(f => typeof f === 'string')
.join(' ')
.trim();
// null podName to show all pods
const podName = filters.find(f => f?.type === TOKEN_TYPE_POD_NAME)?.value?.data ?? null;
return { search, podName };
};
export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => { export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => {
commit(types.SET_TIME_RANGE, timeRange); commit(types.SET_TIME_RANGE, timeRange);
commit(types.SET_PROJECT_ENVIRONMENT, environmentName); commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, podName); commit(types.SET_CURRENT_POD_NAME, podName);
}; };
export const showPodLogs = ({ dispatch, commit }, podName) => { export const showFilteredLogs = ({ dispatch, commit }, filters = []) => {
const { podName, search } = filtersToParams(filters);
commit(types.SET_CURRENT_POD_NAME, podName); commit(types.SET_CURRENT_POD_NAME, podName);
commit(types.SET_SEARCH, search);
dispatch('fetchLogs'); dispatch('fetchLogs');
}; };
export const setSearch = ({ dispatch, commit }, searchQuery) => { export const showPodLogs = ({ dispatch, commit }, podName) => {
commit(types.SET_SEARCH, searchQuery); commit(types.SET_CURRENT_POD_NAME, podName);
dispatch('fetchLogs'); dispatch('fetchLogs');
}; };
......
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { dateFormatMask } from './constants';
const dateFormatMask = 'UTC:mmm dd HH:MM:ss.l"Z"';
/** /**
* Returns a time range (`start`, `end`) where `start` is the * Returns a time range (`start`, `end`) where `start` is the
......
...@@ -54,6 +54,11 @@ ...@@ -54,6 +54,11 @@
.mh-50vh { max-height: 50vh; } .mh-50vh { max-height: 50vh; }
.min-width-0 {
// By default flex items don't shrink below their minimum content size. To change this, set the item's min-width
min-width: 0;
}
.font-size-inherit { font-size: inherit; } .font-size-inherit { font-size: inherit; }
.gl-w-8 { width: px-to-rem($grid-size); } .gl-w-8 { width: px-to-rem($grid-size); }
.gl-w-16 { width: px-to-rem($grid-size * 2); } .gl-w-16 { width: px-to-rem($grid-size * 2); }
......
---
title: Add filtered search for elastic search in logs
merge_request: 27654
author:
type: added
...@@ -14,7 +14,7 @@ Everything you need to build, test, deploy, and run your app at scale. ...@@ -14,7 +14,7 @@ Everything you need to build, test, deploy, and run your app at scale.
[Kubernetes](https://kubernetes.io) logs can be viewed directly within GitLab. [Kubernetes](https://kubernetes.io) logs can be viewed directly within GitLab.
![Pod logs](img/kubernetes_pod_logs_v12_9.png) ![Pod logs](img/kubernetes_pod_logs_v12_10.png)
## Requirements ## Requirements
...@@ -32,7 +32,7 @@ You can access them in two ways. ...@@ -32,7 +32,7 @@ You can access them in two ways.
Go to **{cloud-gear}** **Operations > Logs** on the sidebar menu. Go to **{cloud-gear}** **Operations > Logs** on the sidebar menu.
![Sidebar menu](img/sidebar_menu_pod_logs_v12_5.png) ![Sidebar menu](img/sidebar_menu_pod_logs_v12_10.png)
### From Deploy Boards ### From Deploy Boards
......
...@@ -7860,9 +7860,6 @@ msgstr "" ...@@ -7860,9 +7860,6 @@ msgstr ""
msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}" msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}"
msgstr "" msgstr ""
msgid "Environments|All pods"
msgstr ""
msgid "Environments|An error occurred while canceling the auto stop, please try again" msgid "Environments|An error occurred while canceling the auto stop, please try again"
msgstr "" msgstr ""
...@@ -7929,9 +7926,6 @@ msgstr "" ...@@ -7929,9 +7926,6 @@ msgstr ""
msgid "Environments|Environments are places where code gets deployed, such as staging or production." msgid "Environments|Environments are places where code gets deployed, such as staging or production."
msgstr "" msgstr ""
msgid "Environments|Filter by pod"
msgstr ""
msgid "Environments|Install Elastic Stack on your cluster to enable advanced querying capabilities such as full text search." msgid "Environments|Install Elastic Stack on your cluster to enable advanced querying capabilities such as full text search."
msgstr "" msgstr ""
...@@ -7971,6 +7965,9 @@ msgstr "" ...@@ -7971,6 +7965,9 @@ msgstr ""
msgid "Environments|Open live environment" msgid "Environments|Open live environment"
msgstr "" msgstr ""
msgid "Environments|Pod name"
msgstr ""
msgid "Environments|Re-deploy" msgid "Environments|Re-deploy"
msgstr "" msgstr ""
...@@ -7998,9 +7995,6 @@ msgstr "" ...@@ -7998,9 +7995,6 @@ msgstr ""
msgid "Environments|Rollback environment %{name}?" msgid "Environments|Rollback environment %{name}?"
msgstr "" msgstr ""
msgid "Environments|Search"
msgstr ""
msgid "Environments|Select environment" msgid "Environments|Select environment"
msgstr "" msgstr ""
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
mockPods, mockPods,
mockLogsResult, mockLogsResult,
mockTrace, mockTrace,
mockPodName,
mockEnvironmentsEndpoint, mockEnvironmentsEndpoint,
mockDocumentationPath, mockDocumentationPath,
} from '../mock_data'; } from '../mock_data';
...@@ -298,11 +297,11 @@ describe('EnvironmentLogs', () => { ...@@ -298,11 +297,11 @@ describe('EnvironmentLogs', () => {
}); });
it('refresh button, trace is refreshed', () => { it('refresh button, trace is refreshed', () => {
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showPodLogs`, expect.anything()); expect(dispatch).not.toHaveBeenCalledWith(`${module}/fetchLogs`, undefined);
findLogControlButtons().vm.$emit('refresh'); findLogControlButtons().vm.$emit('refresh');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPodName); expect(dispatch).toHaveBeenCalledWith(`${module}/fetchLogs`, undefined);
}); });
}); });
}); });
......
import { GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { defaultTimeRange } from '~/vue_shared/constants'; import { defaultTimeRange } from '~/vue_shared/constants';
import { GlFilteredSearch } from '@gitlab/ui';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { createStore } from '~/logs/stores'; import { createStore } from '~/logs/stores';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
import { mockPods, mockSearch } from '../mock_data'; import { mockPods, mockSearch } from '../mock_data';
import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue'; import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
...@@ -15,26 +16,19 @@ describe('LogAdvancedFilters', () => { ...@@ -15,26 +16,19 @@ describe('LogAdvancedFilters', () => {
let wrapper; let wrapper;
let state; let state;
const findPodsDropdown = () => wrapper.find({ ref: 'podsDropdown' }); const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
.findAll(GlDropdownItem)
.filter(item => !item.is('[disabled]'));
const findPodsDropdownItemsSelected = () =>
findPodsDropdownItems()
.filter(item => {
return !item.find(GlIcon).classes('invisible');
})
.at(0);
const findSearchBox = () => wrapper.find({ ref: 'searchBox' });
const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' }); const findTimeRangePicker = () => wrapper.find({ ref: 'dateTimePicker' });
const getSearchToken = type =>
findFilteredSearch()
.props('availableTokens')
.filter(token => token.type === type)[0];
const mockStateLoading = () => { const mockStateLoading = () => {
state.timeRange.selected = defaultTimeRange; state.timeRange.selected = defaultTimeRange;
state.timeRange.current = convertToFixedRange(defaultTimeRange); state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = []; state.pods.options = [];
state.pods.current = null; state.pods.current = null;
state.logs.isLoading = true;
}; };
const mockStateWithData = () => { const mockStateWithData = () => {
...@@ -42,6 +36,7 @@ describe('LogAdvancedFilters', () => { ...@@ -42,6 +36,7 @@ describe('LogAdvancedFilters', () => {
state.timeRange.current = convertToFixedRange(defaultTimeRange); state.timeRange.current = convertToFixedRange(defaultTimeRange);
state.pods.options = mockPods; state.pods.options = mockPods;
state.pods.current = null; state.pods.current = null;
state.logs.isLoading = false;
}; };
const initWrapper = (propsData = {}) => { const initWrapper = (propsData = {}) => {
...@@ -76,11 +71,18 @@ describe('LogAdvancedFilters', () => { ...@@ -76,11 +71,18 @@ describe('LogAdvancedFilters', () => {
expect(wrapper.isVueInstance()).toBe(true); expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false); expect(wrapper.isEmpty()).toBe(false);
expect(findPodsDropdown().exists()).toBe(true); expect(findFilteredSearch().exists()).toBe(true);
expect(findSearchBox().exists()).toBe(true);
expect(findTimeRangePicker().exists()).toBe(true); expect(findTimeRangePicker().exists()).toBe(true);
}); });
it('displays search tokens', () => {
expect(getSearchToken(TOKEN_TYPE_POD_NAME)).toMatchObject({
title: 'Pod name',
unique: true,
operators: [expect.objectContaining({ value: '=' })],
});
});
describe('disabled state', () => { describe('disabled state', () => {
beforeEach(() => { beforeEach(() => {
mockStateLoading(); mockStateLoading();
...@@ -90,9 +92,7 @@ describe('LogAdvancedFilters', () => { ...@@ -90,9 +92,7 @@ describe('LogAdvancedFilters', () => {
}); });
it('displays disabled filters', () => { it('displays disabled filters', () => {
expect(findPodsDropdown().props('text')).toBe('All pods'); expect(findFilteredSearch().attributes('disabled')).toBeTruthy();
expect(findPodsDropdown().attributes('disabled')).toBeTruthy();
expect(findSearchBox().attributes('disabled')).toBeTruthy();
expect(findTimeRangePicker().attributes('disabled')).toBeTruthy(); expect(findTimeRangePicker().attributes('disabled')).toBeTruthy();
}); });
}); });
...@@ -103,16 +103,17 @@ describe('LogAdvancedFilters', () => { ...@@ -103,16 +103,17 @@ describe('LogAdvancedFilters', () => {
initWrapper(); initWrapper();
}); });
it('displays a enabled filters', () => { it('displays a disabled search', () => {
expect(findPodsDropdown().props('text')).toBe('All pods'); expect(findFilteredSearch().attributes('disabled')).toBeTruthy();
expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); });
expect(findSearchBox().attributes('disabled')).toBeFalsy();
it('displays an enable date filter', () => {
expect(findTimeRangePicker().attributes('disabled')).toBeFalsy(); expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
}); });
it('displays an empty pods dropdown', () => { it('displays no pod options when no pods are available, so suggestions can be displayed', () => {
expect(findPodsNoPodsText().exists()).toBe(true); expect(getSearchToken(TOKEN_TYPE_POD_NAME).options).toBe(null);
expect(findPodsDropdownItems()).toHaveLength(0); expect(getSearchToken(TOKEN_TYPE_POD_NAME).loading).toBe(true);
}); });
}); });
...@@ -122,20 +123,24 @@ describe('LogAdvancedFilters', () => { ...@@ -122,20 +123,24 @@ describe('LogAdvancedFilters', () => {
initWrapper(); initWrapper();
}); });
it('displays an enabled pods dropdown', () => { it('displays a single token for pods', () => {
expect(findPodsDropdown().attributes('disabled')).toBeFalsy(); initWrapper();
expect(findPodsDropdown().props('text')).toBe('All pods');
const tokens = findFilteredSearch().props('availableTokens');
expect(tokens).toHaveLength(1);
expect(tokens[0].type).toBe(TOKEN_TYPE_POD_NAME);
}); });
it('displays options in a pods dropdown', () => { it('displays a enabled filters', () => {
const items = findPodsDropdownItems(); expect(findFilteredSearch().attributes('disabled')).toBeFalsy();
expect(items).toHaveLength(mockPods.length + 1); expect(findTimeRangePicker().attributes('disabled')).toBeFalsy();
}); });
it('displays "all pods" selected in a pods dropdown', () => { it('displays options in the pods token', () => {
const selected = findPodsDropdownItemsSelected(); const { options } = getSearchToken(TOKEN_TYPE_POD_NAME);
expect(selected.text()).toBe('All pods'); expect(options).toHaveLength(mockPods.length);
}); });
it('displays options in date time picker', () => { it('displays options in date time picker', () => {
...@@ -146,30 +151,16 @@ describe('LogAdvancedFilters', () => { ...@@ -146,30 +151,16 @@ describe('LogAdvancedFilters', () => {
}); });
describe('when the user interacts', () => { describe('when the user interacts', () => {
it('clicks on a all options, showPodLogs is dispatched with null', () => { it('clicks on the search button, showFilteredLogs is dispatched', () => {
const items = findPodsDropdownItems(); findFilteredSearch().vm.$emit('submit', null);
items.at(0).vm.$emit('click');
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, null);
});
it('clicks on a pod name, showPodLogs is dispatched with pod name', () => {
const items = findPodsDropdownItems();
const index = 2; // any pod
items.at(index + 1).vm.$emit('click'); // skip "All pods" option expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, null);
expect(dispatch).toHaveBeenCalledWith(`${module}/showPodLogs`, mockPods[index]);
}); });
it('clicks on search, a serches is done', () => { it('clicks on the search button, showFilteredLogs is dispatched with null', () => {
expect(findSearchBox().attributes('disabled')).toBeFalsy(); findFilteredSearch().vm.$emit('submit', [mockSearch]);
// input a query and click `search`
findSearchBox().vm.$emit('input', mockSearch);
findSearchBox().vm.$emit('submit');
expect(dispatch).toHaveBeenCalledWith(`${module}/setSearch`, mockSearch); expect(dispatch).toHaveBeenCalledWith(`${module}/showFilteredLogs`, [mockSearch]);
}); });
it('selects a new time range', () => { it('selects a new time range', () => {
......
import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TokenWithLoadingState from '~/logs/components/tokens/token_with_loading_state.vue';
describe('TokenWithLoadingState', () => {
let wrapper;
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const initWrapper = (props = {}, options) => {
wrapper = shallowMount(TokenWithLoadingState, {
propsData: props,
...options,
});
};
beforeEach(() => {});
it('passes entire config correctly', () => {
const config = {
icon: 'pod',
type: 'pod',
title: 'Pod name',
unique: true,
};
initWrapper({ config });
expect(findFilteredSearchToken().props('config')).toEqual(config);
});
describe('suggestions are replaced', () => {
let mockNoOptsText;
let config;
let stubs;
beforeEach(() => {
mockNoOptsText = 'No suggestions available';
config = {
loading: false,
noOptionsText: mockNoOptsText,
};
stubs = {
GlFilteredSearchToken: {
template: `<div><slot name="suggestions"></slot></div>`,
},
};
});
it('renders a loading icon', () => {
config.loading = true;
initWrapper({ config }, { stubs });
expect(findLoadingIcon().exists()).toBe(true);
expect(wrapper.text()).toBe('');
});
it('renders an empty results message', () => {
initWrapper({ config }, { stubs });
expect(findLoadingIcon().exists()).toBe(false);
expect(wrapper.text()).toBe(mockNoOptsText);
});
});
});
...@@ -6,7 +6,7 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range'; ...@@ -6,7 +6,7 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
import logsPageState from '~/logs/stores/state'; import logsPageState from '~/logs/stores/state';
import { import {
setInitData, setInitData,
setSearch, showFilteredLogs,
showPodLogs, showPodLogs,
fetchEnvironments, fetchEnvironments,
fetchLogs, fetchLogs,
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
mockCursor, mockCursor,
mockNextCursor, mockNextCursor,
} from '../mock_data'; } from '../mock_data';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/datetime_range'); jest.mock('~/lib/utils/datetime_range');
...@@ -93,13 +94,80 @@ describe('Logs Store actions', () => { ...@@ -93,13 +94,80 @@ describe('Logs Store actions', () => {
)); ));
}); });
describe('setSearch', () => { describe('showFilteredLogs', () => {
it('should commit search mutation', () => it('empty search should filter with defaults', () =>
testAction( testAction(
setSearch, showFilteredLogs,
mockSearch, undefined,
state, state,
[{ type: types.SET_SEARCH, payload: mockSearch }], [
{ type: types.SET_CURRENT_POD_NAME, payload: null },
{ type: types.SET_SEARCH, payload: '' },
],
[{ type: 'fetchLogs' }],
));
it('text search should filter with a search term', () =>
testAction(
showFilteredLogs,
[mockSearch],
state,
[
{ type: types.SET_CURRENT_POD_NAME, payload: null },
{ type: types.SET_SEARCH, payload: mockSearch },
],
[{ type: 'fetchLogs' }],
));
it('pod search should filter with a search term', () =>
testAction(
showFilteredLogs,
[{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }],
state,
[
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.SET_SEARCH, payload: '' },
],
[{ type: 'fetchLogs' }],
));
it('pod search should filter with a pod selection and a search term', () =>
testAction(
showFilteredLogs,
[{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } }, mockSearch],
state,
[
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.SET_SEARCH, payload: mockSearch },
],
[{ type: 'fetchLogs' }],
));
it('pod search should filter with a pod selection and two search terms', () =>
testAction(
showFilteredLogs,
['term1', 'term2'],
state,
[
{ type: types.SET_CURRENT_POD_NAME, payload: null },
{ type: types.SET_SEARCH, payload: `term1 term2` },
],
[{ type: 'fetchLogs' }],
));
it('pod search should filter with a pod selection and a search terms before and after', () =>
testAction(
showFilteredLogs,
[
'term1',
{ type: TOKEN_TYPE_POD_NAME, value: { data: mockPodName, operator: '=' } },
'term2',
],
state,
[
{ type: types.SET_CURRENT_POD_NAME, payload: mockPodName },
{ type: types.SET_SEARCH, payload: `term1 term2` },
],
[{ type: 'fetchLogs' }], [{ type: 'fetchLogs' }],
)); ));
}); });
......
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