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>2021-11-17 21:14:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-17 21:14:12 +0300
commit2c06e006d832757e90e5199112ab062b27df7433 (patch)
tree36811d1041a1a2b88b041ecac07e9d3aa41c6b1f /app
parenta331169e6e84f93fd9b841b56465ac113b6d03f9 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/invite_members/components/confetti.vue33
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue101
-rw-r--r--app/assets/javascripts/invite_members/constants.js119
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue14
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js5
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue2
-rw-r--r--app/helpers/learn_gitlab_helper.rb21
-rw-r--r--app/models/deployment.rb14
-rw-r--r--app/services/deployments/archive_in_project_service.rb27
-rw-r--r--app/views/projects/learn_gitlab/index.html.haml9
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/deployments/archive_in_project_worker.rb19
13 files changed, 296 insertions, 79 deletions
diff --git a/app/assets/javascripts/invite_members/components/confetti.vue b/app/assets/javascripts/invite_members/components/confetti.vue
new file mode 100644
index 00000000000..2e5744afcd4
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/confetti.vue
@@ -0,0 +1,33 @@
+<script>
+import confetti from 'canvas-confetti';
+
+export default {
+ mounted() {
+ confetti.create(this.$refs.canvas, {
+ resize: true,
+ useWorker: true,
+ disableForReducedMotion: true,
+ });
+
+ this.basicCannon();
+ },
+ methods: {
+ basicCannon() {
+ confetti({
+ particleCount: 100,
+ spread: 70,
+ origin: { y: 0.2 },
+ scalar: 2,
+ shapes: ['square'],
+ colors: ['#FC6D26', '#6B4FBB', '#FDB997'],
+ zIndex: 1045,
+ gravity: 1.5,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <canvas ref="canvas" width="0" height="0"></canvas>
+</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index ee77ba110bb..cf4f434a7a8 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -18,19 +18,21 @@ import ExperimentTracking from '~/experimentation/experiment_tracking';
import { sanitize } from '~/lib/dompurify';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
-import { s__, sprintf } from '~/locale';
+import { sprintf } from '~/locale';
import {
INVITE_MEMBERS_IN_COMMENT,
GROUP_FILTERS,
USERS_FILTER_ALL,
MEMBER_AREAS_OF_FOCUS,
INVITE_MEMBERS_FOR_TASK,
+ MODAL_LABELS,
} from '../constants';
import eventHub from '../event_hub';
import {
responseMessageFromError,
responseMessageFromSuccess,
} from '../utils/response_message_parser';
+import ModalConfetti from './confetti.vue';
import GroupSelect from './group_select.vue';
import MembersTokenSelect from './members_token_select.vue';
@@ -50,6 +52,7 @@ export default {
GlFormCheckboxGroup,
MembersTokenSelect,
GroupSelect,
+ ModalConfetti,
},
inject: ['newProjectPath'],
props: {
@@ -129,22 +132,30 @@ export default {
source: 'unknown',
invalidFeedbackMessage: '',
isLoading: false,
+ mode: 'default',
};
},
computed: {
+ isCelebration() {
+ return this.mode === 'celebrate';
+ },
validationState() {
return this.invalidFeedbackMessage === '' ? null : false;
},
isInviteGroup() {
return this.inviteeType === 'group';
},
+ modalTitle() {
+ return this.$options.labels[this.inviteeType].modal[this.mode].title;
+ },
introText() {
- const inviteTo = this.isProject ? 'toProject' : 'toGroup';
-
- return sprintf(this.$options.labels[this.inviteeType][inviteTo].introText, {
+ return sprintf(this.$options.labels[this.inviteeType][this.inviteTo][this.mode].introText, {
name: this.name,
});
},
+ inviteTo() {
+ return this.isProject ? 'toProject' : 'toGroup';
+ },
toastOptions() {
return {
onComplete: () => {
@@ -234,7 +245,8 @@ export default {
usersToAddById.map((user) => user.id).join(','),
];
},
- openModal({ inviteeType, source }) {
+ openModal({ mode = 'default', inviteeType, source }) {
+ this.mode = mode;
this.inviteeType = inviteeType;
this.source = source;
@@ -381,60 +393,7 @@ export default {
return unescape(sanitize(message, { ALLOWED_TAGS: [] }));
},
},
- labels: {
- members: {
- modalTitle: s__('InviteMembersModal|Invite members'),
- searchField: s__('InviteMembersModal|GitLab member or email address'),
- placeHolder: s__('InviteMembersModal|Select members or type email addresses'),
- toGroup: {
- introText: s__(
- "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
- ),
- },
- toProject: {
- introText: s__(
- "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
- ),
- },
- tasksToBeDone: {
- title: s__(
- 'InviteMembersModal|Create issues for your new team member to work on (optional)',
- ),
- noProjects: s__(
- 'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
- ),
- },
- tasksProject: {
- title: s__('InviteMembersModal|Choose a project for the issues'),
- },
- },
- group: {
- modalTitle: s__('InviteMembersModal|Invite a group'),
- searchField: s__('InviteMembersModal|Select a group to invite'),
- placeHolder: s__('InviteMembersModal|Search for a group to invite'),
- toGroup: {
- introText: s__(
- "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
- ),
- },
- toProject: {
- introText: s__(
- "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
- ),
- },
- },
- accessLevel: s__('InviteMembersModal|Select a role'),
- accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
- toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
- invalidFeedbackMessageDefault: s__('InviteMembersModal|Something went wrong'),
- readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
- inviteButtonText: s__('InviteMembersModal|Invite'),
- cancelButtonText: s__('InviteMembersModal|Cancel'),
- headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
- areasOfFocusLabel: s__(
- 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
- ),
- },
+ labels: MODAL_LABELS,
membersTokenSelectLabelId: 'invite-members-input',
};
</script>
@@ -445,20 +404,28 @@ export default {
size="sm"
data-qa-selector="invite_members_modal_content"
data-testid="invite-members-modal"
- :title="$options.labels[inviteeType].modalTitle"
+ :title="modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
@hidden="resetFields"
@close="resetFields"
@hide="resetFields"
>
<div>
- <p ref="introText">
- <gl-sprintf :message="introText">
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
+ <div class="gl-display-flex">
+ <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div>
+ <div>
+ <p ref="introText">
+ <gl-sprintf :message="introText">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ <br />
+ <span v-if="isCelebration">{{ $options.labels.members.modal.celebrate.intro }} </span>
+ <modal-confetti v-if="isCelebration" />
+ </p>
+ </div>
+ </div>
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index c1a1107ebe3..59d4c2f3077 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const SEARCH_DELAY = 200;
@@ -27,3 +27,120 @@ export const USERS_FILTER_ALL = 'all';
export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id';
export const TRIGGER_ELEMENT_BUTTON = 'button';
export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav';
+export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
+export const MEMBERS_MODAL_CELEBRATE_TITLE = s__(
+ 'InviteMembersModal|GitLab is better with colleagues!',
+);
+export const MEMBERS_MODAL_CELEBRATE_INTRO = s__(
+ 'InviteMembersModal|How about inviting a colleague or two to join you?',
+);
+export const MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} group.",
+);
+
+export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting members to the %{strongStart}%{name}%{strongEnd} project.",
+);
+export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
+ "InviteMembersModal|Congratulations on creating your project, you're almost there!",
+);
+export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|GitLab member or email address');
+export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
+export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
+ 'InviteMembersModal|Create issues for your new team member to work on (optional)',
+);
+export const MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS = s__(
+ 'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
+);
+export const MEMBERS_TASKS_PROJECTS_TITLE = s__(
+ 'InviteMembersModal|Choose a project for the issues',
+);
+
+export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
+export const GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group.",
+);
+export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
+ "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
+);
+
+export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
+export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
+
+export const ACCESS_LEVEL = s__('InviteMembersModal|Select a role');
+export const ACCESS_EXPIRE_DATE = s__('InviteMembersModal|Access expiration date (optional)');
+export const TOAST_MESSAGE_SUCCESSFUL = s__('InviteMembersModal|Members were successfully added');
+export const INVALID_FEEDBACK_MESSAGE_DEFAULT = s__('InviteMembersModal|Something went wrong');
+export const READ_MORE_TEXT = s__(
+ `InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`,
+);
+export const INVITE_BUTTON_TEXT = s__('InviteMembersModal|Invite');
+export const CANCEL_BUTTON_TEXT = s__('InviteMembersModal|Cancel');
+export const HEADER_CLOSE_LABEL = s__('InviteMembersModal|Close invite team members');
+export const AREAS_OF_FOCUS_LABEL = s__(
+ 'InviteMembersModal|What would you like new member(s) to focus on? (optional)',
+);
+
+export const MODAL_LABELS = {
+ members: {
+ modal: {
+ default: {
+ title: MEMBERS_MODAL_DEFAULT_TITLE,
+ },
+ celebrate: {
+ title: MEMBERS_MODAL_CELEBRATE_TITLE,
+ intro: MEMBERS_MODAL_CELEBRATE_INTRO,
+ },
+ },
+ toGroup: {
+ default: {
+ introText: MEMBERS_TO_GROUP_DEFAULT_INTRO_TEXT,
+ },
+ },
+ toProject: {
+ default: {
+ introText: MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ },
+ celebrate: {
+ introText: MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
+ },
+ },
+ searchField: MEMBERS_SEARCH_FIELD,
+ placeHolder: MEMBERS_PLACEHOLDER,
+ tasksToBeDone: {
+ title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
+ noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
+ },
+ tasksProject: {
+ title: MEMBERS_TASKS_PROJECTS_TITLE,
+ },
+ },
+ group: {
+ modal: {
+ default: {
+ title: GROUP_MODAL_DEFAULT_TITLE,
+ },
+ },
+ toGroup: {
+ default: {
+ introText: GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT,
+ },
+ },
+ toProject: {
+ default: {
+ introText: GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT,
+ },
+ },
+ searchField: GROUP_SEARCH_FIELD,
+ placeHolder: GROUP_PLACEHOLDER,
+ },
+ accessLevel: ACCESS_LEVEL,
+ accessExpireDate: ACCESS_EXPIRE_DATE,
+ toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
+ invalidFeedbackMessageDefault: INVALID_FEEDBACK_MESSAGE_DEFAULT,
+ readMoreText: READ_MORE_TEXT,
+ inviteButtonText: INVITE_BUTTON_TEXT,
+ cancelButtonText: CANCEL_BUTTON_TEXT,
+ headerCloseLabel: HEADER_CLOSE_LABEL,
+ areasOfFocusLabel: AREAS_OF_FOCUS_LABEL,
+};
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 855e06e82ab..b06c804f3ca 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -46,7 +46,7 @@ export default {
return sprintf(
s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
- and all related resources, including issues and merge requests. Once you confirm and press
+ and all related resources, including issues and merge requests. After you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
projectName: `<strong>${escape(this.projectName)}</strong>`,
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
index 51980b2d971..95afcb6bda8 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
@@ -1,5 +1,6 @@
<script>
import { GlProgressBar, GlSprintf } from '@gitlab/ui';
+import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
@@ -22,6 +23,11 @@ export default {
required: true,
type: Object,
},
+ inviteMembersOpen: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
maxValue: Object.keys(ACTION_LABELS).length,
actionSections: Object.keys(ACTION_SECTIONS),
@@ -33,7 +39,15 @@ export default {
return Math.round((this.progressValue / this.$options.maxValue) * 100);
},
},
+ mounted() {
+ if (this.inviteMembersOpen) {
+ this.openInviteMembersModal('celebrate');
+ }
+ },
methods: {
+ openInviteMembersModal(mode) {
+ eventHub.$emit('openModal', { mode, inviteeType: 'members', source: 'learn-gitlab' });
+ },
actionsFor(section) {
const actions = Object.fromEntries(
Object.entries(this.actions).filter(
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index 6da0a8fd212..ea9eec2595f 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue';
@@ -11,15 +12,17 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
+ const { inviteMembersOpen } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
- props: { actions, sections },
+ props: { actions, sections, inviteMembersOpen },
});
},
});
}
+initInviteMembersModal();
initLearnGitlab();
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index bac876317b8..eaf93e2da4f 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -42,7 +42,7 @@ export default {
strings: {
alertTitle: __('You are about to permanently delete this project'),
alertBody: __(
- 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
+ 'After a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
),
isNotForkMessage: __(
'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:',
diff --git a/app/helpers/learn_gitlab_helper.rb b/app/helpers/learn_gitlab_helper.rb
index a7c77bcad67..08a30c4d53b 100644
--- a/app/helpers/learn_gitlab_helper.rb
+++ b/app/helpers/learn_gitlab_helper.rb
@@ -7,6 +7,20 @@ module LearnGitlabHelper
learn_gitlab_onboarding_available?(project)
end
+ def learn_gitlab_data(project)
+ {
+ actions: onboarding_actions_data(project).to_json,
+ sections: onboarding_sections_data.to_json
+ }
+ end
+
+ def learn_gitlab_onboarding_available?(project)
+ OnboardingProgress.onboarding?(project.namespace) &&
+ LearnGitlab::Project.new(current_user).available?
+ end
+
+ private
+
def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys
@@ -42,13 +56,6 @@ module LearnGitlabHelper
}
end
- def learn_gitlab_onboarding_available?(project)
- OnboardingProgress.onboarding?(project.namespace) &&
- LearnGitlab::Project.new(current_user).available?
- end
-
- private
-
def action_urls
LearnGitlab::Onboarding::ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }
.merge(LearnGitlab::Onboarding::ACTION_DOC_URLS)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 5fddc661602..ade19ce02a8 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -12,6 +12,8 @@ class Deployment < ApplicationRecord
StatusUpdateError = Class.new(StandardError)
StatusSyncError = Class.new(StandardError)
+ ARCHIVABLE_OFFSET = 50_000
+
belongs_to :project, required: true
belongs_to :environment, required: true
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
@@ -100,6 +102,10 @@ class Deployment < ApplicationRecord
deployment.run_after_commit do
Deployments::UpdateEnvironmentWorker.perform_async(id)
Deployments::LinkMergeRequestWorker.perform_async(id)
+
+ if ::Feature.enabled?(:deployments_archive, deployment.project, default_enabled: :yaml)
+ Deployments::ArchiveInProjectWorker.perform_async(deployment.project_id)
+ end
end
end
@@ -133,6 +139,14 @@ class Deployment < ApplicationRecord
skipped: 5
}
+ def self.archivables_in(project, limit:)
+ start_iid = project.deployments.order(iid: :desc).limit(1)
+ .select("(iid - #{ARCHIVABLE_OFFSET}) AS start_iid")
+
+ project.deployments.preload(:environment).where('iid <= (?)', start_iid)
+ .where(archived: false).limit(limit)
+ end
+
def self.last_for_environment(environment)
ids = self
.for_environment(environment)
diff --git a/app/services/deployments/archive_in_project_service.rb b/app/services/deployments/archive_in_project_service.rb
new file mode 100644
index 00000000000..a593721f390
--- /dev/null
+++ b/app/services/deployments/archive_in_project_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Deployments
+ # This service archives old deploymets and deletes deployment refs for
+ # keeping the project repository performant.
+ class ArchiveInProjectService < ::BaseService
+ BATCH_SIZE = 100
+
+ def execute
+ unless ::Feature.enabled?(:deployments_archive, project, default_enabled: :yaml)
+ return error('Feature flag is not enabled')
+ end
+
+ deployments = Deployment.archivables_in(project, limit: BATCH_SIZE)
+
+ return success(result: :empty) if deployments.empty?
+
+ ids = deployments.map(&:id)
+ ref_paths = deployments.map(&:ref_path)
+
+ project.repository.delete_refs(*ref_paths)
+ project.deployments.id_in(ids).update_all(archived: true)
+
+ success(result: :archived, count: ids.count)
+ end
+ end
+end
diff --git a/app/views/projects/learn_gitlab/index.html.haml b/app/views/projects/learn_gitlab/index.html.haml
index 4935b72d3fa..9b17be99da0 100644
--- a/app/views/projects/learn_gitlab/index.html.haml
+++ b/app/views/projects/learn_gitlab/index.html.haml
@@ -1,5 +1,12 @@
- breadcrumb_title _("Learn GitLab")
- page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab'
+- data = learn_gitlab_data(@project)
+- invite_members_open = session.delete(:confetti_post_signup)
-#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json, sections: onboarding_sections_data.to_json } }
+- experiment(:confetti_post_signup, actor: current_user) do |e|
+ - e.control do
+ #js-learn-gitlab-app{ data: data }
+ - e.candidate do
+ = render 'projects/invite_members_modal', project: @project
+ #js-learn-gitlab-app{ data: data.merge(invite_members_open: invite_members_open) }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 4fa74195a99..699744b355c 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -723,6 +723,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: deployment:deployments_archive_in_project
+ :worker_name: Deployments::ArchiveInProjectWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 3
+ :idempotent: true
+ :tags: []
- :name: deployment:deployments_drop_older_deployments
:worker_name: Deployments::DropOlderDeploymentsWorker
:feature_category: :continuous_delivery
diff --git a/app/workers/deployments/archive_in_project_worker.rb b/app/workers/deployments/archive_in_project_worker.rb
new file mode 100644
index 00000000000..2de4cacbbd6
--- /dev/null
+++ b/app/workers/deployments/archive_in_project_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Deployments
+ class ArchiveInProjectWorker
+ include ApplicationWorker
+
+ queue_namespace :deployment
+ feature_category :continuous_delivery
+ idempotent!
+ deduplicate :until_executed, including_scheduled: true
+ data_consistency :delayed
+
+ def perform(project_id)
+ Project.find_by_id(project_id).try do |project|
+ Deployments::ArchiveInProjectService.new(project, nil).execute
+ end
+ end
+ end
+end