diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-07 15:09:49 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-07 15:09:49 +0000 |
commit | f276d294878605e289c84beb53032b40c53ba961 (patch) | |
tree | 20ca851593d31f5d5fabcaa15a23c9b607df04f5 /app | |
parent | 100b1a03e603487ff1966f513ba1a177a8adaefd (diff) | |
download | gitlab-ce-f276d294878605e289c84beb53032b40c53ba961.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 } } |