Commit edad5b3e authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'pipeline-filter-by-source' into 'master'

Allow pipelines to be filtered by source in UI

See merge request gitlab-org/gitlab!67964
parents fc0cfaa0 d8fad86f
......@@ -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