summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js75
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue525
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue50
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue6
-rw-r--r--app/assets/javascripts/pages/projects/security/configuration/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/serverless/index.js6
-rw-r--r--app/assets/javascripts/registry/explorer/constants/list.js10
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql19
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue52
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue23
-rw-r--r--app/assets/javascripts/security_configuration/components/configuration_table.vue97
-rw-r--r--app/assets/javascripts/security_configuration/components/features_constants.js112
-rw-r--r--app/assets/javascripts/security_configuration/components/manage_sast.vue57
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade.vue26
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js29
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue66
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js6
-rw-r--r--app/controllers/concerns/notes_actions.rb3
-rw-r--r--app/controllers/concerns/redis_tracking.rb16
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/wiki_actions.rb3
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/graphql/queries/container_registry/get_container_repositories.query.graphql19
-rw-r--r--app/graphql/types/user_type.rb3
-rw-r--r--app/models/ci/pipeline.rb1
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/member.rb19
-rw-r--r--app/models/project_services/prometheus_service.rb12
-rw-r--r--app/models/project_statistics.rb1
-rw-r--r--app/policies/project_policy.rb5
-rw-r--r--app/services/ci/abort_project_pipelines_service.rb25
-rw-r--r--app/services/ci/cancel_user_pipelines_service.rb1
-rw-r--r--app/services/design_management/save_designs_service.rb1
-rw-r--r--app/services/members/create_service.rb13
-rw-r--r--app/services/projects/destroy_service.rb3
-rw-r--r--app/views/groups/registry/repositories/index.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml353
-rw-r--r--app/views/help/shortcuts.js.haml3
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/projects/security/configuration/show.html.haml2
48 files changed, 1153 insertions, 523 deletions
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 6cdf083378b..d586c0c8dd0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -3,12 +3,11 @@ import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
import { flatten } from 'lodash';
-import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
-import axios from '../../lib/utils/axios_utils';
-import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
-import findAndFollowLink from '../../lib/utils/navigation_utility';
+import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility';
+import findAndFollowLink from '~/lib/utils/navigation_utility';
+import { parseBoolean } from '~/lib/utils/common_utils';
+
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
-import ShortcutsToggle from './shortcuts_toggle.vue';
import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
@@ -20,15 +19,6 @@ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo
return defaultStopCallback.call(this, e, element, combo);
};
-function initToggleButton() {
- return new Vue({
- el: document.querySelector('.js-toggle-shortcuts'),
- render(createElement) {
- return createElement(ShortcutsToggle);
- },
- });
-}
-
/**
* The key used to save and fetch the local Mousetrap instance
* attached to a `<textarea>` element using `jQuery.data`
@@ -65,7 +55,8 @@ function getToolbarBtnToShortcutsMap($textarea) {
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
- this.enabledHelp = [];
+ this.helpModalElement = null;
+ this.helpModalVueInstance = null;
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
@@ -107,11 +98,33 @@ export default class Shortcuts {
}
onToggleHelp(e) {
- if (e.preventDefault) {
+ if (e?.preventDefault) {
e.preventDefault();
}
- Shortcuts.toggleHelp(this.enabledHelp);
+ if (this.helpModalElement && this.helpModalVueInstance) {
+ this.helpModalVueInstance.$destroy();
+ this.helpModalElement.remove();
+ this.helpModalElement = null;
+ this.helpModalVueInstance = null;
+ } else {
+ this.helpModalElement = document.createElement('div');
+ document.body.append(this.helpModalElement);
+
+ this.helpModalVueInstance = new Vue({
+ el: this.helpModalElement,
+ components: {
+ ShortcutsHelp: () => import('./shortcuts_help.vue'),
+ },
+ render: (createElement) => {
+ return createElement('shortcuts-help', {
+ on: {
+ hidden: this.onToggleHelp,
+ },
+ });
+ },
+ });
+ }
}
static onTogglePerfBar(e) {
@@ -144,34 +157,6 @@ export default class Shortcuts {
$(document).triggerHandler('markdown-preview:toggle', [e]);
}
- static toggleHelp(location) {
- const $modal = $('#modal-shortcuts');
-
- if ($modal.length) {
- $modal.modal('toggle');
- return null;
- }
-
- return axios
- .get(gon.shortcuts_path, {
- responseType: 'text',
- })
- .then(({ data }) => {
- $.globalEval(data, { nonce: getCspNonceValue() });
-
- if (location && location.length > 0) {
- const results = [];
- for (let i = 0, len = location.length; i < len; i += 1) {
- results.push($(location[i]).show());
- }
- return results;
- }
-
- return $('.js-more-help-button').remove();
- })
- .then(initToggleButton);
- }
-
focusFilter(e) {
if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls');
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
new file mode 100644
index 00000000000..1277dd0ed37
--- /dev/null
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -0,0 +1,525 @@
+<script>
+/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlIcon, GlModal } from '@gitlab/ui';
+import ShortcutsToggle from './shortcuts_toggle.vue';
+
+export default {
+ components: {
+ GlIcon,
+ GlModal,
+ ShortcutsToggle,
+ },
+ computed: {
+ ctrlCharacter() {
+ return window.gl.client.isMac ? '⌘' : 'ctrl';
+ },
+ onDotCom() {
+ return window.gon.dot_com;
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ modal-id="keyboard-shortcut-modal"
+ size="lg"
+ data-testid="modal-shortcuts"
+ :visible="true"
+ :hide-footer="true"
+ @hidden="$emit('hidden')"
+ >
+ <template #modal-title>
+ <shortcuts-toggle />
+ </template>
+ <div class="row">
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Global Shortcuts') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>?</kbd>
+ </td>
+ <td>{{ __('Toggle this dialog') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift p</kbd>
+ </td>
+ <td>{{ __('Go to your projects') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift g</kbd>
+ </td>
+ <td>{{ __('Go to your groups') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift a</kbd>
+ </td>
+ <td>{{ __('Go to the activity feed') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift l</kbd>
+ </td>
+ <td>{{ __('Go to the milestone list') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift s</kbd>
+ </td>
+ <td>{{ __('Go to your snippets') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>s</kbd>
+ /
+ <kbd>/</kbd>
+ </td>
+ <td>{{ __('Start search') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift i</kbd>
+ </td>
+ <td>{{ __('Go to your issues') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift m</kbd>
+ </td>
+ <td>{{ __('Go to your merge requests') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift t</kbd>
+ </td>
+ <td>{{ __('Go to your To-Do list') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>p</kbd>
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Toggle the Performance Bar') }}</td>
+ </tr>
+ <tr v-if="onDotCom">
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>x</kbd>
+ </td>
+ <td>{{ __('Toggle GitLab Next') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Editing') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} shift p</kbd>
+ </td>
+ <td>{{ __('Toggle Markdown preview') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ </td>
+ <td>
+ {{ __('Edit your most recent comment in a thread (from an empty textarea)') }}
+ </td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Wiki') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Edit wiki page') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Repository Graph') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-left" />
+ </kbd>
+ /
+ <kbd>h</kbd>
+ </td>
+ <td>{{ __('Scroll left') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-right" />
+ </kbd>
+ /
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Scroll right') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ /
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Scroll up') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-down" />
+ </kbd>
+ /
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Scroll down') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ shift
+ <gl-icon name="arrow-up" />
+ / k
+ </kbd>
+ </td>
+ <td>{{ __('Scroll to top') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ shift
+ <gl-icon name="arrow-down" />
+ / j
+ </kbd>
+ </td>
+ <td>{{ __('Scroll to bottom') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Project') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>p</kbd>
+ </td>
+ <td>{{ __("Go to the project's overview page") }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>v</kbd>
+ </td>
+ <td>{{ __("Go to the project's activity feed") }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>r</kbd>
+ </td>
+ <td>{{ __('Go to releases') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>f</kbd>
+ </td>
+ <td>{{ __('Go to files') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>t</kbd>
+ </td>
+ <td>{{ __('Go to find file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>c</kbd>
+ </td>
+ <td>{{ __('Go to commits') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>n</kbd>
+ </td>
+ <td>{{ __('Go to repository graph') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>d</kbd>
+ </td>
+ <td>{{ __('Go to repository charts') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>i</kbd>
+ </td>
+ <td>{{ __('Go to issues') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>i</kbd>
+ </td>
+ <td>{{ __('New issue') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Go to issue boards') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>m</kbd>
+ </td>
+ <td>{{ __('Go to merge requests') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Go to jobs') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Go to metrics') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Go to environments') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Go to kubernetes') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>s</kbd>
+ </td>
+ <td>{{ __('Go to snippets') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>w</kbd>
+ </td>
+ <td>{{ __('Go to wiki') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Project Files') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ </td>
+ <td>{{ __('Move selection up') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-down" />
+ </kbd>
+ </td>
+ <td>{{ __('Move selection down') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>enter</kbd>
+ </td>
+ <td>{{ __('Open Selection') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>esc</kbd>
+ </td>
+ <td>{{ __('Go back (while searching for files)') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>y</kbd>
+ </td>
+ <td>{{ __('Go to file permalink (while viewing a file)') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Epics, Issues, and Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>r</kbd>
+ </td>
+ <td>{{ __('Comment/Reply (quoting selected text)') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Edit description') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Change label') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Issues and Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>a</kbd>
+ </td>
+ <td>{{ __('Change assignee') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>m</kbd>
+ </td>
+ <td>{{ __('Change milestone') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>]</kbd>
+ /
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Next file in diff') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>[</kbd>
+ /
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Previous file in diff') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} p</kbd>
+ </td>
+ <td>{{ __('Go to file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>n</kbd>
+ </td>
+ <td>{{ __('Next unresolved discussion') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>p</kbd>
+ </td>
+ <td>{{ __('Previous unresolved discussion') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Copy source branch name') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Merge Request Commits') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>c</kbd>
+ </td>
+ <td>{{ __('Next commit') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>x</kbd>
+ </td>
+ <td>{{ __('Previous commit') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Web IDE') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} p</kbd>
+ </td>
+ <td>{{ __('Go to file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} enter</kbd>
+ </td>
+ <td>{{ __('Commit (when editing commit message)') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index bc2b2d6d5d0..e57d42dc63c 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -1,9 +1,8 @@
<script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { isEmpty } from 'lodash';
import Autosize from 'autosize';
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -34,6 +33,10 @@ export default {
TimelineEntryItem,
GlIcon,
CommentFieldLayout,
+ GlFormCheckbox,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: {
@@ -46,8 +49,8 @@ export default {
return {
note: '',
noteType: constants.COMMENT,
+ noteIsConfidential: false,
isSubmitting: false,
- isSubmitButtonDisabled: true,
};
},
computed: {
@@ -80,6 +83,9 @@ export default {
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
+ canSetConfidential() {
+ return this.getNoteableData.current_user.can_update;
+ },
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
@@ -146,13 +152,11 @@ export default {
hasCloseAndCommentButton() {
return !this.glFeatures.removeCommentCloseReopen;
},
- },
- watch: {
- note(newNote) {
- this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
+ confidentialNotesEnabled() {
+ return Boolean(this.glFeatures.confidentialNotes);
},
- isSubmitting(newValue) {
- this.setIsSubmitButtonDisabled(this.note, newValue);
+ disableSubmitButton() {
+ return this.note.length === 0 || this.isSubmitting;
},
},
mounted() {
@@ -173,13 +177,6 @@ export default {
'reopenIssuable',
'toggleIssueLocalState',
]),
- setIsSubmitButtonDisabled(note, isSubmitting) {
- if (!isEmpty(note) && !isSubmitting) {
- this.isSubmitButtonDisabled = false;
- } else {
- this.isSubmitButtonDisabled = true;
- }
- },
handleSave(withIssueAction) {
if (this.note.length) {
const noteData = {
@@ -189,6 +186,7 @@ export default {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
+ confidential: this.noteIsConfidential,
note: this.note,
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
@@ -252,6 +250,7 @@ export default {
if (shouldClear) {
this.note = '';
+ this.noteIsConfidential = false;
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
@@ -340,11 +339,26 @@ export default {
</markdown-field>
</comment-field-layout>
<div class="note-form-actions">
+ <gl-form-checkbox
+ v-if="confidentialNotesEnabled && canSetConfidential"
+ v-model="noteIsConfidential"
+ class="gl-mb-6"
+ data-testid="confidential-note-checkbox"
+ >
+ {{ s__('Notes|Make this comment confidential') }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question"
+ :size="16"
+ :title="s__('Notes|Confidential comments are only visible to project members')"
+ class="gl-text-gray-500"
+ />
+ </gl-form-checkbox>
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<gl-button
- :disabled="isSubmitButtonDisabled"
+ :disabled="disableSubmitButton"
class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button"
data-testid="comment-button"
@@ -357,7 +371,7 @@ export default {
>{{ commentButtonTitle }}</gl-button
>
<gl-button
- :disabled="isSubmitButtonDisabled"
+ :disabled="disableSubmitButton"
name="button"
category="primary"
variant="success"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 17a995018d3..23b1f1e2ac1 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -210,9 +210,9 @@ export default {
v-gl-tooltip:tooltipcontainer.bottom
data-testid="confidentialIndicator"
name="eye-slash"
- :size="14"
- :title="s__('Notes|Private comments are accessible by internal staff only')"
- class="gl-ml-1 gl-text-gray-700 align-middle"
+ :size="16"
+ :title="s__('Notes|This comment is confidential and only visible to project members')"
+ class="gl-ml-1 gl-text-orange-700 align-middle"
/>
<slot name="extra-controls"></slot>
<gl-loading-icon
diff --git a/app/assets/javascripts/pages/projects/security/configuration/index.js b/app/assets/javascripts/pages/projects/security/configuration/index.js
new file mode 100644
index 00000000000..101cb8356b2
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/security/configuration/index.js
@@ -0,0 +1,3 @@
+import { initStaticSecurityConfiguration } from '~/security_configuration';
+
+initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
diff --git a/app/assets/javascripts/pages/projects/serverless/index.js b/app/assets/javascripts/pages/projects/serverless/index.js
index a883737ac9b..640301dd478 100644
--- a/app/assets/javascripts/pages/projects/serverless/index.js
+++ b/app/assets/javascripts/pages/projects/serverless/index.js
@@ -1,7 +1,5 @@
import ServerlessBundle from '~/serverless/serverless_bundle';
import initServerlessSurveyBanner from '~/serverless/survey_banner';
-document.addEventListener('DOMContentLoaded', () => {
- initServerlessSurveyBanner();
- new ServerlessBundle(); // eslint-disable-line no-new
-});
+initServerlessSurveyBanner();
+new ServerlessBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/registry/explorer/constants/list.js
index 37ced72861e..f59b9d7a9f5 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/registry/explorer/constants/list.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
// Translations strings
@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
-export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
-export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10;
+
+export const SORT_FIELDS = [
+ { orderBy: 'UPDATED', label: __('Updated') },
+ { orderBy: 'CREATED', label: __('Created') },
+ { orderBy: 'NAME', label: __('Name') },
+];
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 8b6d778c655..01cb7fa1cab 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
@@ -6,9 +6,17 @@ query getContainerRepositoriesDetails(
$after: String
$before: String
$isGroupPage: Boolean!
+ $sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails(
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index d362b79789b..c710d125797 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -7,12 +7,12 @@ import {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import Tracking from '~/tracking';
import createFlash from '~/flash';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
import DeleteImage from '../components/delete_image.vue';
@@ -25,12 +25,11 @@ import {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
+ SORT_FIELDS,
} from '../constants/index';
export default {
@@ -58,9 +57,9 @@ export default {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
RegistryHeader,
DeleteImage,
+ RegistrySearch,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -77,11 +76,10 @@ export default {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
+ searchConfig: SORT_FIELDS,
apollo: {
baseImages: {
query: getContainerRepositoriesQuery,
@@ -123,7 +121,8 @@ export default {
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
- searchValue: null,
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null,
mutationLoading: false,
fetchAdditionalDetails: false,
@@ -142,6 +141,7 @@ export default {
queryVariables() {
return {
name: this.name,
+ sort: this.sortBy,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE,
@@ -166,6 +166,10 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE;
},
+ sortBy() {
+ const { orderBy, sort } = this.sorting;
+ return `${orderBy}_${sort}`.toUpperCase();
+ },
},
mounted() {
// If the two graphql calls - which are not batched - resolve togheter we will have a race
@@ -231,6 +235,16 @@ export default {
this.track('confirm_delete');
this.mutationLoading = true;
},
+ updateSorting(value) {
+ this.sorting = {
+ ...this.sorting,
+ ...value,
+ };
+ },
+ doFilter() {
+ const search = this.filter.find((i) => i.type === 'filtered-search-term');
+ this.name = search?.value?.data;
+ },
},
};
</script>
@@ -283,6 +297,16 @@ export default {
</template>
</registry-header>
+ <registry-search
+ :filter="filter"
+ :sorting="sorting"
+ :tokens="[]"
+ :sortable-fields="$options.searchConfig"
+ @sorting:changed="updateSorting"
+ @filter:changed="filter = $event"
+ @filter:submit="doFilter"
+ />
+
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
@@ -298,20 +322,6 @@ export default {
</div>
<template v-else>
<template v-if="images.length > 0 || name">
- <div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
- <div class="gl-flex-fill-1">
- <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
- </div>
- <div>
- <gl-search-box-by-click
- v-model="searchValue"
- :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
- @clear="name = null"
- @submit="name = $event"
- />
- </div>
- </div>
-
<image-list
v-if="images.length"
:images="images"
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
new file mode 100644
index 00000000000..513a7353d28
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -0,0 +1,23 @@
+<script>
+import ConfigurationTable from './configuration_table.vue';
+
+export default {
+ components: {
+ ConfigurationTable,
+ },
+};
+</script>
+
+<template>
+ <article>
+ <header>
+ <h4 class="gl-my-5">
+ {{ __('Security Configuration') }}
+ </h4>
+ <h5 class="gl-font-lg gl-mt-7">
+ {{ s__('SecurityConfiguration|Testing & Compliance') }}
+ </h5>
+ </header>
+ <configuration-table />
+ </article>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue
new file mode 100644
index 00000000000..2d9e8e63826
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/configuration_table.vue
@@ -0,0 +1,97 @@
+<script>
+import { GlLink, GlSprintf, GlTable, GlAlert } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+import ManageSast from './manage_sast.vue';
+import Upgrade from './upgrade.vue';
+import { features } from './features_constants';
+
+const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
+const thClass = `gl-text-gray-900 gl-bg-transparent! ${borderClasses}`;
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ GlTable,
+ GlAlert,
+ },
+ data: () => ({
+ features,
+ errorMessage: '',
+ }),
+ methods: {
+ getFeatureDocumentationLinkLabel(item) {
+ return sprintf(s__('SecurityConfiguration|Feature documentation for %{featureName}'), {
+ featureName: item.name,
+ });
+ },
+ onError(value) {
+ this.errorMessage = value;
+ },
+ getComponentForItem(item) {
+ const COMPONENTS = {
+ [REPORT_TYPE_SAST]: ManageSast,
+ [REPORT_TYPE_DAST]: Upgrade,
+ [REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
+ [REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
+ [REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
+ [REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
+ };
+
+ return COMPONENTS[item.type];
+ },
+ },
+ table: {
+ fields: [
+ {
+ key: 'feature',
+ label: s__('SecurityConfiguration|Security Control'),
+ thClass,
+ },
+ {
+ key: 'manage',
+ label: s__('SecurityConfiguration|Manage'),
+ thClass,
+ },
+ ],
+ items: features,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
+ {{ errorMessage }}
+ </gl-alert>
+ <gl-table :items="$options.table.items" :fields="$options.table.fields" stacked="md">
+ <template #cell(feature)="{ item }">
+ <div class="gl-text-gray-900">
+ {{ item.name }}
+ </div>
+ <div>
+ {{ item.description }}
+ <gl-link
+ target="_blank"
+ :href="item.link"
+ :aria-label="getFeatureDocumentationLinkLabel(item)"
+ >
+ {{ s__('SecurityConfiguration|More information') }}
+ </gl-link>
+ </div>
+ </template>
+
+ <template #cell(manage)="{ item }">
+ <component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/features_constants.js b/app/assets/javascripts/security_configuration/components/features_constants.js
new file mode 100644
index 00000000000..c21c18fbf60
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/features_constants.js
@@ -0,0 +1,112 @@
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_SECRET_DETECTION,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+
+/**
+ * Translations & helpPagePaths for Static Security Configuration Page
+ */
+export const SAST_NAME = s__('Static Application Security Testing (SAST)');
+export const SAST_DESCRIPTION = s__('Analyze your source code for known vulnerabilities.');
+export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
+
+export const DAST_NAME = s__('Dynamic Application Security Testing (DAST)');
+export const DAST_DESCRIPTION = s__('Analyze a review version of your web application.');
+export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
+
+export const SECRET_DETECTION_NAME = s__('Secret Detection');
+export const SECRET_DETECTION_DESCRIPTION = s__(
+ 'Analyze your source code and git history for secrets.',
+);
+export const SECRET_DETECTION_HELP_PATH = helpPagePath(
+ 'user/application_security/secret_detection/index',
+);
+
+export const DEPENDENCY_SCANNING_NAME = s__('Dependency Scanning');
+export const DEPENDENCY_SCANNING_DESCRIPTION = s__(
+ 'Analyze your dependencies for known vulnerabilities.',
+);
+export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/dependency_scanning/index',
+);
+
+export const CONTAINER_SCANNING_NAME = s__('Container Scanning');
+export const CONTAINER_SCANNING_DESCRIPTION = s__(
+ 'Check your Docker images for known vulnerabilities.',
+);
+export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/container_scanning/index',
+);
+
+export const COVERAGE_FUZZING_NAME = s__('Coverage Fuzzing');
+export const COVERAGE_FUZZING_DESCRIPTION = s__(
+ 'Find bugs in your code with coverage-guided fuzzing.',
+);
+export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
+ 'user/application_security/coverage_fuzzing/index',
+);
+
+export const LICENSE_COMPLIANCE_NAME = s__('License Compliance');
+export const LICENSE_COMPLIANCE_DESCRIPTION = s__(
+ 'Search your project dependencies for their licenses and apply policies.',
+);
+export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
+ 'user/compliance/license_compliance/index',
+);
+
+export const UPGRADE_CTA = s__(
+ 'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}',
+);
+
+export const features = [
+ {
+ name: SAST_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ },
+ {
+ name: DAST_NAME,
+ description: DAST_DESCRIPTION,
+ helpPath: DAST_HELP_PATH,
+ type: REPORT_TYPE_DAST,
+ },
+ {
+ name: SECRET_DETECTION_NAME,
+ description: SECRET_DETECTION_DESCRIPTION,
+ helpPath: SECRET_DETECTION_HELP_PATH,
+ type: REPORT_TYPE_SECRET_DETECTION,
+ },
+ {
+ name: DEPENDENCY_SCANNING_NAME,
+ description: DEPENDENCY_SCANNING_DESCRIPTION,
+ helpPath: DEPENDENCY_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_DEPENDENCY_SCANNING,
+ },
+ {
+ name: CONTAINER_SCANNING_NAME,
+ description: CONTAINER_SCANNING_DESCRIPTION,
+ helpPath: CONTAINER_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_CONTAINER_SCANNING,
+ },
+ {
+ name: COVERAGE_FUZZING_NAME,
+ description: COVERAGE_FUZZING_DESCRIPTION,
+ helpPath: COVERAGE_FUZZING_HELP_PATH,
+ type: REPORT_TYPE_COVERAGE_FUZZING,
+ },
+ {
+ name: LICENSE_COMPLIANCE_NAME,
+ description: LICENSE_COMPLIANCE_DESCRIPTION,
+ helpPath: LICENSE_COMPLIANCE_HELP_PATH,
+ type: REPORT_TYPE_LICENSE_COMPLIANCE,
+ },
+];
diff --git a/app/assets/javascripts/security_configuration/components/manage_sast.vue b/app/assets/javascripts/security_configuration/components/manage_sast.vue
new file mode 100644
index 00000000000..49bd3ba64e5
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/manage_sast.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ inject: {
+ projectPath: {
+ from: 'projectPath',
+ default: '',
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ }),
+ methods: {
+ async mutate() {
+ this.isLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: configureSastMutation,
+ variables: {
+ input: {
+ projectPath: this.projectPath,
+ configuration: { global: [], pipeline: [], analyzers: [] },
+ },
+ },
+ });
+ const { errors, successPath } = data.configureSast;
+
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
+ }
+
+ if (!successPath) {
+ throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
+ }
+
+ redirectTo(successPath);
+ } catch (e) {
+ this.$emit('error', e.message);
+ this.isLoading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
+ s__('SecurityConfiguration|Configure via Merge Request')
+ }}</gl-button>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/upgrade.vue b/app/assets/javascripts/security_configuration/components/upgrade.vue
new file mode 100644
index 00000000000..166ee4ff194
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/upgrade.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { UPGRADE_CTA } from './features_constants';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ i18n: {
+ UPGRADE_CTA,
+ },
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="$options.i18n.UPGRADE_CTA">
+ <template #link="{ content }">
+ <gl-link target="_blank" href="https://about.gitlab.com/pricing/">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql
new file mode 100644
index 00000000000..9e826cf9e4b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql
@@ -0,0 +1,6 @@
+mutation configureSast($input: ConfigureSastInput!) {
+ configureSast(input: $input) {
+ successPath
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
new file mode 100644
index 00000000000..c98fa46b32b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import SecurityConfigurationApp from './components/app.vue';
+
+export const initStaticSecurityConfiguration = (el) => {
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ },
+ render(createElement) {
+ return createElement(SecurityConfigurationApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index be5fd93f77c..cbd68f2513a 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -1,11 +1,9 @@
<script>
-// NOTE! For the first iteration, we are simply copying the implementation of Assignees
-// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
-const DEFAULT_RENDER_COUNT = 5;
+const LOADING_STATE = 'loading';
+const SUCCESS_STATE = 'success';
export default {
components: {
@@ -34,35 +32,21 @@ export default {
data() {
return {
showLess: true,
- loading: false,
- requestedReviewSuccess: false,
+ loadingStates: {},
};
},
- computed: {
- firstUser() {
- return this.users[0];
- },
- hasOneUser() {
- return this.users.length === 1;
- },
- hiddenReviewersLabel() {
- const { numberOfHiddenReviewers } = this;
- return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
- },
- renderShowMoreSection() {
- return this.users.length > DEFAULT_RENDER_COUNT;
- },
- numberOfHiddenReviewers() {
- return this.users.length - DEFAULT_RENDER_COUNT;
- },
- uncollapsedUsers() {
- const uncollapsedLength = this.showLess
- ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
- : this.users.length;
- return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
- },
- username() {
- return `@${this.firstUser.username}`;
+ watch: {
+ users: {
+ handler(users) {
+ this.loadingStates = users.reduce(
+ (acc, user) => ({
+ ...acc,
+ [user.id]: acc[user.id] || null,
+ }),
+ this.loadingStates,
+ );
+ },
+ immediate: true,
},
},
methods: {
@@ -70,21 +54,23 @@ export default {
this.showLess = !this.showLess;
},
reRequestReview(userId) {
- this.loading = true;
+ this.loadingStates[userId] = LOADING_STATE;
this.$emit('request-review', { userId, callback: this.requestReviewComplete });
},
- requestReviewComplete(success) {
+ requestReviewComplete(userId, success) {
if (success) {
- this.requestedReviewSuccess = true;
+ this.loadingStates[userId] = SUCCESS_STATE;
setTimeout(() => {
- this.requestedReviewSuccess = false;
+ this.loadingStates[userId] = null;
}, 1500);
+ } else {
+ this.loadingStates[userId] = null;
}
-
- this.loading = false;
},
},
+ LOADING_STATE,
+ SUCCESS_STATE,
};
</script>
@@ -100,20 +86,22 @@ export default {
<div class="gl-ml-3">@{{ user.username }}</div>
</reviewer-avatar-link>
<gl-icon
- v-if="requestedReviewSuccess"
+ v-if="loadingStates[user.id] === $options.SUCCESS_STATE"
:size="24"
name="check"
class="float-right gl-text-green-500"
+ data-testid="re-request-success"
/>
<gl-button
v-else-if="user.can_update_merge_request && user.reviewed"
v-gl-tooltip.left
:title="__('Re-request review')"
- :loading="loading"
+ :loading="loadingStates[user.id] === $options.LOADING_STATE"
class="float-right gl-text-gray-500!"
size="small"
icon="redo"
variant="link"
+ data-testid="re-request-button"
@click="reRequestReview(user.id)"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index b23788f81fe..c4fe1be83fc 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -58,9 +58,9 @@ export default class SidebarMediator {
.then(() => {
this.store.updateReviewer(userId);
toast(__('Requested review'));
- callback(true);
+ callback(userId, true);
})
- .catch(() => callback(false));
+ .catch(() => callback(userId, false));
}
setMoveToProjectId(projectId) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 92223c9058e..f430bf80cbb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -56,7 +56,10 @@ export default {
mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds,
};
- this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch;
+ this.removeSourceBranch =
+ data.project.mergeRequest.shouldRemoveSourceBranch ||
+ data.project.mergeRequest.forceRemoveSourceBranch ||
+ false;
this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
this.isSquashReadOnly = data.project.squashReadOnly;
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 9479ef3cf79..8ee45b05431 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -5,6 +5,7 @@ fragment ReadyToMerge on Project {
mergeRequest(iid: $iid) {
autoMergeEnabled
shouldRemoveSourceBranch
+ forceRemoveSourceBranch
defaultMergeCommitMessage
defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index dd591f7bba3..aac5a5c1def 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
+export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
+export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
+export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
+export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_compliance';
+export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/**
* SecurityReportTypeEnum values for use with GraphQL.
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 2cef43f19ab..036d95622ef 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -243,7 +243,8 @@ module NotesActions
:type,
:note,
:line_code, # LegacyDiffNote
- :position # DiffNote
+ :position, # DiffNote
+ :confidential
).tap do |create_params|
create_params.merge!(
params.permit(:merge_request_diff_head_sha, :in_reply_to_discussion_id)
diff --git a/app/controllers/concerns/redis_tracking.rb b/app/controllers/concerns/redis_tracking.rb
index d71935356b8..a7e75f802a8 100644
--- a/app/controllers/concerns/redis_tracking.rb
+++ b/app/controllers/concerns/redis_tracking.rb
@@ -7,30 +7,26 @@
#
# include RedisTracking
#
-# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature
-#
-# if the feature flag is enabled by default you should use
-# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature, feature_default_enabled: true
+# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score'
#
# You can also pass custom conditions using `if:`, using the same format as with Rails callbacks.
module RedisTracking
extend ActiveSupport::Concern
class_methods do
- def track_redis_hll_event(*controller_actions, name:, feature:, feature_default_enabled: false, if: nil)
+ def track_redis_hll_event(*controller_actions, name:, if: nil)
custom_conditions = Array.wrap(binding.local_variable_get('if'))
conditions = [:trackable_request?, *custom_conditions]
after_action only: controller_actions, if: conditions do
- track_unique_redis_hll_event(name, feature, feature_default_enabled)
+ track_unique_redis_hll_event(name)
end
end
end
private
- def track_unique_redis_hll_event(event_name, feature, feature_default_enabled)
- return unless metric_feature_enabled?(feature, feature_default_enabled)
+ def track_unique_redis_hll_event(event_name)
return unless visitor_id
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: visitor_id)
@@ -40,10 +36,6 @@ module RedisTracking
request.format.html? && request.headers['DNT'] != '1'
end
- def metric_feature_enabled?(feature, default_enabled)
- Feature.enabled?(feature, default_enabled: default_enabled)
- end
-
def visitor_id
return cookies[:visitor_id] if cookies[:visitor_id].present?
return unless current_user
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index c93e75b438b..0ee8d0c9307 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -15,7 +15,7 @@ module SnippetsActions
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
- track_redis_hll_event :show, name: 'i_snippets_show', feature: :usage_data_i_snippets_show, feature_default_enabled: true
+ track_redis_hll_event :show, name: 'i_snippets_show'
respond_to :html
end
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 1ae90edd8f7..4014e4f0024 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -36,8 +36,7 @@ module WikiActions
# NOTE: We want to include wiki page views in the same counter as the other
# Event-based wiki actions tracked through TrackUniqueEvents, so we use the same event name.
- track_redis_hll_event :show, name: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION.to_s,
- feature: :track_unique_wiki_page_views, feature_default_enabled: true
+ track_redis_hll_event :show, name: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION.to_s
helper_method :view_file_button, :diff_file_html_data
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 8c66f45dd79..3bb00978aac 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
record_experiment_user(:ci_syntax_templates, namespace_id: @project.namespace_id) if params[:file_name] == @project.ci_config_path_or_default
end
- track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true
+ track_redis_hll_event :create, :update, name: 'g_edit_by_sfe'
feature_category :source_code_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ff8c790f43d..c8bdbe548c8 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
+ push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 74936abe59c..59b14bbb91d 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -9,6 +9,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :set_pipeline_path, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_read_build!, only: [:index]
+ before_action :authorize_read_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 40e6590d85c..820b00a902e 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -5,7 +5,7 @@ class SearchController < ApplicationController
include SearchHelper
include RedisTracking
- track_redis_hll_event :show, name: 'i_search_total', feature: :search_track_unique_users, feature_default_enabled: true
+ track_redis_hll_event :show, name: 'i_search_total'
around_action :allow_gitaly_ref_name_caching
diff --git a/app/graphql/queries/container_registry/get_container_repositories.query.graphql b/app/graphql/queries/container_registry/get_container_repositories.query.graphql
index 6171233c446..4683ef9dfdb 100644
--- a/app/graphql/queries/container_registry/get_container_repositories.query.graphql
+++ b/app/graphql/queries/container_registry/get_container_repositories.query.graphql
@@ -6,11 +6,19 @@ query getProjectContainerRepositories(
$after: String
$before: String
$isGroupPage: Boolean!
+ $sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
__typename
containerRepositoriesCount
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
__typename
nodes {
id
@@ -35,7 +43,14 @@ query getProjectContainerRepositories(
group(fullPath: $fullPath) @include(if: $isGroupPage) {
__typename
containerRepositoriesCount
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
__typename
nodes {
id
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 8e32661d4cb..0cefc84633d 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -12,6 +12,9 @@ module Types
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the user.'
+ field :bot, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates if the user is a bot.',
+ method: :bot?
field :username, GraphQL::STRING_TYPE, null: false,
description: 'Username of the user. Unique within this instance of GitLab.'
field :name, GraphQL::STRING_TYPE, null: false,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 58aaadd5d49..3be107ea2e1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -16,6 +16,7 @@ module Ci
include ShaAttribute
include FromUnion
include UpdatedAtFilterable
+ include EachBatch
MAX_OPEN_MERGE_REQUESTS_REFS = 4
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e0c2b308247..2f0fd0af63b 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -55,6 +55,7 @@ class CommitStatus < ApplicationRecord
scope :for_ids, -> (ids) { where(id: ids) }
scope :for_ref, -> (ref) { where(ref: ref) }
scope :by_name, -> (name) { where(name: name) }
+ scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :for_project_paths, -> (paths) do
where(project: Project.where_full_path_in(Array(paths)))
diff --git a/app/models/member.rb b/app/models/member.rb
index 2e79b50d6b7..62fe757683f 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -47,6 +47,19 @@ class Member < ApplicationRecord
},
if: :project_bot?
+ scope :in_hierarchy, ->(source) do
+ groups = source.root_ancestor.self_and_descendants
+ group_members = Member.default_scoped.where(source: groups)
+
+ projects = source.root_ancestor.all_projects
+ project_members = Member.default_scoped.where(source: projects)
+
+ Member.default_scoped.from_union([
+ group_members,
+ project_members
+ ]).merge(self)
+ end
+
# This scope encapsulates (most of) the conditions a row in the member table
# must satisfy if it is a valid permission. Of particular note:
#
@@ -79,12 +92,18 @@ class Member < ApplicationRecord
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
+
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
+
+ scope :created_today, -> do
+ now = Date.current
+ where(created_at: now.beginning_of_day..now.end_of_day)
+ end
scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
scope :has_access, -> { active.where('access_level > 0') }
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index d0e62a1afba..ab043227832 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -183,7 +183,17 @@ class PrometheusService < MonitoringService
manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present?
end
+ def clean_google_iap_service_account
+ return unless google_iap_service_account_json
+
+ google_iap_service_account_json
+ .then { |json| Gitlab::Json.parse(json) }
+ .except('token_credential_uri')
+ end
+
def iap_client
- @iap_client ||= Google::Auth::Credentials.new(Gitlab::Json.parse(google_iap_service_account_json), target_audience: google_iap_audience_client_id).client
+ @iap_client ||= Google::Auth::Credentials
+ .new(clean_google_iap_service_account, target_audience: google_iap_audience_client_id)
+ .client
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 7605ef54d5b..8c3dcaa7c0f 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -31,6 +31,7 @@ class ProjectStatistics < ApplicationRecord
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
+ scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
def total_repository_size
repository_size + lfs_objects_size
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index f97d94c14f0..aaf985d6c63 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -221,6 +221,7 @@ class ProjectPolicy < BasePolicy
enable :read_pages_content
enable :read_release
enable :read_analytics
+ enable :read_insights
end
# These abilities are not allowed to admins that are not members of the project,
@@ -450,6 +451,9 @@ class ProjectPolicy < BasePolicy
rule { analytics_disabled }.policy do
prevent(:read_analytics)
+ prevent(:read_insights)
+ prevent(:read_cycle_analytics)
+ prevent(:read_repository_graphs)
end
rule { wiki_disabled }.policy do
@@ -523,6 +527,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :read_pages_content
enable :read_analytics
+ enable :read_insights
# NOTE: may be overridden by IssuePolicy
enable :read_issue
diff --git a/app/services/ci/abort_project_pipelines_service.rb b/app/services/ci/abort_project_pipelines_service.rb
new file mode 100644
index 00000000000..0b2fa9ed3c0
--- /dev/null
+++ b/app/services/ci/abort_project_pipelines_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ci
+ class AbortProjectPipelinesService
+ # Danger: Cancels in bulk without callbacks
+ # Only for pipeline abandonment scenarios (current example: project delete)
+ def execute(project)
+ return unless Feature.enabled?(:abort_deleted_project_pipelines, default_enabled: :yaml)
+
+ pipelines = project.all_pipelines.cancelable
+ bulk_abort!(pipelines, status: :canceled)
+
+ ServiceResponse.success(message: 'Pipelines canceled')
+ end
+
+ private
+
+ def bulk_abort!(pipelines, status:)
+ pipelines.each_batch do |pipeline_batch|
+ CommitStatus.in_pipelines(pipeline_batch).in_batches.update_all(status: status) # rubocop: disable Cop/InBatches
+ pipeline_batch.update_all(status: status)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/cancel_user_pipelines_service.rb b/app/services/ci/cancel_user_pipelines_service.rb
index 3a8b5e91088..3d3a8032e8e 100644
--- a/app/services/ci/cancel_user_pipelines_service.rb
+++ b/app/services/ci/cancel_user_pipelines_service.rb
@@ -6,6 +6,7 @@ module Ci
# This is a bug with CodeReuse/ActiveRecord cop
# https://gitlab.com/gitlab-org/gitlab/issues/32332
def execute(user)
+ # TODO: fix N+1 queries https://gitlab.com/gitlab-org/gitlab/-/issues/300685
user.pipelines.cancelable.find_each(&:cancel_running)
ServiceResponse.success(message: 'Pipeline canceled')
diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb
index da653e2524a..c26d2e7ab47 100644
--- a/app/services/design_management/save_designs_service.rb
+++ b/app/services/design_management/save_designs_service.rb
@@ -18,7 +18,6 @@ module DesignManagement
return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES
return error("Duplicate filenames are not allowed!") if files.map(&:original_filename).uniq.length != files.length
return error("Design copy is in progress") if design_collection.copy_in_progress?
- return error("Filenames contained invalid characters and could not be saved") if files.any?(&:filename_sanitized?)
uploaded_designs, version = upload_designs!
skipped_designs = designs - uploaded_designs
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 5fcf2d711b0..cffccda1a44 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -2,12 +2,12 @@
module Members
class CreateService < Members::BaseService
+ include Gitlab::Utils::StrongMemoize
+
DEFAULT_LIMIT = 100
def execute(source)
- return error(s_('AddMember|No users specified.')) if params[:user_ids].blank?
-
- user_ids = params[:user_ids].split(',').uniq.flatten
+ return error(s_('AddMember|No users specified.')) if user_ids.blank?
return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && user_ids.size > user_limit
@@ -47,6 +47,13 @@ module Members
private
+ def user_ids
+ strong_memoize(:user_ids) do
+ ids = params[:user_ids] || ''
+ ids.split(',').uniq.flatten
+ end
+ end
+
def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index bec75657530..c1501625300 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -21,11 +21,14 @@ module Projects
def execute
return false unless can?(current_user, :remove_project, project)
+ project.update_attribute(:pending_delete, true)
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
flush_caches(project)
+ ::Ci::AbortProjectPipelinesService.new.execute(project)
+
Projects::UnlinkForkService.new(project, current_user).execute
attempt_destroy(project)
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index 4f4b6c1089c..899e58050af 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil} )
%section
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
deleted file mode 100644
index 9ad87518b1e..00000000000
--- a/app/views/help/_shortcuts.html.haml
+++ /dev/null
@@ -1,353 +0,0 @@
-#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog.modal-lg.modal-1040
- .modal-content
- .modal-header
- .js-toggle-shortcuts
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
- .modal-body
- .row
- .col-lg-4
- %table.shortcut-mappings.text-2
- %tbody
- %tr
- %th
- %th= _('Global Shortcuts')
- %tr
- %td.shortcut
- %kbd ?
- %td= _('Toggle this dialog')
- %tr
- %td.shortcut
- %kbd shift p
- %td= _('Go to your projects')
- %tr
- %td.shortcut
- %kbd shift g
- %td= _('Go to your groups')
- %tr
- %td.shortcut
- %kbd shift a
- %td= _('Go to the activity feed')
- %tr
- %td.shortcut
- %kbd shift l
- %td= _('Go to the milestone list')
- %tr
- %td.shortcut
- %kbd shift s
- %td= _('Go to your snippets')
- %tr
- %td.shortcut
- %kbd s
- \/
- %kbd /
- %td= _('Start search')
- %tr
- %td.shortcut
- %kbd shift i
- %td= _('Go to your issues')
- %tr
- %td.shortcut
- %kbd shift m
- %td= _('Go to your merge requests')
- %tr
- %td.shortcut
- %kbd shift t
- %td= _('Go to your To-Do list')
- %tr
- %td.shortcut
- %kbd p
- %kbd b
- %td= _('Toggle the Performance Bar')
- - if Gitlab.com?
- %tr
- %td.shortcut
- %kbd g
- %kbd x
- %td= _('Toggle GitLab Next')
- %tbody
- %tr
- %th
- %th= _('Editing')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; shift p
- - else
- %kbd ctrl shift p
- %td= _('Toggle Markdown preview')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-up', size: 12)
- %td= _('Edit your most recent comment in a thread (from an empty textarea)')
- %tbody
- %tr
- %th
- %th= _('Wiki')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit wiki page')
- %tbody
- %tr
- %th
- %th= _('Repository Graph')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-left', size: 12)
- \/
- %kbd h
- %td= _('Scroll left')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-right', size: 12)
- \/
- %kbd l
- %td= _('Scroll right')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-up', size: 12)
- \/
- %kbd k
- %td= _('Scroll up')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-down', size: 12)
- \/
- %kbd j
- %td= _('Scroll down')
- %tr
- %td.shortcut
- %kbd
- shift
- = sprite_icon('arrow-up', size: 12)
- \/ k
- %td= _('Scroll to top')
- %tr
- %td.shortcut
- %kbd
- shift
- = sprite_icon('arrow-down', size: 12)
- \/ j
- %td= _('Scroll to bottom')
- .col-lg-4
- %table.shortcut-mappings.text-2
- %tbody
- %tr
- %th
- %th= _('Project')
- %tr
- %td.shortcut
- %kbd g
- %kbd p
- %td= _('Go to the project\'s overview page')
- %tr
- %td.shortcut
- %kbd g
- %kbd v
- %td= _('Go to the project\'s activity feed')
- %tr
- %td.shortcut
- %kbd g
- %kbd r
- %td= _('Go to releases')
- %tr
- %td.shortcut
- %kbd g
- %kbd f
- %td= _('Go to files')
- %tr
- %td.shortcut
- %kbd t
- %td= _('Go to find file')
- %tr
- %td.shortcut
- %kbd g
- %kbd c
- %td= _('Go to commits')
- %tr
- %td.shortcut
- %kbd g
- %kbd n
- %td= _('Go to repository graph')
- %tr
- %td.shortcut
- %kbd g
- %kbd d
- %td= _('Go to repository charts')
- %tr
- %td.shortcut
- %kbd g
- %kbd i
- %td= _('Go to issues')
- %tr
- %td.shortcut
- %kbd i
- %td= _('New issue')
- %tr
- %td.shortcut
- %kbd g
- %kbd b
- %td= _('Go to issue boards')
- %tr
- %td.shortcut
- %kbd g
- %kbd m
- %td= _('Go to merge requests')
- %tr
- %td.shortcut
- %kbd g
- %kbd j
- %td= _('Go to jobs')
- %tr
- %td.shortcut
- %kbd g
- %kbd l
- %td= _('Go to metrics')
- %tr
- %td.shortcut
- %kbd g
- %kbd e
- %td= _('Go to environments')
- %tr
- %td.shortcut
- %kbd g
- %kbd k
- %td= _('Go to kubernetes')
- %tr
- %td.shortcut
- %kbd g
- %kbd s
- %td= _('Go to snippets')
- %tr
- %td.shortcut
- %kbd g
- %kbd w
- %td= _('Go to wiki')
- %tbody
- %tr
- %th
- %th= _('Project Files')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-up', size: 12)
- %td= _('Move selection up')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-down', size: 12)
- %td= _('Move selection down')
- %tr
- %td.shortcut
- %kbd enter
- %td= _('Open Selection')
- %tr
- %td.shortcut
- %kbd esc
- %td= _('Go back (while searching for files)')
- %tr
- %td.shortcut
- %kbd y
- %td= _('Go to file permalink (while viewing a file)')
- .col-lg-4
- %table.shortcut-mappings.text-2
- %tbody
- %tr
- %th
- %th= _('Epics, Issues, and Merge Requests')
- %tr
- %td.shortcut
- %kbd r
- %td= _('Comment/Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit description')
- %tr
- %td.shortcut
- %kbd l
- %td= _('Change label')
- %tbody
- %tr
- %th
- %th= _('Issues and Merge Requests')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
- %tbody
- %tr
- %th
- %th= _('Merge Requests')
- %tr
- %td.shortcut
- %kbd ]
- \/
- %kbd j
- %td= _('Next file in diff')
- %tr
- %td.shortcut
- %kbd [
- \/
- %kbd k
- %td= _('Previous file in diff')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; p
- - else
- %kbd ctrl p
- %td= _('Go to file')
- %tr
- %td.shortcut
- %kbd n
- %td= _('Next unresolved discussion')
- %tr
- %td.shortcut
- %kbd p
- %td= _('Previous unresolved discussion')
- %tr
- %td.shortcut
- %kbd b
- %td= _('Copy source branch name')
- %tbody
- %tr
- %th
- %th= _('Merge Request Commits')
- %tr
- %td.shortcut
- %kbd c
- %td= _('Next commit')
- %tr
- %td.shortcut
- %kbd x
- %td= _('Previous commit')
- %tbody
- %tr
- %th
- %th= _('Web IDE')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; p
- - else
- %kbd ctrl p
- %td= _('Go to file')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; enter
- - else
- %kbd ctrl enter
- %td= _('Commit (when editing commit message)')
diff --git a/app/views/help/shortcuts.js.haml b/app/views/help/shortcuts.js.haml
deleted file mode 100644
index 99ed042ea3b..00000000000
--- a/app/views/help/shortcuts.js.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-:plain
- $("body").append("#{escape_javascript(render('shortcuts'))}");
- $("#modal-shortcuts").modal();
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 8004a5facd7..7c896cd71ef 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -28,7 +28,7 @@
= _('GPG Key ID:')
%span.monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
+ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link gl-display-block')
%a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 93e94928110..a2009b96c0d 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil} )
%section
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
diff --git a/app/views/projects/security/configuration/show.html.haml b/app/views/projects/security/configuration/show.html.haml
index 1a371955be8..fe47ce327c2 100644
--- a/app/views/projects/security/configuration/show.html.haml
+++ b/app/views/projects/security/configuration/show.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title _("Security Configuration")
- page_title _("Security Configuration")
-#js-security-configuration-static
+#js-security-configuration-static{ data: {project_path: @project.full_path} }