Commit 04b77a6c authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'filter-from-url-query-params-pipeline' into 'master'

Filter pipelines based on url query params

See merge request gitlab-org/gitlab!32230
parents aa735684 1eda0077
...@@ -51,6 +51,7 @@ document.addEventListener( ...@@ -51,6 +51,7 @@ document.addEventListener(
ciLintPath: this.dataset.ciLintPath, ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath, resetCachePath: this.dataset.resetCachePath,
projectId: this.dataset.projectId, projectId: this.dataset.projectId,
params: JSON.parse(this.dataset.params),
}, },
}); });
}, },
......
<script> <script>
import { isEqual } from 'lodash'; import { isEqual, pickBy } from 'lodash';
import { __, sprintf, s__ } from '../../locale'; import { __, sprintf, s__ } from '../../locale';
import createFlash from '../../flash'; import createFlash from '../../flash';
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
...@@ -10,7 +10,7 @@ import NavigationControls from './nav_controls.vue'; ...@@ -10,7 +10,7 @@ import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '../../lib/utils/common_utils'; import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING } from '../constants'; import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, SUPPORTED_FILTER_PARAMETERS } from '../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
...@@ -86,6 +86,10 @@ export default { ...@@ -86,6 +86,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
params: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -220,10 +224,15 @@ export default { ...@@ -220,10 +224,15 @@ export default {
canFilterPipelines() { canFilterPipelines() {
return this.glFeatures.filterPipelinesSearch; return this.glFeatures.filterPipelinesSearch;
}, },
validatedParams() {
return pickBy(this.params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope }; this.requestData = { page: this.page, scope: this.scope };
Object.assign(this.requestData, this.validatedParams);
}, },
methods: { methods: {
successCallback(resp) { successCallback(resp) {
...@@ -306,6 +315,7 @@ export default { ...@@ -306,6 +315,7 @@ export default {
v-if="canFilterPipelines" v-if="canFilterPipelines"
:pipelines="state.pipelines" :pipelines="state.pipelines"
:project-id="projectId" :project-id="projectId"
:params="validatedParams"
@filterPipelines="filterPipelines" @filterPipelines="filterPipelines"
/> />
......
...@@ -3,9 +3,7 @@ import { GlFilteredSearch } from '@gitlab/ui'; ...@@ -3,9 +3,7 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue'; import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue'; import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import Api from '~/api'; import { map } from 'lodash';
import createFlash from '~/flash';
import { FETCH_AUTHOR_ERROR_MESSAGE, FETCH_BRANCH_ERROR_MESSAGE } from '../constants';
export default { export default {
components: { components: {
...@@ -20,12 +18,10 @@ export default { ...@@ -20,12 +18,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
}, params: {
data() { type: Object,
return { required: true,
projectUsers: null, },
projectBranches: null,
};
}, },
computed: { computed: {
tokens() { tokens() {
...@@ -37,7 +33,6 @@ export default { ...@@ -37,7 +33,6 @@ export default {
unique: true, unique: true,
token: PipelineTriggerAuthorToken, token: PipelineTriggerAuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }], operators: [{ value: '=', description: __('is'), default: 'true' }],
triggerAuthors: this.projectUsers,
projectId: this.projectId, projectId: this.projectId,
}, },
{ {
...@@ -47,30 +42,16 @@ export default { ...@@ -47,30 +42,16 @@ export default {
unique: true, unique: true,
token: PipelineBranchNameToken, token: PipelineBranchNameToken,
operators: [{ value: '=', description: __('is'), default: 'true' }], operators: [{ value: '=', description: __('is'), default: 'true' }],
branches: this.projectBranches,
projectId: this.projectId, projectId: this.projectId,
}, },
]; ];
}, },
}, paramsValue() {
created() { return map(this.params, (val, key) => ({
Api.projectUsers(this.projectId) type: key,
.then(users => { value: { data: val, operator: '=' },
this.projectUsers = users; }));
}) },
.catch(err => {
createFlash(FETCH_AUTHOR_ERROR_MESSAGE);
throw err;
});
Api.branches(this.projectId)
.then(({ data }) => {
this.projectBranches = data.map(branch => branch.name);
})
.catch(err => {
createFlash(FETCH_BRANCH_ERROR_MESSAGE);
throw err;
});
}, },
methods: { methods: {
onSubmit(filters) { onSubmit(filters) {
...@@ -85,6 +66,7 @@ export default { ...@@ -85,6 +66,7 @@ export default {
<gl-filtered-search <gl-filtered-search
:placeholder="__('Filter pipelines')" :placeholder="__('Filter pipelines')"
:available-tokens="tokens" :available-tokens="tokens"
:value="paramsValue"
@submit="onSubmit" @submit="onSubmit"
/> />
</div> </div>
......
...@@ -23,15 +23,18 @@ export default { ...@@ -23,15 +23,18 @@ export default {
}, },
data() { data() {
return { return {
branches: this.config.branches, branches: null,
loading: true, loading: true,
}; };
}, },
created() {
this.fetchBranches();
},
methods: { methods: {
fetchBranchBySearchTerm(searchTerm) { fetchBranches(searchterm) {
Api.branches(this.config.projectId, searchTerm) Api.branches(this.config.projectId, searchterm)
.then(res => { .then(({ data }) => {
this.branches = res.data.map(branch => branch.name); this.branches = data.map(branch => branch.name);
this.loading = false; this.loading = false;
}) })
.catch(err => { .catch(err => {
...@@ -41,7 +44,7 @@ export default { ...@@ -41,7 +44,7 @@ export default {
}); });
}, },
searchBranches: debounce(function debounceSearch({ data }) { searchBranches: debounce(function debounceSearch({ data }) {
this.fetchBranchBySearchTerm(data); this.fetchBranches(data);
}, FILTER_PIPELINES_SEARCH_DELAY), }, FILTER_PIPELINES_SEARCH_DELAY),
}, },
}; };
......
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
}, },
data() { data() {
return { return {
users: this.config.triggerAuthors, users: [],
loading: true, loading: true,
}; };
}, },
...@@ -50,11 +50,14 @@ export default { ...@@ -50,11 +50,14 @@ export default {
}); });
}, },
}, },
created() {
this.fetchProjectUsers();
},
methods: { methods: {
fetchAuthorBySearchTerm(searchTerm) { fetchProjectUsers(searchTerm) {
Api.projectUsers(this.config.projectId, searchTerm) Api.projectUsers(this.config.projectId, searchTerm)
.then(res => { .then(users => {
this.users = res; this.users = users;
this.loading = false; this.loading = false;
}) })
.catch(err => { .catch(err => {
...@@ -64,7 +67,7 @@ export default { ...@@ -64,7 +67,7 @@ export default {
}); });
}, },
searchAuthors: debounce(function debounceSearch({ data }) { searchAuthors: debounce(function debounceSearch({ data }) {
this.fetchAuthorBySearchTerm(data); this.fetchProjectUsers(data);
}, FILTER_PIPELINES_SEARCH_DELAY), }, FILTER_PIPELINES_SEARCH_DELAY),
}, },
}; };
......
...@@ -5,6 +5,7 @@ export const PIPELINES_TABLE = 'PIPELINES_TABLE'; ...@@ -5,6 +5,7 @@ export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300; export const LAYOUT_CHANGE_DELAY = 300;
export const FILTER_PIPELINES_SEARCH_DELAY = 200; export const FILTER_PIPELINES_SEARCH_DELAY = 200;
export const ANY_TRIGGER_AUTHOR = 'Any'; export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref'];
export const TestStatus = { export const TestStatus = {
FAILED: 'failed', FAILED: 'failed',
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
project_id: @project.id, project_id: @project.id,
params: params.to_json,
"help-page-path" => help_page_path('ci/quick_start/README'), "help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
......
---
title: Filter pipelines based on url query params
merge_request: 32230
author:
type: added
...@@ -3,13 +3,7 @@ import { mount } from '@vue/test-utils'; ...@@ -3,13 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_filtered_search.vue'; import PipelinesFilteredSearch from '~/pipelines/components/pipelines_filtered_search.vue';
import { import { users, mockSearch, pipelineWithStages, branches } from '../mock_data';
users,
mockSearch,
pipelineWithStages,
branches,
mockBranchesAfterMap,
} from '../mock_data';
import { GlFilteredSearch } from '@gitlab/ui'; import { GlFilteredSearch } from '@gitlab/ui';
describe('Pipelines filtered search', () => { describe('Pipelines filtered search', () => {
...@@ -22,11 +16,12 @@ describe('Pipelines filtered search', () => { ...@@ -22,11 +16,12 @@ describe('Pipelines filtered search', () => {
.props('availableTokens') .props('availableTokens')
.find(token => token.type === type); .find(token => token.type === type);
const createComponent = () => { const createComponent = (params = {}) => {
wrapper = mount(PipelinesFilteredSearch, { wrapper = mount(PipelinesFilteredSearch, {
propsData: { propsData: {
pipelines: [pipelineWithStages], pipelines: [pipelineWithStages],
projectId: '21', projectId: '21',
params,
}, },
attachToDocument: true, attachToDocument: true,
}); });
...@@ -60,7 +55,6 @@ describe('Pipelines filtered search', () => { ...@@ -60,7 +55,6 @@ describe('Pipelines filtered search', () => {
icon: 'user', icon: 'user',
title: 'Trigger author', title: 'Trigger author',
unique: true, unique: true,
triggerAuthors: users,
projectId: '21', projectId: '21',
operators: [expect.objectContaining({ value: '=' })], operators: [expect.objectContaining({ value: '=' })],
}); });
...@@ -70,28 +64,49 @@ describe('Pipelines filtered search', () => { ...@@ -70,28 +64,49 @@ describe('Pipelines filtered search', () => {
icon: 'branch', icon: 'branch',
title: 'Branch name', title: 'Branch name',
unique: true, unique: true,
branches: mockBranchesAfterMap,
projectId: '21', projectId: '21',
operators: [expect.objectContaining({ value: '=' })], operators: [expect.objectContaining({ value: '=' })],
}); });
}); });
it('fetches and sets project users', () => {
expect(Api.projectUsers).toHaveBeenCalled();
expect(wrapper.vm.projectUsers).toEqual(users);
});
it('fetches and sets branches', () => {
expect(Api.branches).toHaveBeenCalled();
expect(wrapper.vm.projectBranches).toEqual(mockBranchesAfterMap);
});
it('emits filterPipelines on submit with correct filter', () => { it('emits filterPipelines on submit with correct filter', () => {
findFilteredSearch().vm.$emit('submit', mockSearch); findFilteredSearch().vm.$emit('submit', mockSearch);
expect(wrapper.emitted('filterPipelines')).toBeTruthy(); expect(wrapper.emitted('filterPipelines')).toBeTruthy();
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]); expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
}); });
describe('Url query params', () => {
const params = {
username: 'deja.green',
ref: 'master',
};
beforeEach(() => {
createComponent(params);
});
it('sets default value if url query params', () => {
const expectedValueProp = [
{
type: 'username',
value: {
data: params.username,
operator: '=',
},
},
{
type: 'ref',
value: {
data: params.ref,
operator: '=',
},
},
{ type: 'filtered-search-term', value: { data: '' } },
];
expect(findFilteredSearch().props('value')).toEqual(expectedValueProp);
expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length);
});
});
}); });
...@@ -56,6 +56,7 @@ describe('Pipelines', () => { ...@@ -56,6 +56,7 @@ describe('Pipelines', () => {
propsData: { propsData: {
store: new Store(), store: new Store(),
projectId: '21', projectId: '21',
params: {},
...props, ...props,
}, },
methods: { methods: {
......
import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PipelineBranchNameToken from '~/pipelines/components/tokens/pipeline_branch_name_token.vue'; import PipelineBranchNameToken from '~/pipelines/components/tokens/pipeline_branch_name_token.vue';
import { branches } from '../mock_data'; import { branches, mockBranchesAfterMap } from '../mock_data';
describe('Pipeline Branch Name Token', () => { describe('Pipeline Branch Name Token', () => {
let wrapper; let wrapper;
...@@ -46,6 +47,8 @@ describe('Pipeline Branch Name Token', () => { ...@@ -46,6 +47,8 @@ describe('Pipeline Branch Name Token', () => {
}; };
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
createComponent(); createComponent();
}); });
...@@ -58,6 +61,13 @@ describe('Pipeline Branch Name Token', () => { ...@@ -58,6 +61,13 @@ describe('Pipeline Branch Name Token', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
}); });
it('fetches and sets project branches', () => {
expect(Api.branches).toHaveBeenCalled();
expect(wrapper.vm.branches).toEqual(mockBranchesAfterMap);
expect(findLoadingIcon().exists()).toBe(false);
});
describe('displays loading icon correctly', () => { describe('displays loading icon correctly', () => {
it('shows loading icon', () => { it('shows loading icon', () => {
createComponent({ stubs }, { loading: true }); createComponent({ stubs }, { loading: true });
......
import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui'; import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import PipelineTriggerAuthorToken from '~/pipelines/components/tokens/pipeline_trigger_author_token.vue'; import PipelineTriggerAuthorToken from '~/pipelines/components/tokens/pipeline_trigger_author_token.vue';
...@@ -45,6 +46,8 @@ describe('Pipeline Trigger Author Token', () => { ...@@ -45,6 +46,8 @@ describe('Pipeline Trigger Author Token', () => {
}; };
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
createComponent(); createComponent();
}); });
...@@ -57,6 +60,13 @@ describe('Pipeline Trigger Author Token', () => { ...@@ -57,6 +60,13 @@ describe('Pipeline Trigger Author Token', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config); expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
}); });
it('fetches and sets project users', () => {
expect(Api.projectUsers).toHaveBeenCalled();
expect(wrapper.vm.users).toEqual(users);
expect(findLoadingIcon().exists()).toBe(false);
});
describe('displays loading icon correctly', () => { describe('displays loading icon correctly', () => {
it('shows loading icon', () => { it('shows loading icon', () => {
createComponent({ stubs }, { loading: true }); createComponent({ stubs }, { loading: true });
......
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