diff options
Diffstat (limited to 'app')
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 } × - .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 ⌘ 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 ⌘ 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 ⌘ p - - else - %kbd ctrl p - %td= _('Go to file') - %tr - %td.shortcut - - if browser.platform.mac? - %kbd ⌘ 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} } |