Skip to content
代码片段 群组 项目
提交 cc2334e8 编辑于 作者: Jose Ivan Vargas's avatar Jose Ivan Vargas
浏览文件

Merge branch '367566-retain-job-filter-in-url' into 'master'

Add query string filtering - Jobs filtered search

See merge request gitlab-org/gitlab!95740
No related branches found
No related tags found
无相关合并请求
export const jobStatusValues = [
'CANCELED',
'CREATED',
'FAILED',
'MANUAL',
'SUCCESS',
'PENDING',
'PREPARING',
'RUNNING',
'SCHEDULED',
'SKIPPED',
'WAITING_FOR_RESOURCE',
];
......@@ -11,6 +11,13 @@ export default {
components: {
GlFilteredSearch,
},
props: {
queryString: {
type: Object,
required: false,
default: null,
},
},
computed: {
tokens() {
return [
......@@ -24,6 +31,20 @@ export default {
},
];
},
filteredSearchValue() {
if (this.queryString?.statuses) {
return [
{
type: 'status',
value: {
data: this.queryString?.statuses,
operator: '=',
},
},
];
}
return [];
},
},
methods: {
onSubmit(filters) {
......@@ -37,6 +58,7 @@ export default {
<gl-filtered-search
:placeholder="s__('Jobs|Filter jobs')"
:available-tokens="tokens"
:value="filteredSearchValue"
@submit="onSubmit"
/>
</template>
import { jobStatusValues } from './constants';
// validates query string used for filtered search
// on jobs table to ensure GraphQL query is called correctly
export const validateQueryString = (queryStringObj) => {
// currently only one token is supported `statuses`
// this code will need to be expanded as more tokens
// are introduced
const filters = Object.keys(queryStringObj);
if (filters.includes('statuses')) {
const found = jobStatusValues.find((status) => status === queryStringObj.statuses);
if (found) {
return queryStringObj;
}
return null;
}
return null;
};
......@@ -2,7 +2,9 @@
import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
import { validateQueryString } from '../filtered_search/utils';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue';
......@@ -37,6 +39,7 @@ export default {
variables() {
return {
fullPath: this.fullPath,
...this.validatedQueryString,
};
},
update(data) {
......@@ -95,6 +98,11 @@ export default {
jobsCount() {
return this.jobs.count;
},
validatedQueryString() {
const queryStringObject = queryToObject(window.location.search);
return validateQueryString(queryStringObject);
},
},
watch: {
// this watcher ensures that the count on the all tab
......@@ -133,6 +141,10 @@ export default {
}
if (filter.type === 'status') {
updateHistory({
url: setUrlParams({ statuses: filter.value.data }, window.location.href, true),
});
this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
}
});
......@@ -175,6 +187,7 @@ export default {
<jobs-filtered-search
v-if="showFilteredSearch"
:class="$options.filterSearchBoxStyles"
:query-string="validatedQueryString"
@filterJobsBySearch="filterJobsBySearch"
/>
......
......@@ -15,23 +15,27 @@ describe('Jobs filtered search', () => {
const findStatusToken = () => getSearchToken('status');
const createComponent = () => {
wrapper = shallowMount(JobsFilteredSearch);
const createComponent = (props) => {
wrapper = shallowMount(JobsFilteredSearch, {
propsData: {
...props,
},
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('displays filtered search', () => {
createComponent();
expect(findFilteredSearch().exists()).toBe(true);
});
it('displays status token', () => {
createComponent();
expect(findStatusToken()).toMatchObject({
type: 'status',
icon: 'status',
......@@ -42,8 +46,26 @@ describe('Jobs filtered search', () => {
});
it('emits filter token to parent component', () => {
createComponent();
findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
});
it('filtered search value is empty array when no query string is passed', () => {
createComponent();
expect(findFilteredSearch().props('value')).toEqual([]);
});
it('filtered search returns correct data shape when passed query string', () => {
const value = 'SUCCESS';
createComponent({ queryString: { statuses: value } });
expect(findFilteredSearch().props('value')).toEqual([
{ type: 'status', value: { data: value, operator: '=' } },
]);
});
});
import { validateQueryString } from '~/jobs/components/filtered_search/utils';
describe('Filtered search utils', () => {
describe('validateQueryString', () => {
it.each`
queryStringObject | expected
${{ statuses: 'SUCCESS' }} | ${{ statuses: 'SUCCESS' }}
${{ wrong: 'SUCCESS' }} | ${null}
${{ statuses: 'wrong' }} | ${null}
${{ wrong: 'wrong' }} | ${null}
`(
'when provided $queryStringObject, the expected result is $expected',
({ queryStringObject, expected }) => {
expect(validateQueryString(queryStringObject)).toEqual(expected);
},
);
});
});
......@@ -11,12 +11,14 @@ import VueApollo from 'vue-apollo';
import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import createFlash from '~/flash';
import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
import * as urlUtils from '~/lib/utils/url_utility';
import {
mockJobsResponsePaginated,
mockJobsResponseEmpty,
......@@ -230,5 +232,17 @@ describe('Job table app', () => {
expect(createFlash).toHaveBeenCalledWith(expectedWarning);
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
});
it('updates URL query string when filtering jobs by status', async () => {
createComponent();
jest.spyOn(urlUtils, 'updateHistory');
await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?statuses=FAILED`,
});
});
});
});
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册