Commit 53ae6b7e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent cfe63cce
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'));
}
});
}
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) {
......
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();
}
<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>
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"
......
......@@ -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();
}
......
......@@ -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,
......
......@@ -3,10 +3,12 @@
export default {
computed: {
canSeeDescriptionVersion() {},
canDeleteDescriptionVersion() {},
shouldShowDescriptionVersion() {},
descriptionVersionToggleIcon() {},
},
methods: {
toggleDescriptionVersion() {},
deleteDescriptionVersion() {},
},
};
......@@ -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 () => {};
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;
......
......@@ -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,
......
......@@ -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';
......@@ -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;
},
};
......@@ -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;
})
......
......@@ -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>
......
......@@ -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;
}
}
......
......@@ -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,
......
......@@ -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: {
......
......@@ -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'
......
......@@ -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]) }
......
......@@ -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
......
......@@ -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
......
......@@ -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',
......
# 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
......@@ -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 } &times;
.modal-body
......
......@@ -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
......
......@@ -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) } }
......@@ -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:
......
# 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
---
title: Allow keyboard shortcuts to be disabled
merge_request: 18782
author:
type: added
---
title: Allow to deploy only forward deployments
merge_request: 22959
author:
type: changed
---
title: Correctly render mermaid digrams inside details blocks
merge_request: 23662
author:
type: fixed
# frozen_string_literal: true
class AddRestrictDeploymentOrderToProjectCiCdSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :project_ci_cd_settings, :forward_deployment_enabled, :boolean
end
end
......@@ -4,32 +4,11 @@ class ScheduleLinkLfsObjects < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'LinkLfsObjects'
BATCH_SIZE = 1_000
disable_ddl_transaction!
class Project < ActiveRecord::Base
include EachBatch
self.table_name = 'projects'
end
def up
fork_network_members =
Gitlab::BackgroundMigration::LinkLfsObjects::ForkNetworkMember
.select(1)
.with_non_existing_lfs_objects
.where('fork_network_members.project_id = projects.id')
forks = Project.where('EXISTS (?)', fork_network_members)
queue_background_migration_jobs_by_range_at_intervals(
forks,
MIGRATION,
BackgroundMigrationWorker.minimum_interval,
batch_size: BATCH_SIZE
)
# no-op as background migration being schedule times out in some instances
end
def down
......
......@@ -3166,6 +3166,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_204737) do
t.boolean "group_runners_enabled", default: true, null: false
t.boolean "merge_pipelines_enabled"
t.integer "default_git_depth"
t.boolean "forward_deployment_enabled"
t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true
end
......
......@@ -6,7 +6,10 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html'
# GitLab keyboard shortcuts
GitLab has many useful keyboard shortcuts to make it easier to access different features.
You can see the quick reference sheet within GitLab itself with <kbd>Shift</kbd> + <kbd>?</kbd>.
You can see a modal listing keyboard shortcuts within GitLab itself by pressing <kbd>?</kbd>,
or clicking **Keyboard shortcuts** in the Help menu at the top right.
From [GitLab 12.8 onwards](https://gitlab.com/gitlab-org/gitlab/issues/22113),
keyboard shortcuts can be disabled using the **Enable**/**Disable** toggle in this modal window.
The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must
be in specific pages for the other shortcuts to be available, as explained in each
......
......@@ -24,24 +24,7 @@ module Gitlab
end
def perform(start_id, end_id)
select_query =
ForkNetworkMember
.select('lop.lfs_object_id, fork_network_members.project_id')
.with_non_existing_lfs_objects
.where(project_id: start_id..end_id)
return if select_query.empty?
execute <<-SQL
INSERT INTO lfs_objects_projects (lfs_object_id, project_id)
#{select_query.to_sql}
SQL
end
private
def execute(sql)
::ActiveRecord::Base.connection.execute(sql)
# no-op as some queries times out
end
end
end
......
......@@ -19,6 +19,7 @@ module Gitlab
unmet_prerequisites: 'unmet prerequisites',
scheduler_failure: 'scheduler failure',
data_integrity_failure: 'data integrity failure',
forward_deployment_failure: 'forward deployment failure',
invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
downstream_bridge_project_not_found: 'downstream project could not be found',
insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
......
......@@ -63,7 +63,7 @@ module Quality
'get',
RESOURCE_LIST,
%(--namespace "#{namespace}"),
'-o custom-columns=NAME:.metadata.name'
'-o name'
]
run_command(command).lines.map(&:strip)
end
......
......@@ -375,6 +375,12 @@ msgid_plural "%{releases} releases"
msgstr[0] ""
msgstr[1] ""
msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Disabled"
msgstr ""
msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled"
msgstr ""
msgid "%{service_title} activated."
msgstr ""
......@@ -7155,6 +7161,9 @@ msgstr ""
msgid "Enable mirror configuration"
msgstr ""
msgid "Enable or disable keyboard shortcuts"
msgstr ""
msgid "Enable or disable the Pseudonymizer data collection."
msgstr ""
......@@ -10945,6 +10954,9 @@ msgstr ""
msgid "Keyboard Shortcuts"
msgstr ""
msgid "Keyboard shortcuts"
msgstr ""
msgid "Kubernetes"
msgstr ""
......@@ -15857,6 +15869,9 @@ msgstr ""
msgid "Remove child epic from an epic"
msgstr ""
msgid "Remove description history"
msgstr ""
msgid "Remove due date"
msgstr ""
......@@ -17780,6 +17795,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while deleting description changes. Please try again."
msgstr ""
msgid "Something went wrong while deleting the image."
msgstr ""
......
......@@ -37,6 +37,7 @@ FactoryBot.define do
group_runners_enabled { nil }
import_status { nil }
import_jid { nil }
forward_deployment_enabled { nil }
end
after(:create) do |project, evaluator|
......
......@@ -68,4 +68,34 @@ describe 'Mermaid rendering', :js do
expect(page).to have_selector('pre.mermaid')
end
end
it 'correctly sizes mermaid diagram inside <details> block', :js do
description = <<~MERMAID
<details>
<summary>Click to show diagram</summary>
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
</details>
MERMAID
project = create(:project, :public)
issue = create(:issue, project: project, description: description)
visit project_issue_path(project, issue)
page.within('.description') do
page.find('summary').click
svg = page.find('svg.mermaid')
expect(svg[:width].to_i).to be_within(5).of(120)
expect(svg[:height].to_i).to be_within(5).of(220)
end
end
end
......@@ -17,6 +17,59 @@ describe 'User uses shortcuts', :js do
wait_for_requests
end
context 'disabling shortcuts' do
before do
page.evaluate_script("localStorage.removeItem('shortcutsDisabled')")
end
it 'can disable shortcuts from help menu' do
open_modal_shortcut_keys
click_toggle_button
close_modal
open_modal_shortcut_keys
# modal-shortcuts still in the DOM, but hidden
expect(find('#modal-shortcuts', visible: false)).not_to be_visible
page.refresh
open_modal_shortcut_keys
# after reload, shortcuts modal doesn't exist at all until we add it
expect(page).not_to have_selector('#modal-shortcuts')
end
it 're-enables shortcuts' do
open_modal_shortcut_keys
click_toggle_button
close_modal
open_modal_from_help_menu
click_toggle_button
close_modal
open_modal_shortcut_keys
expect(find('#modal-shortcuts')).to be_visible
end
def open_modal_shortcut_keys
find('body').native.send_key('?')
end
def open_modal_from_help_menu
find('.header-help-dropdown-toggle').click
find('button', text: 'Keyboard shortcuts').click
end
def click_toggle_button
find('.js-toggle-shortcuts .gl-toggle').click
end
def close_modal
find('.modal button[aria-label="Close"]').click
end
end
context 'when navigating to the Project pages' do
it 'redirects to the details page' do
visit project_issues_path(project)
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::LinkLfsObjects, :migration, schema: 2020_02_10_062432 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:fork_networks) { table(:fork_networks) }
let(:fork_network_members) { table(:fork_network_members) }
let(:lfs_objects) { table(:lfs_objects) }
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') }
let(:source_project) { projects.create(namespace_id: namespace.id) }
let(:another_source_project) { projects.create(namespace_id: namespace.id) }
let(:project) { projects.create(namespace_id: namespace.id) }
let(:another_project) { projects.create(namespace_id: namespace.id) }
let(:other_project) { projects.create(namespace_id: namespace.id) }
let(:linked_project) { projects.create(namespace_id: namespace.id) }
let(:fork_network) { fork_networks.create(root_project_id: source_project.id) }
let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) }
let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) }
let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) }
before do
# Create links between projects
fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
[project, another_project, linked_project].each do |p|
fork_network_members.create(
fork_network_id: fork_network.id,
project_id: p.id,
forked_from_project_id: fork_network.root_project_id
)
end
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: other_project.id, forked_from_project_id: another_fork_network.root_project_id)
# Links LFS objects to some projects
[source_project, another_source_project, linked_project].each do |p|
lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
it 'creates LfsObjectsProject records for forks within the specified range of project IDs' do
expect { subject.perform(project.id, other_project.id) }.to change { lfs_objects_projects.count }.by(6)
expect(lfs_object_ids_for(project)).to match_array(lfs_object_ids_for(source_project))
expect(lfs_object_ids_for(another_project)).to match_array(lfs_object_ids_for(source_project))
expect(lfs_object_ids_for(other_project)).to match_array(lfs_object_ids_for(another_source_project))
expect { subject.perform(project.id, other_project.id) }.not_to change { lfs_objects_projects.count }
end
context 'when it is not necessary to create LfsObjectProject records' do
it 'does not create LfsObjectProject records' do
expect { subject.perform(linked_project.id, linked_project.id) }
.not_to change { lfs_objects_projects.count }
end
end
def lfs_object_ids_for(project)
lfs_objects_projects.where(project_id: project.id).pluck(:lfs_object_id)
end
end
......@@ -102,7 +102,7 @@ RSpec.describe Quality::KubernetesClient do
it 'calls kubectl to retrieve the resource names' do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with(["kubectl get #{described_class::RESOURCE_LIST} " +
%(--namespace "#{namespace}" -o custom-columns=NAME:.metadata.name)])
%(--namespace "#{namespace}" -o name)])
.and_return(Gitlab::Popen::Result.new([], raw_resource_names_str, '', double(success?: true)))
expect(subject.__send__(:raw_resource_names)).to eq(raw_resource_names)
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200210062432_schedule_link_lfs_objects.rb')
describe ScheduleLinkLfsObjects, :migration, :sidekiq do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:fork_networks) { table(:fork_networks) }
let(:fork_network_members) { table(:fork_network_members) }
let(:lfs_objects) { table(:lfs_objects) }
let(:lfs_objects_projects) { table(:lfs_objects_projects) }
let(:namespace) { namespaces.create(name: 'GitLab', path: 'gitlab') }
let(:fork_network) { fork_networks.create(root_project_id: source_project.id) }
let(:another_fork_network) { fork_networks.create(root_project_id: another_source_project.id) }
let(:source_project) { projects.create(namespace_id: namespace.id) }
let(:another_source_project) { projects.create(namespace_id: namespace.id) }
let(:project) { projects.create(namespace_id: namespace.id) }
let(:another_project) { projects.create(namespace_id: namespace.id) }
let(:other_project) { projects.create(namespace_id: namespace.id) }
let(:linked_project) { projects.create(namespace_id: namespace.id) }
let(:lfs_object) { lfs_objects.create(oid: 'abc123', size: 100) }
let(:another_lfs_object) { lfs_objects.create(oid: 'def456', size: 200) }
before do
# Create links between projects
fork_network_members.create(fork_network_id: fork_network.id, project_id: source_project.id, forked_from_project_id: nil)
[project, another_project, linked_project].each do |p|
fork_network_members.create(
fork_network_id: fork_network.id,
project_id: p.id,
forked_from_project_id: fork_network.root_project_id
)
end
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: another_source_project.id, forked_from_project_id: nil)
fork_network_members.create(fork_network_id: another_fork_network.id, project_id: other_project.id, forked_from_project_id: another_fork_network.root_project_id)
end
context 'when there are forks to be backfilled' do
before do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
# Links LFS objects to some projects
[source_project, another_source_project, linked_project].each do |p|
lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
it 'schedules background migration to link LFS objects' do
Sidekiq::Testing.fake! do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(2.minutes, project.id, another_project.id)
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(4.minutes, other_project.id, other_project.id)
end
end
end
context 'when there are no forks to be backfilled' do
before do
# Links LFS objects to all projects
projects.all.each do |p|
lfs_objects_projects.create(lfs_object_id: lfs_object.id, project_id: p.id)
lfs_objects_projects.create(lfs_object_id: another_lfs_object.id, project_id: p.id)
end
end
it 'does not schedule any job' do
Sidekiq::Testing.fake! do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(0)
end
end
end
end
......@@ -281,6 +281,45 @@ describe Deployment do
expect(last_deployments).to match_array(deployments.last(2))
end
end
describe 'active' do
subject { described_class.active }
it 'retrieves the active deployments' do
deployment1 = create(:deployment, status: :created )
deployment2 = create(:deployment, status: :running )
create(:deployment, status: :failed )
create(:deployment, status: :canceled )
is_expected.to contain_exactly(deployment1, deployment2)
end
end
describe 'older_than' do
let(:deployment) { create(:deployment) }
subject { described_class.older_than(deployment) }
it 'retrives the correct older deployments' do
older_deployment1 = create(:deployment)
older_deployment2 = create(:deployment)
deployment
create(:deployment)
is_expected.to contain_exactly(older_deployment1, older_deployment2)
end
end
describe 'with_deployable' do
subject { described_class.with_deployable }
it 'retrieves deployments with deployable builds' do
with_deployable = create(:deployment)
create(:deployment, deployable: nil)
is_expected.to contain_exactly(with_deployable)
end
end
end
describe '#includes_commit?' do
......
......@@ -32,6 +32,12 @@ describe ProjectCiCdSetting do
end
end
describe '#forward_deployment_enabled' do
it 'is true by default' do
expect(described_class.new.forward_deployment_enabled).to be_truthy
end
end
describe '#default_git_depth' do
let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
......
# frozen_string_literal: true
require 'spec_helper'
describe Deployments::OlderDeploymentsDropService do
let(:environment) { create(:environment) }
let(:deployment) { create(:deployment, environment: environment) }
let(:service) { described_class.new(deployment) }
describe '#execute' do
subject { service.execute }
shared_examples 'it does not drop any build' do
it do
expect { subject }.to not_change(Ci::Build.failed, :count)
end
end
context 'when deployment is nil' do
let(:deployment) { nil }
it_behaves_like 'it does not drop any build'
end
context 'when a deployment is passed in' do
context 'and there is no active deployment for the related environment' do
let(:deployment) { create(:deployment, :canceled, environment: environment) }
let(:deployment2) { create(:deployment, :canceled, environment: environment) }
before do
deployment
deployment2
end
it_behaves_like 'it does not drop any build'
end
context 'and there are active deployment for the related environment' do
let(:deployment) { create(:deployment, :running, environment: environment) }
let(:deployment2) { create(:deployment, :running, environment: environment) }
context 'and there is no older deployment than "deployment"' do
before do
deployment
deployment2
end
it_behaves_like 'it does not drop any build'
end
context 'and there is an older deployment than "deployment"' do
let(:older_deployment) { create(:deployment, :running, environment: environment) }
before do
older_deployment
deployment
deployment2
end
it 'drops that older deployment' do
deployable = older_deployment.deployable
expect(deployable.failed?).to be_falsey
subject
expect(deployable.reload.failed?).to be_truthy
end
context 'and there is no deployable for that older deployment' do
let(:older_deployment) { create(:deployment, :running, environment: environment, deployable: nil) }
it_behaves_like 'it does not drop any build'
end
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment