diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-28 18:09:17 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-28 18:09:17 +0300 |
commit | 1bb7f81e238569fd0fe2b0c4385f1015407a2d59 (patch) | |
tree | 768c7d44fa3ed641a7e26fdf9db61422902e8294 /app | |
parent | eb3a23aaaa99ef8ae08c7b440fad676e3c71a1af (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
59 files changed, 715 insertions, 422 deletions
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 833fbb8789e..23eb470503e 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import { merge } from 'lodash'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import FilesCommentButton from './files_comment_button'; @@ -82,7 +82,7 @@ export default class Diff { .get(link, { params }) .then(({ data }) => $target.parent().replaceWith(data)) .catch(() => - createFlash({ + createAlert({ message: __('An error occurred while loading diff'), }), ); diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index f5c0776ca35..380aa137a7c 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -11,7 +11,7 @@ import { MR_COMMITS_NEXT_COMMIT, MR_COMMITS_PREVIOUS_COMMIT, } from '~/behaviors/shortcuts/keybindings'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { isSingleViewStyle } from '~/helpers/diffs_helper'; import { helpPagePath } from '~/helpers/help_page_helper'; import { parseBoolean } from '~/lib/utils/common_utils'; @@ -480,7 +480,7 @@ export default { this.updateChangesTabCount(); }) .catch(() => { - createFlash({ + createAlert({ message: __('Something went wrong on our end. Please try again!'), }); }); @@ -495,7 +495,7 @@ export default { this.setDiscussions(); }) .catch(() => { - createFlash({ + createAlert({ message: __('Something went wrong on our end. Please try again!'), }); }); diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue index c5f263eb5f1..b2098b9e82d 100644 --- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue @@ -1,7 +1,7 @@ <script> import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { mapActions } from 'vuex'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { s__, sprintf } from '~/locale'; import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants'; import * as utils from '../store/utils'; @@ -92,7 +92,7 @@ export default { ) { this.loadMoreLines({ endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers }) .catch(() => { - createFlash({ + createAlert({ message: s__('Diffs|Something went wrong while fetching diff lines.'), }); }) diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index aec608007d5..422bf52a1fa 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -10,7 +10,7 @@ import { escape } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; import { IdState } from 'vendor/vue-virtual-scroller'; import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { hasDiff } from '~/helpers/diffs_helper'; import { diffViewerErrors } from '~/ide/constants'; import { scrollToElement } from '~/lib/utils/common_utils'; @@ -309,7 +309,7 @@ export default { }) .catch(() => { idState.isLoadingCollapsedDiff = false; - createFlash({ + createAlert({ message: this.$options.i18n.genericError, }); }); diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 5e74a7206b3..7b523c95c74 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -5,7 +5,7 @@ import { historyPushState, scrollToElement, } from '~/lib/utils/common_utils'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { diffViewerModes } from '~/ide/constants'; import axios from '~/lib/utils/axios_utils'; @@ -246,7 +246,7 @@ export const fetchCoverageFiles = ({ commit, state }) => { } }, errorCallback: () => - createFlash({ + createAlert({ message: __('Something went wrong on our end. Please try again!'), }), }); @@ -509,7 +509,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => { .then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true })) .then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash)) .catch(() => - createFlash({ + createAlert({ message: s__('MergeRequests|Saving the comment failed'), }), ); @@ -619,7 +619,7 @@ export const cacheTreeListWidth = (_, size) => { export const receiveFullDiffError = ({ commit }, filePath) => { commit(types.RECEIVE_FULL_DIFF_ERROR, filePath); - createFlash({ + createAlert({ message: s__('MergeRequest|Error loading full diff. Please try again.'), }); }; @@ -757,7 +757,7 @@ export const setSuggestPopoverDismissed = ({ commit, state }) => commit(types.SET_SHOW_SUGGEST_POPOVER); }) .catch(() => { - createFlash({ + createAlert({ message: s__('MergeRequest|Error dismissing suggestion popover. Please try again.'), }); }); diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js index bc3cb163c39..999e91eed19 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js @@ -1,7 +1,7 @@ import { KeyMod, KeyCode } from 'monaco-editor'; import { debounce } from 'lodash'; import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { sanitize } from '~/lib/dompurify'; import axios from '~/lib/utils/axios_utils'; import syntaxHighlight from '~/syntax_highlight'; @@ -152,7 +152,7 @@ export class EditorMarkdownPreviewExtension { syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight')); previewEl.style.display = 'block'; }) - .catch(() => createFlash(BLOB_PREVIEW_ERROR)); + .catch(() => createAlert(BLOB_PREVIEW_ERROR)); } setupPreviewAction(instance) { diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue index 3173c2bd644..78e1b8d5cb2 100644 --- a/app/assets/javascripts/environments/components/delete_environment_modal.vue +++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue @@ -1,6 +1,6 @@ <script> import { GlTooltipDirective, GlModal } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __, s__, sprintf } from '~/locale'; import eventHub from '../event_hub'; import deleteEnvironmentMutation from '../graphql/mutations/delete_environment.mutation.graphql'; @@ -65,11 +65,11 @@ export default { .then(({ data }) => { const [message] = data?.deleteEvironment?.errors ?? []; if (message) { - createFlash({ message }); + createAlert({ message }); } }) .catch((error) => - createFlash({ + createAlert({ message: s__( 'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.', ), diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue index 3475b38c8c9..b00a0777a03 100644 --- a/app/assets/javascripts/environments/components/deployment.vue +++ b/app/assets/javascripts/environments/components/deployment.vue @@ -10,7 +10,7 @@ import { import { __, s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import deploymentDetails from '../graphql/queries/deployment_details.query.graphql'; import DeploymentStatusBadge from './deployment_status_badge.vue'; import Commit from './commit.vue'; @@ -119,7 +119,7 @@ export default { return data?.project?.deployment?.tags; }, error(error) { - createFlash({ + createAlert({ message: this.$options.i18n.LOAD_ERROR_MESSAGE, captureError: true, error, diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue index 96742a11ebb..901d0f5b34d 100644 --- a/app/assets/javascripts/environments/components/edit_environment.vue +++ b/app/assets/javascripts/environments/components/edit_environment.vue @@ -1,5 +1,5 @@ <script> -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import EnvironmentForm from './environment_form.vue'; @@ -39,7 +39,7 @@ export default { .then(({ data: { path } }) => visitUrl(path)) .catch((error) => { const message = error.response.data.message[0]; - createFlash({ message }); + createAlert({ message }); this.loading = false; }); }, diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue index 14da2668417..bb4d6ab3428 100644 --- a/app/assets/javascripts/environments/components/new_environment.vue +++ b/app/assets/javascripts/environments/components/new_environment.vue @@ -1,5 +1,5 @@ <script> -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import EnvironmentForm from './environment_form.vue'; @@ -32,7 +32,7 @@ export default { .then(({ data: { path } }) => visitUrl(path)) .catch((error) => { const message = error.response.data.message[0]; - createFlash({ message }); + createAlert({ message }); this.loading = false; }); }, diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 8957a3074ed..5e936ad8c96 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -3,7 +3,7 @@ */ import { isEqual, isFunction, omitBy } from 'lodash'; import Visibility from 'visibilityjs'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import Poll from '~/lib/utils/poll'; import { getParameterByName } from '~/lib/utils/url_utility'; import { s__, __ } from '~/locale'; @@ -94,7 +94,7 @@ export default { errorCallback() { this.isLoading = false; - createFlash({ + createAlert({ message: s__('Environments|An error occurred while fetching the environments.'), }); }, @@ -123,7 +123,7 @@ export default { }) .catch((err) => { this.isLoading = false; - createFlash({ + createAlert({ message: isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage, }); }); @@ -179,7 +179,7 @@ export default { window.location.href = url.join('/'); }) .catch(() => { - createFlash({ + createAlert({ message: errorMessage, }); }); diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue index 00ed7f0291b..2dbc9b10836 100644 --- a/app/assets/javascripts/notes/components/discussion_notes.vue +++ b/app/assets/javascripts/notes/components/discussion_notes.vue @@ -142,7 +142,7 @@ export default { :edited-at="discussion.resolved_at" :edited-by="discussion.resolved_by" :action-text="resolvedText" - class-name="discussion-headline-light js-discussion-headline discussion-resolved-text gl-mb-2" + class-name="discussion-headline-light js-discussion-headline discussion-resolved-text gl-mb-2 gl-ml-3" /> </template> <template #avatar-badge> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 6fdafe34d67..930876e90b1 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -281,6 +281,7 @@ export default { > {{ __('Contributor') }} </user-access-role-badge> + <span class="note-actions__mobile-spacer"></span> <gl-button v-if="canResolve" ref="resolveButton" diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue new file mode 100644 index 00000000000..ec70ab88870 --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue @@ -0,0 +1,84 @@ +<script> +import { GlCard, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ProtectionRow from './protection_row.vue'; + +export const i18n = { + rolesTitle: s__('BranchRules|Roles'), + usersTitle: s__('BranchRules|Users'), + groupsTitle: s__('BranchRules|Groups'), +}; + +export default { + name: 'ProtectionDetail', + i18n, + components: { GlCard, GlLink, ProtectionRow }, + props: { + header: { + type: String, + required: true, + }, + headerLinkTitle: { + type: String, + required: true, + }, + headerLinkHref: { + type: String, + required: true, + }, + roles: { + type: Array, + required: false, + default: () => [], + }, + users: { + type: Array, + required: false, + default: () => [], + }, + groups: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + showUsersDivider() { + return Boolean(this.roles.length); + }, + showGroupsDivider() { + return Boolean(this.roles.length || this.users.length); + }, + }, +}; +</script> + +<template> + <gl-card class="gl-mb-5" body-class="gl-py-0"> + <template #header> + <div class="gl-display-flex gl-justify-content-space-between"> + <strong>{{ header }}</strong> + <gl-link :href="headerLinkHref" target="_blank">{{ headerLinkTitle }}</gl-link> + </div> + </template> + + <!-- Roles --> + <protection-row v-if="roles.length" :title="$options.i18n.rolesTitle" :access-levels="roles" /> + + <!-- Users --> + <protection-row + v-if="users.length" + :show-divider="showUsersDivider" + :users="users" + :title="$options.i18n.usersTitle" + /> + + <!-- Groups --> + <protection-row + v-if="groups.length" + :show-divider="showGroupsDivider" + :title="$options.i18n.groupsTitle" + :access-levels="groups" + /> + </gl-card> +</template> diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue new file mode 100644 index 00000000000..56be0198574 --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue @@ -0,0 +1,86 @@ +<script> +import { GlAvatarsInline, GlAvatar, GlAvatarLink, GlTooltipDirective } from '@gitlab/ui'; +import { n__ } from '~/locale'; + +const AVATAR_TOOLTIP_MAX_CHARS = 100; +export const MAX_VISIBLE_AVATARS = 4; +export const AVATAR_SIZE = 32; + +export default { + name: 'ProtectionRow', + AVATAR_TOOLTIP_MAX_CHARS, + MAX_VISIBLE_AVATARS, + AVATAR_SIZE, + components: { GlAvatarsInline, GlAvatar, GlAvatarLink }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + title: { + type: String, + required: false, + default: null, + }, + accessLevels: { + type: Array, + required: false, + default: () => [], + }, + showDivider: { + type: Boolean, + required: false, + default: false, + }, + users: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + avatarBadgeSrOnlyText() { + return n__( + '%d additional user', + '%d additional users', + this.users.length - this.$options.MAX_VISIBLE_AVATARS, + ); + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4" + :class="{ 'gl-border-t-solid': showDivider }" + > + <div class="gl-mr-7">{{ title }}</div> + + <gl-avatars-inline + v-if="users.length" + :avatars="users" + :collapsed="true" + :max-visible="$options.MAX_VISIBLE_AVATARS" + :avatar-size="$options.AVATAR_SIZE" + badge-tooltip-prop="name" + :badge-tooltip-max-chars="$options.AVATAR_TOOLTIP_MAX_CHARS" + :badge-sr-only-text="avatarBadgeSrOnlyText" + > + <template #avatar="{ avatar }"> + <gl-avatar-link + :key="avatar.username" + v-gl-tooltip + target="_blank" + :href="avatar.webUrl" + :title="avatar.name" + > + <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="$options.AVATAR_SIZE" /> + </gl-avatar-link> + </template> + </gl-avatars-inline> + + <div v-for="(item, index) in accessLevels" :key="index" data-testid="access-level"> + {{ item.accessLevelDescription }} + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue index 30a0e7c383c..f3186723a49 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue @@ -74,7 +74,7 @@ export default { </script> <template> - <div> + <div class="gl-display-flex"> <gl-dropdown v-if="tertiaryButtons.length" :text="dropdownLabel" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue index 7ba387c79b1..fbd0f6235ba 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue @@ -4,6 +4,7 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m import { visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import eventHub from '../../event_hub'; import MRWidgetService from '../../services/mr_widget_service'; import { MANUAL_DEPLOY, @@ -134,6 +135,7 @@ export default { }); }) .finally(() => { + eventHub.$emit('FetchDeployments'); this.actionInProgress = null; }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 300e2a672cb..7d69a2d78b1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -324,7 +324,7 @@ export default { @mouseup="onRowMouseUp" > <div - class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center" + class="media-body gl-display-flex gl-flex-direction-row! gl-w-full" data-testid="widget-extension-top-level" > <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary"> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index c8a2a8d119b..4ff2643057f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -506,6 +506,13 @@ export default { eventHub.$on('DisablePolling', () => { this.stopPolling(); }); + + eventHub.$on('FetchDeployments', () => { + this.fetchPreMergeDeployments(); + if (this.shouldRenderMergedPipeline) { + this.fetchPostMergeDeployments(); + } + }); }, dismissSuggestPipelines() { this.mr.isDismissedSuggestPipeline = true; diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 32b3a0e22c2..657e4498b53 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -3,7 +3,7 @@ import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { debounce, unescape } from 'lodash'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import GLForm from '~/gl_form'; import axios from '~/lib/utils/axios_utils'; import { stripHtml } from '~/lib/utils/text_utility'; @@ -272,7 +272,7 @@ export default { this.fetchMarkdown() .then((data) => this.renderMarkdown(data)) .catch(() => - createFlash({ + createAlert({ message: __('Error loading markdown preview'), }), ); @@ -315,7 +315,7 @@ export default { this.$nextTick() .then(() => $(this.$refs['markdown-preview']).renderGFM()) .catch(() => - createFlash({ + createAlert({ message: __('Error rendering Markdown preview'), }), ); diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 9b81444fc04..30d72332c90 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,7 +1,7 @@ <script> import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import Vue from 'vue'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; @@ -91,7 +91,7 @@ export default { const suggestionElements = container.querySelectorAll('.js-render-suggestion'); if (this.lineType === 'old') { - createFlash({ + createAlert({ message: __('Unable to apply suggestions to a deleted line.'), parent: this.$el, }); diff --git a/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js b/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js index 832fb891838..1c4e8d332a9 100644 --- a/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js +++ b/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js @@ -1,4 +1,4 @@ -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { s__ } from '~/locale'; import * as types from './mutation_types'; @@ -11,7 +11,7 @@ export const fetchImagesFactory = (service) => async ({ state, commit }) => { commit(types.RECEIVE_METRIC_IMAGES_SUCCESS, response); } catch (error) { commit(types.RECEIVE_METRIC_IMAGES_ERROR); - createFlash({ message: s__('MetricImages|There was an issue loading metric images.') }); + createAlert({ message: s__('MetricImages|There was an issue loading metric images.') }); } }; @@ -34,7 +34,7 @@ export const uploadImageFactory = (service) => async ( commit(types.RECEIVE_METRIC_UPLOAD_SUCCESS, response); } catch (error) { commit(types.RECEIVE_METRIC_UPLOAD_ERROR); - createFlash({ message: s__('MetricImages|There was an issue uploading your image.') }); + createAlert({ message: s__('MetricImages|There was an issue uploading your image.') }); } }; @@ -57,7 +57,7 @@ export const updateImageFactory = (service) => async ( commit(types.RECEIVE_METRIC_UPDATE_SUCCESS, response); } catch (error) { commit(types.RECEIVE_METRIC_UPLOAD_ERROR); - createFlash({ message: s__('MetricImages|There was an issue updating your image.') }); + createAlert({ message: s__('MetricImages|There was an issue updating your image.') }); } }; @@ -68,7 +68,7 @@ export const deleteImageFactory = (service) => async ({ state, commit }, imageId await service.deleteMetricImage({ imageId, id: projectId, modelIid }); commit(types.RECEIVE_METRIC_DELETE_SUCCESS, imageId); } catch (error) { - createFlash({ message: s__('MetricImages|There was an issue deleting the image.') }); + createAlert({ message: s__('MetricImages|There was an issue deleting the image.') }); } }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js index 0c697e624ab..2dab97826b9 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js @@ -1,4 +1,4 @@ -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -16,7 +16,7 @@ export const receiveLabelsSuccess = ({ commit }, labels) => commit(types.RECEIVE_SET_LABELS_SUCCESS, labels); export const receiveLabelsFailure = ({ commit }) => { commit(types.RECEIVE_SET_LABELS_FAILURE); - createFlash({ + createAlert({ message: __('Error fetching labels.'), }); }; @@ -38,7 +38,7 @@ export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LA export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS); export const receiveCreateLabelFailure = ({ commit }) => { commit(types.RECEIVE_CREATE_LABEL_FAILURE); - createFlash({ + createAlert({ message: __('Error creating label.'), }); }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index 5f344ae4214..ce93ad216ec 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -8,7 +8,7 @@ import { GlLoadingIcon, } from '@gitlab/ui'; import produce from 'immer'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import { workspaceLabelsQueries } from '~/sidebar/constants'; import createLabelMutation from './graphql/create_label.mutation.graphql'; @@ -129,7 +129,7 @@ export default { this.$emit('hideCreateView'); } } catch { - createFlash({ message: errorMessage }); + createAlert({ message: errorMessage }); } this.labelCreateInProgress = false; }, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index 8d3d4d5f86a..1d854505d11 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -1,7 +1,7 @@ <script> import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; import { workspaceLabelsQueries } from '~/sidebar/constants'; @@ -62,7 +62,7 @@ export default { }, update: (data) => data.workspace?.labels?.nodes || [], error() { - createFlash({ message: __('Error fetching labels.') }); + createAlert({ message: __('Error fetching labels.') }); }, }, }, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue index 522fbc07f5e..0e8da7281d8 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue @@ -2,7 +2,7 @@ import { debounce } from 'lodash'; import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql'; import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { IssuableType } from '~/issues/constants'; @@ -151,7 +151,7 @@ export default { return data.workspace?.issuable; }, error() { - createFlash({ message: __('Error fetching labels.') }); + createAlert({ message: __('Error fetching labels.') }); }, subscribeToMore: { document() { @@ -275,7 +275,7 @@ export default { }); }) .catch((error) => - createFlash({ + createAlert({ message: __('An error occurred while updating labels.'), captureError: true, error, diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 9c6c12eac7d..536b2c8a281 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -53,7 +53,7 @@ export default { }, computed: { splitContent() { - return this.content.split('\n'); + return this.content.split(/\r?\n/); }, lineNumbers() { return this.splitContent.length; diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 79021161a65..6151d2ff85c 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -10,7 +10,7 @@ import { GlAvatarLabeled, } from '@gitlab/ui'; import { glEmojiTag } from '~/emoji'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { followUser, unfollowUser } from '~/rest_api'; import { isUserBusy } from '~/set_status_modal/utils'; import Tracking from '~/tracking'; @@ -141,7 +141,7 @@ export default { await followUser(this.user.id); this.$emit('follow'); } catch (error) { - createFlash({ + createAlert({ message: I18N_ERROR_FOLLOW, error, captureError: true, @@ -161,7 +161,7 @@ export default { await unfollowUser(this.user.id); this.$emit('unfollow'); } catch (error) { - createFlash({ + createAlert({ message: I18N_ERROR_UNFOLLOW, error, captureError: true, diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue index e0669b3ed27..a4fb30a03a1 100644 --- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue +++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue @@ -1,6 +1,6 @@ <script> import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { s__ } from '~/locale'; import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; @@ -67,7 +67,7 @@ export default { }, methods: { showError(error) { - createFlash({ + createAlert({ message: this.$options.i18n.apiError, captureError: true, error, diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue index f6d85599dba..0e1975e1c09 100644 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { s__ } from '~/locale'; import ReportSection from '~/reports/components/report_section.vue'; import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants'; @@ -160,7 +160,7 @@ export default { this.fetchCounts(); }, showError(error) { - createFlash({ + createAlert({ message: this.$options.i18n.apiError, captureError: true, error, diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 84095819db9..82fe307bde9 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -98,7 +98,14 @@ $system-note-svg-size: 1rem; border-left: 1px solid $border-color; border-right: 1px solid $border-color; background-color: $white; - padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; + + .timeline-content { + padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding; + } + + .timeline-avatar { + margin: $gl-padding-8 0 0 $gl-padding; + } .timeline-discussion-body { margin-left: 2rem; @@ -252,7 +259,7 @@ $system-note-svg-size: 1rem; } .note-body { - padding: $gl-padding-8; + padding: 0 $gl-padding-8 $gl-padding-8; overflow-x: auto; overflow-y: hidden; @@ -281,7 +288,7 @@ $system-note-svg-size: 1rem; padding: $gl-padding-8 0; margin: $gl-padding 0; background-color: transparent; - font-size: $gl-font-size-sm; + font-size: $gl-font-size; .note-header-info { padding-bottom: 0; @@ -815,17 +822,20 @@ $system-note-svg-size: 1rem; } .note-actions { - align-self: flex-start; justify-content: flex-end; flex-shrink: 1; display: inline-flex; align-items: center; - margin-left: 10px; + margin-left: $gl-padding-8; color: $gray-400; @include notes-media('max', map-get($grid-breakpoints, sm) - 1) { + justify-content: flex-start; float: none; - margin-left: 0; + + .note-actions__mobile-spacer { + flex-grow: 1; + } } } diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb index 2f9edfad12d..7b056c228e6 100644 --- a/app/controllers/concerns/boards_actions.rb +++ b/app/controllers/concerns/boards_actions.rb @@ -8,38 +8,37 @@ module BoardsActions include BoardsResponses before_action :authorize_read_board!, only: [:index, :show] - before_action :boards, only: :index - before_action :board, only: :show + before_action :redirect_to_recent_board, only: [:index] + before_action :board, only: [:index, :show] before_action :push_licensed_features, only: [:index, :show] end def index - respond_with_boards + # if no board exists, create one + @board = board_create_service.execute.payload unless board # rubocop:disable Gitlab/ModuleWithInstanceVariables end def show - # Add / update the board in the recent visits table - board_visit_service.new(parent, current_user).execute(board) if request.format.html? + return render_404 unless board - respond_with_board + # Add / update the board in the recent visits table + board_visit_service.new(parent, current_user).execute(board) end private - # Noop on FOSS - def push_licensed_features + def redirect_to_recent_board + return if !parent.multiple_issue_boards_available? || !latest_visited_board + + redirect_to board_path(latest_visited_board.board) end - def boards - strong_memoize(:boards) do - existing_boards = boards_finder.execute - if existing_boards.any? - existing_boards - else - # if no board exists, create one - [board_create_service.execute.payload] - end - end + def latest_visited_board + @latest_visited_board ||= Boards::VisitsFinder.new(parent, current_user).latest + end + + # Noop on FOSS + def push_licensed_features end def board @@ -48,21 +47,9 @@ module BoardsActions end end - def board_type - board_klass.to_type - end - def board_visit_service Boards::Visits::CreateService end - - def serializer - BoardSerializer.new(current_user: current_user) - end - - def serialize_as_json(resource) - serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) - end end BoardsActions.prepend_mod_with('BoardsActions') diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index eb7392648a1..a1e7eaa7f60 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -60,35 +60,6 @@ module BoardsResponses def authorize_action_for!(resource, ability) return render_403 unless can?(current_user, ability, resource) end - - def respond_with_boards - respond_with(@boards) # rubocop:disable Gitlab/ModuleWithInstanceVariables - end - - def respond_with_board - # rubocop:disable Gitlab/ModuleWithInstanceVariables - return render_404 unless @board - - respond_with(@board) - # rubocop:enable Gitlab/ModuleWithInstanceVariables - end - - def serialize_as_json(resource) - serializer.represent(resource).as_json - end - - def respond_with(resource) - respond_to do |format| - format.html - format.json do - render json: serialize_as_json(resource) - end - end - end - - def serializer - BoardSerializer.new - end end BoardsResponses.prepend_mod_with('BoardsResponses') diff --git a/app/controllers/concerns/multiple_boards_actions.rb b/app/controllers/concerns/multiple_boards_actions.rb deleted file mode 100644 index 685c93fc2a2..00000000000 --- a/app/controllers/concerns/multiple_boards_actions.rb +++ /dev/null @@ -1,93 +0,0 @@ -# frozen_string_literal: true - -module MultipleBoardsActions - include Gitlab::Utils::StrongMemoize - extend ActiveSupport::Concern - - included do - include BoardsActions - - before_action :redirect_to_recent_board, only: [:index] - before_action :authenticate_user!, only: [:recent] - before_action :authorize_create_board!, only: [:create] - before_action :authorize_admin_board!, only: [:create, :update, :destroy] - end - - def recent - recent_visits = ::Boards::VisitsFinder.new(parent, current_user).latest(Board::RECENT_BOARDS_SIZE) - recent_boards = recent_visits.map(&:board) - - render json: serialize_as_json(recent_boards) - end - - def create - response = Boards::CreateService.new(parent, current_user, board_params).execute - - respond_to do |format| - format.json do - board = response.payload - - if response.success? - extra_json = { board_path: board_path(board) } - render json: serialize_as_json(board).merge(extra_json) - else - render json: board.errors, status: :unprocessable_entity - end - end - end - end - - def update - service = Boards::UpdateService.new(parent, current_user, board_params) - - respond_to do |format| - format.json do - if service.execute(board) - extra_json = { board_path: board_path(board) } - render json: serialize_as_json(board).merge(extra_json) - else - render json: board.errors, status: :unprocessable_entity - end - end - end - end - - def destroy - service = Boards::DestroyService.new(parent, current_user) - service.execute(board) - - respond_to do |format| - format.json { head :ok } - format.html { redirect_to boards_path, status: :found } - end - end - - private - - def redirect_to_recent_board - return unless board_type == Board.to_type - return if request.format.json? || !parent.multiple_issue_boards_available? || !latest_visited_board - - redirect_to board_path(latest_visited_board.board) - end - - def latest_visited_board - @latest_visited_board ||= Boards::VisitsFinder.new(parent, current_user).latest - end - - def authorize_create_board! - check_multiple_group_issue_boards_available! if group? - end - - def authorize_admin_board! - return render_404 unless can?(current_user, :admin_issue_board, parent) - end - - def serializer - BoardSerializer.new(current_user: current_user) - end - - def serialize_as_json(resource) - serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) - end -end diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index e64d838b7d1..4fa642f7446 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -20,16 +20,6 @@ class Groups::BoardsController < Groups::ApplicationController private - def board_klass - Board - end - - def boards_finder - strong_memoize :boards_finder do - Boards::BoardsFinder.new(parent, current_user) - end - end - def board_finder strong_memoize :board_finder do Boards::BoardsFinder.new(parent, current_user, board_id: params[:id]) diff --git a/app/controllers/jira_connect/public_keys_controller.rb b/app/controllers/jira_connect/public_keys_controller.rb new file mode 100644 index 00000000000..b3144993edb --- /dev/null +++ b/app/controllers/jira_connect/public_keys_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module JiraConnect + class PublicKeysController < ::ApplicationController + # This is not inheriting from JiraConnect::Application controller because + # it doesn't need to handle JWT authentication. + + feature_category :integrations + + skip_before_action :authenticate_user! + + def show + return render_404 if Feature.disabled?(:jira_connect_oauth_self_managed) || !Gitlab.com? + + render plain: public_key.key + end + + private + + def public_key + JiraConnect::PublicKey.find(params[:id]) + end + end +end diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 82b35a22669..082ebca40a3 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Projects::BoardsController < Projects::ApplicationController - include MultipleBoardsActions + include BoardsActions include IssuableCollections before_action :check_issues_available! @@ -20,16 +20,6 @@ class Projects::BoardsController < Projects::ApplicationController private - def board_klass - Board - end - - def boards_finder - strong_memoize :boards_finder do - Boards::BoardsFinder.new(parent, current_user) - end - end - def board_finder strong_memoize :board_finder do Boards::BoardsFinder.new(parent, current_user, board_id: params[:id]) diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index 896c88cf8c3..43952a2efe4 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -41,9 +41,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController end def create - @domain = @project.pages_domains.create(create_params) + @domain = PagesDomains::CreateService.new(@project, current_user, create_params).execute - if @domain.valid? + if @domain&.persisted? redirect_to project_pages_domain_path(@project, @domain) else render 'new' @@ -63,7 +63,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController end def destroy - @domain.destroy + PagesDomains::DeleteService + .new(@project, current_user) + .execute(@domain) respond_to do |format| format.html do diff --git a/app/events/pages_domains/pages_domain_created_event.rb b/app/events/pages_domains/pages_domain_created_event.rb new file mode 100644 index 00000000000..a86718f4681 --- /dev/null +++ b/app/events/pages_domains/pages_domain_created_event.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module PagesDomains + class PagesDomainCreatedEvent < ::Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'properties' => { + 'project_id' => { 'type' => 'integer' }, + 'namespace_id' => { 'type' => 'integer' }, + 'root_namespace_id' => { 'type' => 'integer' }, + 'domain' => { 'type' => 'string' } + }, + 'required' => %w[project_id namespace_id root_namespace_id] + } + end + end +end diff --git a/app/events/pages_domains/pages_domain_deleted_event.rb b/app/events/pages_domains/pages_domain_deleted_event.rb new file mode 100644 index 00000000000..7fe165a7249 --- /dev/null +++ b/app/events/pages_domains/pages_domain_deleted_event.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module PagesDomains + class PagesDomainDeletedEvent < ::Gitlab::EventStore::Event + def schema + { + 'type' => 'object', + 'properties' => { + 'project_id' => { 'type' => 'integer' }, + 'namespace_id' => { 'type' => 'integer' }, + 'root_namespace_id' => { 'type' => 'integer' }, + 'domain' => { 'type' => 'string' } + }, + 'required' => %w[project_id namespace_id root_namespace_id] + } + end + end +end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index f98e70e41d8..a14b51d42bf 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -2,7 +2,7 @@ module BoardsHelper def board - @board ||= @board || @boards.first + @board end def board_data @@ -125,14 +125,6 @@ module BoardsHelper def can_admin_issue? can?(current_user, :admin_issue, current_board_parent) end - - def serializer - CurrentBoardSerializer.new - end - - def current_board_json - serializer.represent(board).as_json - end end BoardsHelper.prepend_mod_with('BoardsHelper') diff --git a/app/models/jira_connect/public_key.rb b/app/models/jira_connect/public_key.rb new file mode 100644 index 00000000000..8959884861b --- /dev/null +++ b/app/models/jira_connect/public_key.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module JiraConnect + class PublicKey + # Public keys are created with JWT tokens via JiraConnect::CreateAsymmetricJwtService + # They need to be available for third party applications to verify the token. + # This should happen right after the application received the token so public keys + # only need to exist for a few minutes. + REDIS_EXPIRY_TIME = 5.minutes.to_i.freeze + + attr_reader :key, :uuid + + def self.create!(key:) + new(key: key, uuid: Gitlab::UUID.v5(SecureRandom.hex)).save! + end + + def self.find(uuid) + Gitlab::Redis::SharedState.with do |redis| + key = redis.get(redis_key(uuid)) + + raise ActiveRecord::RecordNotFound if key.nil? + + new(key: key, uuid: uuid) + end + end + + def initialize(key:, uuid:) + key = OpenSSL::PKey.read(key) unless key.is_a?(OpenSSL::PKey::RSA) + + @key = key.to_s + @uuid = uuid + rescue OpenSSL::PKey::PKeyError + raise ArgumentError, 'Invalid public key' + end + + def save! + Gitlab::Redis::SharedState.with do |redis| + redis.set(self.class.redis_key(uuid), key, ex: REDIS_EXPIRY_TIME) + end + + self + end + + def self.redis_key(uuid) + "JiraConnect:public_key:uuid=#{uuid}" + end + end +end diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb index 4496d5f2507..23813fa138f 100644 --- a/app/models/jira_connect_installation.rb +++ b/app/models/jira_connect_installation.rb @@ -33,4 +33,20 @@ class JiraConnectInstallation < ApplicationRecord instance_url end + + def audience_url + return unless proxy? + + Gitlab::Utils.append_path(instance_url, '/-/jira_connect') + end + + def audience_installed_event_url + return unless proxy? + + Gitlab::Utils.append_path(instance_url, '/-/jira_connect/events/installed') + end + + def proxy? + instance_url.present? + end end diff --git a/app/serializers/board_serializer.rb b/app/serializers/board_serializer.rb deleted file mode 100644 index 70a4c9ae282..00000000000 --- a/app/serializers/board_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class BoardSerializer < BaseSerializer - entity BoardSimpleEntity -end diff --git a/app/serializers/board_simple_entity.rb b/app/serializers/board_simple_entity.rb deleted file mode 100644 index ab625490966..00000000000 --- a/app/serializers/board_simple_entity.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class BoardSimpleEntity < Grape::Entity - expose :id - expose :name -end - -BoardSimpleEntity.prepend_mod_with('BoardSimpleEntity') diff --git a/app/serializers/current_board_entity.rb b/app/serializers/current_board_entity.rb deleted file mode 100644 index 530f7f5dea3..00000000000 --- a/app/serializers/current_board_entity.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class CurrentBoardEntity < Grape::Entity - expose :id - expose :name - expose :hide_backlog_list - expose :hide_closed_list -end - -CurrentBoardEntity.prepend_mod_with('CurrentBoardEntity') diff --git a/app/serializers/current_board_serializer.rb b/app/serializers/current_board_serializer.rb deleted file mode 100644 index c58c77194f2..00000000000 --- a/app/serializers/current_board_serializer.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -class CurrentBoardSerializer < BaseSerializer - entity CurrentBoardEntity -end diff --git a/app/services/jira_connect/create_asymmetric_jwt_service.rb b/app/services/jira_connect/create_asymmetric_jwt_service.rb new file mode 100644 index 00000000000..71aba6feddd --- /dev/null +++ b/app/services/jira_connect/create_asymmetric_jwt_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module JiraConnect + class CreateAsymmetricJwtService + ARGUMENT_ERROR_MESSAGE = 'jira_connect_installation is not a proxy installation' + + def initialize(jira_connect_installation) + raise ArgumentError, ARGUMENT_ERROR_MESSAGE unless jira_connect_installation.proxy? + + @jira_connect_installation = jira_connect_installation + end + + def execute + JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers) + end + + private + + def jwt_claims + { aud: aud_claim, iss: iss_claim, qsh: qsh_claim } + end + + def aud_claim + @jira_connect_installation.audience_url + end + + def iss_claim + @jira_connect_installation.client_key + end + + def qsh_claim + Atlassian::Jwt.create_query_string_hash( + @jira_connect_installation.audience_installed_event_url, + 'POST', + @jira_connect_installation.audience_url + ) + end + + def private_key + @private_key ||= OpenSSL::PKey::RSA.generate(3072) + end + + def public_key_storage + @public_key_storage ||= JiraConnect::PublicKey.create!(key: private_key.public_key) + end + + def jwt_headers + { kid: public_key_storage.uuid } + end + end +end diff --git a/app/services/pages_domains/create_service.rb b/app/services/pages_domains/create_service.rb new file mode 100644 index 00000000000..1f771ca3a05 --- /dev/null +++ b/app/services/pages_domains/create_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module PagesDomains + class CreateService < BaseService + def execute + return unless authorized? + + domain = project.pages_domains.create(params) + + publish_event(domain) if domain.persisted? + + domain + end + + private + + def authorized? + current_user.can?(:update_pages, project) + end + + def publish_event(domain) + event = PagesDomainCreatedEvent.new( + data: { + project_id: project.id, + namespace_id: project.namespace_id, + root_namespace_id: project.root_namespace.id, + domain: domain.domain + } + ) + + Gitlab::EventStore.publish(event) + end + end +end diff --git a/app/services/pages_domains/delete_service.rb b/app/services/pages_domains/delete_service.rb new file mode 100644 index 00000000000..af69e1845a9 --- /dev/null +++ b/app/services/pages_domains/delete_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module PagesDomains + class DeleteService < BaseService + def execute(domain) + return unless authorized? + + domain.destroy + + publish_event(domain) + end + + private + + def authorized? + current_user.can?(:update_pages, project) + end + + def publish_event(domain) + event = PagesDomainDeletedEvent.new( + data: { + project_id: project.id, + namespace_id: project.namespace_id, + root_namespace_id: project.root_namespace.id, + domain: domain.domain + } + ) + + Gitlab::EventStore.publish(event) + end + end +end diff --git a/app/services/projects/container_repository/cleanup_tags_base_service.rb b/app/services/projects/container_repository/cleanup_tags_base_service.rb index 8ea4ae4830a..5393c2c080d 100644 --- a/app/services/projects/container_repository/cleanup_tags_base_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb @@ -60,23 +60,6 @@ module Projects service.execute(container_repository) end - def can_destroy? - return true if container_expiration_policy - - can?(current_user, :destroy_container_image, project) - end - - def valid_regex? - %w[name_regex_delete name_regex name_regex_keep].each do |param_name| - regex = params[param_name] - ::Gitlab::UntrustedRegexp.new(regex) unless regex.blank? - end - true - rescue RegexpError => e - ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id) - false - end - def older_than params['older_than'] end diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index 285c3e252ef..0b31ac0c877 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -2,101 +2,58 @@ module Projects module ContainerRepository - class CleanupTagsService < CleanupTagsBaseService - def initialize(container_repository:, current_user: nil, params: {}) - super - - @params = params.dup - @counts = { cached_tags_count: 0 } - end - + class CleanupTagsService < BaseContainerRepositoryService def execute return error('access denied') unless can_destroy? return error('invalid regex') unless valid_regex? - tags = container_repository.tags - @counts[:original_size] = tags.size - - filter_out_latest!(tags) - filter_by_name!(tags) - - tags = truncate(tags) - populate_from_cache(tags) - - tags = filter_keep_n(tags) - tags = filter_by_older_than(tags) - - @counts[:before_delete_size] = tags.size - - delete_tags(tags).merge(@counts).tap do |result| - result[:deleted_size] = result[:deleted]&.size - - result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size] - end + cleanup_tags_service_class.new(container_repository: container_repository, current_user: current_user, params: params) + .execute end private - def filter_keep_n(tags) - tags, tags_to_keep = partition_by_keep_n(tags) - - cache_tags(tags_to_keep) - - tags - end - - def filter_by_older_than(tags) - tags, tags_to_keep = partition_by_older_than(tags) - - cache_tags(tags_to_keep) - - tags + def cleanup_tags_service_class + log_data = { + container_repository_id: container_repository.id, + container_repository_path: container_repository.path, + project_id: project.id + } + + if use_gitlab_service? + log_info(log_data.merge(gitlab_cleanup_tags_service: true)) + ::Projects::ContainerRepository::Gitlab::CleanupTagsService + else + log_info(log_data.merge(third_party_cleanup_tags_service: true)) + ::Projects::ContainerRepository::ThirdParty::CleanupTagsService + end end - def pushed_at(tag) - tag.created_at + def use_gitlab_service? + Feature.enabled?(:container_registry_new_cleanup_service, project) && + container_repository.migrated? && + container_repository.gitlab_api_client.supports_gitlab_api? end - def truncate(tags) - @counts[:before_truncate_size] = tags.size - @counts[:after_truncate_size] = tags.size - - return tags if max_list_size == 0 - - # truncate the list to make sure that after the #filter_keep_n - # execution, the resulting list will be max_list_size - truncated_size = max_list_size + keep_n_as_integer - - return tags if tags.size <= truncated_size + def can_destroy? + return true if container_expiration_policy - tags = tags.sample(truncated_size) - @counts[:after_truncate_size] = tags.size - tags + can?(current_user, :destroy_container_image, project) end - def populate_from_cache(tags) - @counts[:cached_tags_count] = cache.populate(tags) if caching_enabled? - end - - def cache_tags(tags) - cache.insert(tags, older_than_in_seconds) if caching_enabled? - end - - def cache - strong_memoize(:cache) do - ::Gitlab::ContainerRepository::Tags::Cache.new(container_repository) + def valid_regex? + %w[name_regex_delete name_regex name_regex_keep].each do |param_name| + regex = params[param_name] + ::Gitlab::UntrustedRegexp.new(regex) unless regex.blank? end + true + rescue RegexpError => e + ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id) + false end - def caching_enabled? - result = ::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_caching && - container_expiration_policy && - older_than.present? - !!result - end - - def max_list_size - ::Gitlab::CurrentSettings.current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i + def container_expiration_policy + params['container_expiration_policy'] end end end diff --git a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb index 81bb94c867a..e947e9575e2 100644 --- a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb @@ -14,9 +14,6 @@ module Projects end def execute - return error('access denied') unless can_destroy? - return error('invalid regex') unless valid_regex? - with_timeout do |start_time, result| container_repository.each_tags_page(page_size: TAGS_PAGE_SIZE) do |tags| execute_for_tags(tags, result) diff --git a/app/services/projects/container_repository/third_party/cleanup_tags_service.rb b/app/services/projects/container_repository/third_party/cleanup_tags_service.rb new file mode 100644 index 00000000000..c6335629b52 --- /dev/null +++ b/app/services/projects/container_repository/third_party/cleanup_tags_service.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Projects + module ContainerRepository + module ThirdParty + class CleanupTagsService < CleanupTagsBaseService + def initialize(container_repository:, current_user: nil, params: {}) + super + + @params = params.dup + @counts = { cached_tags_count: 0 } + end + + def execute + tags = container_repository.tags + @counts[:original_size] = tags.size + + filter_out_latest!(tags) + filter_by_name!(tags) + + tags = truncate(tags) + populate_from_cache(tags) + + tags = filter_keep_n(tags) + tags = filter_by_older_than(tags) + + @counts[:before_delete_size] = tags.size + + delete_tags(tags).merge(@counts).tap do |result| + result[:deleted_size] = result[:deleted]&.size + + result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size] + end + end + + private + + def filter_keep_n(tags) + tags, tags_to_keep = partition_by_keep_n(tags) + + cache_tags(tags_to_keep) + + tags + end + + def filter_by_older_than(tags) + tags, tags_to_keep = partition_by_older_than(tags) + + cache_tags(tags_to_keep) + + tags + end + + def pushed_at(tag) + tag.created_at + end + + def truncate(tags) + @counts[:before_truncate_size] = tags.size + @counts[:after_truncate_size] = tags.size + + return tags if max_list_size == 0 + + # truncate the list to make sure that after the #filter_keep_n + # execution, the resulting list will be max_list_size + truncated_size = max_list_size + keep_n_as_integer + + return tags if tags.size <= truncated_size + + tags = tags.sample(truncated_size) + @counts[:after_truncate_size] = tags.size + tags + end + + def populate_from_cache(tags) + @counts[:cached_tags_count] = cache.populate(tags) if caching_enabled? + end + + def cache_tags(tags) + cache.insert(tags, older_than_in_seconds) if caching_enabled? + end + + def cache + strong_memoize(:cache) do + ::Gitlab::ContainerRepository::Tags::Cache.new(container_repository) + end + end + + def caching_enabled? + result = current_application_settings.container_registry_expiration_policies_caching && + container_expiration_policy && + older_than.present? + !!result + end + + def max_list_size + current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i + end + + def current_application_settings + ::Gitlab::CurrentSettings.current_application_settings + end + end + end + end +end diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml index a9f5c560b41..3ccf3ef4f2a 100644 --- a/app/views/admin/users/_projects.html.haml +++ b/app/views/admin/users/_projects.html.haml @@ -1,13 +1,17 @@ - if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? - .card.contributed-projects - .card-header= _('Projects contributed to') - = render 'shared/projects/list', - projects: contributed_projects.sort_by(&:star_count).reverse, - projects_limit: 5, stars: true, avatar: false + = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }) do |c| + - c.header do + = _('Projects contributed to') + - c.body do + = render 'shared/projects/list', + projects: contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false - if local_assigns.has_key?(:projects) && projects.present? - .card - .card-header= _('Personal projects') - = render 'shared/projects/list', - projects: projects.sort_by(&:star_count).reverse, - projects_limit: 10, stars: true, avatar: false + = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }) do |c| + - c.header do + = _('Personal projects') + - c.body do + = render 'shared/projects/list', + projects: projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false diff --git a/app/views/groups/boards/index.html.haml b/app/views/groups/boards/index.html.haml index bb56769bd3f..e5b5f6404bb 100644 --- a/app/views/groups/boards/index.html.haml +++ b/app/views/groups/boards/index.html.haml @@ -1 +1 @@ -= render "shared/boards/show", board: @boards.first += render "shared/boards/show", board: @board diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 91a66edd3b4..1ca85812f68 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -2,45 +2,49 @@ - page_title _("Projects") - @content_class = "limit-container-width" unless fluid_layout -.card.gl-mt-3.js-search-settings-section - .card-header - %strong= @group.name - projects: += render Pajamas::CardComponent.new(card_options: { class: 'gl-mt-3 js-search-settings-section' }, header_options: { class: 'gl-display-flex' }, body_options: { class: 'gl-py-0' }) do |c| + - c.header do + .gl-flex-grow-1 + %strong= @group.name + projects: - if can? current_user, :admin_group, @group .controls = link_to new_project_path(namespace_id: @group.id), class: "btn gl-button btn-sm btn-confirm" do New project - %ul.projects-list.content-list.group-settings-projects - - @projects.each do |project| - %li.project-row{ class: ('no-description' if project.description.blank?) } - .controls - = link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button" - = link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button" - = render 'delete_project_button', project: project + - c.body do + %ul.projects-list.content-list.group-settings-projects + - @projects.each do |project| + %li.project-row{ class: ('no-description' if project.description.blank?) } + .controls + = render Pajamas::ButtonComponent.new(href: project_project_members_path(project), button_options: { id: "edit_#{dom_id(project)}" }) do + = _('Members') + = render Pajamas::ButtonComponent.new(href: edit_project_path(project), button_options: { id: "edit_#{dom_id(project)}" }) do + = _('Edit') + = render 'delete_project_button', project: project - .stats - = gl_badge_tag storage_counter(project.statistics&.storage_size) - = render 'project_badges', project: project + .stats + = gl_badge_tag storage_counter(project.statistics&.storage_size) + = render 'project_badges', project: project - .title - = link_to project_path(project), class: 'js-prefetch-document' do - .dash-project-avatar - .avatar-container.rect-avatar.s40 - = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40) - %span.project-full-name - %span.namespace-name - - if project.namespace - = project.namespace.human_name - \/ - %span.project-name - = project.name - %span{ class: visibility_level_color(project.visibility_level) } - = visibility_level_icon(project.visibility_level) + .title + = link_to project_path(project), class: 'js-prefetch-document' do + .dash-project-avatar + .avatar-container.rect-avatar.s40 + = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40) + %span.project-full-name + %span.namespace-name + - if project.namespace + = project.namespace.human_name + \/ + %span.project-name + = project.name + %span{ class: visibility_level_color(project.visibility_level) } + = visibility_level_icon(project.visibility_level) - - if project.description.present? - .description - = markdown_field(project, :description) - - if @projects.blank? - .nothing-here-block This group has no projects yet + - if project.description.present? + .description + = markdown_field(project, :description) + - if @projects.blank? + .nothing-here-block This group has no projects yet = paginate @projects, theme: "gitlab" diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml index bb56769bd3f..e5b5f6404bb 100644 --- a/app/views/projects/boards/index.html.haml +++ b/app/views/projects/boards/index.html.haml @@ -1 +1 @@ -= render "shared/boards/show", board: @boards.first += render "shared/boards/show", board: @board diff --git a/app/views/projects/jobs/_user.html.haml b/app/views/projects/jobs/_user.html.haml index 90ce581a903..03cbabb0c2a 100644 --- a/app/views/projects/jobs/_user.html.haml +++ b/app/views/projects/jobs/_user.html.haml @@ -1,7 +1,7 @@ by %a{ href: user_path(@build.user) } %span.d-none.d-sm-inline - = image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24" + = render Pajamas::AvatarComponent.new(@build.user, size: 24, alt: "") %strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } } = @build.user.name %strong.d-inline.d-sm-none= @build.user.to_reference |