Commit b33040ee authored by Payton Burdette's avatar Payton Burdette Committed by Stan Hu

Filter pipelines by status

Create the pipeline status token
for filtering pipelines by status.
Add specs for new token and update
query params.
parent bcd225d7
<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
......
......@@ -15966,6 +15966,9 @@ msgstr ""
msgid "Pipeline|Branch name"
msgstr ""
msgid "Pipeline|Canceled"
msgstr ""
msgid "Pipeline|Commit"
msgstr ""
......@@ -15975,6 +15978,9 @@ msgstr ""
msgid "Pipeline|Coverage"
msgstr ""
msgid "Pipeline|Created"
msgstr ""
msgid "Pipeline|Date"
msgstr ""
......@@ -15987,9 +15993,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 ""
......@@ -15999,6 +16011,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 ""
......@@ -16014,9 +16032,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