summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-07 15:09:49 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-07 15:09:49 +0000
commitf276d294878605e289c84beb53032b40c53ba961 (patch)
tree20ca851593d31f5d5fabcaa15a23c9b607df04f5 /app
parent100b1a03e603487ff1966f513ba1a177a8adaefd (diff)
downloadgitlab-ce-f276d294878605e289c84beb53032b40c53ba961.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue12
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue11
-rw-r--r--app/assets/javascripts/frequent_items/index.js5
-rw-r--r--app/assets/javascripts/frequent_items/store/index.js7
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue96
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue136
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue35
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js30
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue2
-rw-r--r--app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql17
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql13
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql1
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js3
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js4
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss4
-rw-r--r--app/models/namespace_onboarding_action.rb1
-rw-r--r--app/services/merge_requests/after_create_service.rb2
-rw-r--r--app/validators/json_schemas/vulnerability_finding_details.json179
-rw-r--r--app/views/layouts/nav/groups_dropdown/_show.html.haml4
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml6
25 files changed, 546 insertions, 105 deletions
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 61080fb5487..c4f61b839e4 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
-import store from '../store';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
@@ -11,7 +10,6 @@ import FrequentItemsList from './frequent_items_list.vue';
import frequentItemsMixin from './frequent_items_mixin';
export default {
- store,
components: {
FrequentItemsSearchInput,
FrequentItemsList,
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 1203f389931..3260d768fd9 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,13 +1,18 @@
<script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
+import { mapState } from 'vuex';
import Identicon from '~/vue_shared/components/identicon.vue';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
export default {
components: {
Identicon,
},
+ mixins: [trackingMixin],
props: {
matcher: {
type: String,
@@ -37,6 +42,7 @@ export default {
},
},
computed: {
+ ...mapState(['dropdownType']),
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
@@ -49,7 +55,11 @@ export default {
<template>
<li class="frequent-items-list-item-container">
- <a :href="webUrl" class="clearfix">
+ <a
+ :href="webUrl"
+ class="clearfix"
+ @click="track('click_link', { label: `${dropdownType}_dropdown_frequent_items_list_item` })"
+ >
<div
ref="frequentItemsItemAvatarContainer"
class="frequent-items-item-avatar-container avatar-container rect-avatar s32"
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
index 19cb09f0dcc..8042e8c7bc9 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -1,27 +1,34 @@
<script>
import { debounce } from 'lodash';
-import { mapActions } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { GlIcon } from '@gitlab/ui';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
+import Tracking from '~/tracking';
+
+const trackingMixin = Tracking.mixin();
export default {
components: {
GlIcon,
},
- mixins: [frequentItemsMixin],
+ mixins: [frequentItemsMixin, trackingMixin],
data() {
return {
searchQuery: '',
};
},
computed: {
+ ...mapState(['dropdownType']),
translations() {
return this.getTranslations(['searchInputPlaceholder']);
},
},
watch: {
searchQuery: debounce(function debounceSearchQuery() {
+ this.track('type_search_query', {
+ label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
+ });
this.setSearchQuery(this.searchQuery);
}, 500),
},
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
index 1998bf4358a..639562bf961 100644
--- a/app/assets/javascripts/frequent_items/index.js
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import eventHub from './event_hub';
+import { createStore } from '~/frequent_items/store';
Vue.use(Translate);
@@ -28,11 +29,15 @@ export default function initFrequentItemDropdowns() {
return;
}
+ const dropdownType = namespace;
+ const store = createStore({ dropdownType });
+
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
+ store,
data() {
const { dataset } = this.$options.el;
const item = {
diff --git a/app/assets/javascripts/frequent_items/store/index.js b/app/assets/javascripts/frequent_items/store/index.js
index ece9e6419dd..83176d69802 100644
--- a/app/assets/javascripts/frequent_items/store/index.js
+++ b/app/assets/javascripts/frequent_items/store/index.js
@@ -7,10 +7,11 @@ import state from './state';
Vue.use(Vuex);
-export default () =>
- new Vuex.Store({
+export const createStore = (initState = {}) => {
+ return new Vuex.Store({
actions,
getters,
mutations,
- state: state(),
+ state: state(initState),
});
+};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
index 75b04febee4..c5c0b25fdf2 100644
--- a/app/assets/javascripts/frequent_items/store/state.js
+++ b/app/assets/javascripts/frequent_items/store/state.js
@@ -1,5 +1,6 @@
-export default () => ({
+export default ({ dropdownType = '' } = {}) => ({
namespace: '',
+ dropdownType,
storageKey: '',
searchQuery: '',
isLoadingItems: false,
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 7704f2ba0fd..2f050302db5 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -1,10 +1,14 @@
<script>
+import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
+import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
-import { MAIN } from './constants';
+import { DOWNSTREAM, MAIN, UPSTREAM } from './constants';
export default {
name: 'PipelineGraph',
components: {
+ LinkedGraphWrapper,
+ LinkedPipelinesColumn,
StageColumnComponent,
},
props: {
@@ -23,10 +27,60 @@ export default {
default: MAIN,
},
},
+ pipelineTypeConstants: {
+ DOWNSTREAM,
+ UPSTREAM,
+ },
+ data() {
+ return {
+ hoveredJobName: '',
+ pipelineExpanded: {
+ jobName: '',
+ expanded: false,
+ },
+ };
+ },
computed: {
+ downstreamPipelines() {
+ return this.hasDownstreamPipelines ? this.pipeline.downstream : [];
+ },
graph() {
return this.pipeline.stages;
},
+ hasDownstreamPipelines() {
+ return Boolean(this.pipeline?.downstream?.length > 0);
+ },
+ hasUpstreamPipelines() {
+ return Boolean(this.pipeline?.upstream?.length > 0);
+ },
+ // The two show checks prevent upstream / downstream from showing redundant linked columns
+ showDownstreamPipelines() {
+ return (
+ this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
+ );
+ },
+ showUpstreamPipelines() {
+ return (
+ this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
+ );
+ },
+ upstreamPipelines() {
+ return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
+ },
+ },
+ methods: {
+ handleError(errorType) {
+ this.$emit('error', errorType);
+ },
+ setJob(jobName) {
+ this.hoveredJobName = jobName;
+ },
+ togglePipelineExpanded(jobName, expanded) {
+ this.pipelineExpanded = {
+ expanded,
+ jobName: expanded ? jobName : '',
+ };
+ },
},
};
</script>
@@ -36,13 +90,39 @@ export default {
class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap"
:class="{ 'gl-py-5': !isLinkedPipeline }"
>
- <stage-column-component
- v-for="stage in graph"
- :key="stage.name"
- :title="stage.name"
- :groups="stage.groups"
- :action="stage.status.action"
- />
+ <linked-graph-wrapper>
+ <template #upstream>
+ <linked-pipelines-column
+ v-if="showUpstreamPipelines"
+ :linked-pipelines="upstreamPipelines"
+ :column-title="__('Upstream')"
+ :type="$options.pipelineTypeConstants.UPSTREAM"
+ @error="handleError"
+ />
+ </template>
+ <template #main>
+ <stage-column-component
+ v-for="stage in graph"
+ :key="stage.name"
+ :title="stage.name"
+ :groups="stage.groups"
+ :action="stage.status.action"
+ :job-hovered="hoveredJobName"
+ :pipeline-expanded="pipelineExpanded"
+ />
+ </template>
+ <template #downstream>
+ <linked-pipelines-column
+ v-if="showDownstreamPipelines"
+ :linked-pipelines="downstreamPipelines"
+ :column-title="__('Downstream')"
+ :type="$options.pipelineTypeConstants.DOWNSTREAM"
+ @downstreamHovered="setJob"
+ @pipelineExpandToggle="togglePipelineExpanded"
+ @error="handleError"
+ />
+ </template>
+ </linked-graph-wrapper>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 49a2feab0fc..cb2f4d0d623 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -42,7 +42,7 @@ export default {
};
},
update(data) {
- return unwrapPipelineData(this.pipelineIid, data);
+ return unwrapPipelineData(this.pipelineProjectPath, data);
},
error() {
this.reportFailure(LOAD_FAILURE);
@@ -77,13 +77,11 @@ export default {
};
</script>
<template>
- <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
- {{ alert.text }}
- </gl-alert>
- <gl-loading-icon
- v-else-if="$apollo.queries.pipeline.loading"
- class="gl-mx-auto gl-my-4"
- size="lg"
- />
- <pipeline-graph v-else :pipeline="pipeline" />
+ <div>
+ <gl-alert v-if="showAlert" :variant="alert.variant" @dismiss="hideAlert">
+ {{ alert.text }}
+ </gl-alert>
+ <gl-loading-icon v-if="$apollo.queries.pipeline.loading" class="gl-mx-auto gl-my-4" size="lg" />
+ <pipeline-graph v-if="pipeline" :pipeline="pipeline" @error="reportFailure" />
+ </div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 97e5a309215..1a179de64cd 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -25,23 +25,33 @@ export default {
type: String,
required: true,
},
- pipeline: {
- type: Object,
+ expanded: {
+ type: Boolean,
required: true,
},
- projectId: {
- type: Number,
+ pipeline: {
+ type: Object,
required: true,
},
type: {
type: String,
required: true,
},
- },
- data() {
- return {
- expanded: false,
- };
+ /*
+ 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,
+ },
},
computed: {
tooltipText() {
@@ -74,6 +84,9 @@ export default {
}
return __('Multi-project');
},
+ pipelineIsLoading() {
+ return Boolean(this.isLoading || this.pipeline.isLoading);
+ },
isDownstream() {
return this.type === DOWNSTREAM;
},
@@ -81,7 +94,9 @@ export default {
return this.type === UPSTREAM;
},
isSameProject() {
- return this.projectId === this.pipeline.project.id;
+ return this.projectId > -1
+ ? this.projectId === this.pipeline.project.id
+ : !this.pipeline.multiproject;
},
sourceJobName() {
return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
@@ -101,16 +116,15 @@ export default {
},
methods: {
onClickLinkedPipeline() {
- this.$root.$emit('bv::hide::tooltip', this.buttonId);
- this.expanded = !this.expanded;
+ this.hideTooltips();
this.$emit('pipelineClicked', this.$refs.linkedPipeline);
- this.$emit('pipelineExpandToggle', this.pipeline.source_job.name, this.expanded);
+ this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
},
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
},
onDownstreamHovered() {
- this.$emit('downstreamHovered', this.pipeline.source_job.name);
+ this.$emit('downstreamHovered', this.sourceJobName);
},
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
@@ -120,10 +134,10 @@ export default {
</script>
<template>
- <li
+ <div
ref="linkedPipeline"
v-gl-tooltip
- class="linked-pipeline build"
+ class="linked-pipeline build gl-pipeline-job-width"
:title="tooltipText"
:class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
@@ -136,8 +150,9 @@ export default {
>
<div class="gl-display-flex">
<ci-status
- v-if="!pipeline.isLoading"
+ v-if="!pipelineIsLoading"
:status="pipelineStatus"
+ :size="24"
css-classes="gl-top-0 gl-pr-2"
/>
<div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
@@ -160,10 +175,10 @@ export default {
class="gl-absolute gl-top-0 gl-bottom-0 gl-shadow-none! gl-rounded-0!"
:class="`js-pipeline-expand-${pipeline.id} ${expandButtonPosition}`"
:icon="expandedIcon"
- data-testid="expandPipelineButton"
+ data-testid="expand-pipeline-button"
data-qa-selector="expand_pipeline_button"
@click="onClickLinkedPipeline"
/>
</div>
- </li>
+ </div>
</template>
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 2ca33e6d33e..58757a88102 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -1,10 +1,14 @@
<script>
+import getPipelineDetails from '../../graphql/queries/get_pipeline_details.query.graphql';
import LinkedPipeline from './linked_pipeline.vue';
+import { LOAD_FAILURE } from '../../constants';
import { UPSTREAM } from './constants';
+import { unwrapPipelineData } from './utils';
export default {
components: {
LinkedPipeline,
+ PipelineGraph: () => import('./graph_component.vue'),
},
props: {
columnTitle: {
@@ -19,11 +23,22 @@ export default {
type: String,
required: true,
},
- projectId: {
- type: Number,
- required: true,
- },
},
+ data() {
+ return {
+ currentPipeline: null,
+ loadingPipelineId: null,
+ pipelineExpanded: false,
+ };
+ },
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ 'gl-mb-5',
+ ],
computed: {
columnClass() {
const positionValues = {
@@ -35,14 +50,66 @@ export default {
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
- // Refactor string match when BE returns Upstream/Downstream indicators
isUpstream() {
return this.type === UPSTREAM;
},
+ computedTitleClasses() {
+ const positionalClasses = this.isUpstream
+ ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
+ : [];
+
+ return [...this.$options.titleClasses, ...positionalClasses];
+ },
},
methods: {
- onPipelineClick(downstreamNode, pipeline, index) {
- this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
+ getPipelineData(pipeline) {
+ const projectPath = pipeline.project.fullPath;
+
+ this.$apollo.addSmartQuery('currentPipeline', {
+ query: getPipelineDetails,
+ variables() {
+ return {
+ projectPath,
+ iid: pipeline.iid,
+ };
+ },
+ update(data) {
+ return unwrapPipelineData(projectPath, data);
+ },
+ result() {
+ this.loadingPipelineId = null;
+ },
+ error() {
+ this.$emit('error', LOAD_FAILURE);
+ },
+ });
+ },
+ isExpanded(id) {
+ return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id);
+ },
+ isLoadingPipeline(id) {
+ return this.loadingPipelineId === id;
+ },
+ onPipelineClick(pipeline) {
+ /* If the clicked pipeline has been expanded already, close it, clear, exit */
+ if (this.currentPipeline?.id === pipeline.id) {
+ this.pipelineExpanded = false;
+ this.currentPipeline = null;
+ return;
+ }
+
+ /* Set the loading id */
+ this.loadingPipelineId = pipeline.id;
+
+ /*
+ Expand the pipeline.
+ If this was not a toggle close action, and
+ it was already showing a different pipeline, then
+ this will be a no-op, but that doesn't matter.
+ */
+ this.pipelineExpanded = true;
+
+ this.getPipelineData(pipeline);
},
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
@@ -60,25 +127,40 @@ export default {
</script>
<template>
- <div :class="columnClass" class="stage-column linked-pipelines-column">
- <div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
- <div v-if="isUpstream" class="cross-project-triangle"></div>
- <ul>
- <linked-pipeline
- v-for="(pipeline, index) in linkedPipelines"
- :key="pipeline.id"
- :class="{
- active: pipeline.isExpanded,
- 'left-connector': pipeline.isExpanded && graphPosition === 'left',
- }"
- :pipeline="pipeline"
- :column-title="columnTitle"
- :project-id="projectId"
- :type="type"
- @pipelineClicked="onPipelineClick($event, pipeline, index)"
- @downstreamHovered="onDownstreamHovered"
- @pipelineExpandToggle="onPipelineExpandToggle"
- />
- </ul>
+ <div class="gl-display-flex">
+ <div :class="columnClass" class="linked-pipelines-column">
+ <div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses">
+ {{ columnTitle }}
+ </div>
+ <ul class="gl-pl-0">
+ <li
+ v-for="pipeline in linkedPipelines"
+ :key="pipeline.id"
+ class="gl-display-flex gl-mb-4"
+ :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
+ >
+ <linked-pipeline
+ class="gl-display-inline-block"
+ :is-loading="isLoadingPipeline(pipeline.id)"
+ :pipeline="pipeline"
+ :column-title="columnTitle"
+ :type="type"
+ :expanded="isExpanded(pipeline.id)"
+ @downstreamHovered="onDownstreamHovered"
+ @pipelineClicked="onPipelineClick(pipeline)"
+ @pipelineExpandToggle="onPipelineExpandToggle"
+ />
+ <div v-if="isExpanded(pipeline.id)" class="gl-display-inline-block">
+ <pipeline-graph
+ v-if="currentPipeline"
+ :type="type"
+ class="d-inline-block gl-mt-n2"
+ :pipeline="currentPipeline"
+ :is-linked-pipeline="true"
+ />
+ </div>
+ </li>
+ </ul>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
index 2ca33e6d33e..7d371b33220 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column_legacy.vue
@@ -35,7 +35,9 @@ export default {
graphPosition() {
return this.isUpstream ? 'left' : 'right';
},
- // Refactor string match when BE returns Upstream/Downstream indicators
+ isExpanded() {
+ return this.pipeline?.isExpanded || false;
+ },
isUpstream() {
return this.type === UPSTREAM;
},
@@ -64,21 +66,22 @@ export default {
<div class="stage-name linked-pipelines-column-title">{{ columnTitle }}</div>
<div v-if="isUpstream" class="cross-project-triangle"></div>
<ul>
- <linked-pipeline
- v-for="(pipeline, index) in linkedPipelines"
- :key="pipeline.id"
- :class="{
- active: pipeline.isExpanded,
- 'left-connector': pipeline.isExpanded && graphPosition === 'left',
- }"
- :pipeline="pipeline"
- :column-title="columnTitle"
- :project-id="projectId"
- :type="type"
- @pipelineClicked="onPipelineClick($event, pipeline, index)"
- @downstreamHovered="onDownstreamHovered"
- @pipelineExpandToggle="onPipelineExpandToggle"
- />
+ <li v-for="(pipeline, index) in linkedPipelines" :key="pipeline.id">
+ <linked-pipeline
+ :class="{
+ active: pipeline.isExpanded,
+ 'left-connector': pipeline.isExpanded && graphPosition === 'left',
+ }"
+ :pipeline="pipeline"
+ :column-title="columnTitle"
+ :project-id="projectId"
+ :type="type"
+ :expanded="isExpanded"
+ @pipelineClicked="onPipelineClick($event, pipeline, index)"
+ @downstreamHovered="onDownstreamHovered"
+ @pipelineExpandToggle="onPipelineExpandToggle"
+ />
+ </li>
</ul>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
index df3615772ce..7bf44b160ef 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/pipelines/components/graph/utils.js
@@ -1,28 +1,42 @@
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { unwrapStagesWithNeeds } from '../unwrapping_utils';
-const addMulti = (mainId, pipeline) => {
- return { ...pipeline, multiproject: mainId !== pipeline.id };
+const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
+ return {
+ ...linkedPipeline,
+ multiproject: mainPipelineProjectPath !== linkedPipeline.project.fullPath,
+ };
};
-const unwrapPipelineData = (mainPipelineId, data) => {
+const transformId = linkedPipeline => {
+ return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
+};
+
+const unwrapPipelineData = (mainPipelineProjectPath, data) => {
if (!data?.project?.pipeline) {
return null;
}
+ const { pipeline } = data.project;
+
const {
- id,
upstream,
downstream,
stages: { nodes: stages },
- } = data.project.pipeline;
+ } = pipeline;
const nodes = unwrapStagesWithNeeds(stages);
return {
- id,
+ ...pipeline,
+ id: getIdFromGraphQLId(pipeline.id),
stages: nodes,
- upstream: upstream ? [upstream].map(addMulti.bind(null, mainPipelineId)) : [],
- downstream: downstream ? downstream.map(addMulti.bind(null, mainPipelineId)) : [],
+ upstream: upstream
+ ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
+ : [],
+ downstream: downstream
+ ? downstream.nodes.map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
+ : [],
};
};
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue
new file mode 100644
index 00000000000..fb2280d971a
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue
@@ -0,0 +1,7 @@
+<template>
+ <div class="gl-display-flex">
+ <slot name="upstream"></slot>
+ <slot name="main"></slot>
+ <slot name="downstream"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
index 205ee0fb414..1c9e3236d56 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
@@ -17,7 +17,7 @@ export default {
<template>
<div>
<div
- class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-py-4 gl-mb-5"
+ class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-mb-5"
:class="stageClasses"
>
<slot name="stages"> </slot>
diff --git a/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql b/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql
new file mode 100644
index 00000000000..3bf6d8dc9d8
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/fragments/linked_pipelines.fragment.graphql
@@ -0,0 +1,17 @@
+fragment LinkedPipelineData on Pipeline {
+ id
+ iid
+ path
+ status: detailedStatus {
+ group
+ label
+ icon
+ }
+ sourceJob {
+ name
+ }
+ project {
+ name
+ fullPath
+ }
+}
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql
index 6d80e7b8d51..25aede49631 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_details.query.graphql
@@ -1,7 +1,18 @@
+#import "../fragments/linked_pipelines.fragment.graphql"
+
query getPipelineDetails($projectPath: ID!, $iid: ID!) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
- id: iid
+ id
+ iid
+ downstream {
+ nodes {
+ ...LinkedPipelineData
+ }
+ }
+ upstream {
+ ...LinkedPipelineData
+ }
stages {
nodes {
name
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
index 06083daeca0..1b3f80b1f18 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
@@ -2,6 +2,7 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
pipeline(iid: $iid) {
id
+ iid
status
retryable
cancelable
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
index faa4026c064..d6a54176a3b 100644
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ b/app/assets/javascripts/static_site_editor/constants.js
@@ -21,4 +21,7 @@ export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
+export const USAGE_PING_TRACKING_ACTION_CREATE_COMMIT = 'static_site_editor_commits';
+export const USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST = 'static_site_editor_merge_requests';
+
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
index 8623a671a7d..e7aeb73e88b 100644
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
@@ -10,6 +10,8 @@ import {
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
TRACKING_ACTION_CREATE_COMMIT,
TRACKING_ACTION_CREATE_MERGE_REQUEST,
+ USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
+ USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
} from '../constants';
const createBranch = (projectId, branch) =>
@@ -47,6 +49,7 @@ const createImageActions = (images, markdown) => {
const commitContent = (projectId, message, branch, sourcePath, content, images) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
+ Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_COMMIT);
return Api.commitMultiple(
projectId,
@@ -75,6 +78,7 @@ const createMergeRequest = (
targetBranch = DEFAULT_TARGET_BRANCH,
) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_MERGE_REQUEST);
+ Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
return Api.createProjectMergeRequest(
projectId,
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 7b424882ffa..6b70cda4891 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -139,6 +139,10 @@
width: 186px;
}
+.gl-linked-pipeline-padding {
+ padding-right: 120px;
+}
+
.gl-build-content {
@include build-content();
}
diff --git a/app/models/namespace_onboarding_action.rb b/app/models/namespace_onboarding_action.rb
index e1121279e2e..bf4df7de13f 100644
--- a/app/models/namespace_onboarding_action.rb
+++ b/app/models/namespace_onboarding_action.rb
@@ -8,6 +8,7 @@ class NamespaceOnboardingAction < ApplicationRecord
ACTIONS = {
subscription_created: 1,
git_write: 2,
+ merge_request_created: 3,
git_read: 4
}.freeze
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index f0c85ae03c9..fbb9d5fa9dc 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -11,6 +11,8 @@ module MergeRequests
merge_request.diffs(include_stats: false).write_cache
merge_request.create_cross_references!(current_user)
+
+ NamespaceOnboardingAction.create_action(merge_request.target_project.namespace, :merge_request_created)
end
end
end
diff --git a/app/validators/json_schemas/vulnerability_finding_details.json b/app/validators/json_schemas/vulnerability_finding_details.json
index 8b44ac62dfc..f2940866f4b 100644
--- a/app/validators/json_schemas/vulnerability_finding_details.json
+++ b/app/validators/json_schemas/vulnerability_finding_details.json
@@ -1,5 +1,182 @@
{
"type": "object",
"description": "The schema for vulnerability finding details",
- "additionalProperties": false
+ "additionalProperties": false,
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ { "$ref": "#/definitions/named_field" },
+ { "$ref": "#/definitions/type_list" }
+ ]
+ }
+ },
+ "definitions": {
+ "type_list": {
+ "oneOf": [
+ { "$ref": "#/definitions/named_list" },
+ { "$ref": "#/definitions/list" },
+ { "$ref": "#/definitions/table" },
+
+ { "$ref": "#/definitions/text" },
+ { "$ref": "#/definitions/url" },
+ { "$ref": "#/definitions/code" },
+ { "$ref": "#/definitions/int" },
+
+ { "$ref": "#/definitions/commit" },
+ { "$ref": "#/definitions/file_location" },
+ { "$ref": "#/definitions/module_location" }
+ ]
+ },
+ "lang_text": {
+ "type": "object",
+ "required": [ "value", "lang" ],
+ "properties": {
+ "lang": { "type": "string" },
+ "value": { "type": "string" }
+ }
+ },
+ "lang_text_list": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/lang_text" }
+ },
+ "named_field": {
+ "type": "object",
+ "required": [ "name" ],
+ "properties": {
+ "name": { "$ref": "#/definitions/lang_text_list" },
+ "description": { "$ref": "#/definitions/lang_text_list" }
+ }
+ },
+ "named_list": {
+ "type": "object",
+ "description": "An object with named and typed fields",
+ "required": [ "type", "items" ],
+ "properties": {
+ "type": { "const": "named-list" },
+ "items": {
+ "type": "object",
+ "patternProperties": {
+ "^.*$": {
+ "allOf": [
+ { "$ref": "#/definitions/named_field" },
+ { "$ref": "#/definitions/type_list" }
+ ]
+ }
+ }
+ }
+ }
+ },
+ "list": {
+ "type": "object",
+ "description": "A list of typed fields",
+ "required": [ "type", "items" ],
+ "properties": {
+ "type": { "const": "list" },
+ "items": {
+ "type": "array",
+ "items": { "$ref": "#/definitions/type_list" }
+ }
+ }
+ },
+ "table": {
+ "type": "object",
+ "description": "A table of typed fields",
+ "required": [],
+ "properties": {
+ "type": { "const": "table" },
+ "items": {
+ "type": "object",
+ "properties": {
+ "header": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/type_list"
+ }
+ },
+ "rows": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/type_list"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "text": {
+ "type": "object",
+ "description": "Raw text",
+ "required": [ "type", "value" ],
+ "properties": {
+ "type": { "const": "text" },
+ "value": { "$ref": "#/definitions/lang_text_list" }
+ }
+ },
+ "url": {
+ "type": "object",
+ "description": "A single URL",
+ "required": [ "type", "href" ],
+ "properties": {
+ "type": { "const": "url" },
+ "text": { "$ref": "#/definitions/lang_text_list" },
+ "href": { "type": "string" }
+ }
+ },
+ "code": {
+ "type": "object",
+ "description": "A codeblock",
+ "required": [ "type", "value" ],
+ "properties": {
+ "type": { "const": "code" },
+ "value": { "type": "string" },
+ "lang": { "type": "string" }
+ }
+ },
+ "int": {
+ "type": "object",
+ "description": "An integer",
+ "required": [ "type", "value" ],
+ "properties": {
+ "type": { "const": "int" },
+ "value": { "type": "integer" },
+ "format": {
+ "type": "string",
+ "enum": [ "default", "hex" ]
+ }
+ }
+ },
+ "commit": {
+ "type": "object",
+ "description": "A specific commit within the project",
+ "required": [ "type", "value" ],
+ "properties": {
+ "type": { "const": "commit" },
+ "value": { "type": "string", "description": "The commit SHA" }
+ }
+ },
+ "file_location": {
+ "type": "object",
+ "description": "A location within a file in the project",
+ "required": [ "type", "file_name", "line_start" ],
+ "properties": {
+ "type": { "const": "file-location" },
+ "file_name": { "type": "string" },
+ "line_start": { "type": "integer" },
+ "line_end": { "type": "integer" }
+ }
+ },
+ "module_location": {
+ "type": "object",
+ "description": "A location within a binary module of the form module+relative_offset",
+ "required": [ "type", "module_name", "offset" ],
+ "properties": {
+ "type": { "const": "module-location" },
+ "module_name": { "type": "string" },
+ "offset": { "type": "integer" }
+ }
+ }
+ }
}
diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml
index 3ce1fa6bcca..d0394451a61 100644
--- a/app/views/layouts/nav/groups_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/groups_dropdown/_show.html.haml
@@ -3,10 +3,10 @@
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/groups#index') do
- = link_to dashboard_groups_path, class: 'qa-your-groups-link' do
+ = link_to dashboard_groups_path, class: 'qa-your-groups-link', data: { track_label: "groups_dropdown_your_groups", track_event: "click_link" } do
= _('Your groups')
= nav_link(path: 'groups#explore') do
- = link_to explore_groups_path do
+ = link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do
= _('Explore groups')
.frequent-items-dropdown-content
#js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index f2170f71532..91f999a9a74 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -3,13 +3,13 @@
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
- = link_to dashboard_projects_path, class: 'qa-your-projects-link' do
+ = link_to dashboard_projects_path, class: 'qa-your-projects-link', data: { track_label: "projects_dropdown_your_projects", track_event: "click_link" } do
= _('Your projects')
= nav_link(path: 'projects#starred') do
- = link_to starred_dashboard_projects_path do
+ = link_to starred_dashboard_projects_path, data: { track_label: "projects_dropdown_starred_projects", track_event: "click_link" } do
= _('Starred projects')
= nav_link(path: 'projects#trending') do
- = link_to explore_root_path do
+ = link_to explore_root_path, data: { track_label: "projects_dropdown_explore_projects", track_event: "click_link" } do
= _('Explore projects')
.frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }