Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-09 18:10:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-09 18:10:12 +0300
commite91cb68359c900aa51ffdb1863502168742e94f0 (patch)
treeb7dd1749da6e2a11899905b4eae258236cd4f6a6 /app
parent1361891b0a87187364d1586395df176a8984e914 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue121
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue16
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue149
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_width_mixin.js50
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue6
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js8
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js13
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue5
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue48
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js23
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql23
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue119
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/utils.js22
-rw-r--r--app/assets/stylesheets/utilities.scss14
-rw-r--r--app/controllers/boards/lists_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/graphql/mutations/boards/lists/create.rb20
-rw-r--r--app/models/concerns/enums/ci/pipeline.rb2
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/services/boards/lists/create_service.rb28
-rw-r--r--app/services/boards/lists/generate_service.rb6
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml2
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/gitlab_performance_bar_stats_worker.rb34
29 files changed, 568 insertions, 207 deletions
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 48e81b168ec..347828888dc 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,13 +1,14 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { isEmpty } from 'lodash';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
+import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
@@ -30,6 +31,7 @@ export default {
CommitComponent,
ExternalUrlComponent,
GlIcon,
+ GlLink,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
@@ -38,6 +40,7 @@ export default {
TerminalButtonComponent,
TooltipOnTruncate,
UserAvatarLink,
+ CiIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -81,6 +84,24 @@ export default {
},
/**
+ * @returns {Object|Undefined} The `upcoming_deployment` object if it exists.
+ * Otherwise, `undefined`.
+ */
+ upcomingDeployment() {
+ return this.model?.upcoming_deployment;
+ },
+
+ /**
+ * @returns {String} Text that will be shown in the tooltip when
+ * the user hovers over the upcoming deployment's status icon.
+ */
+ upcomingDeploymentTooltipText() {
+ return sprintf(s__('Environments|Deployment %{status}'), {
+ status: this.upcomingDeployment.deployable.status.text,
+ });
+ },
+
+ /**
* Checkes whether the row displayed is a folder.
*
* @returns {Boolean}
@@ -235,6 +256,18 @@ export default {
},
/**
+ * Same as `userImageAltDescription`, but for the
+ * upcoming deployment's user
+ *
+ * @returns {String}
+ */
+ upcomingDeploymentUserImageAltDescription() {
+ return sprintf(__("%{username}'s avatar"), {
+ username: this.upcomingDeployment.user.username,
+ });
+ },
+
+ /**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
@@ -382,6 +415,15 @@ export default {
},
/**
+ * Same as `deploymentInternalId`, but for the upcoming deployment
+ *
+ * @returns {String}
+ */
+ upcomingDeploymentInternalId() {
+ return `#${this.upcomingDeployment.iid}`;
+ },
+
+ /**
* Verifies if the user object is present under last_deployment object.
*
* @returns {Boolean}
@@ -503,6 +545,13 @@ export default {
folderIconName() {
return this.model.isOpen ? 'chevron-down' : 'chevron-right';
},
+
+ upcomingDeploymentCellClasses() {
+ return [
+ this.tableData.upcoming.spacing,
+ { 'gl-display-none gl-display-md-block': !this.upcomingDeployment },
+ ];
+ },
},
methods: {
@@ -512,6 +561,19 @@ export default {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model);
},
+
+ /**
+ * Returns the field title that will be shown in the field's row
+ * in the mobile view.
+ *
+ * @returns `field.mobileTitle` if present;
+ * if not, falls back to `field.title`.
+ */
+ getMobileViewTitleForField(fieldName) {
+ const field = this.tableData[fieldName];
+
+ return field.mobileTitle || field.title;
+ },
},
};
</script>
@@ -530,7 +592,7 @@ export default {
role="gridcell"
>
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
- {{ tableData.name.title }}
+ {{ getMobileViewTitleForField('name') }}
</div>
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
@@ -609,7 +671,9 @@ export default {
</div>
<div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
+ <div role="rowheader" class="table-mobile-header">
+ {{ getMobileViewTitleForField('commit') }}
+ </div>
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
@@ -623,7 +687,9 @@ export default {
</div>
<div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
+ <div role="rowheader" class="table-mobile-header">
+ {{ getMobileViewTitleForField('date') }}
+ </div>
<span
v-if="canShowDeploymentDate"
v-gl-tooltip
@@ -636,8 +702,51 @@ export default {
</span>
</div>
+ <div
+ v-if="!isFolder"
+ class="table-section"
+ :class="upcomingDeploymentCellClasses"
+ role="gridcell"
+ data-testid="upcoming-deployment"
+ >
+ <div role="rowheader" class="table-mobile-header">
+ {{ getMobileViewTitleForField('upcoming') }}
+ </div>
+ <div
+ v-if="upcomingDeployment"
+ class="gl-w-full gl-display-flex gl-flex-direction-row gl-md-flex-direction-column! gl-justify-content-end"
+ data-testid="upcoming-deployment-content"
+ >
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-mr-2">{{ upcomingDeploymentInternalId }}</span>
+ <gl-link
+ v-if="upcomingDeployment.deployable"
+ v-gl-tooltip
+ :href="upcomingDeployment.deployable.build_path"
+ :title="upcomingDeploymentTooltipText"
+ data-testid="upcoming-deployment-status-link"
+ >
+ <ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" />
+ </gl-link>
+ </div>
+ <div class="gl-display-flex">
+ <span v-if="upcomingDeployment.user" class="text-break-word">
+ by
+ <user-avatar-link
+ :link-href="upcomingDeployment.user.web_url"
+ :img-src="upcomingDeployment.user.avatar_url"
+ :img-alt="upcomingDeploymentUserImageAltDescription"
+ :tooltip-text="upcomingDeployment.user.username"
+ />
+ </span>
+ </div>
+ </div>
+ </div>
+
<div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
- <div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
+ <div role="rowheader" class="table-mobile-header">
+ {{ getMobileViewTitleForField('autoStop') }}
+ </div>
<span
v-if="canShowAutoStopDate"
v-gl-tooltip
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index c1b3eabec16..3cfff686c01 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -71,7 +71,7 @@ export default {
// percent spacing for cols, should add up to 100
name: {
title: s__('Environments|Environment'),
- spacing: 'section-15',
+ spacing: 'section-10',
},
deploy: {
title: s__('Environments|Deployment'),
@@ -83,18 +83,23 @@ export default {
},
commit: {
title: s__('Environments|Commit'),
- spacing: 'section-20',
+ spacing: 'section-15',
},
date: {
title: s__('Environments|Updated'),
spacing: 'section-10',
},
+ upcoming: {
+ title: s__('Environments|Upcoming'),
+ mobileTitle: s__('Environments|Upcoming deployment'),
+ spacing: 'section-10',
+ },
autoStop: {
title: s__('Environments|Auto stop in'),
- spacing: 'section-5',
+ spacing: 'section-10',
},
actions: {
- spacing: 'section-25',
+ spacing: 'section-20',
},
};
},
@@ -160,6 +165,9 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }}
</div>
+ <div class="table-section" :class="tableData.upcoming.spacing" role="columnheader">
+ {{ tableData.upcoming.title }}
+ </div>
<div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
{{ tableData.autoStop.title }}
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
index 769fff6b111..9ca4dc1e27a 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue
@@ -2,7 +2,6 @@
import { escape, capitalize } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
-import GraphWidthMixin from '../../mixins/graph_width_mixin';
import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
@@ -14,7 +13,7 @@ export default {
LinkedPipelinesColumnLegacy,
StageColumnComponentLegacy,
},
- mixins: [GraphWidthMixin, GraphBundleMixin],
+ mixins: [GraphBundleMixin],
props: {
isLoading: {
type: Boolean,
@@ -183,87 +182,83 @@ export default {
class="pipeline-visualization pipeline-graph"
:class="{ 'pipeline-tab-content': !isLinkedPipeline }"
>
- <div
- :style="{
- paddingLeft: `${graphLeftPadding}px`,
- paddingRight: `${graphRightPadding}px`,
- }"
- >
- <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
-
- <pipeline-graph-legacy
- v-if="pipelineTypeUpstream"
- :type="$options.upstream"
- class="d-inline-block upstream-pipeline"
- :class="`js-upstream-pipeline-${expandedUpstream.id}`"
- :is-loading="false"
- :pipeline="expandedUpstream"
- :is-linked-pipeline="true"
- :mediator="mediator"
- @onClickUpstreamPipeline="clickUpstreamPipeline"
- @refreshPipelineGraph="requestRefreshPipelineGraph"
- />
+ <div class="gl-w-full">
+ <div class="container-fluid container-limited">
+ <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
+ <pipeline-graph-legacy
+ v-if="pipelineTypeUpstream"
+ :type="$options.upstream"
+ class="d-inline-block upstream-pipeline"
+ :class="`js-upstream-pipeline-${expandedUpstream.id}`"
+ :is-loading="false"
+ :pipeline="expandedUpstream"
+ :is-linked-pipeline="true"
+ :mediator="mediator"
+ @onClickUpstreamPipeline="clickUpstreamPipeline"
+ @refreshPipelineGraph="requestRefreshPipelineGraph"
+ />
- <linked-pipelines-column-legacy
- v-if="hasUpstream"
- :type="$options.upstream"
- :linked-pipelines="upstreamPipelines"
- :column-title="__('Upstream')"
- :project-id="pipelineProjectId"
- @linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
- />
+ <linked-pipelines-column-legacy
+ v-if="hasUpstream"
+ :type="$options.upstream"
+ :linked-pipelines="upstreamPipelines"
+ :column-title="__('Upstream')"
+ :project-id="pipelineProjectId"
+ @linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
+ />
- <ul
- v-if="!isLoading"
- :class="{
- 'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
- }"
- class="stage-column-list align-top"
- >
- <stage-column-component-legacy
- v-for="(stage, index) in graph"
- :key="stage.name"
+ <ul
+ v-if="!isLoading"
:class="{
- 'has-upstream gl-ml-11': hasUpstreamColumn(index),
- 'has-only-one-job': hasOnlyOneJob(stage),
- 'gl-mr-26': shouldAddRightMargin(index),
+ 'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
}"
- :title="capitalizeStageName(stage.name)"
- :groups="stage.groups"
- :stage-connector-class="stageConnectorClass(index, stage)"
- :is-first-column="isFirstColumn(index)"
- :has-upstream="hasUpstream"
- :action="stage.status.action"
- :job-hovered="jobName"
- :pipeline-expanded="pipelineExpanded"
- @refreshPipelineGraph="refreshPipelineGraph"
- />
- </ul>
+ class="stage-column-list align-top"
+ >
+ <stage-column-component-legacy
+ v-for="(stage, index) in graph"
+ :key="stage.name"
+ :class="{
+ 'has-upstream gl-ml-11': hasUpstreamColumn(index),
+ 'has-only-one-job': hasOnlyOneJob(stage),
+ 'gl-mr-26': shouldAddRightMargin(index),
+ }"
+ :title="capitalizeStageName(stage.name)"
+ :groups="stage.groups"
+ :stage-connector-class="stageConnectorClass(index, stage)"
+ :is-first-column="isFirstColumn(index)"
+ :has-upstream="hasUpstream"
+ :action="stage.status.action"
+ :job-hovered="jobName"
+ :pipeline-expanded="pipelineExpanded"
+ @refreshPipelineGraph="refreshPipelineGraph"
+ />
+ </ul>
- <linked-pipelines-column-legacy
- v-if="hasDownstream"
- :type="$options.downstream"
- :linked-pipelines="downstreamPipelines"
- :column-title="__('Downstream')"
- :project-id="pipelineProjectId"
- @linkedPipelineClick="handleClickedDownstream"
- @downstreamHovered="setJob"
- @pipelineExpandToggle="setPipelineExpanded"
- />
+ <linked-pipelines-column-legacy
+ v-if="hasDownstream"
+ :type="$options.downstream"
+ :linked-pipelines="downstreamPipelines"
+ :column-title="__('Downstream')"
+ :project-id="pipelineProjectId"
+ @linkedPipelineClick="handleClickedDownstream"
+ @downstreamHovered="setJob"
+ @pipelineExpandToggle="setPipelineExpanded"
+ />
- <pipeline-graph-legacy
- v-if="pipelineTypeDownstream"
- :type="$options.downstream"
- class="d-inline-block"
- :class="`js-downstream-pipeline-${expandedDownstream.id}`"
- :is-loading="false"
- :pipeline="expandedDownstream"
- :is-linked-pipeline="true"
- :style="{ 'margin-top': downstreamMarginTop }"
- :mediator="mediator"
- @onClickDownstreamPipeline="clickDownstreamPipeline"
- @refreshPipelineGraph="requestRefreshPipelineGraph"
- />
+ <pipeline-graph-legacy
+ v-if="pipelineTypeDownstream"
+ :type="$options.downstream"
+ class="d-inline-block"
+ :class="`js-downstream-pipeline-${expandedDownstream.id}`"
+ :is-loading="false"
+ :pipeline="expandedDownstream"
+ :is-linked-pipeline="true"
+ :style="{ 'margin-top': downstreamMarginTop }"
+ :mediator="mediator"
+ @onClickDownstreamPipeline="clickDownstreamPipeline"
+ @refreshPipelineGraph="requestRefreshPipelineGraph"
+ />
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/mixins/graph_width_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_width_mixin.js
deleted file mode 100644
index 2dbaa5a5c9a..00000000000
--- a/app/assets/javascripts/pipelines/mixins/graph_width_mixin.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
-import { LAYOUT_CHANGE_DELAY } from '~/pipelines/constants';
-
-export default {
- debouncedResize: null,
- sidebarMutationObserver: null,
- data() {
- return {
- graphLeftPadding: 0,
- graphRightPadding: 0,
- };
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.$options.debouncedResize);
-
- if (this.$options.sidebarMutationObserver) {
- this.$options.sidebarMutationObserver.disconnect();
- }
- },
- created() {
- this.$options.debouncedResize = debounceByAnimationFrame(this.setGraphPadding);
- window.addEventListener('resize', this.$options.debouncedResize);
- },
- mounted() {
- this.setGraphPadding();
-
- this.$options.sidebarMutationObserver = new MutationObserver(this.handleLayoutChange);
- this.$options.sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
- attributes: true,
- childList: false,
- subtree: false,
- });
- },
- methods: {
- setGraphPadding() {
- // only add padding to main graph (not inline upstream/downstream graphs)
- if (this.type && this.type !== 'main') return;
-
- const container = document.querySelector('.js-pipeline-container');
- if (!container) return;
-
- this.graphLeftPadding = container.offsetLeft;
- this.graphRightPadding = window.innerWidth - container.offsetLeft - container.offsetWidth;
- },
- handleLayoutChange() {
- // wait until animations finish, then recalculate padding
- window.setTimeout(this.setGraphPadding, LAYOUT_CHANGE_DELAY);
- },
- },
-};
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 69eabfe5339..b47126cdeb3 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -60,6 +60,7 @@ export default {
},
data() {
return {
+ formattedMarkdown: null,
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
hasMatter: false,
@@ -140,10 +141,14 @@ export default {
onSubmit() {
const preProcessedContent = this.preProcess(false, this.parsedSource.content());
this.$emit('submit', {
+ formattedMarkdown: this.formattedMarkdown,
content: preProcessedContent,
images: this.$options.imageRepository.getAll(),
});
},
+ onEditorLoad({ formattedMarkdown }) {
+ this.formattedMarkdown = formattedMarkdown;
+ },
},
};
</script>
@@ -167,6 +172,7 @@ export default {
@modeChange="onModeChange"
@input="onInputChange"
@uploadImage="onUploadImage"
+ @load="onEditorLoad"
/>
<unsaved-changes-confirm-dialog :modified="isSaveable" />
<publish-toolbar
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
index d6a54176a3b..4cabd943e22 100644
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ b/app/assets/javascripts/static_site_editor/constants.js
@@ -15,6 +15,14 @@ export const LOAD_CONTENT_ERROR = __(
'An error ocurred while loading your content. Please try again.',
);
+export const DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE = s__(
+ 'StaticSiteEditor|Automatic formatting changes',
+);
+
+export const DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION = s__(
+ 'StaticSiteEditor|Markdown formatting preferences introduced by the Static Site Editor',
+);
+
export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
index 4137ede49c6..1bd79d40071 100644
--- a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
@@ -4,7 +4,17 @@ import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
const submitContentChangesResolver = (
_,
- { input: { project: projectId, username, sourcePath, content, images, mergeRequestMeta } },
+ {
+ input: {
+ project: projectId,
+ username,
+ sourcePath,
+ content,
+ images,
+ mergeRequestMeta,
+ formattedMarkdown,
+ },
+ },
{ cache },
) => {
return submitContentChanges({
@@ -14,6 +24,7 @@ const submitContentChangesResolver = (
content,
images,
mergeRequestMeta,
+ formattedMarkdown,
}).then(savedContentMeta => {
const data = produce(savedContentMeta, draftState => {
return {
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 68943113c14..1e52e73294e 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -53,6 +53,7 @@ export default {
return {
content: null,
images: null,
+ formattedMarkdown: null,
submitChangesError: null,
isSavingChanges: false,
};
@@ -79,9 +80,10 @@ export default {
onDismissError() {
this.submitChangesError = null;
},
- onPrepareSubmit({ content, images }) {
+ onPrepareSubmit({ formattedMarkdown, content, images }) {
this.content = content;
this.images = images;
+ this.formattedMarkdown = formattedMarkdown;
this.isSavingChanges = true;
this.$refs.editMetaModal.show();
@@ -110,6 +112,7 @@ export default {
username: this.appData.username,
sourcePath: this.appData.sourcePath,
content: this.content,
+ formattedMarkdown: this.formattedMarkdown,
images: this.images,
mergeRequestMeta,
},
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 e7aeb73e88b..e57028ea05a 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
@@ -12,6 +12,8 @@ import {
TRACKING_ACTION_CREATE_MERGE_REQUEST,
USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
+ DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
+ DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
} from '../constants';
const createBranch = (projectId, branch) =>
@@ -47,7 +49,15 @@ const createImageActions = (images, markdown) => {
return actions;
};
-const commitContent = (projectId, message, branch, sourcePath, content, images) => {
+const createUpdateSourceFileAction = (sourcePath, content) => [
+ convertObjectPropsToSnakeCase({
+ action: 'update',
+ filePath: sourcePath,
+ content,
+ }),
+];
+
+const commit = (projectId, message, branch, actions) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_COMMIT);
@@ -56,14 +66,7 @@ const commitContent = (projectId, message, branch, sourcePath, content, images)
convertObjectPropsToSnakeCase({
branch,
commitMessage: message,
- actions: [
- convertObjectPropsToSnakeCase({
- action: 'update',
- filePath: sourcePath,
- content,
- }),
- ...createImageActions(images, content),
- ],
+ actions,
}),
).catch(() => {
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
@@ -100,6 +103,7 @@ const submitContentChanges = ({
content,
images,
mergeRequestMeta,
+ formattedMarkdown,
}) => {
const branch = generateBranchName(username);
const { title: mergeRequestTitle, description: mergeRequestDescription } = mergeRequestMeta;
@@ -107,10 +111,25 @@ const submitContentChanges = ({
return createBranch(projectId, branch)
.then(({ data: { web_url: url } }) => {
+ const message = `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`;
+
Object.assign(meta, { branch: { label: branch, url } });
- return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content, images);
+ return formattedMarkdown
+ ? commit(
+ projectId,
+ message,
+ branch,
+ createUpdateSourceFileAction(sourcePath, formattedMarkdown),
+ )
+ : meta;
})
+ .then(() =>
+ commit(projectId, mergeRequestTitle, branch, [
+ ...createUpdateSourceFileAction(sourcePath, content),
+ ...createImageActions(images, content),
+ ]),
+ )
.then(({ data: { short_id: label, web_url: url } }) => {
Object.assign(meta, { commit: { label, url } });
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index bb4b66009de..3f1f2144d8e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -469,6 +469,8 @@ export default {
:pipeline-id="mr.pipeline.id"
:project-id="mr.sourceProjectId"
:security-reports-docs-path="mr.securityReportsDocsPath"
+ :target-project-full-path="mr.targetProjectFullPath"
+ :mr-iid="mr.iid"
/>
<grouped-test-reports-app
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
index 9eacf74bba8..fe50a459e52 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -105,6 +105,8 @@ export default {
registerHTMLToMarkdownRenderer(editorApi);
this.addListeners(editorApi);
+
+ this.$emit('load', { formattedMarkdown: editorApi.getMarkdown() });
},
onOpenAddImageModal() {
this.$refs.addImageModal.show();
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
new file mode 100644
index 00000000000..d7c1e27ff3e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ name: 'SecurityReportDownloadDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ artifactText({ name }) {
+ return sprintf(s__('SecurityReports|Download %{artifactName}'), {
+ artifactName: name,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ :text="s__('SecurityReports|Download results')"
+ :loading="loading"
+ icon="download"
+ right
+ >
+ <gl-dropdown-item
+ v-for="artifact in artifacts"
+ :key="artifact.path"
+ :href="artifact.path"
+ download
+ >
+ {{ artifactText(artifact) }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index 413b4a70b40..68241a8c5be 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -1,3 +1,5 @@
+import { invert } from 'lodash';
+
export const FEEDBACK_TYPE_DISMISSAL = 'dismissal';
export const FEEDBACK_TYPE_ISSUE = 'issue';
export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
@@ -7,3 +9,24 @@ export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
*/
export const REPORT_TYPE_SAST = 'sast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
+
+/**
+ * SecurityReportTypeEnum values for use with GraphQL.
+ *
+ * These should correspond to the lowercase security scan report types.
+ */
+export const SECURITY_REPORT_TYPE_ENUM_SAST = 'SAST';
+export const SECURITY_REPORT_TYPE_ENUM_SECRET_DETECTION = 'SECRET_DETECTION';
+
+/**
+ * A mapping from security scan report types to SecurityReportTypeEnum values.
+ */
+export const reportTypeToSecurityReportTypeEnum = {
+ [REPORT_TYPE_SAST]: SECURITY_REPORT_TYPE_ENUM_SAST,
+ [REPORT_TYPE_SECRET_DETECTION]: SECURITY_REPORT_TYPE_ENUM_SECRET_DETECTION,
+};
+
+/**
+ * A mapping from SecurityReportTypeEnum values to security scan report types.
+ */
+export const securityReportTypeEnumToReportType = invert(reportTypeToSecurityReportTypeEnum);
diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql
new file mode 100644
index 00000000000..310d8d88904
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_download_paths.query.graphql
@@ -0,0 +1,23 @@
+query securityReportDownloadPaths(
+ $projectPath: ID!
+ $iid: String!
+ $reportTypes: [SecurityReportTypeEnum!]
+) {
+ project(fullPath: $projectPath) {
+ mergeRequest(iid: $iid) {
+ headPipeline {
+ jobs(securityReportTypes: $reportTypes) {
+ nodes {
+ name
+ artifacts {
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index b61783ed7b0..3f4a790d24e 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -8,10 +8,17 @@ import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import Api from '~/api';
+import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
import store from './store';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
-import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION } from './constants';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SECRET_DETECTION,
+ reportTypeToSecurityReportTypeEnum,
+} from './constants';
+import securityReportDownloadPathsQuery from './queries/security_report_download_paths.query.graphql';
+import { extractSecurityReportArtifacts } from './utils';
export default {
store,
@@ -20,6 +27,7 @@ export default {
GlLink,
GlSprintf,
ReportSection,
+ SecurityReportDownloadDropdown,
SecuritySummary,
},
mixins: [glFeatureFlagsMixin()],
@@ -46,6 +54,16 @@ export default {
required: false,
default: '',
},
+ targetProjectFullPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ mrIid: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -60,8 +78,44 @@ export default {
status: ERROR,
};
},
+ apollo: {
+ reportArtifacts: {
+ query: securityReportDownloadPathsQuery,
+ variables() {
+ return {
+ projectPath: this.targetProjectFullPath,
+ iid: String(this.mrIid),
+ reportTypes: this.$options.reportTypes.map(
+ reportType => reportTypeToSecurityReportTypeEnum[reportType],
+ ),
+ };
+ },
+ skip() {
+ return !this.canShowDownloads;
+ },
+ update(data) {
+ return extractSecurityReportArtifacts(this.$options.reportTypes, data);
+ },
+ error(error) {
+ this.showError(error);
+ },
+ result({ loading }) {
+ if (loading) {
+ return;
+ }
+
+ // Query has completed, so populate the availableSecurityReports.
+ this.onCheckingAvailableSecurityReports(
+ this.reportArtifacts.map(({ reportType }) => reportType),
+ );
+ },
+ },
+ },
computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']),
+ canShowDownloads() {
+ return this.glFeatures.coreSecurityMrWidgetDownloads;
+ },
hasSecurityReports() {
return this.availableSecurityReports.length > 0;
},
@@ -71,23 +125,26 @@ export default {
hasSecretDetectionReports() {
return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION);
},
- isLoaded() {
- return this.summaryStatus !== LOADING;
+ isLoadingReportArtifacts() {
+ return this.$apollo.queries.reportArtifacts.loading;
+ },
+ shouldShowDownloadGuidance() {
+ return !this.canShowDownloads && this.summaryStatus !== LOADING;
+ },
+ scansHaveRunMessage() {
+ return this.canShowDownloads
+ ? this.$options.i18n.scansHaveRun
+ : this.$options.i18n.scansHaveRunWithDownloadGuidance;
},
},
created() {
- this.checkAvailableSecurityReports(this.$options.reportTypes)
- .then(availableSecurityReports => {
- this.availableSecurityReports = Array.from(availableSecurityReports);
- this.fetchCounts();
- })
- .catch(error => {
- createFlash({
- message: this.$options.i18n.apiError,
- captureError: true,
- error,
- });
- });
+ if (!this.canShowDownloads) {
+ this.checkAvailableSecurityReports(this.$options.reportTypes)
+ .then(availableSecurityReports => {
+ this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
+ })
+ .catch(this.showError);
+ }
},
methods: {
...mapActions(MODULE_SAST, {
@@ -150,13 +207,25 @@ export default {
window.mrTabs.tabShown('pipelines');
}
},
+ onCheckingAvailableSecurityReports(availableSecurityReports) {
+ this.availableSecurityReports = availableSecurityReports;
+ this.fetchCounts();
+ },
+ showError(error) {
+ createFlash({
+ message: this.$options.i18n.apiError,
+ captureError: true,
+ error,
+ });
+ },
},
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
i18n: {
apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
- scansHaveRun: s__(
+ scansHaveRun: s__('SecurityReports|Security scans have run'),
+ scansHaveRunWithDownloadGuidance: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
downloadFromPipelineTab: s__(
@@ -190,7 +259,7 @@ export default {
</span>
</template>
- <template v-if="isLoaded" #sub-heading>
+ <template v-if="shouldShowDownloadGuidance" #sub-heading>
<span class="gl-font-sm">
<gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
<template #link="{ content }">
@@ -204,6 +273,13 @@ export default {
</gl-sprintf>
</span>
</template>
+
+ <template v-if="canShowDownloads" #action-buttons>
+ <security-report-download-dropdown
+ :artifacts="reportArtifacts"
+ :loading="isLoadingReportArtifacts"
+ />
+ </template>
</report-section>
<!-- TODO: Remove this section when removing core_security_mr_widget_counts
@@ -216,7 +292,7 @@ export default {
data-testid="security-mr-widget"
>
<template #error>
- <gl-sprintf :message="$options.i18n.scansHaveRun">
+ <gl-sprintf :message="scansHaveRunMessage">
<template #link="{ content }">
<gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
content
@@ -233,5 +309,12 @@ export default {
<gl-icon name="question" />
</gl-link>
</template>
+
+ <template v-if="canShowDownloads" #action-buttons>
+ <security-report-download-dropdown
+ :artifacts="reportArtifacts"
+ :loading="isLoadingReportArtifacts"
+ />
+ </template>
</report-section>
</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/utils.js b/app/assets/javascripts/vue_shared/security_reports/utils.js
new file mode 100644
index 00000000000..827a87f9aaf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/utils.js
@@ -0,0 +1,22 @@
+import { securityReportTypeEnumToReportType } from './constants';
+
+export const extractSecurityReportArtifacts = (reportTypes, data) => {
+ const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
+
+ return jobs.reduce((acc, job) => {
+ const artifacts = job.artifacts?.nodes ?? [];
+
+ artifacts.forEach(({ downloadPath, fileType }) => {
+ const reportType = securityReportTypeEnumToReportType[fileType];
+ if (reportType && reportTypes.includes(reportType)) {
+ acc.push({
+ name: job.name,
+ reportType,
+ path: downloadPath,
+ });
+ }
+ });
+
+ return acc;
+ }, []);
+};
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index a3bb7c868df..ab330ed69c6 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -129,3 +129,17 @@
content: '';
display: flex;
}
+
+// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1085
+.gl-md-flex-direction-column {
+ @media (min-width: $breakpoint-md) {
+ flex-direction: column;
+ }
+}
+
+// Same as above
+.gl-md-flex-direction-column\! {
+ @media (min-width: $breakpoint-md) {
+ flex-direction: column !important;
+ }
+}
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index aecd287370f..19a4508c061 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -19,12 +19,12 @@ module Boards
end
def create
- list = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
+ response = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
- if list.valid?
- render json: serialize_as_json(list)
+ if response.success?
+ render json: serialize_as_json(response.payload[:list])
else
- render json: list.errors, status: :unprocessable_entity
+ render json: { errors: response.errors }, status: :unprocessable_entity
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 64faa2a15d9..212aef29a07 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -40,6 +40,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
+ push_frontend_feature_flag(:core_security_mr_widget_downloads, @project)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:test_failure_history, @project)
push_frontend_feature_flag(:diffs_gradual_load, @project)
diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb
index 3fe1052315f..f6df63365b2 100644
--- a/app/graphql/mutations/boards/lists/create.rb
+++ b/app/graphql/mutations/boards/lists/create.rb
@@ -27,30 +27,16 @@ module Mutations
board = authorized_find!(id: args[:board_id])
params = create_list_params(args)
- authorize_list_type_resource!(board, params)
-
- list = create_list(board, params)
+ response = create_list(board, params)
{
- list: list.valid? ? list : nil,
- errors: errors_on_object(list)
+ list: response.success? ? response.payload[:list] : nil,
+ errors: response.errors
}
end
private
- # Overridden in EE
- def authorize_list_type_resource!(board, params)
- return unless params[:label_id]
-
- labels = ::Labels::AvailableLabelsService.new(current_user, board.resource_parent, params)
- .filter_labels_ids_in_param(:label_id)
-
- unless labels.present?
- raise Gitlab::Graphql::Errors::ArgumentError, 'Label not found!'
- end
- end
-
def create_list(board, params)
create_list_service =
::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb
index a5706af0e52..e1f07fa162c 100644
--- a/app/models/concerns/enums/ci/pipeline.rb
+++ b/app/models/concerns/enums/ci/pipeline.rb
@@ -25,8 +25,6 @@ module Enums
schedule: 4,
api: 5,
external: 6,
- # TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
- # https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7,
chat: 8,
webide: 9,
diff --git a/app/models/list.rb b/app/models/list.rb
index ec211dfd497..1df565c83e6 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -7,7 +7,7 @@ class List < ApplicationRecord
belongs_to :label
has_many :list_user_preferences
- enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4 }
+ enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4, iteration: 5 }
validates :board, :list_type, presence: true, unless: :importing?
validates :label, :position, presence: true, if: :label?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 21be5bb5669..6231d8c9421 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -274,7 +274,7 @@ class MergeRequest < ApplicationRecord
scope :with_api_entity_associations, -> {
preload_routables
.preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
- :timelogs, :latest_merge_request_diff,
+ :timelogs, :latest_merge_request_diff, :reviewers,
target_project: :project_feature,
metrics: [:latest_closed_by, :merged_by])
}
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 9c7a165776e..a21ceee083f 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -6,17 +6,21 @@ module Boards
include Gitlab::Utils::StrongMemoize
def execute(board)
- List.transaction do
- case type
- when :backlog
- create_backlog(board)
- else
- target = target(board)
- position = next_position(board)
-
- create_list(board, type, target, position)
- end
- end
+ list = case type
+ when :backlog
+ create_backlog(board)
+ else
+ target = target(board)
+ position = next_position(board)
+
+ return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
+
+ create_list(board, type, target, position)
+ end
+
+ return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
+
+ ServiceResponse.success(payload: { list: list })
end
private
@@ -33,7 +37,7 @@ module Boards
def target(board)
strong_memoize(:target) do
- available_labels.find(params[:label_id])
+ available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index 4fbf1026019..d74320e92a3 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -7,7 +7,11 @@ module Boards
return false unless board.lists.movable.empty?
List.transaction do
- label_params.each { |params| create_list(board, params) }
+ label_params.each do |params|
+ response = create_list(board, params)
+
+ raise ActiveRecord::Rollback unless response.success?
+ end
end
true
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 52e3e0fd997..509ed62b39d 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -2,7 +2,7 @@
.branch-commit.cgray
- if deployment.ref
%span.icon-container.gl-display-inline-block
- = deployment.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
+ = deployment.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
.icon-container.commit-icon
= custom_icon("icon_commit")
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 8955b568741..b41c3f4fc27 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -26,7 +26,7 @@
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content
- #js-tab-pipeline.tab-pane.gl-absolute.gl-left-0.gl-w-full
+ #js-tab-pipeline.tab-pane.gl-w-full
#js-pipeline-graph-vue
#js-tab-builds.tab-pane
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 416c10e46fe..de0016f64d6 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1610,6 +1610,14 @@
:weight: 1
:idempotent:
:tags: []
+- :name: gitlab_performance_bar_stats
+ :feature_category: :metrics
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: gitlab_shell
:feature_category: :source_code_management
:has_external_dependencies:
diff --git a/app/workers/gitlab_performance_bar_stats_worker.rb b/app/workers/gitlab_performance_bar_stats_worker.rb
new file mode 100644
index 00000000000..d63f8111864
--- /dev/null
+++ b/app/workers/gitlab_performance_bar_stats_worker.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class GitlabPerformanceBarStatsWorker
+ include ApplicationWorker
+
+ LEASE_KEY = 'gitlab:performance_bar_stats'
+ LEASE_TIMEOUT = 600
+ WORKER_DELAY = 120
+ STATS_KEY = 'performance_bar_stats:pending_request_ids'
+
+ feature_category :metrics
+ idempotent!
+
+ def perform(lease_uuid)
+ Gitlab::Redis::SharedState.with do |redis|
+ request_ids = fetch_request_ids(redis, lease_uuid)
+ stats = Gitlab::PerformanceBar::Stats.new(redis)
+
+ request_ids.each do |id|
+ stats.process(id)
+ end
+ end
+ end
+
+ private
+
+ def fetch_request_ids(redis, lease_uuid)
+ ids = redis.smembers(STATS_KEY)
+ redis.del(STATS_KEY)
+ Gitlab::ExclusiveLease.cancel(LEASE_KEY, lease_uuid)
+
+ ids
+ end
+end