diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-28 15:07:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-28 15:07:50 +0300 |
commit | eb3a23aaaa99ef8ae08c7b440fad676e3c71a1af (patch) | |
tree | bb74f64f73f4a20d4b4e3443c3e7defd4733a78d /app | |
parent | f3cfb235c76426ce5a18003bb80ba625097bf1d0 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
51 files changed, 363 insertions, 252 deletions
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js index 667aa878261..5b5abbdf50b 100644 --- a/app/assets/javascripts/api/projects_api.js +++ b/app/assets/javascripts/api/projects_api.js @@ -5,6 +5,7 @@ import { buildApiUrl } from './api_utils'; const PROJECTS_PATH = '/api/:version/projects.json'; const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id'; const PROJECT_REPOSITORY_SIZE_PATH = '/api/:version/projects/:id/repository_size'; +const PROJECT_TRANSFER_LOCATIONS_PATH = 'api/:version/projects/:id/transfer_locations'; export function getProjects(query, options, callback = () => {}) { const url = buildApiUrl(PROJECTS_PATH); @@ -42,3 +43,10 @@ export function updateRepositorySize(projectPath) { ); return axios.post(url); } + +export const getTransferLocations = (projectId, params = {}) => { + const url = buildApiUrl(PROJECT_TRANSFER_LOCATIONS_PATH).replace(':id', projectId); + const defaultParams = { per_page: DEFAULT_PER_PAGE }; + + return axios.get(url, { params: { ...defaultParams, ...params } }); +}; diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js index df5214ea7ab..75e4ae63c18 100644 --- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js +++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js @@ -22,7 +22,11 @@ export const draftsPerFileHashAndLine = (state) => acc[draft.file_hash] = {}; } - acc[draft.file_hash][draft.line_code] = draft; + if (!acc[draft.file_hash][draft.line_code]) { + acc[draft.file_hash][draft.line_code] = []; + } + + acc[draft.file_hash][draft.line_code].push(draft); } return acc; @@ -61,18 +65,15 @@ export const shouldRenderDraftRowInDiscussion = (state, getters) => (discussionI export const draftForDiscussion = (state, getters) => (discussionId) => getters.draftsPerDiscussionId[discussionId] || {}; -export const draftForLine = (state, getters) => (diffFileSha, line, side = null) => { +export const draftsForLine = (state, getters) => (diffFileSha, line, side = null) => { const draftsForFile = getters.draftsPerFileHashAndLine[diffFileSha]; - const key = side !== null ? parallelLineKey(line, side) : line.line_code; + const showDraftsForThisSide = showDraftOnSide(line, side); - if (draftsForFile) { - const draft = draftsForFile[key]; - if (draft && showDraftOnSide(line, side)) { - return draft; - } + if (showDraftsForThisSide && draftsForFile?.[key]) { + return draftsForFile[key]; } - return {}; + return []; }; export const draftsForFile = (state) => (diffFileSha) => diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js index f610ac979ca..7732badde34 100644 --- a/app/assets/javascripts/diffs/components/diff_row_utils.js +++ b/app/assets/javascripts/diffs/components/diff_row_utils.js @@ -108,7 +108,7 @@ export const mapParallel = (content) => (line) => { ...left, renderDiscussion: hasExpandedDiscussionOnLeft, hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line), - lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'), + lineDrafts: content.draftsForLine(content.diffFile.file_hash, line, 'left'), hasCommentForm: left.hasForm, isConflictMarker: line.left.type === CONFLICT_MARKER_OUR || line.left.type === CONFLICT_MARKER_THEIR, @@ -123,7 +123,7 @@ export const mapParallel = (content) => (line) => { hasExpandedDiscussionOnRight && right.type && right.type !== EXPANDED_LINE_TYPE, ), hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line), - lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'), + lineDrafts: content.draftsForLine(content.diffFile.file_hash, line, 'right'), hasCommentForm: Boolean(right.hasForm && right.type && right.type !== EXPANDED_LINE_TYPE), emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR }, addCommentTooltip: addCommentTooltip(line.right), @@ -145,7 +145,7 @@ export const mapParallel = (content) => (line) => { lineCode: lineCode(line), isMetaLineLeft: isMetaLine(left?.type), isMetaLineRight: isMetaLine(right?.type), - draftRowClasses: left?.lineDraft > 0 || right?.lineDraft > 0 ? '' : 'js-temp-notes-holder', + draftRowClasses: left?.hasDraft || right?.hasDraft ? '' : 'js-temp-notes-holder', renderCommentRow, commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder', }; diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue index 91bf3283379..5ea118afe78 100644 --- a/app/assets/javascripts/diffs/components/diff_view.vue +++ b/app/assets/javascripts/diffs/components/diff_view.vue @@ -177,6 +177,12 @@ export default { getCodeQualityLine(line) { return (line.left ?? line.right)?.codequality?.[0]?.line; }, + lineDrafts(line, side) { + return (line[side]?.lineDrafts || []).filter((entry) => entry.isDraft); + }, + lineHasDrafts(line, side) { + return this.lineDrafts(line, side).length > 0; + }, }, userColorScheme: window.gon.user_color_scheme, }; @@ -297,19 +303,19 @@ export default { class="diff-grid-drafts diff-tr notes_holder" > <div - v-if="!inline || (line.left && line.left.lineDraft.isDraft)" + v-if="!inline || lineHasDrafts(line, 'left')" class="diff-td notes-content parallel old" > - <div v-if="line.left && line.left.lineDraft.isDraft" class="content"> - <draft-note :draft="line.left.lineDraft" :line="line.left" /> + <div v-for="draft in lineDrafts(line, 'left')" :key="draft.id" class="content"> + <draft-note :draft="draft" :line="line.left" /> </div> </div> <div - v-if="!inline || (line.right && line.right.lineDraft.isDraft)" + v-if="!inline || lineHasDrafts(line, 'right')" class="diff-td notes-content parallel new" > - <div v-if="line.right && line.right.lineDraft.isDraft" class="content"> - <draft-note :draft="line.right.lineDraft" :line="line.right" /> + <div v-for="draft in lineDrafts(line, 'right')" :key="draft.id" class="content"> + <draft-note :draft="draft" :line="line.right" /> </div> </div> </div> diff --git a/app/assets/javascripts/diffs/mixins/draft_comments.js b/app/assets/javascripts/diffs/mixins/draft_comments.js index 693b4a84694..d41bb160e96 100644 --- a/app/assets/javascripts/diffs/mixins/draft_comments.js +++ b/app/assets/javascripts/diffs/mixins/draft_comments.js @@ -5,7 +5,7 @@ export default { ...mapGetters('batchComments', [ 'shouldRenderDraftRow', 'shouldRenderParallelDraftRow', - 'draftForLine', + 'draftsForLine', 'draftsForFile', 'hasParallelDraftLeft', 'hasParallelDraftRight', diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue index c13753da00b..8299efed0a4 100644 --- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue +++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue @@ -1,13 +1,14 @@ <script> -import { GlFormGroup } from '@gitlab/ui'; -import produce from 'immer'; +import { GlFormGroup, GlAlert } from '@gitlab/ui'; +import { debounce } from 'lodash'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { getTransferLocations } from '~/api/projects_api'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import searchNamespacesWhereUserCanTransferProjects from '../graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql'; - -const GROUPS_PER_PAGE = 25; +import { s__, __ } from '~/locale'; +import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql'; export default { name: 'TransferProjectForm', @@ -15,7 +16,15 @@ export default { GlFormGroup, NamespaceSelect, ConfirmDanger, + GlAlert, + }, + i18n: { + errorMessage: s__( + 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.', + ), + alertDismissAlert: __('Dismiss'), }, + inject: ['projectId'], props: { confirmationPhrase: { type: String, @@ -26,93 +35,131 @@ export default { required: true, }, }, - apollo: { - currentUser: { - query: searchNamespacesWhereUserCanTransferProjects, - debounce: DEBOUNCE_DELAY, - variables() { - return { - search: this.searchTerm, - after: null, - first: GROUPS_PER_PAGE, - }; - }, - result() { - this.isLoadingMoreGroups = false; - this.isSearchLoading = false; - }, - }, - }, data() { return { - currentUser: {}, + userNamespaces: [], + groupNamespaces: [], + initialNamespacesLoaded: false, selectedNamespace: null, - isLoadingMoreGroups: false, + hasError: false, + isLoading: false, isSearchLoading: false, searchTerm: '', + page: 1, + totalPages: 1, }; }, computed: { hasSelectedNamespace() { return Boolean(this.selectedNamespace?.id); }, - groupNamespaces() { - return this.currentUser.groups?.nodes?.map(this.formatNamespace) || []; - }, - userNamespaces() { - const { namespace } = this.currentUser; - - return namespace ? [this.formatNamespace(namespace)] : []; - }, hasNextPageOfGroups() { - return this.currentUser.groups?.pageInfo?.hasNextPage || false; + return this.page < this.totalPages; }, }, methods: { + async handleShow() { + if (this.initialNamespacesLoaded) { + return; + } + + this.isLoading = true; + + [this.groupNamespaces, this.userNamespaces] = await Promise.all([ + this.getGroupNamespaces(), + this.getUserNamespaces(), + ]); + + this.isLoading = false; + this.initialNamespacesLoaded = true; + }, handleSelect(selectedNamespace) { this.selectedNamespace = selectedNamespace; this.$emit('selectNamespace', selectedNamespace.id); }, - handleLoadMoreGroups() { - this.isLoadingMoreGroups = true; + async getGroupNamespaces() { + try { + const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, { + page: this.page, + search: this.searchTerm, + }); + + const { totalPages } = parseIntPagination(normalizeHeaders(headers)); + this.totalPages = totalPages; - this.$apollo.queries.currentUser.fetchMore({ - variables: { - after: this.currentUser.groups.pageInfo.endCursor, - first: GROUPS_PER_PAGE, - }, - updateQuery( - previousResult, + return groupNamespaces.map(({ id, full_name: humanName }) => ({ + id, + humanName, + })); + } catch (error) { + this.hasError = true; + + return []; + } + }, + async getUserNamespaces() { + try { + const { + data: { + currentUser: { namespace }, + }, + } = await this.$apollo.query({ + query: currentUserNamespace, + }); + + if (!namespace) { + return []; + } + + return [ { - fetchMoreResult: { - currentUser: { groups: newGroups }, - }, + id: getIdFromGraphQLId(namespace.id), + humanName: namespace.fullName, }, - ) { - const previousGroups = previousResult.currentUser.groups; + ]; + } catch (error) { + this.hasError = true; - return produce(previousResult, (draftData) => { - draftData.currentUser.groups.nodes = [...previousGroups.nodes, ...newGroups.nodes]; - draftData.currentUser.groups.pageInfo = newGroups.pageInfo; - }); - }, - }); + return []; + } }, - handleSearch(searchTerm) { + async handleLoadMoreGroups() { + this.isLoading = true; + this.page += 1; + + const groupNamespaces = await this.getGroupNamespaces(); + this.groupNamespaces.push(...groupNamespaces); + + this.isLoading = false; + }, + debouncedSearch: debounce(async function debouncedSearch() { this.isSearchLoading = true; + + this.groupNamespaces = await this.getGroupNamespaces(); + + this.isSearchLoading = false; + }, DEBOUNCE_DELAY), + handleSearch(searchTerm) { this.searchTerm = searchTerm; + this.page = 1; + + this.debouncedSearch(); }, - formatNamespace({ id, fullName }) { - return { - id: getIdFromGraphQLId(id), - humanName: fullName, - }; + handleAlertDismiss() { + this.hasError = false; }, }, }; </script> <template> <div> + <gl-alert + v-if="hasError" + variant="danger" + :dismiss-label="$options.i18n.alertDismissLabel" + @dismiss="handleAlertDismiss" + >{{ $options.i18n.errorMessage }}</gl-alert + > <gl-form-group> <namespace-select data-testid="transfer-project-namespace" @@ -121,12 +168,13 @@ export default { :user-namespaces="userNamespaces" :selected-namespace="selectedNamespace" :has-next-page-of-groups="hasNextPageOfGroups" - :is-loading-more-groups="isLoadingMoreGroups" + :is-loading="isLoading" :is-search-loading="isSearchLoading" :should-filter-namespaces="false" @select="handleSelect" @load-more-groups="handleLoadMoreGroups" @search="handleSearch" + @show="handleShow" /> </gl-form-group> <confirm-danger diff --git a/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql new file mode 100644 index 00000000000..7ae6ee1428b --- /dev/null +++ b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql @@ -0,0 +1,9 @@ +query currentUserNamespace { + currentUser { + id + namespace { + id + fullName + } + } +} diff --git a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql deleted file mode 100644 index d4bcb8c869c..00000000000 --- a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql +++ /dev/null @@ -1,24 +0,0 @@ -#import "~/graphql_shared/fragments/page_info.fragment.graphql" - -query searchNamespacesWhereUserCanTransferProjects( - $search: String = "" - $after: String = "" - $first: Int = null -) { - currentUser { - id - groups(permissionScope: TRANSFER_PROJECTS, search: $search, after: $after, first: $first) { - nodes { - id - fullName - } - pageInfo { - ...PageInfo - } - } - namespace { - id - fullName - } - } -} diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js index bc1aff640d2..89c158a9ba8 100644 --- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js +++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js @@ -12,6 +12,7 @@ export default () => { Vue.use(VueApollo); const { + projectId, targetFormId = null, targetHiddenInputId = null, buttonText: confirmButtonText = '', @@ -26,6 +27,7 @@ export default () => { }), provide: { confirmDangerMessage, + projectId, }, render(createElement) { return createElement(TransferProjectForm, { diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index f232cb2709a..120f75d4f0c 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -74,7 +74,7 @@ export default class ProtectedBranchCreate { $allowedToPush.length ); - this.$form.find('input[type="submit"]').attr('disabled', toggle); + this.$form.find('button[type="submit"]').attr('disabled', toggle); } static getProtectedBranches(term, callback) { diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue index e5f6ce07189..86049a2b781 100644 --- a/app/assets/javascripts/set_status_modal/set_status_form.vue +++ b/app/assets/javascripts/set_status_modal/set_status_form.vue @@ -131,9 +131,9 @@ export default { i18n: { statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`), clearStatusButtonLabel: s__('SetStatusModal|Clear status'), - availabilityCheckboxLabel: s__('SetStatusModal|Busy'), + availabilityCheckboxLabel: s__('SetStatusModal|Set yourself as busy'), availabilityCheckboxHelpText: s__( - 'SetStatusModal|An indicator appears next to your name and avatar', + 'SetStatusModal|Displays that you are busy or not able to respond', ), clearStatusAfterDropdownLabel: s__('SetStatusModal|Clear status after'), clearStatusAfterMessage: s__('SetStatusModal|Your status resets on %{date}.'), diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue index a88a4ca5cb8..75386a3cd01 100644 --- a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue @@ -1,6 +1,6 @@ <script> import { isString } from 'lodash'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { s__ } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants'; @@ -97,7 +97,7 @@ export default { return DEFAULT_COLOR; }, error() { - createFlash({ + createAlert({ message: this.$options.i18n.fetchingError, captureError: true, }); @@ -161,7 +161,7 @@ export default { }); }) .catch((error) => - createFlash({ + createAlert({ message: this.$options.i18n.updatingError, captureError: true, error, diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index e311df6e66f..32d2e91903d 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -12,7 +12,7 @@ import { import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import { SortDirection } from './constants'; @@ -197,7 +197,7 @@ export default { .catch((error) => { if (error.name === 'RecentSearchesServiceError') return undefined; - createFlash({ + createAlert({ message: __('An error occurred while parsing recent searches'), }); diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js index 7c4e372dda1..8a6053b7001 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js @@ -1,5 +1,5 @@ import Api from '~/api'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import * as types from './mutation_types'; @@ -24,7 +24,7 @@ export function fetchBranches({ commit, state }, search = '') { .catch(({ response }) => { const { status } = response; commit(types.RECEIVE_BRANCHES_ERROR, status); - createFlash({ + createAlert({ message: __('Failed to load branches. Please try again.'), }); }); @@ -43,7 +43,7 @@ export const fetchMilestones = ({ commit, state }, searchTitle = '') => { .catch(({ response }) => { const { status } = response; commit(types.RECEIVE_MILESTONES_ERROR, status); - createFlash({ + createAlert({ message: __('Failed to load milestones. Please try again.'), }); }); @@ -61,7 +61,7 @@ export const fetchLabels = ({ commit, state }, search = '') => { .catch(({ response }) => { const { status } = response; commit(types.RECEIVE_LABELS_ERROR, status); - createFlash({ + createAlert({ message: __('Failed to load labels. Please try again.'), }); }); @@ -86,7 +86,7 @@ function fetchUser(options = {}) { .catch(({ response }) => { const { status } = response; commit(`RECEIVE_${action}_ERROR`, status); - createFlash({ + createAlert({ message: errorMessage, }); }); diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index 848c49c48c7..7c184a3c391 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -1,7 +1,7 @@ <script> import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui'; import { compact } from 'lodash'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import { DEFAULT_NONE_ANY } from '../constants'; @@ -65,7 +65,7 @@ export default { this.authors = Array.isArray(res) ? compact(res) : compact(res.data); }) .catch(() => - createFlash({ + createAlert({ message: __('There was a problem fetching users.'), }), ) diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue index aa5161ca93c..741395b3193 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -46,7 +46,7 @@ export default { this.branches = data; }) .catch(() => { - createFlash({ message: __('There was a problem fetching branches.') }); + createAlert({ message: __('There was a problem fetching branches.') }); }) .finally(() => { this.loading = false; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue index adfe0559b62..d34cfb922a9 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue @@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import { ITEM_TYPE } from '~/groups/constants'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { isPositiveInteger } from '~/lib/utils/number_utils'; import { __ } from '~/locale'; import searchCrmContactsQuery from '../queries/search_crm_contacts.query.graphql'; @@ -81,7 +81,7 @@ export default { : data[this.namespace]?.contacts.nodes; }) .catch(() => - createFlash({ + createAlert({ message: __('There was a problem fetching CRM contacts.'), }), ) diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue index e6ab944449e..c7c9350ee93 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue @@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui'; import { ITEM_TYPE } from '~/groups/constants'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { isPositiveInteger } from '~/lib/utils/number_utils'; import { __ } from '~/locale'; import searchCrmOrganizationsQuery from '../queries/search_crm_organizations.query.graphql'; @@ -78,7 +78,7 @@ export default { : data[this.namespace]?.organizations.nodes; }) .catch(() => - createFlash({ + createAlert({ message: __('There was a problem fetching CRM organizations.'), }), ) diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue index 210d814d22a..929823f7308 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { DEFAULT_NONE_ANY } from '../constants'; @@ -48,7 +48,7 @@ export default { this.emojis = Array.isArray(response) ? response : response.data; }) .catch(() => { - createFlash({ message: __('There was a problem fetching emojis.') }); + createAlert({ message: __('There was a problem fetching emojis.') }); }) .finally(() => { this.loading = false; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue index 178c57a5666..bce0c11aafd 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue @@ -1,7 +1,7 @@ <script> import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; @@ -81,7 +81,7 @@ export default { this.labels = Array.isArray(res) ? res : res.data; }) .catch(() => - createFlash({ + createAlert({ message: __('There was a problem fetching labels.'), }), ) diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue index 69265d0fdc9..b9ee4d51db1 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import { sortMilestonesByDueDate } from '~/milestones/utils'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; @@ -65,7 +65,7 @@ export default { } }) .catch(() => { - createFlash({ message: __('There was a problem fetching milestones.') }); + createAlert({ message: __('There was a problem fetching milestones.') }); }) .finally(() => { this.loading = false; diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue index 9e68c92af5d..59701b4959e 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue @@ -1,6 +1,6 @@ <script> import { GlFilteredSearchSuggestion } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { DEFAULT_NONE_ANY } from '../constants'; @@ -47,7 +47,7 @@ export default { this.releases = response; }) .catch(() => { - createFlash({ message: __('There was a problem fetching releases.') }); + createAlert({ message: __('There was a problem fetching releases.') }); }) .finally(() => { this.loading = false; diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue index e9f278a5db5..71e10acf7b2 100644 --- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue +++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue @@ -78,7 +78,7 @@ export default { required: false, default: false, }, - isLoadingMoreGroups: { + isLoading: { type: Boolean, required: false, default: false, @@ -152,7 +152,12 @@ export default { }; </script> <template> - <gl-dropdown :text="selectedNamespaceText" :block="fullWidth" data-qa-selector="namespaces_list"> + <gl-dropdown + :text="selectedNamespaceText" + :block="fullWidth" + data-qa-selector="namespaces_list" + @show="$emit('show')" + > <template #header> <gl-search-box-by-type v-model.trim="searchTerm" @@ -201,8 +206,7 @@ export default { >{{ item.humanName }}</gl-dropdown-item > </div> - <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')"> - <gl-loading-icon v-if="isLoadingMoreGroups" class="gl-mb-3" size="sm" /> - </gl-intersection-observer> + <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" /> + <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')" /> </gl-dropdown> </template> diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index 15d2828bdfb..8e3f00ee63c 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -3,7 +3,6 @@ @import './pages/commits'; @import './pages/deploy_keys'; @import './pages/detail_page'; -@import './pages/environment_logs'; @import './pages/events'; @import './pages/groups'; @import './pages/hierarchy'; diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss index b991096d925..7fff675b970 100644 --- a/app/assets/stylesheets/framework/diffs.scss +++ b/app/assets/stylesheets/framework/diffs.scss @@ -619,6 +619,10 @@ table.code { .diff-grid-drafts { display: grid; grid-template-columns: 1fr 1fr; + + .content + .content { + @include gl-border-t; + } } &.inline-diff-view { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index b51daf0e4dc..e9e5d6da1f5 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -79,6 +79,19 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25); .gl-alert { @include gl-my-4; } + + &.flash-container-no-margin { + .gl-alert { + @include gl-my-0; + } + + .flash-alert, + .flash-notice, + .flash-success, + .flash-warning { + @include gl-mt-0; + } + } } @include media-breakpoint-down(sm) { diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss deleted file mode 100644 index f8f40076142..00000000000 --- a/app/assets/stylesheets/pages/environment_logs.scss +++ /dev/null @@ -1,54 +0,0 @@ -.environment-logs-page { - .content-wrapper { - padding-bottom: 0; - } -} - -.environment-logs-viewer { - height: calc(100vh - #{$environment-logs-difference-xs-up}); - min-height: 700px; - - @include media-breakpoint-up(md) { - height: calc(100vh - #{$environment-logs-difference-md-up}); - } - - .with-performance-bar & { - height: calc(100vh - #{$environment-logs-difference-xs-up} - #{$performance-bar-height}); - - @include media-breakpoint-up(md) { - height: calc(100vh - #{$environment-logs-difference-md-up} - #{$performance-bar-height}); - } - } - - .top-bar { - .date-time-picker-wrapper, - .dropdown-toggle { - @include media-breakpoint-up(md) { - width: 140px; - } - - @include media-breakpoint-up(lg) { - width: 160px; - } - } - } - - .log-lines, - .gl-infinite-scroll-container { - // makes scrollbar visible by creating contrast - background: $black; - height: 100%; - } - - .build-log { - @include build-log($black); - } - - .gl-infinite-scroll-legend { - margin: 0; - } - - .build-loader-animation { - @include build-loader-animation; - } -} diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index a6b22a28b17..896c88cf8c3 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -51,7 +51,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController end def update - if @domain.update(update_params) + service = ::PagesDomains::UpdateService.new(@project, current_user, update_params) + + if service.execute(@domain) redirect_to project_pages_path(@project), status: :found, notice: 'Domain was updated' @@ -74,9 +76,10 @@ class Projects::PagesDomainsController < Projects::ApplicationController end def clean_certificate - unless @domain.update(user_provided_certificate: nil, user_provided_key: nil) - flash[:alert] = @domain.errors.full_messages.join(', ') - end + update_params = { user_provided_certificate: nil, user_provided_key: nil } + service = ::PagesDomains::UpdateService.new(@project, current_user, update_params) + + flash[:alert] = @domain.errors.full_messages.join(', ') unless service.execute(@domain) redirect_to project_pages_domain_path(@project, @domain) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3c1a3534912..2ee9832547c 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -35,7 +35,7 @@ class UsersController < ApplicationController feature_category :source_code_management, [:gpg_keys] # TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357914 - urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups, :calendar] + urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups, :calendar, :snippets] urgency :default, [:followers, :following, :starred] urgency :high, [:exists] diff --git a/app/events/pages_domains/pages_domain_updated_event.rb b/app/events/pages_domains/pages_domain_updated_event.rb new file mode 100644 index 00000000000..641fb2f6a53 --- /dev/null +++ b/app/events/pages_domains/pages_domain_updated_event.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module PagesDomains + class PagesDomainUpdatedEvent < ::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/application_helper.rb b/app/helpers/application_helper.rb index a75c1b16145..2ae41f1bd32 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -313,7 +313,6 @@ module ApplicationHelper class_names = [] class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards) class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards) - class_names << 'environment-logs-page' if current_controller?(:logs) class_names << 'with-performance-bar' if performance_bar_enabled? class_names << system_message_class class_names << marketing_header_experiment_class diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index d8baa185370..a9fd219bbac 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -76,8 +76,8 @@ module Users user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project) end - def show_merge_request_settings_callout? - !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) + def show_merge_request_settings_callout?(project) + !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) && project.merge_requests_enabled? end def ultimate_feature_removal_banner_dismissed?(project) diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb index 64d178b7507..058cb7752f6 100644 --- a/app/models/concerns/counter_attribute.rb +++ b/app/models/concerns/counter_attribute.rb @@ -95,7 +95,7 @@ module CounterAttribute next if increment_value == 0 transaction do - unsafe_update_counters(id, attribute => increment_value) + update_counters_with_lease({ attribute => increment_value }) redis_state { |redis| redis.del(flushed_key) } new_db_value = reset.read_attribute(attribute) end @@ -130,9 +130,18 @@ module CounterAttribute end end - def clear_counter!(attribute) + def update_counters_with_lease(increments) + detect_race_on_record(log_fields: increments.merge({ caller: __method__ })) do + self.class.update_counters(id, increments) + end + end + + def reset_counter!(attribute) if counter_attribute_enabled?(attribute) - redis_state { |redis| redis.del(counter_key(attribute)) } + detect_race_on_record(log_fields: { caller: __method__ }) do + update!(attribute => 0) + clear_counter!(attribute) + end log_clear_counter(attribute) end @@ -164,14 +173,20 @@ module CounterAttribute private + def database_lock_key + "project:{#{project_id}}:#{self.class}:#{id}" + end + def steal_increments(increment_key, flushed_key) redis_state do |redis| redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key]) end end - def unsafe_update_counters(id, increments) - self.class.update_counters(id, increments) + def clear_counter!(attribute) + redis_state do |redis| + redis.del(counter_key(attribute)) + end end def execute_after_flush_callbacks @@ -192,6 +207,32 @@ module CounterAttribute # a worker is already updating the counters end + # detect_race_on_record uses a lease to monitor access + # to the project statistics row. This is needed to detect + # concurrent attempts to increment columns, which could result in a + # race condition. + # + # As the purpose is to detect and warn concurrent attempts, + # it falls back to direct update on the row if it fails to obtain the lease. + # + # It does not guarantee that there will not be any concurrent updates. + def detect_race_on_record(log_fields: {}) + return yield unless Feature.enabled?(:counter_attribute_db_lease_for_update, project) + + in_lock(database_lock_key, retries: 2) do + yield + end + rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + Gitlab::AppLogger.warn( + message: 'Concurrent update to project statistics detected', + project_statistics_id: id, + **log_fields, + **Gitlab::ApplicationContext.current + ) + + yield + end + def log_increment_counter(attribute, increment, new_value) payload = Gitlab::ApplicationContext.current.merge( message: 'Increment counter attribute', diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index a91e0291438..40838fe8726 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -49,7 +49,9 @@ class ProjectStatistics < ApplicationRecord schedule_namespace_aggregation_worker end - save! + detect_race_on_record(log_fields: { caller: __method__ }) do + save! + end end def update_commit_count @@ -110,8 +112,10 @@ class ProjectStatistics < ApplicationRecord end def refresh_storage_size! - update_storage_size - save! + detect_race_on_record(log_fields: { caller: __method__ }) do + update_storage_size + save! + end end # Since this incremental update method does not call update_storage_size above through before_save, @@ -129,36 +133,34 @@ class ProjectStatistics < ApplicationRecord if counter_attribute_enabled?(key) project_statistics.delayed_increment_counter(key, amount) else - legacy_increment_statistic(project, key, amount) + project_statistics.legacy_increment_statistic(key, amount) end end end - def self.legacy_increment_statistic(project, key, amount) - where(project_id: project.id).columns_to_increment(key, amount) + def self.incrementable_attribute?(key) + INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key) + end + + def legacy_increment_statistic(key, amount) + increment_columns!(key, amount) Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker project.namespace_id) end - def self.columns_to_increment(key, amount) - updates = ["#{key} = COALESCE(#{key}, 0) + (#{amount})"] + private - if (additional = INCREMENTABLE_COLUMNS[key]) - additional.each do |column| - updates << "#{column} = COALESCE(#{column}, 0) + (#{amount})" - end + def increment_columns!(key, amount) + increments = { key => amount } + additional = INCREMENTABLE_COLUMNS.fetch(key, []) + additional.each do |column| + increments[column] = amount end - update_all(updates.join(', ')) + update_counters_with_lease(increments) end - def self.incrementable_attribute?(key) - INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key) - end - - private - def schedule_namespace_aggregation_worker run_after_commit do Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb index e66e1d5b42f..2ffc7478178 100644 --- a/app/models/projects/build_artifacts_size_refresh.rb +++ b/app/models/projects/build_artifacts_size_refresh.rb @@ -80,9 +80,7 @@ module Projects end def reset_project_statistics! - statistics = project.statistics - statistics.update!(build_artifacts_size: 0) - statistics.clear_counter!(:build_artifacts_size) + project.statistics.reset_counter!(:build_artifacts_size) end def next_batch(limit:) diff --git a/app/services/pages_domains/update_service.rb b/app/services/pages_domains/update_service.rb new file mode 100644 index 00000000000..b038aaa95b6 --- /dev/null +++ b/app/services/pages_domains/update_service.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module PagesDomains + class UpdateService < BaseService + def execute(domain) + return unless authorized? + + return false unless domain.update(params) + + publish_event(domain) + + true + end + + private + + def authorized? + current_user.can?(:update_pages, project) + end + + def publish_event(domain) + event = PagesDomainUpdatedEvent.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/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml index 7919fde631f..a5e10846488 100644 --- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml +++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml @@ -47,4 +47,4 @@ = f.text_field :external_authorization_service_default_label, class: 'form-control gl-form-input' %span.form-text.text-muted = external_authorization_client_url_help_text - = f.submit _('Save changes'), class: "gl-button btn btn-confirm" + = f.submit _('Save changes'), pajamas_button: true diff --git a/app/views/admin/application_settings/_import_export_limits.html.haml b/app/views/admin/application_settings/_import_export_limits.html.haml index bc4a1577f90..8cb7915f847 100644 --- a/app/views/admin/application_settings/_import_export_limits.html.haml +++ b/app/views/admin/application_settings/_import_export_limits.html.haml @@ -1,4 +1,4 @@ -= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-import-export-limits-settings'), html: { class: 'fieldset-form' } do |f| += gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-import-export-limits-settings'), html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset @@ -35,4 +35,4 @@ = f.label :group_download_export_limit, _('Maximum group export download requests per minute'), class: 'label-bold' = f.number_field :group_download_export_limit, class: 'form-control gl-form-input' - = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' } + = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index 4362ae9cb9b..01d7bf0af67 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -66,4 +66,4 @@ .form-text.text-muted = html_escape(_("If blank, defaults to %{code_open}Retry later%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe } - = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' } + = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true diff --git a/app/views/admin/application_settings/_pipeline_limits.html.haml b/app/views/admin/application_settings/_pipeline_limits.html.haml index e93823172db..b7dffe63777 100644 --- a/app/views/admin/application_settings/_pipeline_limits.html.haml +++ b/app/views/admin/application_settings/_pipeline_limits.html.haml @@ -6,4 +6,4 @@ = f.label :pipeline_limit_per_project_user_sha, _('Maximum number of requests per minute') = f.number_field :pipeline_limit_per_project_user_sha, class: 'form-control gl-form-input' - = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' } + = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 856db32e088..6a8ef86a56e 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -34,4 +34,4 @@ = f.gitlab_ui_checkbox_component :container_registry_expiration_policies_caching, _("Enable container expiration caching."), help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link } - = f.submit _('Save changes'), class: "gl-button btn btn-confirm" + = f.submit _('Save changes'), pajamas_button: true diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml index 6d2cab06010..15ce9b692f0 100644 --- a/app/views/admin/background_jobs/show.html.haml +++ b/app/views/admin/background_jobs/show.html.haml @@ -7,5 +7,4 @@ = html_escape(_('GitLab uses %{linkStart}Sidekiq%{linkEnd} to process background jobs')) % { linkStart: sidekiq_link_start, linkEnd: '</a>'.html_safe } %hr -.card.gl-rounded-0 - %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" } +%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" } diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index ab4b3cf6afd..a74076dfa46 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -1,8 +1,11 @@ +- flash_container_no_margin = local_assigns.fetch(:flash_container_no_margin, false) +- flash_container_class = ('flash-container-no-margin' if flash_container_no_margin) + -# We currently only support `alert`, `notice`, `success`, 'toast', and 'raw' - icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'} - type_to_variant = {'alert' => 'danger', 'notice' => 'info', 'success' => 'success'} - closable = %w[alert notice success] -.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } } +.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' }, class: flash_container_class } - flash.each do |key, value| - if key == 'toast' && value .js-toast-message{ data: { message: value } } diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index 61a57240ed5..2f5b5b3d199 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -14,7 +14,7 @@ = render 'shared/outdated_browser' = render "layouts/broadcast" = yield :flash_message - = render "layouts/flash" + = render "layouts/flash", flash_container_no_margin: true .content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch" } = yield = render "layouts/nav/top_nav_responsive", class: "gl-flex-grow-1 gl-overflow-y-auto" diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml index 02aa1f7e93b..e3aa2d8afc9 100644 --- a/app/views/projects/_transfer.html.haml +++ b/app/views/projects/_transfer.html.haml @@ -1,7 +1,7 @@ - return unless can?(current_user, :change_namespace, @project) - form_id = "transfer-project-form" - hidden_input_id = "new_namespace_id" -- initial_data = { button_text: s_('ProjectSettings|Transfer project'), confirm_danger_message: transfer_project_message(@project), phrase: @project.name, target_form_id: form_id, target_hidden_input_id: hidden_input_id } +- initial_data = { button_text: s_('ProjectSettings|Transfer project'), confirm_danger_message: transfer_project_message(@project), phrase: @project.name, target_form_id: form_id, target_hidden_input_id: hidden_input_id, project_id: @project.id } .sub-section %h4.danger-title= _('Transfer project') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index f607a21ad21..eaf906ad89f 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,15 +1,13 @@ - if current_user + - starred = current_user.starred?(@project) + - icon = starred ? 'star' : 'star-o' + - button_text = starred ? s_('ProjectOverview|Unstar') : s_('ProjectOverview|Star') + - button_text_classes = starred ? 'starred' : '' .count-badge.d-inline-flex.align-item-stretch.gl-mr-3.btn-group - %button.gl-button.btn.btn-default.btn-sm.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } } - - if current_user.starred?(@project) - = sprite_icon('star', css_class: 'icon') - %span.starred= s_('ProjectOverview|Unstar') - - else - = sprite_icon('star-o', css_class: 'icon') - %span= s_('ProjectOverview|Star') + = render Pajamas::ButtonComponent.new(size: :small, icon: icon, button_text_classes: button_text_classes, button_options: { class: 'star-btn toggle-star', data: { endpoint: toggle_star_project_path(@project, :json) } }) do + - button_text = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do = @project.star_count - - else .count-badge.d-inline-flex.align-item-stretch.gl-mr-3.btn-group = link_to new_user_session_path, class: 'gl-button btn btn-default btn-sm has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 70df995cdf3..f6e3c15c08b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -28,7 +28,7 @@ %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) } -- if show_merge_request_settings_callout? +- if show_merge_request_settings_callout?(@project) %section.settings.expanded = render Pajamas::AlertComponent.new(variant: :info, title: _('Merge requests and approvals settings have moved.'), diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index b350455807d..ca71990f5e3 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -12,7 +12,7 @@ = gitlab_ui_form_for [@project, @hook], as: :hook, url: project_hook_path(@project, @hook) do |f| = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } - = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3' + = f.submit _('Save changes'), pajamas_button: true = render 'shared/web_hooks/test_button', hook: @hook = link_to _('Delete'), project_hook_path(@project, @hook), method: :delete, class: 'btn gl-button btn-danger float-right', aria: { label: s_('Webhooks|Delete webhook') }, data: { confirm: s_('Webhooks|Are you sure you want to delete this project hook?'), confirm_btn_variant: 'danger' } diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index 277cbf00034..770d79943b3 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -1,4 +1,4 @@ -= form_for [@project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f| += gitlab_ui_form_for [@project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f| %input{ type: 'hidden', name: 'update_section', value: 'js-protected-branches-settings' } = render Pajamas::CardComponent.new(card_options: { class: "gl-mb-5" }) do |c| - c.header do @@ -32,4 +32,4 @@ = (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f - c.footer do - = f.submit s_('ProtectedBranch|Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_button' } + = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { qa_selector: 'protect_button' }, pajamas_button: true diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index 0c88ac66b8b..7f7f8a96625 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -46,4 +46,4 @@ = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes, f: f .gl-mt-3 - = f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' } + = f.submit _('Create %{type}') % { type: type }, data: { qa_selector: 'create_token_button' }, pajamas_button: true diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml index d76ef8feb62..11fa44fe282 100644 --- a/app/views/shared/deploy_keys/_project_group_form.html.haml +++ b/app/views/shared/deploy_keys/_project_group_form.html.haml @@ -17,4 +17,4 @@ help_text: _('Allow this key to push to this repository') .form-group.row - = f.submit _("Add key"), class: "btn gl-button btn-confirm", data: { qa_selector: "add_deploy_key_button"} + = f.submit _("Add key"), data: { qa_selector: "add_deploy_key_button"}, pajamas_button: true |