Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-28 18:09:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-28 18:09:17 +0300
commit1bb7f81e238569fd0fe2b0c4385f1015407a2d59 (patch)
tree768c7d44fa3ed641a7e26fdf9db61422902e8294 /app
parenteb3a23aaaa99ef8ae08c7b440fad676e3c71a1af (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/diff.js4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js10
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js4
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue6
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue4
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue4
-rw-r--r--app/assets/javascripts/environments/components/new_environment.vue4
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js8
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue1
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue84
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue86
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/store/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue4
-rw-r--r--app/assets/stylesheets/pages/notes.scss22
-rw-r--r--app/controllers/concerns/boards_actions.rb47
-rw-r--r--app/controllers/concerns/boards_responses.rb29
-rw-r--r--app/controllers/concerns/multiple_boards_actions.rb93
-rw-r--r--app/controllers/groups/boards_controller.rb10
-rw-r--r--app/controllers/jira_connect/public_keys_controller.rb24
-rw-r--r--app/controllers/projects/boards_controller.rb12
-rw-r--r--app/controllers/projects/pages_domains_controller.rb8
-rw-r--r--app/events/pages_domains/pages_domain_created_event.rb18
-rw-r--r--app/events/pages_domains/pages_domain_deleted_event.rb18
-rw-r--r--app/helpers/boards_helper.rb10
-rw-r--r--app/models/jira_connect/public_key.rb48
-rw-r--r--app/models/jira_connect_installation.rb16
-rw-r--r--app/serializers/board_serializer.rb5
-rw-r--r--app/serializers/board_simple_entity.rb8
-rw-r--r--app/serializers/current_board_entity.rb10
-rw-r--r--app/serializers/current_board_serializer.rb5
-rw-r--r--app/services/jira_connect/create_asymmetric_jwt_service.rb51
-rw-r--r--app/services/pages_domains/create_service.rb34
-rw-r--r--app/services/pages_domains/delete_service.rb32
-rw-r--r--app/services/projects/container_repository/cleanup_tags_base_service.rb17
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb111
-rw-r--r--app/services/projects/container_repository/gitlab/cleanup_tags_service.rb3
-rw-r--r--app/services/projects/container_repository/third_party/cleanup_tags_service.rb106
-rw-r--r--app/views/admin/users/_projects.html.haml24
-rw-r--r--app/views/groups/boards/index.html.haml2
-rw-r--r--app/views/groups/projects.html.haml70
-rw-r--r--app/views/projects/boards/index.html.haml2
-rw-r--r--app/views/projects/jobs/_user.html.haml2
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