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 15:07:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-28 15:07:50 +0300
commiteb3a23aaaa99ef8ae08c7b440fad676e3c71a1af (patch)
treebb74f64f73f4a20d4b4e3443c3e7defd4733a78d /app
parentf3cfb235c76426ce5a18003bb80ba625097bf1d0 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/api/projects_api.js8
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js19
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js6
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue18
-rw-r--r--app/assets/javascripts/diffs/mixins/draft_comments.js2
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue168
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js2
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue14
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/framework/diffs.scss4
-rw-r--r--app/assets/stylesheets/framework/flash.scss13
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss54
-rw-r--r--app/controllers/projects/pages_domains_controller.rb11
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/events/pages_domains/pages_domain_updated_event.rb18
-rw-r--r--app/helpers/application_helper.rb1
-rw-r--r--app/helpers/users/callouts_helper.rb4
-rw-r--r--app/models/concerns/counter_attribute.rb51
-rw-r--r--app/models/project_statistics.rb40
-rw-r--r--app/models/projects/build_artifacts_size_refresh.rb4
-rw-r--r--app/services/pages_domains/update_service.rb34
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_import_export_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_pipeline_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_registry.html.haml2
-rw-r--r--app/views/admin/background_jobs/show.html.haml3
-rw-r--r--app/views/layouts/_flash.html.haml5
-rw-r--r--app/views/layouts/fullscreen.html.haml2
-rw-r--r--app/views/projects/_transfer.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml14
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml4
-rw-r--r--app/views/shared/access_tokens/_form.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml2
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