diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-09 18:09:13 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-09 18:09:13 +0300 |
commit | 9c39a0a9b81f06f6345a6b6e071b8e8cd249c064 (patch) | |
tree | 45caaf45fac61866d8cc9098487a78b17c1016a0 /app | |
parent | 53af44b90f87cdd8d7126d64669848b0e2be5960 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
16 files changed, 213 insertions, 87 deletions
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 2c00daf39dd..8910734eb12 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -12,7 +12,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import createFlash from '~/flash'; import { TYPE_USER } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; import IssuableList from '~/issuable_list/components/issuable_list_root.vue'; @@ -20,7 +20,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants'; import { CREATED_DESC, i18n, - initialPageParams, issuesCountSmartQueryBase, MAX_LIST_SIZE, PAGE_SIZE, @@ -46,12 +45,13 @@ import { convertToUrlParams, getDueDateValue, getFilterTokens, + getInitialPageParams, getSortKey, getSortOptions, } from '~/issues_list/utils'; import axios from '~/lib/utils/axios_utils'; import { scrollUp } from '~/lib/utils/scroll_utils'; -import { getParameterByName } from '~/lib/utils/url_utility'; +import { getParameterByName, joinPaths } from '~/lib/utils/url_utility'; import { DEFAULT_NONE_ANY, OPERATOR_IS_ONLY, @@ -73,6 +73,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; import eventHub from '../eventhub'; +import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql'; import searchIterationsQuery from '../queries/search_iterations.query.graphql'; import searchLabelsQuery from '../queries/search_labels.query.graphql'; import searchMilestonesQuery from '../queries/search_milestones.query.graphql'; @@ -161,6 +162,7 @@ export default { }, data() { const state = getParameterByName(PARAM_STATE); + const sortKey = getSortKey(getParameterByName(PARAM_SORT)); const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC; return { @@ -169,9 +171,9 @@ export default { filterTokens: getFilterTokens(window.location.search), issues: [], pageInfo: {}, - pageParams: initialPageParams, + pageParams: getInitialPageParams(sortKey), showBulkEditSidebar: false, - sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey, + sortKey: sortKey || defaultSortKey, state: state || IssuableStates.Opened, }; }, @@ -516,12 +518,12 @@ export default { }, handleClickTab(state) { if (this.state !== state) { - this.pageParams = initialPageParams; + this.pageParams = getInitialPageParams(this.sortKey); } this.state = state; }, handleFilter(filter) { - this.pageParams = initialPageParams; + this.pageParams = getInitialPageParams(this.sortKey); this.filterTokens = filter; }, handleNextPage() { @@ -558,14 +560,16 @@ export default { } return axios - .put(`${this.issuesPath}/${issueToMove.iid}/reorder`, { - move_before_id: isMovingToBeginning ? null : moveBeforeId, - move_after_id: isMovingToEnd ? null : moveAfterId, + .put(joinPaths(this.issuesPath, issueToMove.iid, 'reorder'), { + move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId), + move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId), }) .then(() => { - // Move issue to new position in list - this.issues.splice(oldIndex, 1); - this.issues.splice(newIndex, 0, issueToMove); + const serializedVariables = JSON.stringify(this.queryVariables); + this.$apollo.mutate({ + mutation: reorderIssuesMutation, + variables: { oldIndex, newIndex, serializedVariables }, + }); }) .catch(() => { createFlash({ message: this.$options.i18n.reorderError }); @@ -573,7 +577,7 @@ export default { }, handleSort(sortKey) { if (this.sortKey !== sortKey) { - this.pageParams = initialPageParams; + this.pageParams = getInitialPageParams(sortKey); } this.sortKey = sortKey; }, diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js index e49eec96f81..e07518efba4 100644 --- a/app/assets/javascripts/issues_list/constants.js +++ b/app/assets/javascripts/issues_list/constants.js @@ -109,10 +109,14 @@ export const PARAM_DUE_DATE = 'due_date'; export const PARAM_SORT = 'sort'; export const PARAM_STATE = 'state'; -export const initialPageParams = { +export const defaultPageSizeParams = { firstPageSize: PAGE_SIZE, }; +export const largePageSizeParams = { + firstPageSize: PAGE_SIZE_MANUAL, +}; + export const DUE_DATE_NONE = '0'; export const DUE_DATE_ANY = ''; export const DUE_DATE_OVERDUE = 'overdue'; diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js index 8ff7222814b..b4def1b6e00 100644 --- a/app/assets/javascripts/issues_list/index.js +++ b/app/assets/javascripts/issues_list/index.js @@ -1,5 +1,7 @@ +import produce from 'immer'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; @@ -82,7 +84,27 @@ export function mountIssuesListApp() { Vue.use(VueApollo); - const defaultClient = createDefaultClient({}, { assumeImmutableResults: true }); + const resolvers = { + Mutation: { + reorderIssues: (_, { oldIndex, newIndex, serializedVariables }, { cache }) => { + const variables = JSON.parse(serializedVariables); + const sourceData = cache.readQuery({ query: getIssuesQuery, variables }); + + const data = produce(sourceData, (draftData) => { + const issues = draftData.project.issues.nodes.slice(); + const issueToMove = issues[oldIndex]; + issues.splice(oldIndex, 1); + issues.splice(newIndex, 0, issueToMove); + + draftData.project.issues.nodes = issues; + }); + + cache.writeQuery({ query: getIssuesQuery, variables, data }); + }, + }, + }; + + const defaultClient = createDefaultClient(resolvers, { assumeImmutableResults: true }); const apolloProvider = new VueApollo({ defaultClient, }); diff --git a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql new file mode 100644 index 00000000000..5927e3e83c7 --- /dev/null +++ b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql @@ -0,0 +1,7 @@ +mutation reorderIssues($oldIndex: Int, $newIndex: Int, $serializedVariables: String) { + reorderIssues( + oldIndex: $oldIndex + newIndex: $newIndex + serializedVariables: $serializedVariables + ) @client +} diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js index 287c85b60d7..7f44ba6bab4 100644 --- a/app/assets/javascripts/issues_list/utils.js +++ b/app/assets/javascripts/issues_list/utils.js @@ -3,12 +3,14 @@ import { BLOCKING_ISSUES_DESC, CREATED_ASC, CREATED_DESC, + defaultPageSizeParams, DUE_DATE_ASC, DUE_DATE_DESC, DUE_DATE_VALUES, filters, LABEL_PRIORITY_ASC, LABEL_PRIORITY_DESC, + largePageSizeParams, MILESTONE_DUE_ASC, MILESTONE_DUE_DESC, NORMAL_FILTER, @@ -36,6 +38,9 @@ import { OPERATOR_IS_NOT, } from '~/vue_shared/components/filtered_search_bar/constants'; +export const getInitialPageParams = (sortKey) => + sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams; + export const getSortKey = (sort) => Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue index 65e396b34e9..8d360cd2c28 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue @@ -20,13 +20,14 @@ import { numberToHumanSize } from '~/lib/utils/number_utils'; import { objectToQuery } from '~/lib/utils/url_utility'; import { s__, __ } from '~/locale'; // import DependencyRow from '~/packages/details/components/dependency_row.vue'; -// import PackageFiles from '~/packages/details/components/package_files.vue'; // import PackageListRow from '~/packages/shared/components/package_list_row.vue'; import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; import { packageTypeToTrackCategory } from '~/packages/shared/utils'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; +import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue'; +import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue'; import { PACKAGE_TYPE_NUGET, PACKAGE_TYPE_COMPOSER, @@ -40,8 +41,12 @@ import { SHOW_DELETE_SUCCESS_ALERT, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, DELETE_PACKAGE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, } from '~/packages_and_registries/package_registry/constants'; + import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql'; +import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import Tracking from '~/tracking'; @@ -55,17 +60,14 @@ export default { GlTab, GlTabs, GlSprintf, - PackageTitle: () => - import('~/packages_and_registries/package_registry/components/details/package_title.vue'), - TerraformTitle: () => - import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'), + PackageTitle, PackagesListLoader, // PackageListRow, // DependencyRow, PackageHistory, AdditionalMetadata, InstallationCommands, - // PackageFiles, + PackageFiles, }, directives: { GlTooltip: GlTooltipDirective, @@ -74,7 +76,6 @@ export default { mixins: [Tracking.mixin()], inject: [ 'packageId', - 'titleComponent', 'projectName', 'canDelete', 'svgPath', @@ -123,7 +124,7 @@ export default { }; }, packageFiles() { - return this.packageEntity.packageFiles; + return this.packageEntity?.packageFiles?.nodes; }, isLoading() { return this.$apollo.queries.packageEntity.loading; @@ -133,7 +134,7 @@ export default { }, tracking() { return { - category: packageTypeToTrackCategory(this.packageEntity.package_type), + category: packageTypeToTrackCategory(this.packageEntity.packageType), }; }, hasVersions() { @@ -143,10 +144,10 @@ export default { return this.packageEntity.dependency_links || []; }, showDependencies() { - return this.packageEntity.package_type === PACKAGE_TYPE_NUGET; + return this.packageEntity.packageType === PACKAGE_TYPE_NUGET; }, showFiles() { - return this.packageEntity?.package_type !== PACKAGE_TYPE_COMPOSER; + return this.packageEntity?.packageType !== PACKAGE_TYPE_COMPOSER; }, }, methods: { @@ -158,19 +159,17 @@ export default { // this.fetchPackageVersions(); } }, - deletePackage() { - return this.$apollo - .mutate({ - mutation: destroyPackageMutation, - variables: { - id: this.packageEntity.id, - }, - }) - .then(({ data }) => { - if (data?.destroyPackage?.errors[0]) { - throw data.destroyPackage.errors[0]; - } - }); + async deletePackage() { + const { data } = await this.$apollo.mutate({ + mutation: destroyPackageMutation, + variables: { + id: this.packageEntity.id, + }, + }); + + if (data?.destroyPackage?.errors[0]) { + throw data.destroyPackage.errors[0]; + } }, async confirmPackageDeletion() { this.track(DELETE_PACKAGE_TRACKING_ACTION); @@ -195,6 +194,37 @@ export default { }); } }, + async deletePackageFile(id) { + try { + const { data } = await this.$apollo.mutate({ + mutation: destroyPackageFileMutation, + variables: { + id, + }, + awaitRefetchQueries: true, + refetchQueries: [ + { + query: getPackageDetails, + variables: this.queryVariables, + }, + ], + }); + if (data?.destroyPackageFile?.errors[0]) { + throw data.destroyPackageFile.errors[0]; + } + createFlash({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + type: 'success', + }); + } catch (error) { + createFlash({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + type: 'warning', + captureError: true, + error, + }); + } + }, handleFileDelete(file) { this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION); this.fileToDelete = { ...file }; @@ -202,7 +232,7 @@ export default { }, confirmFileDelete() { this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); - // this.deletePackageFile(this.fileToDelete.id); + this.deletePackageFile(this.fileToDelete.id); this.fileToDelete = null; }, }, @@ -245,7 +275,7 @@ export default { /> <div v-else class="packages-app"> - <component :is="titleComponent" :package-entity="packageEntity"> + <package-title :package-entity="packageEntity"> <template #delete-button> <gl-button v-if="canDelete" @@ -258,7 +288,7 @@ export default { {{ __('Delete') }} </gl-button> </template> - </component> + </package-title> <gl-tabs> <gl-tab :title="__('Detail')"> @@ -270,13 +300,12 @@ export default { <additional-metadata :package-entity="packageEntity" /> </div> - <!-- <package-files + <package-files v-if="showFiles" :package-files="packageFiles" - :can-delete="canDelete" @download-file="track($options.trackingActions.PULL_PACKAGE)" @delete-file="handleFileDelete" - /> --> + /> </gl-tab> <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab"> @@ -288,11 +317,11 @@ export default { </template> <template v-if="packageDependencies.length > 0"> - <dependency-row + <!-- <dependency-row v-for="(dep, index) in packageDependencies" :key="index" :dependency="dep" - /> + /> --> </template> <p v-else class="gl-mt-3" data-testid="no-dependencies-message"> @@ -329,6 +358,7 @@ export default { <gl-modal ref="deleteModal" modal-id="delete-modal" + data-testid="delete-modal" :action-primary="$options.modal.packageDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" @primary="confirmPackageDeletion" @@ -351,6 +381,7 @@ export default { modal-id="delete-file-modal" :action-primary="$options.modal.fileDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" + data-testid="delete-file-modal" @primary="confirmFileDelete" @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" > diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue index 3c2876e902b..bf7fe6fb91b 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue @@ -22,17 +22,13 @@ export default { FileSha, }, mixins: [Tracking.mixin()], + inject: ['canDelete'], props: { packageFiles: { type: Array, required: false, default: () => [], }, - canDelete: { - type: Boolean, - default: false, - required: false, - }, }, computed: { filesTableRows() { @@ -43,6 +39,8 @@ export default { })); }, showCommitColumn() { + // note that this is always false for now since we do not return + // pipelines associated to files for performance concerns return this.filesTableRows.some((row) => Boolean(row.pipeline?.id)); }, filesTableHeaderFields() { @@ -80,7 +78,7 @@ export default { return numberToHumanSize(size); }, hasDetails(item) { - return item.file_sha256 || item.file_md5 || item.file_sha1; + return item.fileSha256 || item.fileMd5 || item.fileSha1; }, }, i18n: { @@ -107,32 +105,32 @@ export default { @click="toggleDetails" /> <gl-link - :href="item.download_path" + :href="item.downloadPath" class="gl-text-gray-500" data-testid="download-link" @click="$emit('download-file')" > <file-icon - :file-name="item.file_name" + :file-name="item.fileName" css-classes="gl-relative file-icon" class="gl-mr-1 gl-relative" /> - <span>{{ item.file_name }}</span> + <span>{{ item.fileName }}</span> </gl-link> </template> <template #cell(commit)="{ item }"> <gl-link - v-if="item.pipeline && item.pipeline.project" - :href="item.pipeline.project.commit_url" + v-if="item.pipeline && item.pipeline" + :href="item.pipeline.commitPath" class="gl-text-gray-500" data-testid="commit-link" - >{{ item.pipeline.git_commit_message }}</gl-link + >{{ item.pipeline.sha }}</gl-link > </template> <template #cell(created)="{ item }"> - <time-ago-tooltip :time="item.created_at" /> + <time-ago-tooltip :time="item.createdAt" /> </template> <template #cell(actions)="{ item }"> @@ -151,13 +149,13 @@ export default { class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100" > <file-sha - v-if="item.file_sha256" + v-if="item.fileSha256" data-testid="sha-256" title="SHA-256" - :sha="item.file_sha256" + :sha="item.fileSha256" /> - <file-sha v-if="item.file_md5" data-testid="md5" title="MD5" :sha="item.file_md5" /> - <file-sha v-if="item.file_sha1" data-testid="sha-1" title="SHA-1" :sha="item.file_sha1" /> + <file-sha v-if="item.fileMd5" data-testid="md5" title="MD5" :sha="item.fileMd5" /> + <file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" /> </div> </template> </gl-table> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql new file mode 100644 index 00000000000..f016640f57d --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql @@ -0,0 +1,5 @@ +mutation destroyPackageFile($id: PackagesPackageFileID!) { + destroyPackageFile(input: { id: $id }) { + errors + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 2dff256525a..6deb406f0db 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -38,6 +38,8 @@ query getPackageDetails($id: ID!) { fileSha1 fileSha256 size + createdAt + downloadPath } } metadata { diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js index f25b01f88d5..d94bbd21035 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js @@ -18,7 +18,6 @@ export default () => { apolloProvider, provide: { canDelete: parseBoolean(canDelete), - titleComponent: 'PackageTitle', ...datasetOptions, }, render(createElement) { diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index 08cd451db94..5516e6bc2c0 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -2,6 +2,7 @@ module Integrations class Datadog < Integration + include ActionView::Helpers::UrlHelper include HasWebHook extend Gitlab::Utils::Override @@ -47,11 +48,12 @@ module Integrations end def description - 'Trace your GitLab pipelines with Datadog' + s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.') end def help - nil + docs_link = link_to s_('DatadogIntegration|How do I set up this integration?'), Rails.application.routes.url_helpers.help_page_url('integration/datadog'), target: '_blank', rel: 'noopener noreferrer' + s_('DatadogIntegration|Send CI/CD pipeline information to Datadog to monitor for job failures and troubleshoot performance issues. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end def self.to_param @@ -64,14 +66,19 @@ module Integrations type: 'text', name: 'datadog_site', placeholder: DEFAULT_DOMAIN, - help: 'Choose the Datadog site to send data to. Set to "datadoghq.eu" to send data to the EU site', + help: ERB::Util.html_escape( + s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.') + ) % { + codeOpen: '<code>'.html_safe, + codeClose: '</code>'.html_safe + }, required: false }, { type: 'text', name: 'api_url', - title: 'API URL', - help: '(Advanced) Define the full URL for your Datadog site directly', + title: s_('DatadogIntegration|API URL'), + help: s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.'), required: false }, { @@ -80,21 +87,34 @@ module Integrations title: _('API key'), non_empty_password_title: s_('ProjectService|Enter new API key'), non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'), - help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog", + help: ERB::Util.html_escape( + s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.') + ) % { + linkOpen: '<a href="%s" target="_blank" rel="noopener noreferrer">'.html_safe % api_keys_url, + linkClose: '</a>'.html_safe + }, required: true }, { type: 'text', name: 'datadog_service', - title: 'Service', + title: s_('DatadogIntegration|Service'), placeholder: 'gitlab-ci', - help: 'Name of this GitLab instance that all data will be tagged with' + help: s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.') }, { type: 'text', name: 'datadog_env', - title: 'Env', - help: 'The environment tag that traces will be tagged with' + title: s_('DatadogIntegration|Environment'), + placeholder: 'ci', + help: ERB::Util.html_escape( + s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}') + ) % { + codeOpen: '<code>'.html_safe, + codeClose: '</code>'.html_safe, + linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe, + linkClose: '</a>'.html_safe + } } ] end diff --git a/app/models/member.rb b/app/models/member.rb index 0a0f4b1e273..e003a5d15db 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -393,11 +393,6 @@ class Member < ApplicationRecord # error or not doing any meaningful work. # rubocop: disable CodeReuse/ServiceClass def refresh_member_authorized_projects - # If user/source is being destroyed, project access are going to be - # destroyed eventually because of DB foreign keys, so we shouldn't bother - # with refreshing after each member is destroyed through association - return if destroyed_by_association.present? - UserProjectAccessChangedService.new(user_id).execute end # rubocop: enable CodeReuse/ServiceClass diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index ab044b80133..a13133c90e9 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class GroupMember < Member + extend ::Gitlab::Utils::Override include FromUnion include CreatedAtFilterable @@ -49,6 +50,19 @@ class GroupMember < Member { group: group } end + override :refresh_member_authorized_projects + def refresh_member_authorized_projects + # Here, `destroyed_by_association` will be present if the + # GroupMember is being destroyed due to the `dependent: :destroy` + # callback on Group. In this case, there is no need to refresh the + # authorizations, because whenever a Group is being destroyed, + # its projects are also destroyed, so the removal of project_authorizations + # will happen behind the scenes via DB foreign keys anyway. + return if destroyed_by_association.present? + + super + end + private def access_level_inclusion diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 8ea12a043d4..49144d9dbf3 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ProjectMember < Member + extend ::Gitlab::Utils::Override SOURCE_TYPE = 'Project' belongs_to :project, foreign_key: 'source_id' @@ -89,6 +90,22 @@ class ProjectMember < Member { project: project } end + override :refresh_member_authorized_projects + def refresh_member_authorized_projects + return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh) + return unless user + + # rubocop:disable CodeReuse/ServiceClass + AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute + + # Until we compare the inconsistency rates of the new, specialized service and + # the old approach, we still run AuthorizedProjectsWorker + # but with some delay and lower urgency as a safety net. + UserProjectAccessChangedService.new(user_id) + .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY) + # rubocop:enable CodeReuse/ServiceClass + end + private def send_invite diff --git a/app/services/ci/drop_pipeline_service.rb b/app/services/ci/drop_pipeline_service.rb index f510943251b..16d3abcbfa0 100644 --- a/app/services/ci/drop_pipeline_service.rb +++ b/app/services/ci/drop_pipeline_service.rb @@ -2,6 +2,9 @@ module Ci class DropPipelineService + PRELOADED_COMMIT_STATUS_RELATIONS = [:project, :pipeline].freeze + PRELOADED_CI_BUILD_RELATIONS = [:metadata, :deployment, :taggings].freeze + # execute service asynchronously for each cancelable pipeline def execute_async_for_all(pipelines, failure_reason, context_user) pipelines.cancelable.select(:id).find_in_batches do |pipelines_batch| @@ -27,11 +30,11 @@ module Ci private - def preload_associations_for_drop(builds_batch) - ActiveRecord::Associations::Preloader.new.preload( # rubocop: disable CodeReuse/ActiveRecord - builds_batch, - [:project, :pipeline, :metadata, :deployment, :taggings] - ) + # rubocop: disable CodeReuse/ActiveRecord + def preload_associations_for_drop(commit_status_batch) + ActiveRecord::Associations::Preloader.new.preload(commit_status_batch, PRELOADED_COMMIT_STATUS_RELATIONS) + ActiveRecord::Associations::Preloader.new.preload(commit_status_batch.select { |job| job.is_a?(Ci::Build) }, PRELOADED_CI_BUILD_RELATIONS) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index a810993f622..302c047a65f 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -65,7 +65,7 @@ module Projects save_project_and_import_data - Gitlab::ApplicationContext.with_context(related_class: "Projects::CreateService", project: @project) do + Gitlab::ApplicationContext.with_context(project: @project) do after_create_actions if @project.persisted? import_schedule |