Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									df8f194428
								
							
						
					
					
						commit
						f8eba0f714
					
				| 
						 | 
				
			
			@ -328,11 +328,21 @@ jest predictive:
 | 
			
		|||
  extends:
 | 
			
		||||
    - jest
 | 
			
		||||
    - .frontend:rules:jest:predictive
 | 
			
		||||
  needs:
 | 
			
		||||
    - !reference [jest, needs]
 | 
			
		||||
    - "detect-tests"
 | 
			
		||||
  script:
 | 
			
		||||
    - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive-without-fixtures"; fi
 | 
			
		||||
 | 
			
		||||
jest-with-fixtures predictive:
 | 
			
		||||
  extends:
 | 
			
		||||
    - jest-with-fixtures
 | 
			
		||||
    - .frontend:rules:jest:predictive
 | 
			
		||||
  needs:
 | 
			
		||||
    - !reference [jest-with-fixtures, needs]
 | 
			
		||||
    - "detect-tests"
 | 
			
		||||
  script:
 | 
			
		||||
    - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive"; fi
 | 
			
		||||
    - if [[ -s "$RSPEC_CHANGED_FILES_PATH" ]] || [[ -s "$RSPEC_MATCHING_JS_FILES_PATH" ]]; then run_timed_command "yarn jest:ci:predictive-with-fixtures"; fi
 | 
			
		||||
 | 
			
		||||
jest-integration:
 | 
			
		||||
  extends:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,3 +25,10 @@ export const IID_FAILURE = 'missing_iid';
 | 
			
		|||
 | 
			
		||||
export const RETRY_ACTION_TITLE = 'Retry';
 | 
			
		||||
export const MANUAL_ACTION_TITLE = 'Run';
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
  this poll interval is shared between the graph,
 | 
			
		||||
  pipeline header, jobs tab and failed jobs tab to
 | 
			
		||||
  keep all the data relatively in sync
 | 
			
		||||
*/
 | 
			
		||||
export const POLL_INTERVAL = 10000;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ import {
 | 
			
		|||
  SKIP_RETRY_MODAL_KEY,
 | 
			
		||||
  STAGE_VIEW,
 | 
			
		||||
  VIEW_TYPE_KEY,
 | 
			
		||||
  POLL_INTERVAL,
 | 
			
		||||
} from './constants';
 | 
			
		||||
import PipelineGraph from './components/graph_component.vue';
 | 
			
		||||
