Commit d8fad86f authored by Balasankar "Balu" C's avatar Balasankar "Balu" C

Add option to filter pipelines based on source in the UI

Allow users to filter pipelines in the pipelines index page based on the
source due to which pipelines were started.
Signed-off-by: default avatarBalasankar "Balu" C <balasankar@gitlab.com>
parent bfd7e162
......@@ -4,6 +4,7 @@ import { map } from 'lodash';
import { s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineSourceToken from './tokens/pipeline_source_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue';
import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
......@@ -13,6 +14,7 @@ export default {
branchType: 'ref',
tagType: 'tag',
statusType: 'status',
sourceType: 'source',
defaultTokensLength: 1,
components: {
GlFilteredSearch,
......@@ -37,7 +39,7 @@ export default {
return this.value.map((i) => i.type);
},
tokens() {
return [
const tokens = [
{
type: this.$options.userType,
icon: 'user',
......@@ -76,6 +78,19 @@ export default {
operators: OPERATOR_IS_ONLY,
},
];
if (gon.features.pipelineSourceFilter) {
tokens.push({
type: this.$options.sourceType,
icon: 'trigger-source',
title: s__('Pipeline|Source'),
unique: true,
token: PipelineSourceToken,
operators: OPERATOR_IS_ONLY,
});
}
return tokens;
},
parsedParams() {
return map(this.params, (val, key) => ({
......
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
},
props: {
config: {
type: Object,
required: true,
},
value: {
type: Object,
required: true,
},
},
computed: {
sources() {
return [
{
text: s__('Pipeline|Source|Push'),
value: 'push',
},
{
text: s__('Pipeline|Source|Web'),
value: 'web',
},
{
text: s__('Pipeline|Source|Trigger'),
value: 'trigger',
},
{
text: s__('Pipeline|Source|Schedule'),
value: 'schedule',
},
{
text: s__('Pipeline|Source|API'),
value: 'api',
},
{
text: s__('Pipeline|Source|External'),
value: 'external',
},
{
text: s__('Pipeline|Source|Pipeline'),
value: 'pipeline',
},
{
text: s__('Pipeline|Source|Chat'),
value: 'chat',
},
{
text: s__('Pipeline|Source|Web IDE'),
value: 'webide',
},
{
text: s__('Pipeline|Source|Merge Request'),
value: 'merge_request_event',
},
{
text: s__('Pipeline|Source|External Pull Request'),
value: 'external_pull_request_event',
},
{
text: s__('Pipeline|Source|Parent Pipeline'),
value: 'parent_pipeline',
},
{
text: s__('Pipeline|Source|On-Demand DAST Scan'),
value: 'ondemand_dast_scan',
},
{
text: s__('Pipeline|Source|On-Demand DAST Validation'),
value: 'ondemand_dast_validation',
},
];
},
findActiveSource() {
return this.sources.find((source) => source.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">
<span>{{ findActiveSource.text }}</span>
</div>
</template>
<template #suggestions>
<gl-filtered-search-suggestion
v-for="source in sources"
:key="source.value"
:value="source.value"
>
{{ source.text }}
</gl-filtered-search-suggestion>
</template>
</gl-filtered-search-token>
</template>
......@@ -4,7 +4,7 @@ export const CANCEL_REQUEST = 'CANCEL_REQUEST';
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', 'status'];
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
......
......@@ -14,6 +14,10 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action do
push_frontend_feature_flag(:pipeline_source_filter, project, type: :development, default_enabled: :yaml)
end
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
......
......@@ -24792,6 +24792,51 @@ msgstr ""
msgid "Pipeline|Skipped"
msgstr ""
msgid "Pipeline|Source"
msgstr ""
msgid "Pipeline|Source|API"
msgstr ""
msgid "Pipeline|Source|Chat"
msgstr ""
msgid "Pipeline|Source|External"
msgstr ""
msgid "Pipeline|Source|External Pull Request"
msgstr ""
msgid "Pipeline|Source|Merge Request"
msgstr ""
msgid "Pipeline|Source|On-Demand DAST Scan"
msgstr ""
msgid "Pipeline|Source|On-Demand DAST Validation"
msgstr ""
msgid "Pipeline|Source|Parent Pipeline"
msgstr ""
msgid "Pipeline|Source|Pipeline"
msgstr ""
msgid "Pipeline|Source|Push"
msgstr ""
msgid "Pipeline|Source|Schedule"
msgstr ""
msgid "Pipeline|Source|Trigger"
msgstr ""
msgid "Pipeline|Source|Web"
msgstr ""
msgid "Pipeline|Source|Web IDE"
msgstr ""
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default."
msgstr ""
......
......@@ -20,6 +20,7 @@ describe('Pipelines filtered search', () => {
const findTagToken = () => getSearchToken('tag');
const findUserToken = () => getSearchToken('username');
const findStatusToken = () => getSearchToken('status');
const findSourceToken = () => getSearchToken('source');
const createComponent = (params = {}) => {
wrapper = mount(PipelinesFilteredSearch, {
......@@ -32,6 +33,8 @@ describe('Pipelines filtered search', () => {
};
beforeEach(() => {
window.gon = { features: { pipelineSourceFilter: true } };
mock = new MockAdapter(axios);
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
......@@ -70,6 +73,14 @@ describe('Pipelines filtered search', () => {
operators: OPERATOR_IS_ONLY,
});
expect(findSourceToken()).toMatchObject({
type: 'source',
icon: 'trigger-source',
title: 'Source',
unique: true,
operators: OPERATOR_IS_ONLY,
});
expect(findStatusToken()).toMatchObject({
type: 'status',
icon: 'status',
......
......@@ -105,6 +105,8 @@ describe('Pipelines', () => {
});
beforeEach(() => {
window.gon = { features: { pipelineSourceFilter: true } };
mock = new MockAdapter(axios);
jest.spyOn(window.history, 'pushState');
......
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import PipelineSourceToken from '~/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue';
describe('Pipeline Source Token', () => {
let wrapper;
const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
const defaultProps = {
config: {
type: 'source',
icon: 'trigger-source',
title: 'Source',
unique: true,
},
value: {
data: '',
},
};
const createComponent = () => {
wrapper = shallowMount(PipelineSourceToken, {
propsData: {
...defaultProps,
},
stubs: {
GlFilteredSearchToken: stubComponent(GlFilteredSearchToken, {
template: `<div><slot name="suggestions"></slot></div>`,
}),
},
});
};
beforeEach(() => {
createComponent();
});
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
describe('shows sources correctly', () => {
it('renders all pipeline sources available', () => {
expect(findAllFilteredSearchSuggestions()).toHaveLength(wrapper.vm.sources.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