diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-17 09:08:52 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-17 09:08:52 +0000 |
commit | 53ae6b7e3f83591ad251a3f771f5bf3b8cf087ba (patch) | |
tree | 5180b96d6a84f36a515cedfa8e81d72de5ccf4fb /app | |
parent | cfe63cce6a90a1c70397c1b9f6d90480f25cae0a (diff) | |
download | gitlab-ce-53ae6b7e3f83591ad251a3f771f5bf3b8cf087ba.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
29 files changed, 324 insertions, 25 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index c86a4c9f178..3856832de90 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -1,4 +1,5 @@ import flash from '~/flash'; +import $ from 'jquery'; import { sprintf, __ } from '../../locale'; // Renders diagrams and flowcharts from text using Mermaid in any element with the @@ -18,7 +19,7 @@ import { sprintf, __ } from '../../locale'; // This is an arbitrary number; Can be iterated upon when suitable. const MAX_CHAR_LIMIT = 5000; -export default function renderMermaid($els) { +function renderMermaids($els) { if (!$els.length) return; // A diagram may have been truncated in search results which will cause errors, so abort the render. @@ -95,3 +96,19 @@ export default function renderMermaid($els) { flash(`Can't load mermaid module: ${err}`); }); } + +export default function renderMermaid($els) { + if (!$els.length) return; + + const visibleMermaids = $els.filter(function filter() { + return $(this).closest('details').length === 0; + }); + + renderMermaids(visibleMermaids); + + $els.closest('details').one('toggle', function toggle() { + if (this.open) { + renderMermaids($(this).find('.js-render-mermaid')); + } + }); +} diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 66cb9fd7672..85636f3e5d2 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -1,6 +1,9 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; +import Vue from 'vue'; +import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; +import ShortcutsToggle from './shortcuts_toggle.vue'; import axios from '../../lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; @@ -15,6 +18,15 @@ Mousetrap.stopCallback = (e, element, combo) => { return defaultStopCallback(e, element, combo); }; +function initToggleButton() { + return new Vue({ + el: document.querySelector('.js-toggle-shortcuts'), + render(createElement) { + return createElement(ShortcutsToggle); + }, + }); +} + export default class Shortcuts { constructor() { this.onToggleHelp = this.onToggleHelp.bind(this); @@ -48,6 +60,14 @@ export default class Shortcuts { $(this).remove(); e.preventDefault(); }); + + $('.js-shortcuts-modal-trigger') + .off('click') + .on('click', this.onToggleHelp); + + if (shouldDisableShortcuts()) { + disableShortcuts(); + } } onToggleHelp(e) { @@ -104,7 +124,8 @@ export default class Shortcuts { } return $('.js-more-help-button').remove(); - }); + }) + .then(initToggleButton); } focusFilter(e) { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js new file mode 100644 index 00000000000..66aa1b752ae --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js @@ -0,0 +1,22 @@ +import Mousetrap from 'mousetrap'; +import 'mousetrap/plugins/pause/mousetrap-pause'; + +const shorcutsDisabledKey = 'shortcutsDisabled'; + +export const shouldDisableShortcuts = () => { + try { + return localStorage.getItem(shorcutsDisabledKey) === 'true'; + } catch (e) { + return false; + } +}; + +export function enableShortcuts() { + localStorage.setItem(shorcutsDisabledKey, false); + Mousetrap.unpause(); +} + +export function disableShortcuts() { + localStorage.setItem(shorcutsDisabledKey, true); + Mousetrap.pause(); +} diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue new file mode 100644 index 00000000000..a53b1b06be9 --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue @@ -0,0 +1,60 @@ +<script> +import { GlToggle, GlSprintf } from '@gitlab/ui'; +import AccessorUtilities from '~/lib/utils/accessor'; +import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; + +export default { + components: { + GlSprintf, + GlToggle, + }, + data() { + return { + localStorageUsable: AccessorUtilities.isLocalStorageAccessSafe(), + shortcutsEnabled: !shouldDisableShortcuts(), + }; + }, + methods: { + onChange(value) { + this.shortcutsEnabled = value; + if (value) { + enableShortcuts(); + } else { + disableShortcuts(); + } + }, + }, +}; +</script> + +<template> + <div v-if="localStorageUsable" class="d-inline-flex align-items-center js-toggle-shortcuts"> + <gl-toggle + v-model="shortcutsEnabled" + aria-describedby="shortcutsToggle" + class="prepend-left-10 mb-0" + label-position="right" + @change="onChange" + > + <template #labelOn> + <gl-sprintf + :message="__('%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled')" + > + <template #screenreaderOnly="{ content }"> + <span class="sr-only">{{ content }}</span> + </template> + </gl-sprintf> + </template> + <template #labelOff> + <gl-sprintf + :message="__('%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Disabled')" + > + <template #screenreaderOnly="{ content }"> + <span class="sr-only">{{ content }}</span> + </template> + </gl-sprintf> + </template> + </gl-toggle> + <div id="shortcutsToggle" class="sr-only">{{ __('Enable or disable keyboard shortcuts') }}</div> + </div> +</template> diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index e9a0b3979e6..19778d07983 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import pdfLab from '../../pdf/index.vue'; +import { GlLoadingIcon } from '@gitlab/ui'; export default () => { const el = document.getElementById('js-pdf-viewer'); @@ -8,6 +9,7 @@ export default () => { el, components: { pdfLab, + GlLoadingIcon, }, data() { return { @@ -32,11 +34,7 @@ export default () => { <div class="text-center loading" v-if="loading && !error"> - <i - class="fa fa-spinner fa-spin" - aria-hidden="true" - aria-label="PDF loading"> - </i> + <gl-loading-icon class="mt-5" size="lg"/> </div> <pdf-lab v-if="!loadError" diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 72565c2ca13..2b6e1f25dc6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -12,7 +12,7 @@ export default class FilteredSearchDropdown { this.filter = filter; this.dropdown = dropdown; this.loadingTemplate = `<div class="filter-dropdown-loading"> - <i class="fa fa-spinner fa-spin"></i> + <span class="spinner"></span> </div>`; this.bindEvents(); } diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index 68c117183a1..e9a81bc9553 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -18,6 +18,7 @@ export const HISTORY_ONLY_FILTER_VALUE = 2; export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; export const DISCUSSION_TAB_LABEL = 'show'; export const NOTE_UNDERSCORE = 'note_'; +export const TIME_DIFFERENCE_VALUE = 10; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/mixins/description_version_history.js b/app/assets/javascripts/notes/mixins/description_version_history.js index 12d80f3faa2..66e6685cfd8 100644 --- a/app/assets/javascripts/notes/mixins/description_version_history.js +++ b/app/assets/javascripts/notes/mixins/description_version_history.js @@ -3,10 +3,12 @@ export default { computed: { canSeeDescriptionVersion() {}, + canDeleteDescriptionVersion() {}, shouldShowDescriptionVersion() {}, descriptionVersionToggleIcon() {}, }, methods: { toggleDescriptionVersion() {}, + deleteDescriptionVersion() {}, }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index f3dc6187c3f..594e3a14d56 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -491,23 +491,66 @@ export const convertToDiscussion = ({ commit }, noteId) => export const removeConvertedDiscussion = ({ commit }, noteId) => commit(types.REMOVE_CONVERTED_DISCUSSION, noteId); -export const fetchDescriptionVersion = (_, { endpoint, startingVersion }) => { +export const setCurrentDiscussionId = ({ commit }, discussionId) => + commit(types.SET_CURRENT_DISCUSSION_ID, discussionId); + +export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersion }) => { let requestUrl = endpoint; if (startingVersion) { requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl); } + dispatch('requestDescriptionVersion'); return axios .get(requestUrl) - .then(res => res.data) - .catch(() => { + .then(res => { + dispatch('receiveDescriptionVersion', res.data); + }) + .catch(error => { + dispatch('receiveDescriptionVersionError', error); Flash(__('Something went wrong while fetching description changes. Please try again.')); }); }; -export const setCurrentDiscussionId = ({ commit }, discussionId) => - commit(types.SET_CURRENT_DISCUSSION_ID, discussionId); +export const requestDescriptionVersion = ({ commit }) => { + commit(types.REQUEST_DESCRIPTION_VERSION); +}; +export const receiveDescriptionVersion = ({ commit }, descriptionVersion) => { + commit(types.RECEIVE_DESCRIPTION_VERSION, descriptionVersion); +}; +export const receiveDescriptionVersionError = ({ commit }, error) => { + commit(types.RECEIVE_DESCRIPTION_VERSION_ERROR, error); +}; + +export const softDeleteDescriptionVersion = ({ dispatch }, { endpoint, startingVersion }) => { + let requestUrl = endpoint; + + if (startingVersion) { + requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl); + } + dispatch('requestDeleteDescriptionVersion'); + + return axios + .delete(requestUrl) + .then(() => { + dispatch('receiveDeleteDescriptionVersion'); + }) + .catch(error => { + dispatch('receiveDeleteDescriptionVersionError', error); + Flash(__('Something went wrong while deleting description changes. Please try again.')); + }); +}; + +export const requestDeleteDescriptionVersion = ({ commit }) => { + commit(types.REQUEST_DELETE_DESCRIPTION_VERSION); +}; +export const receiveDeleteDescriptionVersion = ({ commit }) => { + commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION, __('Deleted')); +}; +export const receiveDeleteDescriptionVersionError = ({ commit }, error) => { + commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error); +}; // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js index 3cdcc7a05b8..d94fc626a3f 100644 --- a/app/assets/javascripts/notes/stores/collapse_utils.js +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -1,4 +1,4 @@ -import { DESCRIPTION_TYPE } from '../constants'; +import { DESCRIPTION_TYPE, TIME_DIFFERENCE_VALUE } from '../constants'; /** * Checks the time difference between two notes from their 'created_at' dates @@ -45,7 +45,11 @@ export const collapseSystemNotes = notes => { const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note); // are they less than 10 minutes apart from the same user? - if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) { + if ( + timeDifferenceMinutes > TIME_DIFFERENCE_VALUE || + note.author.id !== lastDescriptionSystemNote.author.id || + lastDescriptionSystemNote.description_version_deleted + ) { // update the previous system note lastDescriptionSystemNote = note; lastDescriptionSystemNoteIndex = acc.length; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 771b80108b8..0e991f2f4f0 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -14,6 +14,7 @@ export default () => ({ isToggleStateButtonLoading: false, isNotesFetched: false, isLoading: true, + isLoadingDescriptionVersion: false, // holds endpoints and permissions provided through haml notesData: { @@ -27,6 +28,7 @@ export default () => ({ commentsDisabled: false, resolvableDiscussionsCount: 0, unresolvedDiscussionsCount: 0, + descriptionVersion: null, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 8eb426d3f9b..6554aee0d5b 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -31,3 +31,11 @@ export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; + +// Description version +export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION'; +export const RECEIVE_DESCRIPTION_VERSION = 'RECEIVE_DESCRIPTION_VERSION'; +export const RECEIVE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DESCRIPTION_VERSION_ERROR'; +export const REQUEST_DELETE_DESCRIPTION_VERSION = 'REQUEST_DELETE_DESCRIPTION_VERSION'; +export const RECEIVE_DELETE_DESCRIPTION_VERSION = 'RECEIVE_DELETE_DESCRIPTION_VERSION'; +export const RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR = 'RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 71091d26b85..d32a88e4c71 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -284,4 +284,25 @@ export default { [types.SET_CURRENT_DISCUSSION_ID](state, discussionId) { state.currentDiscussionId = discussionId; }, + + [types.REQUEST_DESCRIPTION_VERSION](state) { + state.isLoadingDescriptionVersion = true; + }, + [types.RECEIVE_DESCRIPTION_VERSION](state, descriptionVersion) { + state.isLoadingDescriptionVersion = false; + state.descriptionVersion = descriptionVersion; + }, + [types.RECEIVE_DESCRIPTION_VERSION_ERROR](state) { + state.isLoadingDescriptionVersion = false; + }, + [types.REQUEST_DELETE_DESCRIPTION_VERSION](state) { + state.isLoadingDescriptionVersion = true; + }, + [types.RECEIVE_DELETE_DESCRIPTION_VERSION](state, descriptionVersion) { + state.isLoadingDescriptionVersion = false; + state.descriptionVersion = descriptionVersion; + }, + [types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) { + state.isLoadingDescriptionVersion = false; + }, }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 7e14b810c13..d9192d3d76b 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -132,7 +132,7 @@ export default () => { }); axios - .get(dataset.testReportEndpoint) + .get(dataset.testReportsCountEndpoint) .then(({ data }) => { document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count; }) diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 15ca64ba297..0c4d75fb0ad 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -17,11 +17,12 @@ * /> */ import $ from 'jquery'; -import { mapGetters, mapActions } from 'vuex'; -import { GlSkeletonLoading } from '@gitlab/ui'; +import { mapGetters, mapActions, mapState } from 'vuex'; +import { GlButton, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui'; import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history'; import noteHeader from '~/notes/components/note_header.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import TimelineEntryItem from './timeline_entry_item.vue'; import { spriteIcon } from '../../../lib/utils/common_utils'; import initMRPopovers from '~/mr_popover/'; @@ -34,9 +35,13 @@ export default { Icon, noteHeader, TimelineEntryItem, + GlButton, GlSkeletonLoading, }, - mixins: [descriptionVersionHistoryMixin], + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()], props: { note: { type: Object, @@ -50,6 +55,7 @@ export default { }, computed: { ...mapGetters(['targetNoteHash']), + ...mapState(['descriptionVersion', 'isLoadingDescriptionVersion']), noteAnchorId() { return `note_${this.note.id}`; }, @@ -80,7 +86,7 @@ export default { initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request')); }, methods: { - ...mapActions(['fetchDescriptionVersion']), + ...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']), }, }; </script> @@ -122,6 +128,16 @@ export default { <gl-skeleton-loading /> </pre> <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre> + <gl-button + v-if="canDeleteDescriptionVersion" + ref="deleteDescriptionVersionButton" + v-gl-tooltip + :title="__('Remove description history')" + class="btn-transparent delete-description-history" + @click="deleteDescriptionVersion" + > + <icon name="remove" /> + </gl-button> </div> </div> </div> diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 1da9f691639..1a06ae1ed41 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -311,13 +311,18 @@ $note-form-margin-left: 72px; overflow: hidden; .description-version { + position: relative; + + .btn.delete-description-history { + position: absolute; + top: 18px; + right: 0; + } + pre { max-height: $dropdown-max-height-lg; white-space: pre-wrap; - - &.loading-state { - height: 94px; - } + padding-right: 30px; } } diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb index dcf23d112d6..caebff91022 100644 --- a/app/models/commit_status_enums.rb +++ b/app/models/commit_status_enums.rb @@ -18,6 +18,7 @@ module CommitStatusEnums unmet_prerequisites: 10, scheduler_failure: 11, data_integrity_failure: 12, + forward_deployment_failure: 13, insufficient_bridge_permissions: 1_001, downstream_bridge_project_not_found: 1_002, invalid_bridge_trigger: 1_003, diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 68f50b13a07..fe42fb93633 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -41,6 +41,9 @@ class Deployment < ApplicationRecord scope :visible, -> { where(status: %i[running success failed canceled]) } scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } + scope :active, -> { where(status: %i[created running]) } + scope :older_than, -> (deployment) { where('id < ?', deployment.id) } + scope :with_deployable, -> { includes(:deployable).where('deployable_id IS NOT NULL') } state_machine :status, initial: :created do event :run do @@ -74,6 +77,14 @@ class Deployment < ApplicationRecord Deployments::FinishedWorker.perform_async(id) end end + + after_transition any => :running do |deployment| + next unless deployment.project.forward_deployment_enabled? + + deployment.run_after_commit do + Deployments::ForwardDeploymentWorker.perform_async(id) + end + end end enum status: { diff --git a/app/models/environment.rb b/app/models/environment.rb index 4635b05fcc7..bb41c4a066e 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -12,6 +12,7 @@ class Environment < ApplicationRecord has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :successful_deployments, -> { success }, class_name: 'Deployment' + has_many :active_deployments, -> { active }, class_name: 'Deployment' has_many :prometheus_alerts, inverse_of: :environment has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' diff --git a/app/models/member.rb b/app/models/member.rb index 57924161b63..a26a0615a6e 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -75,6 +75,7 @@ class Member < ApplicationRecord scope :reporters, -> { active.where(access_level: REPORTER) } scope :developers, -> { active.where(access_level: DEVELOPER) } scope :maintainers, -> { active.where(access_level: MAINTAINER) } + scope :non_guests, -> { where('members.access_level > ?', GUEST) } scope :masters, -> { maintainers } # @deprecated scope :owners, -> { active.where(access_level: OWNER) } scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } diff --git a/app/models/project.rb b/app/models/project.rb index 1e27ce9f344..e16bd568153 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -343,6 +343,7 @@ class Project < ApplicationRecord delegate :last_pipeline, to: :commit, allow_nil: true delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci + delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings # Validations validates :creator, presence: true, on: :create diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index a495d34c07c..b26a3025b61 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -18,6 +18,8 @@ class ProjectCiCdSetting < ApplicationRecord }, allow_nil: true + default_value_for :forward_deployment_enabled, true + def self.available? @available ||= ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION @@ -28,6 +30,10 @@ class ProjectCiCdSetting < ApplicationRecord super end + def forward_deployment_enabled? + super && ::Feature.enabled?(:forward_deployment_enabled, project) + end + private def set_default_git_depth diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index ed76f95ac62..258852c77c6 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -14,6 +14,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated unmet_prerequisites: 'The job failed to complete prerequisite tasks', scheduler_failure: 'The scheduler failed to assign job to the runner, please try again or contact system administrator', data_integrity_failure: 'There has been a structural integrity problem detected, please contact system administrator', + forward_deployment_failure: 'The deployment job is older than the previously succeeded deployment job, and therefore cannot be run', invalid_bridge_trigger: 'This job could not be executed because downstream pipeline trigger definition is invalid', downstream_bridge_project_not_found: 'This job could not be executed because downstream bridge project could not be found', insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline', diff --git a/app/services/deployments/older_deployments_drop_service.rb b/app/services/deployments/older_deployments_drop_service.rb new file mode 100644 index 00000000000..122f8ac89ed --- /dev/null +++ b/app/services/deployments/older_deployments_drop_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Deployments + class OlderDeploymentsDropService + attr_reader :deployment + + def initialize(deployment_id) + @deployment = Deployment.find_by_id(deployment_id) + end + + def execute + return unless @deployment&.running? + + older_deployments.find_each do |older_deployment| + older_deployment.deployable&.drop!(:forward_deployment_failure) + rescue => e + Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, deployment_id: older_deployment.id) + end + end + + private + + def older_deployments + @deployment + .environment + .active_deployments + .older_than(@deployment) + .with_deployable + end + end +end diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 5f8f2333e40..4b9304cfdb9 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -6,6 +6,7 @@ = _('Keyboard Shortcuts') %small = link_to _('(Show all)'), '#', class: 'js-more-help-button' + .js-toggle-shortcuts %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %span{ "aria-hidden": true } × .modal-body diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml index 93854c212df..a003d6f8903 100644 --- a/app/views/layouts/header/_help_dropdown.html.haml +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -4,6 +4,10 @@ = link_to _("Help"), help_path %li = link_to _("Support"), support_url + %li + %button.js-shortcuts-modal-trigger{ type: "button" } + = _("Keyboard shortcuts") + %span.text-secondary.float-right{ "aria-hidden": true }= '?'.html_safe = render_if_exists "shared/learn_gitlab_menu_item" %li.divider %li diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml index f0b3ab24ea0..f39968eecef 100644 --- a/app/views/projects/pipelines/show.html.haml +++ b/app/views/projects/pipelines/show.html.haml @@ -21,4 +21,5 @@ = render "projects/pipelines/with_tabs", pipeline: @pipeline .js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json), - test_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json) } } + test_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json), + test_reports_count_endpoint: test_reports_count_project_pipeline_path(@project, @pipeline, format: :json) } } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 5ff1a331b09..f6daab73689 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -225,6 +225,12 @@ :latency_sensitive: :resource_boundary: :cpu :weight: 3 +- :name: deployment:deployments_forward_deployment + :feature_category: :continuous_delivery + :has_external_dependencies: + :latency_sensitive: + :resource_boundary: :unknown + :weight: 3 - :name: deployment:deployments_success :feature_category: :continuous_delivery :has_external_dependencies: diff --git a/app/workers/deployments/forward_deployment_worker.rb b/app/workers/deployments/forward_deployment_worker.rb new file mode 100644 index 00000000000..a25b8ca0478 --- /dev/null +++ b/app/workers/deployments/forward_deployment_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Deployments + class ForwardDeploymentWorker + include ApplicationWorker + + queue_namespace :deployment + feature_category :continuous_delivery + + def perform(deployment_id) + Deployments::OlderDeploymentsDropService.new(deployment_id).execute + end + end +end |