import GraphViewSelector from './components/graph_view_selector.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -129,7 +130,7 @@ export default {
 | 
			
		|||
        return getQueryHeaders(this.graphqlResourceEtag);
 | 
			
		||||
      },
 | 
			
		||||
      query: getPipelineDetails,
 | 
			
		||||
      pollInterval: 10000,
 | 
			
		||||
      pollInterval: POLL_INTERVAL,
 | 
			
		||||
      variables() {
 | 
			
		||||
        return {
 | 
			
		||||
          projectPath: this.pipelineProjectPath,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,5 @@
 | 
			
		|||
export const DELETE_MODAL_ID = 'pipeline-delete-modal';
 | 
			
		||||
 | 
			
		||||
export const POLL_INTERVAL = 10000;
 | 
			
		||||
 | 
			
		||||
export const SCHEDULE_SOURCE = 'schedule';
 | 
			
		||||
export const AUTO_DEVOPS_SOURCE = 'AUTO_DEVOPS_SOURCE';
 | 
			
		||||
export const DETACHED_EVENT_TYPE = 'DETACHED';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,10 +14,10 @@ import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutatio
 | 
			
		|||
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
 | 
			
		||||
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
 | 
			
		||||
import { getQueryHeaders } from '../graph/utils';
 | 
			
		||||
import { POLL_INTERVAL } from '../graph/constants';
 | 
			
		||||
import HeaderActions from './components/header_actions.vue';
 | 
			
		||||
import HeaderBadges from './components/header_badges.vue';
 | 
			
		||||
import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
 | 
			
		||||
import { POLL_INTERVAL } from './constants';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'PipelineHeader',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,8 @@
 | 
			
		|||
import { GlLoadingIcon } from '@gitlab/ui';
 | 
			
		||||
import { s__ } from '~/locale';
 | 
			
		||||
import { createAlert } from '~/alert';
 | 
			
		||||
import { getQueryHeaders } from '../graph/utils';
 | 
			
		||||
import { POLL_INTERVAL } from '../graph/constants';
 | 
			
		||||
import GetFailedJobsQuery from './graphql/queries/get_failed_jobs.query.graphql';
 | 
			
		||||
import FailedJobsTable from './components/failed_jobs_table.vue';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -17,10 +19,17 @@ export default {
 | 
			
		|||
    pipelineIid: {
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    graphqlResourceEtag: {
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  apollo: {
 | 
			
		||||
    failedJobs: {
 | 
			
		||||
      context() {
 | 
			
		||||
        return getQueryHeaders(this.graphqlResourceEtag);
 | 
			
		||||
      },
 | 
			
		||||
      query: GetFailedJobsQuery,
 | 
			
		||||
      pollInterval: POLL_INTERVAL,
 | 
			
		||||
      variables() {
 | 
			
		||||
        return {
 | 
			
		||||
          fullPath: this.projectPath,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,8 @@ import { __ } from '~/locale';
 | 
			
		|||
import eventHub from '~/ci/jobs_page/event_hub';
 | 
			
		||||
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
 | 
			
		||||
import { JOBS_TAB_FIELDS } from '~/ci/jobs_page/constants';
 | 
			
		||||
import { getQueryHeaders } from '../graph/utils';
 | 
			
		||||
import { POLL_INTERVAL } from '../graph/constants';
 | 
			
		||||
import getPipelineJobs from './graphql/queries/get_pipeline_jobs.query.graphql';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,10 +25,17 @@ export default {
 | 
			
		|||
    pipelineIid: {
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
    graphqlResourceEtag: {
 | 
			
		||||
      default: '',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  apollo: {
 | 
			
		||||
    jobs: {
 | 
			
		||||
      context() {
 | 
			
		||||
        return getQueryHeaders(this.graphqlResourceEtag);
 | 
			
		||||
      },
 | 
			
		||||
      query: getPipelineJobs,
 | 
			
		||||
      pollInterval: POLL_INTERVAL,
 | 
			
		||||
      variables() {
 | 
			
		||||
        return {
 | 
			
		||||
          ...this.queryVariables,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,11 @@ export default {
 | 
			
		|||
      required: false,
 | 
			
		||||
      default: null,
 | 
			
		||||
    },
 | 
			
		||||
    isEditing: {
 | 
			
		||||
      type: Boolean,
 | 
			
		||||
      required: false,
 | 
			
		||||
      default: false,
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +66,7 @@ export default {
 | 
			
		|||
  },
 | 
			
		||||
  created() {
 | 
			
		||||
    const validatePath = async () => {
 | 
			
		||||
      if (this.isEditingGroup && this.value === this.initialValue) return;
 | 
			
		||||
      if (this.isEditing && this.value === this.initialValue) return;
 | 
			
		||||
 | 
			
		||||
      this.suggestedPath = '';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -162,7 +162,7 @@ export default {
 | 
			
		|||
      };
 | 
			
		||||
    },
 | 
			
		||||
    isEditing() {
 | 
			
		||||
      return this.initialFormValues[FORM_FIELD_ID];
 | 
			
		||||
      return Boolean(this.initialFormValues[FORM_FIELD_ID]);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
| 
						 | 
				
			
			@ -210,6 +210,7 @@ export default {
 | 
			
		|||
          :value="value"
 | 
			
		||||
          :state="validation.state"
 | 
			
		||||
          :base-path="basePath"
 | 
			
		||||
          :is-editing="isEditing"
 | 
			
		||||
          @input="onPathInput($event, input)"
 | 
			
		||||
          @input-suggested-path="input"
 | 
			
		||||
          @blur="blur"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,22 @@
 | 
			
		|||
<script>
 | 
			
		||||
import { GlSprintf } from '@gitlab/ui';
 | 
			
		||||
import NewEditForm from '~/groups/components/new_edit_form.vue';
 | 
			
		||||
import { __ } from '~/locale';
 | 
			
		||||
import { FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_VISIBILITY_LEVEL } from '~/groups/constants';
 | 
			
		||||
import { __, s__ } from '~/locale';
 | 
			
		||||
import { VISIBILITY_LEVELS_INTEGER_TO_STRING } from '~/visibility_level/constants';
 | 
			
		||||
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
 | 
			
		||||
import { createAlert } from '~/alert';
 | 
			
		||||
import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
 | 
			
		||||
import groupUpdateMutation from '../graphql/mutations/group_update.mutation.graphql';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'OrganizationGroupsEditApp',
 | 
			
		||||
  components: { GlSprintf, NewEditForm },
 | 
			
		||||
  components: { GlSprintf, FormErrorsAlert, NewEditForm },
 | 
			
		||||
  i18n: {
 | 
			
		||||
    pageTitle: __('Edit group: %{group_name}'),
 | 
			
		||||
    submitButtonText: __('Save changes'),
 | 
			
		||||
    errorMessage: s__('Groups|An error occurred updating this group. Please try again.'),
 | 
			
		||||
    successMessage: __('Group was successfully updated.'),
 | 
			
		||||
  },
 | 
			
		||||
  inject: [
 | 
			
		||||
    'group',
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +29,57 @@ export default {
 | 
			
		|||
    'pathMaxlength',
 | 
			
		||||
    'pathPattern',
 | 
			
		||||
  ],
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      loading: false,
 | 
			
		||||
      errors: [],
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async onSubmit({
 | 
			
		||||
      [FORM_FIELD_NAME]: name,
 | 
			
		||||
      [FORM_FIELD_PATH]: path,
 | 
			
		||||
      [FORM_FIELD_VISIBILITY_LEVEL]: visibility,
 | 
			
		||||
    }) {
 | 
			
		||||
      try {
 | 
			
		||||
        this.loading = true;
 | 
			
		||||
 | 
			
		||||
        const {
 | 
			
		||||
          data: {
 | 
			
		||||
            groupUpdate: { group, errors },
 | 
			
		||||
          },
 | 
			
		||||
        } = await this.$apollo.mutate({
 | 
			
		||||
          mutation: groupUpdateMutation,
 | 
			
		||||
          variables: {
 | 
			
		||||
            input: {
 | 
			
		||||
              fullPath: this.group.fullPath,
 | 
			
		||||
              name,
 | 
			
		||||
              path,
 | 
			
		||||
              visibility: VISIBILITY_LEVELS_INTEGER_TO_STRING[visibility],
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (errors.length) {
 | 
			
		||||
          this.errors = errors;
 | 
			
		||||
          this.loading = false;
 | 
			
		||||
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        visitUrlWithAlerts(group.organizationEditPath, [
 | 
			
		||||
          {
 | 
			
		||||
            id: 'organization-group-successfully-updated',
 | 
			
		||||
            message: this.$options.i18n.successMessage,
 | 
			
		||||
            variant: 'info',
 | 
			
		||||
          },
 | 
			
		||||
        ]);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        this.loading = false;
 | 
			
		||||
        createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -31,8 +90,9 @@ export default {
 | 
			
		|||
        <template #group_name>{{ group.fullName }}</template>
 | 
			
		||||
      </gl-sprintf>
 | 
			
		||||
    </h1>
 | 
			
		||||
    <form-errors-alert v-model="errors" :scroll-on-error="true" />
 | 
			
		||||
    <new-edit-form
 | 
			
		||||
      :loading="false"
 | 
			
		||||
      :loading="loading"
 | 
			
		||||
      :base-path="basePath"
 | 
			
		||||
      :path-maxlength="pathMaxlength"
 | 
			
		||||
      :path-pattern="pathPattern"
 | 
			
		||||
| 
						 | 
				
			
			@ -41,6 +101,7 @@ export default {
 | 
			
		|||
      :available-visibility-levels="availableVisibilityLevels"
 | 
			
		||||
      :restricted-visibility-levels="restrictedVisibilityLevels"
 | 
			
		||||
      :initial-form-values="group"
 | 
			
		||||
      @submit="onSubmit"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
mutation groupUpdate($input: GroupUpdateInput!) {
 | 
			
		||||
  groupUpdate(input: $input) {
 | 
			
		||||
    group {
 | 
			
		||||
      id
 | 
			
		||||
      organizationEditPath
 | 
			
		||||
    }
 | 
			
		||||
    errors
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
import Vue from 'vue';
 | 
			
		||||
import VueApollo from 'vue-apollo';
 | 
			
		||||
 | 
			
		||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
 | 
			
		||||
import createDefaultClient from '~/lib/graphql';
 | 
			
		||||
import App from './components/app.vue';
 | 
			
		||||
 | 
			
		||||
export const initOrganizationsGroupsEdit = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,9 +25,14 @@ export const initOrganizationsGroupsEdit = () => {
 | 
			
		|||
    pathPattern,
 | 
			
		||||
  } = convertObjectPropsToCamelCase(JSON.parse(appData), { deep: true });
 | 
			
		||||
 | 
			
		||||
  const apolloProvider = new VueApollo({
 | 
			
		||||
    defaultClient: createDefaultClient(),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return new Vue({
 | 
			
		||||
    el,
 | 
			
		||||
    name: 'OrganizationGroupsEditRoot',
 | 
			
		||||
    apolloProvider,
 | 
			
		||||
    provide: {
 | 
			
		||||
      group,
 | 
			
		||||
      basePath,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,9 +22,18 @@ module Mutations
 | 
			
		|||
      argument :math_rendering_limits_enabled, GraphQL::Types::Boolean,
 | 
			
		||||
        required: false,
 | 
			
		||||
        description: copy_field_description(Types::GroupType, :math_rendering_limits_enabled)
 | 
			
		||||
      argument :name, GraphQL::Types::String,
 | 
			
		||||
        required: false,
 | 
			
		||||
        description: copy_field_description(Types::GroupType, :name)
 | 
			
		||||
      argument :path, GraphQL::Types::String,
 | 
			
		||||
        required: false,
 | 
			
		||||
        description: copy_field_description(Types::GroupType, :path)
 | 
			
		||||
      argument :shared_runners_setting, Types::Namespace::SharedRunnersSettingEnum,
 | 
			
		||||
        required: false,
 | 
			
		||||
        description: copy_field_description(Types::GroupType, :shared_runners_setting)
 | 
			
		||||
      argument :visibility, Types::VisibilityLevelsEnum,
 | 
			
		||||
        required: false,
 | 
			
		||||
        description: copy_field_description(Types::GroupType, :visibility)
 | 
			
		||||
 | 
			
		||||
      def resolve(full_path:, **args)
 | 
			
		||||
        group = authorized_find!(full_path: full_path)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@ module Organizations
 | 
			
		|||
 | 
			
		||||
    def organization_groups_edit_app_data(organization, group)
 | 
			
		||||
      {
 | 
			
		||||
        group: group.slice(:id, :full_name, :name, :visibility_level, :path)
 | 
			
		||||
        group: group.slice(:id, :full_name, :name, :visibility_level, :path, :full_path)
 | 
			
		||||
      }.merge(shared_organization_groups_app_data(organization)).to_json
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2392,6 +2392,8 @@ class Project < ApplicationRecord
 | 
			
		|||
      :started
 | 
			
		||||
    elsif export_file_exists?
 | 
			
		||||
      :finished
 | 
			
		||||
    elsif export_failed?
 | 
			
		||||
      :failed
 | 
			
		||||
    else
 | 
			
		||||
      :none
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -2409,6 +2411,12 @@ class Project < ApplicationRecord
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def export_failed?
 | 
			
		||||
    strong_memoize(:export_failed) do
 | 
			
		||||
      ::Projects::ExportJobFinder.new(self, { status: :failed }).execute.present?
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def regeneration_in_progress?
 | 
			
		||||
    (export_enqueued? || export_in_progress?) && export_file_exists?
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ module Projects
 | 
			
		|||
          next if Gitlab::SidekiqStatus.running?(relation_export.jid)
 | 
			
		||||
          next if relation_export.reset.finished?
 | 
			
		||||
 | 
			
		||||
          relation_export.mark_as_failed("Exausted number of retries to export: #{relation_export.relation}")
 | 
			
		||||
          relation_export.mark_as_failed("Exhausted number of retries to export: #{relation_export.relation}")
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,11 @@ class StuckExportJobsWorker
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
    completed_jobs.each do |job|
 | 
			
		||||
      # Parallel export job completes and keeps 'started' state because it has
 | 
			
		||||
      # multiple relation exports running in parallel. Don't mark it as failed
 | 
			
		||||
      # until 6 hours mark
 | 
			
		||||
      next if job.relation_exports.any? && job.created_at > EXPORT_JOBS_EXPIRATION.seconds.ago
 | 
			
		||||
 | 
			
		||||
      job.fail_op
 | 
			
		||||
    end.count
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5223,7 +5223,10 @@ Input type: `GroupUpdateInput`
 | 
			
		|||
| <a id="mutationgroupupdatelockduofeaturesenabled"></a>`lockDuoFeaturesEnabled` | [`Boolean`](#boolean) | Indicates if the GitLab Duo features enabled setting is enforced for all subgroups. Introduced in GitLab 16.10: **Status**: Experiment. |
 | 
			
		||||
| <a id="mutationgroupupdatelockmathrenderinglimitsenabled"></a>`lockMathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are locked for all descendant groups. |
 | 
			
		||||
| <a id="mutationgroupupdatemathrenderinglimitsenabled"></a>`mathRenderingLimitsEnabled` | [`Boolean`](#boolean) | Indicates if math rendering limits are used for this group. |
 | 
			
		||||
| <a id="mutationgroupupdatename"></a>`name` | [`String`](#string) | Name of the namespace. |
 | 
			
		||||
| <a id="mutationgroupupdatepath"></a>`path` | [`String`](#string) | Path of the namespace. |
 | 
			
		||||
| <a id="mutationgroupupdatesharedrunnerssetting"></a>`sharedRunnersSetting` | [`SharedRunnersSetting`](#sharedrunnerssetting) | Shared runners availability for the namespace and its descendants. |
 | 
			
		||||
| <a id="mutationgroupupdatevisibility"></a>`visibility` | [`VisibilityLevelsEnum`](#visibilitylevelsenum) | Visibility of the namespace. |
 | 
			
		||||
 | 
			
		||||
#### Fields
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,7 +174,7 @@ The month-over-month comparison of the AI Usage unique users rate gives a more a
 | 
			
		|||
The baseline for the AI Usage trend is the total number of code contributors, not just users with GitLab Duo seats. This baseline gives a more accurate representation of AI usage by team members.
 | 
			
		||||
 | 
			
		||||
NOTE:
 | 
			
		||||
Usage rate for Code Suggestions is calculated with data starting on 2024-04-04.
 | 
			
		||||
Usage rate for Code Suggestions is calculated with data starting from GitLab 16.11.
 | 
			
		||||
For more information, see [epic 12978](https://gitlab.com/groups/gitlab-org/-/epics/12978).
 | 
			
		||||
 | 
			
		||||
## Enable or disable overview background aggregation
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,8 @@ A deploy token is a pair of values:
 | 
			
		|||
  `gitlab+deploy-token-{n}`. You can specify a custom username when you create the deploy token.
 | 
			
		||||
- **token**: `password` in the HTTP authentication framework.
 | 
			
		||||
 | 
			
		||||
Deploy tokens do not support [SSH authentication](../../ssh.md).
 | 
			
		||||
 | 
			
		||||
You can use a deploy token for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)
 | 
			
		||||
to the following endpoints:
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25636,6 +25636,9 @@ msgstr ""
 | 
			
		|||
msgid "GroupsTree|Search by name"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Groups|An error occurred updating this group. Please try again."
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
msgid "Groups|Avatar will be removed. Are you sure?"
 | 
			
		||||
msgstr ""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,8 @@
 | 
			
		|||
    "jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
 | 
			
		||||
    "jest:ci:without-fixtures": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
 | 
			
		||||
    "jest:ci:with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
 | 
			
		||||
    "jest:ci:predictive": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
 | 
			
		||||
    "jest:ci:predictive-without-fixtures": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
 | 
			
		||||
    "jest:ci:predictive-with-fixtures": "JEST_FIXTURE_JOBS_ONLY=1 jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) $(cat $RSPEC_MATCHING_JS_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/fixture_ci_sequencer.js --shard \"${CI_NODE_INDEX:-1}/${CI_NODE_TOTAL:-1}\" --logHeapUsage",
 | 
			
		||||
    "jest:contract": "PACT_DO_NOT_TRACK=true jest --config jest.config.contract.js --runInBand",
 | 
			
		||||
    "jest:integration": "jest --config jest.config.integration.js",
 | 
			
		||||
    "jest:scripts": "jest --config jest.config.scripts.js",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -294,7 +294,7 @@ RSpec.describe PersonalAccessTokensFinder, :enable_admin_mode, feature_category:
 | 
			
		|||
      with_them do
 | 
			
		||||
        let(:params) { { sort: sort } }
 | 
			
		||||
 | 
			
		||||
        it 'returns ordered tokens' do
 | 
			
		||||
        it 'returns ordered tokens', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/446283' do
 | 
			
		||||
          expect(subject.map(&:id)).to eq(tokens.values_at(*expected_tokens).map(&:id))
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import { createAlert } from '~/alert';
 | 
			
		|||
import FailedJobsApp from '~/ci/pipeline_details/jobs/failed_jobs_app.vue';
 | 
			
		||||
import FailedJobsTable from '~/ci/pipeline_details/jobs/components/failed_jobs_table.vue';
 | 
			
		||||
import GetFailedJobsQuery from '~/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql';
 | 
			
		||||
import { POLL_INTERVAL } from '~/ci/pipeline_details/graph/constants';
 | 
			
		||||
import { mockFailedJobsQueryResponse } from 'jest/ci/pipeline_details/mock_data';
 | 
			
		||||
 | 
			
		||||
Vue.use(VueApollo);
 | 
			
		||||
| 
						 | 
				
			
			@ -27,11 +28,14 @@ describe('Failed Jobs App', () => {
 | 
			
		|||
    return createMockApollo(requestHandlers);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const graphqlResourceEtag = '/api/graphql:pipelines/id/1';
 | 
			
		||||
 | 
			
		||||
  const createComponent = (resolver) => {
 | 
			
		||||
    wrapper = shallowMount(FailedJobsApp, {
 | 
			
		||||
      provide: {
 | 
			
		||||
        fullPath: 'root/ci-project',
 | 
			
		||||
        pipelineIid: 1,
 | 
			
		||||
        graphqlResourceEtag,
 | 
			
		||||
      },
 | 
			
		||||
      apolloProvider: createMockApolloProvider(resolver),
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -77,4 +81,18 @@ describe('Failed Jobs App', () => {
 | 
			
		|||
      message: 'There was a problem fetching the failed jobs.',
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('polling', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      createComponent(resolverSpy);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('polls for query data', () => {
 | 
			
		||||
      expect(resolverSpy).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
      jest.advanceTimersByTime(POLL_INTERVAL);
 | 
			
		||||
 | 
			
		||||
      expect(resolverSpy).toHaveBeenCalledTimes(2);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import { createAlert } from '~/alert';
 | 
			
		|||
import JobsApp from '~/ci/pipeline_details/jobs/jobs_app.vue';
 | 
			
		||||
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
 | 
			
		||||
import getPipelineJobsQuery from '~/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql';
 | 
			
		||||
import { POLL_INTERVAL } from '~/ci/pipeline_details/graph/constants';
 | 
			
		||||
import { mockPipelineJobsQueryResponse } from '../mock_data';
 | 
			
		||||
 | 
			
		||||
Vue.use(VueApollo);
 | 
			
		||||
| 
						 | 
				
			
			@ -31,11 +32,14 @@ describe('Jobs app', () => {
 | 
			
		|||
    return createMockApollo(requestHandlers);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const graphqlResourceEtag = '/api/graphql:pipelines/id/1';
 | 
			
		||||
 | 
			
		||||
  const createComponent = (resolver) => {
 | 
			
		||||
    wrapper = shallowMount(JobsApp, {
 | 
			
		||||
      provide: {
 | 
			
		||||
        projectPath: 'root/ci-project',
 | 
			
		||||
        pipelineIid: 1,
 | 
			
		||||
        graphqlResourceEtag,
 | 
			
		||||
      },
 | 
			
		||||
      apolloProvider: createMockApolloProvider(resolver),
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -124,4 +128,18 @@ describe('Jobs app', () => {
 | 
			
		|||
 | 
			
		||||
    expect(findSkeletonLoader().exists()).toBe(false);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('polling', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      createComponent(resolverSpy);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('polls for query data', () => {
 | 
			
		||||
      expect(resolverSpy).toHaveBeenCalledTimes(1);
 | 
			
		||||
 | 
			
		||||
      jest.advanceTimersByTime(POLL_INTERVAL);
 | 
			
		||||
 | 
			
		||||
      expect(resolverSpy).toHaveBeenCalledTimes(2);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,6 +93,49 @@ RSpec.describe 'Organizations (GraphQL fixtures)', feature_category: :cell do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'organization update group' do
 | 
			
		||||
      base_input_path = 'organizations/groups/edit/graphql/mutations/'
 | 
			
		||||
      base_output_path = 'graphql/organizations/'
 | 
			
		||||
      mutation_name = 'group_update.mutation.graphql'
 | 
			
		||||
 | 
			
		||||
      it "#{base_output_path}#{mutation_name}.json" do
 | 
			
		||||
        mutation = get_graphql_query_as_string("#{base_input_path}#{mutation_name}")
 | 
			
		||||
 | 
			
		||||
        post_graphql(
 | 
			
		||||
          mutation,
 | 
			
		||||
          current_user: current_user,
 | 
			
		||||
          variables: {
 | 
			
		||||
            input: {
 | 
			
		||||
              full_path: group.full_path,
 | 
			
		||||
              name: "#{group.name} updated",
 | 
			
		||||
              path: "#{group.path}-updated"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        expect_graphql_errors_to_be_empty
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it "#{base_output_path}#{mutation_name}_with_errors.json" do
 | 
			
		||||
        mutation = get_graphql_query_as_string("#{base_input_path}#{mutation_name}")
 | 
			
		||||
 | 
			
		||||
        post_graphql(
 | 
			
		||||
          mutation,
 | 
			
		||||
          current_user: current_user,
 | 
			
		||||
          variables: {
 | 
			
		||||
            input: {
 | 
			
		||||
              full_path: group.full_path,
 | 
			
		||||
              name: "#{group.name} updated",
 | 
			
		||||
              path: "#{group.path}-updated",
 | 
			
		||||
              visibility: 'private'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        expect_graphql_errors_to_be_empty
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'organization projects' do
 | 
			
		||||
      base_input_path = 'organizations/shared/graphql/queries/'
 | 
			
		||||
      base_output_path = 'graphql/organizations/'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,6 +62,21 @@ describe('GroupPathField', () => {
 | 
			
		|||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('when editing a group and path is set to initial path', () => {
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        apiMockUnavailablePath();
 | 
			
		||||
 | 
			
		||||
        createComponent({ propsData: { isEditing: true, value: 'foo' } });
 | 
			
		||||
        await wrapper.setProps({ value: 'foo bar' });
 | 
			
		||||
        await waitForPromises();
 | 
			
		||||
        await wrapper.setProps({ value: 'foo' });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('does not call API', () => {
 | 
			
		||||
        expect(getGroupPathAvailability).toHaveBeenCalledTimes(1);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('when value is not the suggested path', () => {
 | 
			
		||||
      describe('when value is an unavailable path', () => {
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -136,6 +136,10 @@ describe('NewEditForm', () => {
 | 
			
		|||
 | 
			
		||||
      expect(findPathField().props('value')).toBe('foo-bar');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('sets `isEditing` prop to `true`', () => {
 | 
			
		||||
      expect(findPathField().props('isEditing')).toBe(true);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('when form is submitted without filling in required fields', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,15 +1,34 @@
 | 
			
		|||
import { GlSprintf } from '@gitlab/ui';
 | 
			
		||||
import VueApollo from 'vue-apollo';
 | 
			
		||||
import Vue, { nextTick } from 'vue';
 | 
			
		||||
 | 
			
		||||
import groupUpdateResponse from 'test_fixtures/graphql/organizations/group_update.mutation.graphql.json';
 | 
			
		||||
import groupUpdateResponseWithErrors from 'test_fixtures/graphql/organizations/group_update.mutation.graphql_with_errors.json';
 | 
			
		||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | 
			
		||||
import App from '~/organizations/groups/edit/components/app.vue';
 | 
			
		||||
import groupUpdateMutation from '~/organizations/groups/edit/graphql/mutations/group_update.mutation.graphql';
 | 
			
		||||
import {
 | 
			
		||||
  VISIBILITY_LEVEL_INTERNAL_INTEGER,
 | 
			
		||||
  VISIBILITY_LEVEL_PRIVATE_INTEGER,
 | 
			
		||||
  VISIBILITY_LEVEL_PUBLIC_INTEGER,
 | 
			
		||||
  VISIBILITY_LEVEL_PRIVATE_STRING,
 | 
			
		||||
} from '~/visibility_level/constants';
 | 
			
		||||
import NewEditForm from '~/groups/components/new_edit_form.vue';
 | 
			
		||||
import { createAlert } from '~/alert';
 | 
			
		||||
import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
 | 
			
		||||
import { FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_VISIBILITY_LEVEL } from '~/groups/constants';
 | 
			
		||||
import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
 | 
			
		||||
import createMockApollo from 'helpers/mock_apollo_helper';
 | 
			
		||||
import waitForPromises from 'helpers/wait_for_promises';
 | 
			
		||||
 | 
			
		||||
jest.mock('~/lib/utils/url_utility');
 | 
			
		||||
jest.mock('~/alert');
 | 
			
		||||
 | 
			
		||||
Vue.use(VueApollo);
 | 
			
		||||
 | 
			
		||||
describe('OrganizationGroupsEditApp', () => {
 | 
			
		||||
  let wrapper;
 | 
			
		||||
  let mockApollo;
 | 
			
		||||
 | 
			
		||||
  const defaultProvide = {
 | 
			
		||||
    group: {
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +36,7 @@ describe('OrganizationGroupsEditApp', () => {
 | 
			
		|||
      fullName: 'Mock namespace / Foo bar',
 | 
			
		||||
      name: 'Foo bar',
 | 
			
		||||
      path: 'foo-bar',
 | 
			
		||||
      fullPath: 'mock-namespace/foo-bar',
 | 
			
		||||
    },
 | 
			
		||||
    basePath: 'https://gitlab.com',
 | 
			
		||||
    groupsAndProjectsOrganizationPath: '/-/organizations/carrot/groups_and_projects?display=groups',
 | 
			
		||||
| 
						 | 
				
			
			@ -32,8 +52,15 @@ describe('OrganizationGroupsEditApp', () => {
 | 
			
		|||
    pathPattern: 'mockPattern',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createComponent = () => {
 | 
			
		||||
  const successfulResponseHandler = jest.fn().mockResolvedValue(groupUpdateResponse);
 | 
			
		||||
 | 
			
		||||
  const createComponent = ({
 | 
			
		||||
    handlers = [[groupUpdateMutation, successfulResponseHandler]],
 | 
			
		||||
  } = {}) => {
 | 
			
		||||
    mockApollo = createMockApollo(handlers);
 | 
			
		||||
 | 
			
		||||
    wrapper = shallowMountExtended(App, {
 | 
			
		||||
      apolloProvider: mockApollo,
 | 
			
		||||
      provide: defaultProvide,
 | 
			
		||||
      stubs: {
 | 
			
		||||
        GlSprintf,
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +69,18 @@ describe('OrganizationGroupsEditApp', () => {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  const findForm = () => wrapper.findComponent(NewEditForm);
 | 
			
		||||
  const submitForm = async () => {
 | 
			
		||||
    findForm().vm.$emit('submit', {
 | 
			
		||||
      [FORM_FIELD_NAME]: 'Foo bar',
 | 
			
		||||
      [FORM_FIELD_PATH]: 'foo-bar',
 | 
			
		||||
      [FORM_FIELD_VISIBILITY_LEVEL]: VISIBILITY_LEVEL_PRIVATE_INTEGER,
 | 
			
		||||
    });
 | 
			
		||||
    await nextTick();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  afterEach(() => {
 | 
			
		||||
    mockApollo = null;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('renders page title', () => {
 | 
			
		||||
    createComponent();
 | 
			
		||||
| 
						 | 
				
			
			@ -66,4 +105,88 @@ describe('OrganizationGroupsEditApp', () => {
 | 
			
		|||
      submitButtonText: 'Save changes',
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('when form is submitted', () => {
 | 
			
		||||
    describe('when API is loading', () => {
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        createComponent();
 | 
			
		||||
 | 
			
		||||
        await submitForm();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('sets `NewEditForm` `loading` prop to `true`', () => {
 | 
			
		||||
        expect(findForm().props('loading')).toBe(true);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('when API request is successful', () => {
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        createComponent();
 | 
			
		||||
        await submitForm();
 | 
			
		||||
        await waitForPromises();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('calls mutation with correct variables and redirects user to organization web url', () => {
 | 
			
		||||
        expect(successfulResponseHandler).toHaveBeenCalledWith({
 | 
			
		||||
          input: {
 | 
			
		||||
            fullPath: defaultProvide.group.fullPath,
 | 
			
		||||
            name: 'Foo bar',
 | 
			
		||||
            path: 'foo-bar',
 | 
			
		||||
            visibility: VISIBILITY_LEVEL_PRIVATE_STRING,
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        expect(visitUrlWithAlerts).toHaveBeenCalledWith(
 | 
			
		||||
          groupUpdateResponse.data.groupUpdate.group.organizationEditPath,
 | 
			
		||||
          [
 | 
			
		||||
            {
 | 
			
		||||
              id: 'organization-group-successfully-updated',
 | 
			
		||||
              message: 'Group was successfully updated.',
 | 
			
		||||
              variant: 'info',
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('when API request is not successful', () => {
 | 
			
		||||
      describe('when there is a network error', () => {
 | 
			
		||||
        const error = new Error();
 | 
			
		||||
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
          createComponent({
 | 
			
		||||
            handlers: [[groupUpdateMutation, jest.fn().mockRejectedValue(error)]],
 | 
			
		||||
          });
 | 
			
		||||
          await submitForm();
 | 
			
		||||
          await waitForPromises();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('displays error alert', () => {
 | 
			
		||||
          expect(createAlert).toHaveBeenCalledWith({
 | 
			
		||||
            message: 'An error occurred updating this group. Please try again.',
 | 
			
		||||
            error,
 | 
			
		||||
            captureError: true,
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      describe('when there are GraphQL errors', () => {
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
          createComponent({
 | 
			
		||||
            handlers: [
 | 
			
		||||
              [groupUpdateMutation, jest.fn().mockResolvedValue(groupUpdateResponseWithErrors)],
 | 
			
		||||
            ],
 | 
			
		||||
          });
 | 
			
		||||
          await submitForm();
 | 
			
		||||
          await waitForPromises();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('displays form errors alert', () => {
 | 
			
		||||
          expect(wrapper.findComponent(FormErrorsAlert).props()).toStrictEqual({
 | 
			
		||||
            errors: groupUpdateResponseWithErrors.data.groupUpdate.errors,
 | 
			
		||||
            scrollOnError: true,
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -326,6 +326,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
 | 
			
		|||
            'full_name' => group.full_name,
 | 
			
		||||
            'name' => group.name,
 | 
			
		||||
            'path' => group.path,
 | 
			
		||||
            'full_path' => group.full_path,
 | 
			
		||||
            "visibility_level" => group.visibility_level
 | 
			
		||||
          },
 | 
			
		||||
          'base_path' => root_url,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7890,6 +7890,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
 | 
			
		|||
      it { expect(project.export_status).to eq :queued }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when project export is failed' do
 | 
			
		||||
      before do
 | 
			
		||||
        project_export_job.fail_op!
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      it { expect(project.export_status).to eq :failed }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when project export is in progress' do
 | 
			
		||||
      before do
 | 
			
		||||
        project_export_job.start!
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -44,6 +44,8 @@ RSpec.describe 'GroupUpdate', feature_category: :groups_and_projects do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  context 'when authorized' do
 | 
			
		||||
    using RSpec::Parameterized::TableSyntax
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      group.add_owner(user)
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -65,6 +67,22 @@ RSpec.describe 'GroupUpdate', feature_category: :groups_and_projects do
 | 
			
		|||
      expect(group.reload.shared_runners_setting).to eq(variables[:shared_runners_setting].downcase)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    where(:field, :value) do
 | 
			
		||||
      'name'   | 'foo bar'
 | 
			
		||||
      'path'   | 'foo-bar'
 | 
			
		||||
      'visibility' | 'private'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    with_them do
 | 
			
		||||
      let(:variables) { { full_path: group.full_path, field => value } }
 | 
			
		||||
 | 
			
		||||
      it "updates #{params[:field]} field" do
 | 
			
		||||
        post_graphql_mutation(mutation, current_user: user)
 | 
			
		||||
 | 
			
		||||
        expect(graphql_data_at(:group_update, :group, field.to_sym)).to eq(value)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when bad arguments are provided' do
 | 
			
		||||
      let(:variables) { { full_path: '', shared_runners_setting: 'INVALID' } }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,30 @@ RSpec.describe StuckExportJobsWorker, feature_category: :importers do
 | 
			
		|||
 | 
			
		||||
          expect(project_export_job.reload.failed?).to be true
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        context 'when export job has relation exports' do
 | 
			
		||||
          before do
 | 
			
		||||
            create(:project_relation_export, project_export_job: project_export_job)
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          context 'when export job updated at is less than expiration time' do
 | 
			
		||||
            it 'does not mark export job as failed' do
 | 
			
		||||
              worker.perform
 | 
			
		||||
 | 
			
		||||
              expect(project_export_job.reload.failed?).to be false
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          context 'when export job updated at is greater than expiration time' do
 | 
			
		||||
            it 'marks export job as failed' do
 | 
			
		||||
              project_export_job.update!(created_at: 12.hours.ago)
 | 
			
		||||
 | 
			
		||||
              worker.perform
 | 
			
		||||
 | 
			
		||||
              expect(project_export_job.reload.failed?).to be true
 | 
			
		||||
            end
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      context 'when the job is not in queue and db record in queued state' do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue