diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-03 15:10:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-03 15:10:23 +0300 |
commit | 5f0d27d131aced1a53e8cbc7db023d9f947f8a1a (patch) | |
tree | 7007c07fc37c95638f3e71c1902dcd055db1d8ca /app | |
parent | cc8ea69201e2e4d020018c43efeb993c44cd8a71 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
36 files changed, 426 insertions, 118 deletions
diff --git a/app/assets/javascripts/crm/components/contacts_root.vue b/app/assets/javascripts/crm/components/contacts_root.vue index 97220a3409d..0242bdab541 100644 --- a/app/assets/javascripts/crm/components/contacts_root.vue +++ b/app/assets/javascripts/crm/components/contacts_root.vue @@ -1,22 +1,28 @@ <script> -import { GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import getGroupContactsQuery from './queries/get_group_contacts.query.graphql'; +import NewContactForm from './new_contact_form.vue'; export default { components: { + GlAlert, GlButton, GlLoadingIcon, GlTable, + NewContactForm, }, directives: { GlTooltip: GlTooltipDirective, }, - inject: ['groupFullPath', 'groupIssuesPath'], + inject: ['groupFullPath', 'groupIssuesPath', 'canAdminCrmContact'], data() { - return { contacts: [] }; + return { + contacts: [], + error: false, + errorMessages: [], + }; }, apollo: { contacts: { @@ -31,12 +37,8 @@ export default { update(data) { return this.extractContacts(data); }, - error(error) { - createFlash({ - message: __('Something went wrong. Please try again.'), - error, - captureError: true, - }); + error() { + this.error = true; }, }, }, @@ -44,12 +46,31 @@ export default { isLoading() { return this.$apollo.queries.contacts.loading; }, + showNewForm() { + return this.$route.path.startsWith('/new'); + }, }, methods: { extractContacts(data) { const contacts = data?.group?.contacts?.nodes || []; return contacts.slice().sort((a, b) => a.firstName.localeCompare(b.firstName)); }, + displayNewForm() { + if (this.showNewForm) return; + + this.$router.push({ path: '/new' }); + }, + hideNewForm() { + this.$router.replace({ path: '/' }); + }, + handleError(errors) { + this.error = true; + if (errors) this.errorMessages = errors; + }, + dismissError() { + this.error = false; + this.errorMessages = []; + }, }, fields: [ { key: 'firstName', sortable: true }, @@ -75,15 +96,41 @@ export default { i18n: { emptyText: s__('Crm|No contacts found'), issuesButtonLabel: __('View issues'), + title: s__('Crm|Customer Relations Contacts'), + newContact: s__('Crm|New contact'), + errorText: __('Something went wrong. Please try again.'), }, }; </script> <template> <div> + <gl-alert v-if="error" variant="danger" class="gl-mt-6" @dismiss="dismissError"> + <div v-if="errorMessages.length == 0">{{ $options.i18n.errorText }}</div> + <div v-for="(message, index) in errorMessages" :key="index">{{ message }}</div> + </gl-alert> + <div + class="gl-display-flex gl-align-items-baseline gl-flex-direction-row gl-justify-content-space-between gl-mt-6" + > + <h2 class="gl-font-size-h2 gl-my-0"> + {{ $options.i18n.title }} + </h2> + <div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end"> + <gl-button + v-if="canAdminCrmContact" + variant="confirm" + data-testid="new-contact-button" + @click="displayNewForm" + > + {{ $options.i18n.newContact }} + </gl-button> + </div> + </div> + <new-contact-form v-if="showNewForm" @close="hideNewForm" @error="handleError" /> <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" /> <gl-table v-else + class="gl-mt-5" :items="contacts" :fields="$options.fields" :empty-text="$options.i18n.emptyText" diff --git a/app/assets/javascripts/crm/components/new_contact_form.vue b/app/assets/javascripts/crm/components/new_contact_form.vue new file mode 100644 index 00000000000..77ff82c5af4 --- /dev/null +++ b/app/assets/javascripts/crm/components/new_contact_form.vue @@ -0,0 +1,140 @@ +<script> +import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { produce } from 'immer'; +import { __, s__ } from '~/locale'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPE_GROUP } from '~/graphql_shared/constants'; +import createContact from './queries/create_contact.mutation.graphql'; +import getGroupContactsQuery from './queries/get_group_contacts.query.graphql'; + +export default { + components: { + GlButton, + GlFormGroup, + GlFormInput, + }, + inject: ['groupFullPath', 'groupId'], + data() { + return { + firstName: '', + lastName: '', + phone: '', + email: '', + description: '', + submitting: false, + }; + }, + computed: { + invalid() { + return this.firstName === '' || this.lastName === '' || this.email === ''; + }, + }, + methods: { + save() { + this.submitting = true; + return this.$apollo + .mutate({ + mutation: createContact, + variables: { + input: { + groupId: convertToGraphQLId(TYPE_GROUP, this.groupId), + firstName: this.firstName, + lastName: this.lastName, + phone: this.phone, + email: this.email, + description: this.description, + }, + }, + update: this.updateCache, + }) + .then(({ data }) => { + if (data.customerRelationsContactCreate.errors.length === 0) this.close(); + + this.submitting = false; + }) + .catch(() => { + this.error(); + this.submitting = false; + }); + }, + close() { + this.$emit('close'); + }, + error(errors = null) { + this.$emit('error', errors); + }, + updateCache(store, { data: { customerRelationsContactCreate } }) { + if (customerRelationsContactCreate.errors.length > 0) { + this.error(customerRelationsContactCreate.errors); + return; + } + + const variables = { + groupFullPath: this.groupFullPath, + }; + const sourceData = store.readQuery({ + query: getGroupContactsQuery, + variables, + }); + + const data = produce(sourceData, (draftState) => { + draftState.group.contacts.nodes = [ + ...sourceData.group.contacts.nodes, + customerRelationsContactCreate.contact, + ]; + }); + + store.writeQuery({ + query: getGroupContactsQuery, + variables, + data, + }); + }, + }, + i18n: { + buttonLabel: s__('Crm|Create new contact'), + cancel: __('Cancel'), + firstName: s__('Crm|First name'), + lastName: s__('Crm|Last name'), + email: s__('Crm|Email'), + phone: s__('Crm|Phone number (optional)'), + description: s__('Crm|Description (optional)'), + }, +}; +</script> + +<template> + <div class="col-md-4"> + <form @submit.prevent="save"> + <gl-form-group :label="$options.i18n.firstName" label-for="contact-first-name"> + <gl-form-input id="contact-first-name" v-model="firstName" /> + </gl-form-group> + <gl-form-group :label="$options.i18n.lastName" label-for="contact-last-name"> + <gl-form-input id="contact-last-name" v-model="lastName" /> + </gl-form-group> + <gl-form-group :label="$options.i18n.email" label-for="contact-email"> + <gl-form-input id="contact-email" v-model="email" /> + </gl-form-group> + <gl-form-group :label="$options.i18n.phone" label-for="contact-phone"> + <gl-form-input id="contact-phone" v-model="phone" /> + </gl-form-group> + <gl-form-group :label="$options.i18n.description" label-for="contact-description"> + <gl-form-input id="contact-description" v-model="description" /> + </gl-form-group> + <div class="form-actions"> + <gl-button + variant="confirm" + :disabled="invalid" + :loading="submitting" + data-testid="create-new-contact-button" + type="submit" + >{{ $options.i18n.buttonLabel }}</gl-button + > + <gl-button data-testid="cancel-button" @click="close"> + {{ $options.i18n.cancel }} + </gl-button> + </div> + </form> + <div class="gl-pb-5"></div> + </div> +</template> diff --git a/app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql b/app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql new file mode 100644 index 00000000000..e0192459609 --- /dev/null +++ b/app/assets/javascripts/crm/components/queries/create_contact.mutation.graphql @@ -0,0 +1,10 @@ +#import "./crm_contact_fields.fragment.graphql" + +mutation createContact($input: CustomerRelationsContactCreateInput!) { + customerRelationsContactCreate(input: $input) { + contact { + ...ContactFragment + } + errors + } +} diff --git a/app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql b/app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql new file mode 100644 index 00000000000..cef4083b446 --- /dev/null +++ b/app/assets/javascripts/crm/components/queries/crm_contact_fields.fragment.graphql @@ -0,0 +1,14 @@ +fragment ContactFragment on CustomerRelationsContact { + __typename + id + firstName + lastName + email + phone + description + organization { + __typename + id + name + } +} diff --git a/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql index f6acd258585..2a8150e42e3 100644 --- a/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql +++ b/app/assets/javascripts/crm/components/queries/get_group_contacts.query.graphql @@ -1,21 +1,12 @@ +#import "./crm_contact_fields.fragment.graphql" + query contacts($groupFullPath: ID!) { group(fullPath: $groupFullPath) { __typename id contacts { nodes { - __typename - id - firstName - lastName - email - phone - description - organization { - __typename - id - name - } + ...ContactFragment } } } diff --git a/app/assets/javascripts/crm/contacts_bundle.js b/app/assets/javascripts/crm/contacts_bundle.js index b0edd0107b6..6ddc53840cc 100644 --- a/app/assets/javascripts/crm/contacts_bundle.js +++ b/app/assets/javascripts/crm/contacts_bundle.js @@ -1,9 +1,11 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import VueRouter from 'vue-router'; import createDefaultClient from '~/lib/graphql'; import CrmContactsRoot from './components/contacts_root.vue'; Vue.use(VueApollo); +Vue.use(VueRouter); export default () => { const el = document.getElementById('js-crm-contacts-app'); @@ -16,12 +18,26 @@ export default () => { return false; } - const { groupFullPath, groupIssuesPath } = el.dataset; + const { basePath, groupFullPath, groupIssuesPath, canAdminCrmContact, groupId } = el.dataset; + + const router = new VueRouter({ + base: basePath, + mode: 'history', + routes: [ + { + // eslint-disable-next-line @gitlab/require-i18n-strings + name: 'Contacts List', + path: '/', + component: CrmContactsRoot, + }, + ], + }); return new Vue({ el, + router, apolloProvider, - provide: { groupFullPath, groupIssuesPath }, + provide: { groupFullPath, groupIssuesPath, canAdminCrmContact, groupId }, render(createElement) { return createElement(CrmContactsRoot); }, diff --git a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js index 119a2aea9eb..33be6cf9e5d 100644 --- a/app/assets/javascripts/editor/extensions/example_source_editor_extension.js +++ b/app/assets/javascripts/editor/extensions/example_source_editor_extension.js @@ -16,11 +16,11 @@ export class MyFancyExtension { * actions, keystrokes, update options, etc. * Is called only once before the extension gets registered * - * @param { Object } [setupOptions] The setupOptions object * @param { Object } [instance] The Source Editor instance + * @param { Object } [setupOptions] The setupOptions object */ // eslint-disable-next-line class-methods-use-this,no-unused-vars - onSetup(setupOptions, instance) {} + onSetup(instance, setupOptions) {} /** * The first thing called after the extension is diff --git a/app/assets/javascripts/editor/source_editor_instance.js b/app/assets/javascripts/editor/source_editor_instance.js index 052a73d7091..fcffdc587be 100644 --- a/app/assets/javascripts/editor/source_editor_instance.js +++ b/app/assets/javascripts/editor/source_editor_instance.js @@ -153,7 +153,7 @@ export default class EditorInstance { const extensionInstance = new EditorExtension(extension); const { setupOptions, obj: extensionObj } = extensionInstance; if (extensionObj.onSetup) { - extensionObj.onSetup(setupOptions, this); + extensionObj.onSetup(this, setupOptions); } if (extensionsStore) { this.registerExtension(extensionInstance, extensionsStore); diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue index 7f6dce05b6e..13e254f138a 100644 --- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue +++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue @@ -1,5 +1,5 @@ <script> -import { GlAlert, GlLink, GlSprintf, GlTable } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf, GlTableLite } from '@gitlab/ui'; import { __ } from '~/locale'; import CiLintResultsParam from './ci_lint_results_param.vue'; import CiLintResultsValue from './ci_lint_results_value.vue'; @@ -36,7 +36,7 @@ export default { GlAlert, GlLink, GlSprintf, - GlTable, + GlTableLite, CiLintWarnings, CiLintResultsValue, CiLintResultsParam, @@ -129,7 +129,7 @@ export default { @dismiss="isWarningDismissed = true" /> - <gl-table + <gl-table-lite v-if="shouldShowTable" :items="jobs" :fields="$options.fields" @@ -142,6 +142,6 @@ export default { <template #cell(value)="{ item }"> <ci-lint-results-value :item="item" :dry-run="dryRun" /> </template> - </gl-table> + </gl-table-lite> </div> </template> diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index 321315d531b..4f3f1365f4a 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -122,7 +122,6 @@ export default function simulateDrag(options) { const firstRect = getRect(firstEl); const lastRect = getRect(lastEl); - const startTime = new Date().getTime(); const duration = options.duration || 1000; simulateEvent(fromEl, 'pointerdown', { @@ -140,8 +139,28 @@ export default function simulateDrag(options) { toRect.cy = lastRect.y + lastRect.h + 50; } - const dragInterval = setInterval(() => { - const progress = (new Date().getTime() - startTime) / duration; + let startTime; + + // Called within dragFn when the drag should finish + const finishFn = () => { + if (options.ondragend) options.ondragend(); + + if (options.performDrop) { + simulateEvent(toEl, 'mouseup'); + } + + window.SIMULATE_DRAG_ACTIVE = 0; + }; + + const dragFn = (timestamp) => { + if (!startTime) { + startTime = timestamp; + } + + const elapsed = timestamp - startTime; + + // Make sure that progress maxes at 1 + const progress = Math.min(elapsed / duration, 1); const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress; const y = fromRect.cy + (toRect.cy - fromRect.cy + options.extraHeight) * progress; const overEl = fromEl.ownerDocument.elementFromPoint(x, y); @@ -152,16 +171,15 @@ export default function simulateDrag(options) { }); if (progress >= 1) { - if (options.ondragend) options.ondragend(); - - if (options.performDrop) { - simulateEvent(toEl, 'mouseup'); - } - - clearInterval(dragInterval); - window.SIMULATE_DRAG_ACTIVE = 0; + // finish on next frame, so we can pause in the correct position for a frame + requestAnimationFrame(finishFn); + } else { + requestAnimationFrame(dragFn); } - }, 100); + }; + + // Start the drag animation + requestAnimationFrame(dragFn); return { target: fromEl, diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 8877cfa39fb..1963d1aa7fe 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -141,6 +141,7 @@ export default { variant="link" :icon="descriptionVersionToggleIcon" data-testid="compare-btn" + class="gl-vertical-align-text-bottom" @click="toggleDescriptionVersion" >{{ __('Compare with previous version') }}</gl-button > @@ -149,6 +150,7 @@ export default { :icon="showLines ? 'chevron-up' : 'chevron-down'" variant="link" data-testid="outdated-lines-change-btn" + class="gl-vertical-align-text-bottom" @click="toggleDiff" > {{ __('Compare changes') }} diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 626093b4588..70bcefe339c 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -23,6 +23,7 @@ module CycleAnalyticsParams opts[:from] = params[:from] || start_date(params) opts[:to] = params[:to] if params[:to] opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter] + opts[:use_aggregated_data_collector] = params[:use_aggregated_data_collector] if params[:use_aggregated_data_collector] opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES)) opts.merge!(date_range(params)) end diff --git a/app/controllers/groups/crm/contacts_controller.rb b/app/controllers/groups/crm/contacts_controller.rb new file mode 100644 index 00000000000..97904fdd2fd --- /dev/null +++ b/app/controllers/groups/crm/contacts_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Groups::Crm::ContactsController < Groups::ApplicationController + feature_category :team_planning + + before_action :authorize_read_crm_contact! + + def new + render action: "index" + end + + private + + def authorize_read_crm_contact! + render_404 unless can?(current_user, :read_crm_contact, group) + end +end diff --git a/app/controllers/groups/crm/organizations_controller.rb b/app/controllers/groups/crm/organizations_controller.rb new file mode 100644 index 00000000000..6f285687e6b --- /dev/null +++ b/app/controllers/groups/crm/organizations_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class Groups::Crm::OrganizationsController < Groups::ApplicationController + feature_category :team_planning + + before_action :authorize_read_crm_organization! + + private + + def authorize_read_crm_organization! + render_404 unless can?(current_user, :read_crm_organization, group) + end +end diff --git a/app/controllers/groups/crm_controller.rb b/app/controllers/groups/crm_controller.rb deleted file mode 100644 index 40661b09be6..00000000000 --- a/app/controllers/groups/crm_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Groups::CrmController < Groups::ApplicationController - feature_category :team_planning - - before_action :authorize_read_crm_contact!, only: [:contacts] - before_action :authorize_read_crm_organization!, only: [:organizations] - - def contacts - respond_to do |format| - format.html - end - end - - def organizations - respond_to do |format| - format.html - end - end - - private - - def authorize_read_crm_contact! - render_404 unless can?(current_user, :read_crm_contact, group) - end - - def authorize_read_crm_organization! - render_404 unless can?(current_user, :read_crm_organization, group) - end -end diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 9dbbd385ea8..1e23db9f32b 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -8,6 +8,8 @@ module Groups feature_category :pipeline_authoring + urgency :low, [:show] + def show respond_to do |format| format.json do diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb index 9dc3194df85..7ef5016ac00 100644 --- a/app/controllers/projects/ci/lints_controller.rb +++ b/app/controllers/projects/ci/lints_controller.rb @@ -6,6 +6,7 @@ class Projects::Ci::LintsController < Projects::ApplicationController feature_category :pipeline_authoring respond_to :json, only: [:create] + urgency :low, [:create] def show end diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb index 600516f95a2..392a6afc636 100644 --- a/app/controllers/projects/ci/pipeline_editor_controller.rb +++ b/app/controllers/projects/ci/pipeline_editor_controller.rb @@ -9,6 +9,8 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController feature_category :pipeline_authoring + urgency :low, [:show] + def show end diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index f93c75a203e..e7bccf5a243 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -5,6 +5,8 @@ class Projects::VariablesController < Projects::ApplicationController feature_category :pipeline_authoring + urgency :low, [:show, :update] + def show respond_to do |format| format.json do diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 18ccea330af..7974710e67b 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -87,9 +87,13 @@ class GroupDescendantsFinder visible_to_user = visible_to_user.or(authorized_to_user) end - hierarchy_for_parent - .descendants - .where(visible_to_user) + group_to_query = if Feature.enabled?(:linear_group_descendants_finder, current_user, default_enabled: :yaml) + parent_group + else + hierarchy_for_parent + end + + group_to_query.descendants.where(visible_to_user) # rubocop: enable CodeReuse/Finder end # rubocop: enable CodeReuse/ActiveRecord @@ -155,7 +159,13 @@ class GroupDescendantsFinder # rubocop: disable CodeReuse/ActiveRecord def projects_matching_filter # rubocop: disable CodeReuse/Finder - projects_nested_in_group = Project.where(namespace_id: hierarchy_for_parent.base_and_descendants.select(:id)) + objects_in_hierarchy = if Feature.enabled?(:linear_group_descendants_finder, current_user, default_enabled: :yaml) + parent_group.self_and_descendants.as_ids + else + hierarchy_for_parent.base_and_descendants.select(:id) + end + + projects_nested_in_group = Project.where(namespace_id: objects_in_hierarchy) params_with_search = params.merge(search: params[:filter]) ProjectsFinder.new(params: params_with_search, diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index 33ce4686e27..38b7da76306 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -134,6 +134,10 @@ class BulkImports::Entity < ApplicationRecord source_type == 'group_entity' end + def update_service + "::#{pluralized_name.capitalize}::UpdateService".constantize + end + private def validate_parent_is_a_group diff --git a/app/models/bulk_imports/file_transfer/base_config.rb b/app/models/bulk_imports/file_transfer/base_config.rb index 4d370315ad5..e735503a47f 100644 --- a/app/models/bulk_imports/file_transfer/base_config.rb +++ b/app/models/bulk_imports/file_transfer/base_config.rb @@ -5,6 +5,8 @@ module BulkImports class BaseConfig include Gitlab::Utils::StrongMemoize + UPLOADS_RELATION = 'uploads' + def initialize(portable) @portable = portable end @@ -78,7 +80,7 @@ module BulkImports end def file_relations - [] + [UPLOADS_RELATION] end def skipped_relations diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb index 9a0434da08a..fdfb0dd0186 100644 --- a/app/models/bulk_imports/file_transfer/project_config.rb +++ b/app/models/bulk_imports/file_transfer/project_config.rb @@ -3,8 +3,6 @@ module BulkImports module FileTransfer class ProjectConfig < BaseConfig - UPLOADS_RELATION = 'uploads' - SKIPPED_RELATIONS = %w( project_members group_members @@ -14,10 +12,6 @@ module BulkImports ::Gitlab::ImportExport.config_file end - def file_relations - [UPLOADS_RELATION] - end - def skipped_relations SKIPPED_RELATIONS end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 2bf33f821ab..a18b760eeb4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -236,7 +236,12 @@ module Ci pipeline.run_after_commit do PipelineHooksWorker.perform_async(pipeline.id) - ExpirePipelineCacheWorker.perform_async(pipeline.id) + + if Feature.enabled?(:expire_job_and_pipeline_cache_synchronously, pipeline.project, default_enabled: :yaml) + Ci::ExpirePipelineCacheService.new.execute(pipeline) # rubocop: disable CodeReuse/ServiceClass + else + ExpirePipelineCacheWorker.perform_async(pipeline.id) + end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 1bbcf8837f6..d80b2fe37dc 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -188,7 +188,12 @@ class CommitStatus < Ci::ApplicationRecord commit_status.run_after_commit do PipelineProcessWorker.perform_async(pipeline_id) unless transition_options[:skip_pipeline_processing] - ExpireJobCacheWorker.perform_async(id) + + if Feature.enabled?(:expire_job_and_pipeline_cache_synchronously, project, default_enabled: :yaml) + expire_etag_cache! + else + ExpireJobCacheWorker.perform_async(id) + end end end @@ -301,6 +306,12 @@ class CommitStatus < Ci::ApplicationRecord .update_all(retried: true, processed: true) end + def expire_etag_cache! + job_path = Gitlab::Routing.url_helpers.project_build_path(project, id, format: :json) + + Gitlab::EtagCaching::Store.new.touch(job_path) + end + private def unrecoverable_failure? diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb index 563d4a924fc..1426bf25a00 100644 --- a/app/services/audit_event_service.rb +++ b/app/services/audit_event_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AuditEventService + include AuditEventSaveType + # Instantiates a new service # # @param [User] author the user who authors the change @@ -10,13 +12,16 @@ class AuditEventService # - Group: events are visible at Group and Instance level # - User: events are visible at Instance level # @param [Hash] details extra data of audit event + # @param [Symbol] save_type the type to save the event + # Can be selected from the following, :database, :stream, :database_and_stream . # # @return [AuditEventService] - def initialize(author, entity, details = {}) + def initialize(author, entity, details = {}, save_type = :database_and_stream) @author = build_author(author) @entity = entity @details = details @ip_address = resolve_ip_address(@author) + @save_type = save_type end # Builds the @details attribute for authentication @@ -133,8 +138,8 @@ class AuditEventService end def save_or_track(event) - event.save! - stream_event_to_external_destinations(event) + event.save! if should_save_database?(@save_type) + stream_event_to_external_destinations(event) if should_save_stream?(@save_type) rescue StandardError => e Gitlab::ErrorTracking.track_exception(e, audit_event_type: event.class.to_s) end diff --git a/app/services/bulk_imports/uploads_export_service.rb b/app/services/bulk_imports/uploads_export_service.rb index 32cc48c152c..7f5ee7b8624 100644 --- a/app/services/bulk_imports/uploads_export_service.rb +++ b/app/services/bulk_imports/uploads_export_service.rb @@ -5,6 +5,7 @@ module BulkImports include Gitlab::ImportExport::CommandLineUtil BATCH_SIZE = 100 + AVATAR_PATH = 'avatar' def initialize(portable, export_path) @portable = portable @@ -34,7 +35,7 @@ module BulkImports def export_subdir_path(upload) subdir = if upload.path == avatar_path - 'avatar' + AVATAR_PATH else upload.try(:secret).to_s end diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb index 177c85cebcc..8622b1a5863 100644 --- a/app/services/ci/expire_pipeline_cache_service.rb +++ b/app/services/ci/expire_pipeline_cache_service.rb @@ -74,20 +74,25 @@ module Ci def update_etag_cache(pipeline, store) project = pipeline.project - store.touch(project_pipelines_path(project)) - store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil? - store.touch(new_merge_request_pipelines_path(project)) + etag_paths = [ + project_pipelines_path(project), + new_merge_request_pipelines_path(project), + graphql_project_on_demand_scan_counts_path(project) + ] + + etag_paths << commit_pipelines_path(project, pipeline.commit) unless pipeline.commit.nil? + each_pipelines_merge_request_path(pipeline) do |path| - store.touch(path) + etag_paths << path end - pipeline.self_with_upstreams_and_downstreams.each do |relative_pipeline| - store.touch(project_pipeline_path(relative_pipeline.project, relative_pipeline)) - store.touch(graphql_pipeline_path(relative_pipeline)) - store.touch(graphql_pipeline_sha_path(relative_pipeline.sha)) + pipeline.self_with_upstreams_and_downstreams.includes(project: [:route, { namespace: :route }]).each do |relative_pipeline| # rubocop: disable CodeReuse/ActiveRecord + etag_paths << project_pipeline_path(relative_pipeline.project, relative_pipeline) + etag_paths << graphql_pipeline_path(relative_pipeline) + etag_paths << graphql_pipeline_sha_path(relative_pipeline.sha) end - store.touch(graphql_project_on_demand_scan_counts_path(project)) + store.touch(*etag_paths) end def url_helpers diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb index 236d660d829..d8ce063ffb4 100644 --- a/app/services/ci/pipeline_processing/atomic_processing_service.rb +++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb @@ -36,6 +36,10 @@ module Ci update_pipeline! update_statuses_processed! + if Feature.enabled?(:expire_job_and_pipeline_cache_synchronously, pipeline.project, default_enabled: :yaml) + Ci::ExpirePipelineCacheService.new.execute(pipeline) + end + true end diff --git a/app/services/concerns/audit_event_save_type.rb b/app/services/concerns/audit_event_save_type.rb new file mode 100644 index 00000000000..6696e4adae7 --- /dev/null +++ b/app/services/concerns/audit_event_save_type.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module AuditEventSaveType + SAVE_TYPES = { + database: 0b01, + stream: 0b10, + database_and_stream: 0b11 + }.freeze + + # def should_save_stream?(type) + # def should_save_database?(type) + [:database, :stream].each do |type| + define_method("should_save_#{type}?") do |param_type| + return false unless save_type_valid?(param_type) + + # If the current type does not support query, the result of the `&` operation is 0 . + SAVE_TYPES[param_type] & SAVE_TYPES[type] != 0 + end + end + + private + + def save_type_valid?(type) + SAVE_TYPES.key?(type) + end +end diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb index f120cb26d22..d2c83f82ff8 100644 --- a/app/services/merge_requests/after_create_service.rb +++ b/app/services/merge_requests/after_create_service.rb @@ -2,6 +2,8 @@ module MergeRequests class AfterCreateService < MergeRequests::BaseService + include Gitlab::Utils::StrongMemoize + def execute(merge_request) prepare_for_mergeability(merge_request) if early_prepare_for_mergeability?(merge_request) prepare_merge_request(merge_request) @@ -48,7 +50,9 @@ module MergeRequests end def early_prepare_for_mergeability?(merge_request) - Feature.enabled?(:early_prepare_for_mergeability, merge_request.target_project) + strong_memoize("early_prepare_for_mergeability_#{merge_request.target_project_id}".to_sym) do + Feature.enabled?(:early_prepare_for_mergeability, merge_request.target_project) + end end def mark_as_unchecked(merge_request) diff --git a/app/views/groups/crm/contacts.html.haml b/app/views/groups/crm/contacts.html.haml deleted file mode 100644 index 7d0ee5b64b1..00000000000 --- a/app/views/groups/crm/contacts.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- breadcrumb_title _('Customer Relations Contacts') -- page_title _('Customer Relations Contacts') - -#js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group) } } diff --git a/app/views/groups/crm/contacts/index.html.haml b/app/views/groups/crm/contacts/index.html.haml new file mode 100644 index 00000000000..81293937f77 --- /dev/null +++ b/app/views/groups/crm/contacts/index.html.haml @@ -0,0 +1,4 @@ +- breadcrumb_title _('Customer Relations Contacts') +- page_title _('Customer Relations Contacts') + +#js-crm-contacts-app{ data: { group_full_path: @group.full_path, group_issues_path: issues_group_path(@group), group_id: @group.id, can_admin_crm_contact: can?(current_user, :admin_crm_contact, @group).to_s, base_path: group_crm_contacts_path(@group) } } diff --git a/app/views/groups/crm/organizations.html.haml b/app/views/groups/crm/organizations/index.html.haml index e83dab9fda6..e83dab9fda6 100644 --- a/app/views/groups/crm/organizations.html.haml +++ b/app/views/groups/crm/organizations/index.html.haml diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 168b240c657..d4e7ee90a84 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -14,8 +14,8 @@ - if can_modify_blob?(@blob) = render 'projects/blob/remove' - - title = "Replace #{@blob.name}" - = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put + - title = _("Replace %{blob_name}") % { blob_name: @blob.name } + = render 'projects/blob/upload', title: title, placeholder: title, button_title: _('Replace file'), form_path: project_update_blob_path(@project, @id), method: :put = render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration? = render 'shared/web_ide_path' diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb index 3c5a7717d70..49f0222e9c9 100644 --- a/app/workers/expire_job_cache_worker.rb +++ b/app/workers/expire_job_cache_worker.rb @@ -15,19 +15,10 @@ class ExpireJobCacheWorker # rubocop:disable Scalability/IdempotentWorker idempotent! def perform(job_id) - job = CommitStatus.preload(:pipeline, :project).find_by_id(job_id) # rubocop: disable CodeReuse/ActiveRecord + job = CommitStatus.find_by_id(job_id) return unless job - pipeline = job.pipeline - project = job.project - - Gitlab::EtagCaching::Store.new.touch(project_job_path(project, job)) - ExpirePipelineCacheWorker.perform_async(pipeline.id) - end - - private - - def project_job_path(project, job) - Gitlab::Routing.url_helpers.project_build_path(project, job.id, format: :json) + job.expire_etag_cache! + ExpirePipelineCacheWorker.perform_async(job.pipeline_id) end end |