Commit 561c76d9 authored by Stan Hu's avatar Stan Hu

Merge branch 'filter-pipelines-by-status' into 'master'

Filter pipelines by status

See merge request gitlab-org/gitlab!32151
parents 1e2de012 b33040ee
<script>
import { isEqual, pickBy } from 'lodash';
import { isEqual } from 'lodash';
import { __, sprintf, s__ } from '../../locale';
import createFlash from '../../flash';
import PipelinesService from '../services/pipelines_service';
......@@ -10,7 +10,8 @@ import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, SUPPORTED_FILTER_PARAMETERS } from '../constants';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING } from '../constants';
import { validateParams } from '../utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
......@@ -225,14 +226,12 @@ export default {
return this.glFeatures.filterPipelinesSearch;
},
validatedParams() {
return pickBy(this.params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
return validateParams(this.params);
},
},
created() {
this.service = new PipelinesService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope };
Object.assign(this.requestData, this.validatedParams);
this.requestData = { page: this.page, scope: this.scope, ...this.validatedParams };
},
methods: {
successCallback(resp) {
......@@ -313,7 +312,6 @@ export default {
<pipelines-filtered-search
v-if="canFilterPipelines"
:pipelines="state.pipelines"
:project-id="projectId"
:params="validatedParams"
@filterPipelines="filterPipelines"
......
......@@ -3,6 +3,7 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue';
import { map } from 'lodash';
export default {
......@@ -10,10 +11,6 @@ export default {
GlFilteredSearch,
},
props: {
pipelines: {
type: Array,
required: true,
},
projectId: {
type: String,
required: true,
......@@ -44,6 +41,14 @@ export default {
operators: [{ value: '=', description: __('is'), default: 'true' }],
projectId: this.projectId,
},
{
type: 'status',
icon: 'status',
title: s__('Pipeline|Status'),
unique: true,
token: PipelineStatusToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
},
];
},
paramsValue() {
......
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlIcon,
},
props: {
config: {
type: Object,
required: true,
},
value: {
type: Object,
required: true,
},
},
computed: {
statuses() {
return [
{
class: 'ci-status-icon-canceled',
icon: 'status_canceled',
text: s__('Pipeline|Canceled'),
value: 'canceled',
},
{
class: 'ci-status-icon-created',
icon: 'status_created',
text: s__('Pipeline|Created'),
value: 'created',
},
{
class: 'ci-status-icon-failed',
icon: 'status_failed',
text: s__('Pipeline|Failed'),
value: 'failed',
},
{
class: 'ci-status-icon-manual',
icon: 'status_manual',
text: s__('Pipeline|Manual'),
value: 'manual',
},
{
class: 'ci-status-icon-success',
icon: 'status_success',
text: s__('Pipeline|Passed'),
value: 'success',
},
{
class: 'ci-status-icon-pending',
icon: 'status_pending',
text: s__('Pipeline|Pending'),
value: 'pending',
},
{
class: 'ci-status-icon-running',
icon: 'status_running',
text: s__('Pipeline|Running'),
value: 'running',
},
{
class: 'ci-status-icon-skipped',
icon: 'status_skipped',
text: s__('Pipeline|Skipped'),
value: 'skipped',
},
];
},
findActiveStatus() {
return this.statuses.find(status => status.value === this.value.data);
},
},
};
</script>
<template>
<gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<template #view>
<div class="gl-display-flex gl-align-items-center">
<div :class="findActiveStatus.class">
<gl-icon :name="findActiveStatus.icon" class="gl-mr-2 gl-display-block" />
</div>
<span>{{ findActiveStatus.text }}</span>
</div>
</template>
<template #suggestions>
<gl-filtered-search-suggestion
v-for="(status, index) in statuses"
:key="index"
:value="status.value"
>
<div class="gl-display-flex" :class="status.class">
<gl-icon :name="status.icon" class="gl-mr-3" />
<span>{{ status.text }}</span>
</div>
</gl-filtered-search-suggestion>
</template>
</gl-filtered-search-token>
</template>
......@@ -5,7 +5,7 @@ export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300;
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref'];
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status'];
export const TestStatus = {
FAILED: 'failed',
......
import axios from '../../lib/utils/axios_utils';
import Api from '~/api';
import { validateParams } from '../utils';
export default class PipelinesService {
/**
......@@ -19,18 +20,10 @@ export default class PipelinesService {
}
getPipelines(data = {}) {
const { scope, page, username, ref } = data;
const { scope, page } = data;
const { CancelToken } = axios;
const queryParams = { scope, page };
if (username) {
queryParams.username = username;
}
if (ref) {
queryParams.ref = ref;
}
const queryParams = { scope, page, ...validateParams(data) };
this.cancelationSource = CancelToken.source();
......
import { pickBy } from 'lodash';
import { SUPPORTED_FILTER_PARAMETERS } from './constants';
export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
export default () => {};
......@@ -5,6 +5,7 @@
* Components need to have `scope`, `page` and `requestData`
*/
import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils';
import { validateParams } from '~/pipelines/utils';
export default {
methods: {
......@@ -35,18 +36,7 @@ export default {
},
onChangeWithFilter(params) {
const { username, ref } = this.requestData;
const paramsData = params;
if (username) {
paramsData.username = username;
}
if (ref) {
paramsData.ref = ref;
}
return paramsData;
return { ...params, ...validateParams(this.requestData) };
},
updateInternalState(parameters) {
......
......@@ -277,7 +277,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def index_params
params.permit(:scope, :username, :ref)
params.permit(:scope, :username, :ref, :status)
end
end
......
---
title: Filter pipelines by status
merge_request: 32151
author:
type: added
......@@ -101,6 +101,7 @@ you can filter the pipeline list by:
- Trigger author
- Branch name
- Status ([since GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/217617))
### Run a pipeline manually
......
......@@ -15981,6 +15981,9 @@ msgstr ""
msgid "Pipeline|Branch name"
msgstr ""
msgid "Pipeline|Canceled"
msgstr ""
msgid "Pipeline|Commit"
msgstr ""
......@@ -15990,6 +15993,9 @@ msgstr ""
msgid "Pipeline|Coverage"
msgstr ""
msgid "Pipeline|Created"
msgstr ""
msgid "Pipeline|Date"
msgstr ""
......@@ -16002,9 +16008,15 @@ msgstr ""
msgid "Pipeline|Existing branch name or tag"
msgstr ""
msgid "Pipeline|Failed"
msgstr ""
msgid "Pipeline|Key"
msgstr ""
msgid "Pipeline|Manual"
msgstr ""
msgid "Pipeline|Merge train pipeline"
msgstr ""
......@@ -16014,6 +16026,12 @@ msgstr ""
msgid "Pipeline|No pipeline has been run for this commit."
msgstr ""
msgid "Pipeline|Passed"
msgstr ""
msgid "Pipeline|Pending"
msgstr ""
msgid "Pipeline|Pipeline"
msgstr ""
......@@ -16029,9 +16047,15 @@ msgstr ""
msgid "Pipeline|Run for"
msgstr ""
msgid "Pipeline|Running"
msgstr ""
msgid "Pipeline|Search branches"
msgstr ""
msgid "Pipeline|Skipped"
msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
msgstr ""
......
......@@ -171,6 +171,40 @@ RSpec.describe Projects::PipelinesController do
end
end
context 'filter by status' do
context 'when pipelines with the status exists' do
it 'returns matched pipelines' do
get_pipelines_index_json(status: 'success')
check_pipeline_response(returned: 1, all: 1, running: 0, pending: 0, finished: 1)
end
context 'when filter by unrelated scope' do
it 'returns empty list' do
get_pipelines_index_json(status: 'success', scope: 'running')
check_pipeline_response(returned: 0, all: 1, running: 0, pending: 0, finished: 1)
end
end
end
context 'when no pipeline with the status exists' do
it 'returns empty list' do
get_pipelines_index_json(status: 'manual')
check_pipeline_response(returned: 0, all: 0, running: 0, pending: 0, finished: 0)
end
end
context 'when invalid status' do
it 'returns all list' do
get_pipelines_index_json(status: 'invalid-status')
check_pipeline_response(returned: 6, all: 6, running: 2, pending: 1, finished: 3)
end
end
end
def get_pipelines_index_json(params = {})
get :index, params: {
namespace_id: project.namespace,
......
......@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_filtered_search.vue';
import { users, mockSearch, pipelineWithStages, branches } from '../mock_data';
import { users, mockSearch, branches } from '../mock_data';
import { GlFilteredSearch } from '@gitlab/ui';
describe('Pipelines filtered search', () => {
......@@ -19,7 +19,6 @@ describe('Pipelines filtered search', () => {
const createComponent = (params = {}) => {
wrapper = mount(PipelinesFilteredSearch, {
propsData: {
pipelines: [pipelineWithStages],
projectId: '21',
params,
},
......@@ -67,6 +66,14 @@ describe('Pipelines filtered search', () => {
projectId: '21',
operators: [expect.objectContaining({ value: '=' })],
});
expect(getSearchToken('status')).toMatchObject({
type: 'status',
icon: 'status',
title: 'Status',
unique: true,
operators: [expect.objectContaining({ value: '=' })],
});
});
it('emits filterPipelines on submit with correct filter', () => {
......
......@@ -563,6 +563,7 @@ export const branches = [
export const mockSearch = [
{ type: 'username', value: { data: 'root', operator: '=' } },
{ type: 'ref', value: { data: 'master', operator: '=' } },
{ type: 'status', value: { data: 'pending', operator: '=' } },
];
export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11'];
......@@ -684,7 +684,13 @@ describe('Pipelines', () => {
});
it('updates request data and query params on filter submit', () => {
const expectedQueryParams = { page: '1', scope: 'all', username: 'root', ref: 'master' };
const expectedQueryParams = {
page: '1',
scope: 'all',
username: 'root',
ref: 'master',
status: 'pending',
};
findFilteredSearch().vm.$emit('submit', mockSearch);
......
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineStatusToken from '~/pipelines/components/tokens/pipeline_status_token.vue';
describe('Pipeline Status Token', () => {
let wrapper;
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
const findAllGlIcons = () => wrapper.findAll(GlIcon);
const stubs = {
GlFilteredSearchToken: {
template: `<div><slot name="suggestions"></slot></div>`,
},
};
const defaultProps = {
config: {
type: 'status',
icon: 'status',
title: 'Status',
unique: true,
},
value: {
data: '',
},
};
const createComponent = options => {
wrapper = shallowMount(PipelineStatusToken, {
propsData: {
...defaultProps,
},
...options,
});
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
describe('shows statuses correctly', () => {
beforeEach(() => {
createComponent({ stubs });
});
it('renders all pipeline statuses available', () => {
expect(findAllFilteredSearchSuggestions()).toHaveLength(wrapper.vm.statuses.length);
expect(findAllGlIcons()).toHaveLength(wrapper.vm.statuses.length);
});
});
});
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