diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 13:18:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-20 13:18:24 +0000 |
commit | 0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch) | |
tree | 4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/assets/javascripts/pipelines/components | |
parent | 744144d28e3e7fddc117924fef88de5d9674fe4c (diff) | |
download | gitlab-ce-0653e08efd039a5905f3fa4f6e9cef9f5d2f799c.tar.gz |
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipelines/components')
15 files changed, 122 insertions, 217 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/accessors.js b/app/assets/javascripts/pipelines/components/graph/accessors.js deleted file mode 100644 index 6ece855bcd8..00000000000 --- a/app/assets/javascripts/pipelines/components/graph/accessors.js +++ /dev/null @@ -1,25 +0,0 @@ -import { get } from 'lodash'; -import { REST, GRAPHQL } from './constants'; - -const accessors = { - [REST]: { - detailsPath: 'details_path', - groupId: 'id', - hasDetails: 'has_details', - pipelineStatus: ['details', 'status'], - sourceJob: ['source_job', 'name'], - }, - [GRAPHQL]: { - detailsPath: 'detailsPath', - groupId: 'name', - hasDetails: 'hasDetails', - pipelineStatus: 'status', - sourceJob: ['sourceJob', 'name'], - }, -}; - -const accessValue = (dataMethod, prop, item) => { - return get(item, accessors[dataMethod][prop]); -}; - -export { accessors, accessValue }; diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js index dd9cdae518f..0b59612b25c 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream'; */ export const ONE_COL_WIDTH = 180; -export const REST = 'rest'; -export const GRAPHQL = 'graphql'; - export const STAGE_VIEW = 'stage'; export const LAYER_VIEW = 'layer'; export const VIEW_TYPE_KEY = 'pipeline_graph_view_type'; diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index b2a3f27e079..6f4360649ff 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -23,6 +23,11 @@ export default { required: false, default: -1, }, + cssClassJobName: { + type: [String, Array], + required: false, + default: '', + }, stageName: { type: String, required: false, @@ -59,7 +64,8 @@ export default { type="button" data-toggle="dropdown" data-display="static" - class="dropdown-menu-toggle build-content gl-build-content gl-pipeline-job-width! gl-pr-4!" + :class="cssClassJobName" + class="dropdown-menu-toggle gl-pipeline-job-width! gl-pr-4!" > <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> <job-item diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 6584d89d87c..fd40ca0b9c9 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; import ActionComponent from '../jobs_shared/action_component.vue'; import JobNameComponent from '../jobs_shared/job_name_component.vue'; -import { accessValue } from './accessors'; -import { REST, SINGLE_JOB } from './constants'; +import { SINGLE_JOB } from './constants'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -47,18 +46,13 @@ export default { GlTooltip: GlTooltipDirective, }, mixins: [delayedJobMixin], - inject: { - dataMethod: { - default: REST, - }, - }, props: { job: { type: Object, required: true, }, cssClassJobName: { - type: String, + type: [String, Array], required: false, default: '', }, @@ -111,10 +105,10 @@ export default { return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; }, detailsPath() { - return accessValue(this.dataMethod, 'detailsPath', this.status); + return this.status.detailsPath; }, hasDetails() { - return accessValue(this.dataMethod, 'hasDetails', this.status); + return this.status.hasDetails; }, isSingleItem() { return this.type === SINGLE_JOB; @@ -189,7 +183,7 @@ export default { if (this.isSingleItem) { /* This is so the jobDropdown still toggles. Issue to refactor: - https://gitlab.com/gitlab-org/gitlab/-/issues/267117 + https://gitlab.com/gitlab-org/gitlab/-/issues/267117 */ evt.stopPropagation(); } @@ -226,11 +220,11 @@ export default { <div class="ci-job-name-component gl-display-flex gl-align-items-center"> <ci-icon :size="24" :status="job.status" class="gl-line-height-0" /> <div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full"> - <div class="gl-text-truncate mw-70p gl-line-height-normal">{{ job.name }}</div> + <div class="gl-text-truncate gl-w-70p gl-line-height-normal">{{ job.name }}</div> <div v-if="showStageName" data-testid="stage-name-in-job" - class="gl-text-truncate mw-70p gl-font-sm gl-text-gray-500 gl-line-height-normal" + class="gl-text-truncate gl-w-70p gl-font-sm gl-text-gray-500 gl-line-height-normal" > {{ stageName }} </div> diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue index dd8a354511a..be47799868b 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue @@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import CiStatus from '~/vue_shared/components/ci_icon.vue'; import { reportToSentry } from '../../utils'; -import { accessValue } from './accessors'; -import { DOWNSTREAM, REST, UPSTREAM } from './constants'; +import { DOWNSTREAM, UPSTREAM } from './constants'; export default { directives: { @@ -18,11 +17,6 @@ export default { GlLoadingIcon, GlBadge, }, - inject: { - dataMethod: { - default: REST, - }, - }, props: { columnTitle: { type: String, @@ -40,20 +34,9 @@ export default { type: String, required: true, }, - /* - The next two props will be removed or required - once the graph transition is done. - See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043 - */ isLoading: { type: Boolean, - required: false, - default: false, - }, - projectId: { - type: Number, - required: false, - default: -1, + required: true, }, }, computed: { @@ -65,7 +48,7 @@ export default { return `js-linked-pipeline-${this.pipeline.id}`; }, pipelineStatus() { - return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline); + return this.pipeline.status; }, projectName() { return this.pipeline.project.name; @@ -97,12 +80,10 @@ export default { return this.type === UPSTREAM; }, isSameProject() { - return this.projectId > -1 - ? this.projectId === this.pipeline.project.id - : !this.pipeline.multiproject; + return !this.pipeline.multiproject; }, sourceJobName() { - return accessValue(this.dataMethod, 'sourceJob', this.pipeline); + return this.pipeline.sourceJob?.name ?? ''; }, sourceJobInfo() { return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : ''; @@ -143,9 +124,8 @@ export default { <div ref="linkedPipeline" v-gl-tooltip - class="linked-pipeline build gl-pipeline-job-width" + class="gl-pipeline-job-width" :title="tooltipText" - :class="{ 'downstream-pipeline': isDownstream }" data-qa-selector="child_pipeline" @mouseover="onDownstreamHovered" @mouseleave="onDownstreamHoverLeave" diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index d251e0d8bd8..3c1208afbf0 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -195,7 +195,7 @@ export default { <template> <div class="gl-display-flex"> <div :class="columnClass" class="linked-pipelines-column"> - <div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses"> + <div data-testid="linked-column-title" :class="computedTitleClasses"> {{ columnTitle }} </div> <ul class="gl-pl-0"> @@ -224,7 +224,7 @@ export default { <pipeline-graph v-if="isExpanded(pipeline.id)" :type="type" - class="d-inline-block gl-mt-n2" + class="gl-inline-block gl-mt-n2" :config-paths="configPaths" :pipeline="currentPipeline" :computed-pipeline-info="getPipelineLayers(pipeline.id)" diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index d34ae8036ed..b0f375c9aeb 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { reportToSentry } from '../../utils'; import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; import ActionComponent from '../jobs_shared/action_component.vue'; -import { accessValue } from './accessors'; -import { GRAPHQL } from './constants'; import JobGroupDropdown from './job_group_dropdown.vue'; import JobItem from './job_item.vue'; @@ -65,6 +63,21 @@ export default { required: true, }, }, + jobClasses: [ + 'gl-py-3', + 'gl-px-4', + 'gl-border-gray-100', + 'gl-border-solid', + 'gl-border-1', + 'gl-bg-white', + 'gl-rounded-7', + 'gl-hover-bg-gray-50', + 'gl-focus-bg-gray-50', + 'gl-hover-text-gray-900', + 'gl-focus-text-gray-900', + 'gl-hover-border-gray-200', + 'gl-focus-border-gray-200', + ], titleClasses: [ 'gl-font-weight-bold', 'gl-pipeline-job-width', @@ -97,7 +110,7 @@ export default { }, methods: { getGroupId(group) { - return accessValue(GRAPHQL, 'groupId', group); + return group.name; }, groupId(group) { return `ci-badge-${escape(group.name)}`; @@ -134,7 +147,7 @@ export default { :action-icon="action.icon" :tooltip-text="action.title" :link="action.path" - class="js-stage-action stage-action rounded" + class="js-stage-action" @pipelineActionRequestComplete="$emit('refreshPipelineGraph')" /> </div> @@ -157,7 +170,7 @@ export default { :pipeline-expanded="pipelineExpanded" :pipeline-id="pipelineId" :stage-name="showStageName ? group.stageName : ''" - css-class-job-name="gl-build-content" + :css-class-job-name="$options.jobClasses" :class="[ { 'gl-opacity-3': isFadedOut(group.name) }, 'gl-transition-duration-slow gl-transition-timing-function-ease', @@ -169,6 +182,7 @@ export default { :group="group" :stage-name="showStageName ? group.stageName : ''" :pipeline-id="pipelineId" + :css-class-job-name="$options.jobClasses" /> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 5db2b604956..4db6a3c9fd8 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -218,7 +218,7 @@ export default { :status="pipeline.detailedStatus" :time="pipeline.createdAt" :user="pipeline.user" - :item-id="Number(pipelineId)" + :item-id="pipelineId" item-name="Pipeline" > <gl-button diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js index 7e7f0572faf..fa7330ce890 100644 --- a/app/assets/javascripts/pipelines/components/parsing_utils.js +++ b/app/assets/javascripts/pipelines/components/parsing_utils.js @@ -1,55 +1,18 @@ import { memoize } from 'lodash'; +import { createNodeDict } from '../utils'; import { createSankey } from './dag/drawing_utils'; /* - The following functions are the main engine in transforming the data as - received from the endpoint into the format the d3 graph expects. - - Input is of the form: - [nodes] - nodes: [{category, name, jobs, size}] - category is the stage name - name is a group name; in the case that the group has one job, it is - also the job name - size is the number of parallel jobs - jobs: [{ name, needs}] - job name is either the same as the group name or group x/y - needs: [job-names] - needs is an array of job-name strings - - Output is of the form: - { nodes: [node], links: [link] } - node: { name, category }, + unused info passed through - link: { source, target, value }, with source & target being node names - and value being a constant - - We create nodes in the GraphQL update function, and then here we create the node dictionary, - then create links, and then dedupe the links, so that in the case where - job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link - from job 1 to job 2 then another from job 2 to job 4. - - CREATE LINKS - nodes.name -> target - nodes.name.needs.each -> source (source is the name of the group, not the parallel job) - 10 -> value (constant) - */ - -export const createNodeDict = (nodes) => { - return nodes.reduce((acc, node) => { - const newNode = { - ...node, - needs: node.jobs.map((job) => job.needs || []).flat(), - }; - - if (node.size > 1) { - node.jobs.forEach((job) => { - acc[job.name] = newNode; - }); - } + A peformant alternative to lodash's isEqual. Because findIndex always finds + the first instance of a match, if the found index is not the first, we know + it is in fact a duplicate. +*/ +const deduplicate = (item, itemIndex, arr) => { + const foundIdx = arr.findIndex((test) => { + return test.source === item.source && test.target === item.target; + }); - acc[node.name] = newNode; - return acc; - }, {}); + return foundIdx === itemIndex; }; export const makeLinksFromNodes = (nodes, nodeDict) => { @@ -83,7 +46,8 @@ export const getAllAncestors = (nodes, nodeDict) => { return nodeDict[node]?.needs || ''; }) .flat() - .filter(Boolean); + .filter(Boolean) + .filter(deduplicate); if (needs.length) { return [...needs, ...getAllAncestors(needs, nodeDict)]; @@ -108,29 +72,15 @@ export const filterByAncestors = (links, nodeDict) => const targetNode = target; const targetNodeNeeds = nodeDict[targetNode].needs; const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source); - const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict); return !allAncestors.includes(source); }); -/* - A peformant alternative to lodash's isEqual. Because findIndex always finds - the first instance of a match, if the found index is not the first, we know - it is in fact a duplicate. -*/ -const deduplicate = (item, itemIndex, arr) => { - const foundIdx = arr.findIndex((test) => { - return test.source === item.source && test.target === item.target; - }); - - return foundIdx === itemIndex; -}; - export const parseData = (nodes) => { const nodeDict = createNodeDict(nodes); const allLinks = makeLinksFromNodes(nodes, nodeDict); - const filteredLinks = filterByAncestors(allLinks, nodeDict); - const links = filteredLinks.filter(deduplicate); + const filteredLinks = allLinks.filter(deduplicate); + const links = filterByAncestors(filteredLinks, nodeDict); return { nodes, links }; }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue index 40ee071f1f5..3470c963ade 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue @@ -100,7 +100,7 @@ export default { <gl-loading-icon v-if="isLoading" size="sm" /> - <gl-dropdown-item v-if="!artifacts.length" data-testid="artifacts-empty-message"> + <gl-dropdown-item v-if="!artifacts.length && !isLoading" data-testid="artifacts-empty-message"> {{ $options.i18n.emptyArtifactsMessage }} </gl-dropdown-item> @@ -110,6 +110,7 @@ export default { :href="artifact.path" rel="nofollow" download + class="gl-word-break-word" data-testid="artifact-item" > <gl-sprintf :message="$options.i18n.downloadArtifact"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue index 24b5c85c9d6..3bd149fc782 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue @@ -1,5 +1,4 @@ <script> -/* eslint-disable vue/no-v-html */ import { GlLink, GlModal } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { __, s__, sprintf } from '~/locale'; @@ -72,7 +71,7 @@ export default { :action-cancel="cancelProps" @primary="emitSubmit($event)" > - <p v-html="modalText"></p> + <p v-html="modalText /* eslint-disable-line vue/no-v-html */"></p> <p v-if="pipeline"> <ci-icon diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue index 0b70e74b8ff..2dfdaa0ea28 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue @@ -39,7 +39,7 @@ export default { return this.value.map((i) => i.type); }, tokens() { - const tokens = [ + return [ { type: this.$options.userType, icon: 'user', @@ -77,20 +77,15 @@ export default { token: PipelineStatusToken, 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) => ({ diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 2475d958e3c..12ee82f0390 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -212,6 +212,7 @@ export default { <linked-pipelines-mini-list v-if="item.triggered.length" :triggered="item.triggered" + :pipeline-path="item.path" data-testid="mini-graph-downstream" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js new file mode 100644 index 00000000000..02baa76f627 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js @@ -0,0 +1,52 @@ +import { s__ } from '~/locale'; + +export const PIPELINE_SOURCES = [ + { + 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', + }, +]; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue index 71efa8b2ab4..9643ddfbd21 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue @@ -1,8 +1,9 @@ <script> import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants'; export default { + PIPELINE_SOURCES, components: { GlFilteredSearchToken, GlFilteredSearchSuggestion, @@ -18,68 +19,8 @@ export default { }, }, 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); + activeSource() { + return PIPELINE_SOURCES.find((source) => source.value === this.value.data); }, }, }; @@ -89,13 +30,13 @@ export default { <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> + <span>{{ activeSource.text }}</span> </div> </template> <template #suggestions> <gl-filtered-search-suggestion - v-for="source in sources" + v-for="source in $options.PIPELINE_SOURCES" :key="source.value" :value="source.value" > |