diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-17 21:14:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-17 21:14:12 +0300 |
commit | 2c06e006d832757e90e5199112ab062b27df7433 (patch) | |
tree | 36811d1041a1a2b88b041ecac07e9d3aa41c6b1f /app | |
parent | a331169e6e84f93fd9b841b56465ac113b6d03f9 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |