Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									c051381589
								
							
						
					
					
						commit
						a0bb115d01
					
				| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
52935d26b797c63c6aa31047cd1319cfddb5bb1c
 | 
					7999217addae25ef054b769e74265cbd2ad28bad
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ export default {
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  components: { GlButton, OrganizationsView },
 | 
					  components: { GlButton, OrganizationsView },
 | 
				
			||||||
  inject: ['newOrganizationUrl'],
 | 
					  inject: ['newOrganizationUrl', 'canCreateOrganization'],
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      organizations: {},
 | 
					      organizations: {},
 | 
				
			||||||
| 
						 | 
					@ -46,9 +46,6 @@ export default {
 | 
				
			||||||
    showHeader() {
 | 
					    showHeader() {
 | 
				
			||||||
      return this.loading || this.organizations.nodes?.length;
 | 
					      return this.loading || this.organizations.nodes?.length;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showNewOrganizationButton() {
 | 
					 | 
				
			||||||
      return gon.features?.allowOrganizationCreation;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    loading() {
 | 
					    loading() {
 | 
				
			||||||
      return this.$apollo.queries.organizations.loading;
 | 
					      return this.$apollo.queries.organizations.loading;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -78,7 +75,7 @@ export default {
 | 
				
			||||||
  <div class="gl-py-6">
 | 
					  <div class="gl-py-6">
 | 
				
			||||||
    <div v-if="showHeader" class="gl-mb-5 gl-flex gl-items-center gl-justify-between">
 | 
					    <div v-if="showHeader" class="gl-mb-5 gl-flex gl-items-center gl-justify-between">
 | 
				
			||||||
      <h1 class="gl-m-0 gl-text-size-h-display">{{ $options.i18n.pageTitle }}</h1>
 | 
					      <h1 class="gl-m-0 gl-text-size-h-display">{{ $options.i18n.pageTitle }}</h1>
 | 
				
			||||||
      <gl-button v-if="showNewOrganizationButton" :href="newOrganizationUrl" variant="confirm">{{
 | 
					      <gl-button v-if="canCreateOrganization" :href="newOrganizationUrl" variant="confirm">{{
 | 
				
			||||||
        $options.i18n.newOrganization
 | 
					        $options.i18n.newOrganization
 | 
				
			||||||
      }}</gl-button>
 | 
					      }}</gl-button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,9 @@ export const initAdminOrganizationsIndex = () => {
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    dataset: { appData },
 | 
					    dataset: { appData },
 | 
				
			||||||
  } = el;
 | 
					  } = el;
 | 
				
			||||||
  const { newOrganizationUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
 | 
					  const { newOrganizationUrl, canCreateOrganization } = convertObjectPropsToCamelCase(
 | 
				
			||||||
 | 
					    JSON.parse(appData),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const apolloProvider = new VueApollo({
 | 
					  const apolloProvider = new VueApollo({
 | 
				
			||||||
    defaultClient: createDefaultClient(),
 | 
					    defaultClient: createDefaultClient(),
 | 
				
			||||||
| 
						 | 
					@ -24,6 +26,7 @@ export const initAdminOrganizationsIndex = () => {
 | 
				
			||||||
    apolloProvider,
 | 
					    apolloProvider,
 | 
				
			||||||
    provide: {
 | 
					    provide: {
 | 
				
			||||||
      newOrganizationUrl,
 | 
					      newOrganizationUrl,
 | 
				
			||||||
 | 
					      canCreateOrganization,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    render(createElement) {
 | 
					    render(createElement) {
 | 
				
			||||||
      return createElement(App);
 | 
					      return createElement(App);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ export default {
 | 
				
			||||||
    GlButton,
 | 
					    GlButton,
 | 
				
			||||||
    OrganizationsView,
 | 
					    OrganizationsView,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  inject: ['newOrganizationUrl'],
 | 
					  inject: ['newOrganizationUrl', 'canCreateOrganization'],
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      organizations: {},
 | 
					      organizations: {},
 | 
				
			||||||
| 
						 | 
					@ -49,9 +49,6 @@ export default {
 | 
				
			||||||
    showHeader() {
 | 
					    showHeader() {
 | 
				
			||||||
      return this.loading || this.organizations.nodes?.length;
 | 
					      return this.loading || this.organizations.nodes?.length;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showNewOrganizationButton() {
 | 
					 | 
				
			||||||
      return gon.features?.allowOrganizationCreation;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    loading() {
 | 
					    loading() {
 | 
				
			||||||
      return this.$apollo.queries.organizations.loading;
 | 
					      return this.$apollo.queries.organizations.loading;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -82,7 +79,7 @@ export default {
 | 
				
			||||||
    <div v-if="showHeader" class="gl-flex gl-items-center">
 | 
					    <div v-if="showHeader" class="gl-flex gl-items-center">
 | 
				
			||||||
      <h1 class="gl-my-4 gl-text-size-h-display">{{ $options.i18n.organizations }}</h1>
 | 
					      <h1 class="gl-my-4 gl-text-size-h-display">{{ $options.i18n.organizations }}</h1>
 | 
				
			||||||
      <div class="gl-ml-auto">
 | 
					      <div class="gl-ml-auto">
 | 
				
			||||||
        <gl-button v-if="showNewOrganizationButton" :href="newOrganizationUrl" variant="confirm">{{
 | 
					        <gl-button v-if="canCreateOrganization" :href="newOrganizationUrl" variant="confirm">{{
 | 
				
			||||||
          $options.i18n.newOrganization
 | 
					          $options.i18n.newOrganization
 | 
				
			||||||
        }}</gl-button>
 | 
					        }}</gl-button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,12 @@ export const initOrganizationsIndex = () => {
 | 
				
			||||||
    defaultClient: createDefaultClient(),
 | 
					    defaultClient: createDefaultClient(),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { newOrganizationUrl } = convertObjectPropsToCamelCase(el.dataset);
 | 
					  const {
 | 
				
			||||||
 | 
					    dataset: { appData },
 | 
				
			||||||
 | 
					  } = el;
 | 
				
			||||||
 | 
					  const { newOrganizationUrl, canCreateOrganization } = convertObjectPropsToCamelCase(
 | 
				
			||||||
 | 
					    JSON.parse(appData),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return new Vue({
 | 
					  return new Vue({
 | 
				
			||||||
    el,
 | 
					    el,
 | 
				
			||||||
| 
						 | 
					@ -21,6 +26,7 @@ export const initOrganizationsIndex = () => {
 | 
				
			||||||
    apolloProvider,
 | 
					    apolloProvider,
 | 
				
			||||||
    provide: {
 | 
					    provide: {
 | 
				
			||||||
      newOrganizationUrl,
 | 
					      newOrganizationUrl,
 | 
				
			||||||
 | 
					      canCreateOrganization,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    render(createElement) {
 | 
					    render(createElement) {
 | 
				
			||||||
      return createElement(OrganizationsIndexApp);
 | 
					      return createElement(OrganizationsIndexApp);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ export default {
 | 
				
			||||||
    OrganizationsList,
 | 
					    OrganizationsList,
 | 
				
			||||||
    GlEmptyState,
 | 
					    GlEmptyState,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  inject: ['newOrganizationUrl'],
 | 
					  inject: ['newOrganizationUrl', 'canCreateOrganization'],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    organizations: {
 | 
					    organizations: {
 | 
				
			||||||
      type: Object,
 | 
					      type: Object,
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ export default {
 | 
				
			||||||
        description: this.$options.i18n.emptyStateDescription,
 | 
					        description: this.$options.i18n.emptyStateDescription,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (gon.features?.allowOrganizationCreation) {
 | 
					      if (this.canCreateOrganization) {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          ...baseProps,
 | 
					          ...baseProps,
 | 
				
			||||||
          primaryButtonLink: this.newOrganizationUrl,
 | 
					          primaryButtonLink: this.newOrganizationUrl,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -87,6 +87,11 @@ export default {
 | 
				
			||||||
    timeTrackingDocsPath() {
 | 
					    timeTrackingDocsPath() {
 | 
				
			||||||
      return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md');
 | 
					      return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md');
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    createTimelogModalId() {
 | 
				
			||||||
 | 
					      return this.workItemId
 | 
				
			||||||
 | 
					        ? `${CREATE_TIMELOG_MODAL_ID}-${this.workItemId}`
 | 
				
			||||||
 | 
					        : CREATE_TIMELOG_MODAL_ID;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    resetModal() {
 | 
					    resetModal() {
 | 
				
			||||||
| 
						 | 
					@ -182,7 +187,6 @@ export default {
 | 
				
			||||||
      return convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId);
 | 
					      return convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  CREATE_TIMELOG_MODAL_ID,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -190,7 +194,7 @@ export default {
 | 
				
			||||||
  <gl-modal
 | 
					  <gl-modal
 | 
				
			||||||
    ref="modal"
 | 
					    ref="modal"
 | 
				
			||||||
    :title="s__('CreateTimelogForm|Add time entry')"
 | 
					    :title="s__('CreateTimelogForm|Add time entry')"
 | 
				
			||||||
    :modal-id="$options.CREATE_TIMELOG_MODAL_ID"
 | 
					    :modal-id="createTimelogModalId"
 | 
				
			||||||
    size="sm"
 | 
					    size="sm"
 | 
				
			||||||
    data-testid="create-timelog-modal"
 | 
					    data-testid="create-timelog-modal"
 | 
				
			||||||
    :action-primary="primaryProps"
 | 
					    :action-primary="primaryProps"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,6 +115,11 @@ export default {
 | 
				
			||||||
        issuableTypeName,
 | 
					        issuableTypeName,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    setTimeEstimateModalId() {
 | 
				
			||||||
 | 
					      return this.workItemId
 | 
				
			||||||
 | 
					        ? `${SET_TIME_ESTIMATE_MODAL_ID}-${this.workItemId}`
 | 
				
			||||||
 | 
					        : SET_TIME_ESTIMATE_MODAL_ID;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
    timeTracking() {
 | 
					    timeTracking() {
 | 
				
			||||||
| 
						 | 
					@ -212,7 +217,6 @@ export default {
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  SET_TIME_ESTIMATE_MODAL_ID,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -220,7 +224,7 @@ export default {
 | 
				
			||||||
  <gl-modal
 | 
					  <gl-modal
 | 
				
			||||||
    ref="modal"
 | 
					    ref="modal"
 | 
				
			||||||
    :title="modalTitle"
 | 
					    :title="modalTitle"
 | 
				
			||||||
    :modal-id="$options.SET_TIME_ESTIMATE_MODAL_ID"
 | 
					    :modal-id="setTimeEstimateModalId"
 | 
				
			||||||
    size="sm"
 | 
					    size="sm"
 | 
				
			||||||
    data-testid="set-time-estimate-modal"
 | 
					    data-testid="set-time-estimate-modal"
 | 
				
			||||||
    :action-primary="primaryProps"
 | 
					    :action-primary="primaryProps"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -220,7 +220,7 @@ export default {
 | 
				
			||||||
            :href="childItemWebUrl"
 | 
					            :href="childItemWebUrl"
 | 
				
			||||||
            :class="{ '!gl-text-subtle': !isChildItemOpen }"
 | 
					            :class="{ '!gl-text-subtle': !isChildItemOpen }"
 | 
				
			||||||
            class="gl-hyphens-auto gl-break-words gl-font-semibold"
 | 
					            class="gl-hyphens-auto gl-break-words gl-font-semibold"
 | 
				
			||||||
            @click.exact="handleTitleClick"
 | 
					            @click.exact.stop="handleTitleClick"
 | 
				
			||||||
            @mouseover="$emit('mouseover')"
 | 
					            @mouseover="$emit('mouseover')"
 | 
				
			||||||
            @mouseout="$emit('mouseout')"
 | 
					            @mouseout="$emit('mouseout')"
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
| 
						 | 
					@ -241,7 +241,7 @@ export default {
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <template #avatar="{ avatar }">
 | 
					            <template #avatar="{ avatar }">
 | 
				
			||||||
              <gl-avatar-link v-gl-tooltip :href="avatar.webUrl" :title="avatar.name">
 | 
					              <gl-avatar-link v-gl-tooltip :href="avatar.webUrl" :title="avatar.name">
 | 
				
			||||||
                <gl-avatar :alt="avatar.name" :src="avatar.avatarUrl" :size="16" />
 | 
					                <gl-avatar :alt="avatar.name" :src="avatar.avatarUrl" :size="16" @click.stop />
 | 
				
			||||||
              </gl-avatar-link>
 | 
					              </gl-avatar-link>
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
          </gl-avatars-inline>
 | 
					          </gl-avatars-inline>
 | 
				
			||||||
| 
						 | 
					@ -286,6 +286,7 @@ export default {
 | 
				
			||||||
          :scoped="showScopedLabel(label)"
 | 
					          :scoped="showScopedLabel(label)"
 | 
				
			||||||
          class="gl-mb-auto gl-mr-2 gl-mt-2"
 | 
					          class="gl-mb-auto gl-mr-2 gl-mt-2"
 | 
				
			||||||
          tooltip-placement="top"
 | 
					          tooltip-placement="top"
 | 
				
			||||||
 | 
					          @click.stop
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					@ -299,7 +300,7 @@ export default {
 | 
				
			||||||
        :aria-label="$options.i18n.remove"
 | 
					        :aria-label="$options.i18n.remove"
 | 
				
			||||||
        :title="$options.i18n.remove"
 | 
					        :title="$options.i18n.remove"
 | 
				
			||||||
        data-testid="remove-work-item-link"
 | 
					        data-testid="remove-work-item-link"
 | 
				
			||||||
        @click="$emit('removeChild', childItem)"
 | 
					        @click.stop="$emit('removeChild', childItem)"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,15 +4,14 @@ import { GlAlert, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui'
 | 
				
			||||||
import noAccessSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
 | 
					import noAccessSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
 | 
				
			||||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
 | 
					import * as Sentry from '~/sentry/sentry_browser_wrapper';
 | 
				
			||||||
import { s__ } from '~/locale';
 | 
					import { s__ } from '~/locale';
 | 
				
			||||||
import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
 | 
					import { getParameterByName } from '~/lib/utils/url_utility';
 | 
				
			||||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 | 
					import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 | 
				
			||||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
 | 
					import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 | 
				
			||||||
import { TYPENAME_GROUP, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
 | 
					import { TYPENAME_GROUP } from '~/graphql_shared/constants';
 | 
				
			||||||
import { isLoggedIn } from '~/lib/utils/common_utils';
 | 
					import { isLoggedIn } from '~/lib/utils/common_utils';
 | 
				
			||||||
import { WORKSPACE_PROJECT } from '~/issues/constants';
 | 
					import { WORKSPACE_PROJECT } from '~/issues/constants';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  i18n,
 | 
					  i18n,
 | 
				
			||||||
  DETAIL_VIEW_QUERY_PARAM_NAME,
 | 
					 | 
				
			||||||
  WIDGET_TYPE_ASSIGNEES,
 | 
					  WIDGET_TYPE_ASSIGNEES,
 | 
				
			||||||
  WIDGET_TYPE_NOTIFICATIONS,
 | 
					  WIDGET_TYPE_NOTIFICATIONS,
 | 
				
			||||||
  WIDGET_TYPE_CURRENT_USER_TODOS,
 | 
					  WIDGET_TYPE_CURRENT_USER_TODOS,
 | 
				
			||||||
| 
						 | 
					@ -59,7 +58,6 @@ import WorkItemAttributesWrapper from './work_item_attributes_wrapper.vue';
 | 
				
			||||||
import WorkItemCreatedUpdated from './work_item_created_updated.vue';
 | 
					import WorkItemCreatedUpdated from './work_item_created_updated.vue';
 | 
				
			||||||
import WorkItemDescription from './work_item_description.vue';
 | 
					import WorkItemDescription from './work_item_description.vue';
 | 
				
			||||||
import WorkItemNotes from './work_item_notes.vue';
 | 
					import WorkItemNotes from './work_item_notes.vue';
 | 
				
			||||||
import WorkItemDetailModal from './work_item_detail_modal.vue';
 | 
					 | 
				
			||||||
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
 | 
					import WorkItemAwardEmoji from './work_item_award_emoji.vue';
 | 
				
			||||||
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
 | 
					import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
 | 
				
			||||||
import WorkItemStickyHeader from './work_item_sticky_header.vue';
 | 
					import WorkItemStickyHeader from './work_item_sticky_header.vue';
 | 
				
			||||||
| 
						 | 
					@ -67,6 +65,7 @@ import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
 | 
				
			||||||
import WorkItemTitle from './work_item_title.vue';
 | 
					import WorkItemTitle from './work_item_title.vue';
 | 
				
			||||||
import WorkItemLoading from './work_item_loading.vue';
 | 
					import WorkItemLoading from './work_item_loading.vue';
 | 
				
			||||||
import WorkItemAbuseModal from './work_item_abuse_modal.vue';
 | 
					import WorkItemAbuseModal from './work_item_abuse_modal.vue';
 | 
				
			||||||
 | 
					import WorkItemDrawer from './work_item_drawer.vue';
 | 
				
			||||||
import DesignWidget from './design_management/design_management_widget.vue';
 | 
					import DesignWidget from './design_management/design_management_widget.vue';
 | 
				
			||||||
import DesignUploadButton from './design_management/upload_button.vue';
 | 
					import DesignUploadButton from './design_management/upload_button.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,13 +95,13 @@ export default {
 | 
				
			||||||
    WorkItemAttributesWrapper,
 | 
					    WorkItemAttributesWrapper,
 | 
				
			||||||
    WorkItemTree,
 | 
					    WorkItemTree,
 | 
				
			||||||
    WorkItemNotes,
 | 
					    WorkItemNotes,
 | 
				
			||||||
    WorkItemDetailModal,
 | 
					 | 
				
			||||||
    WorkItemRelationships,
 | 
					    WorkItemRelationships,
 | 
				
			||||||
    WorkItemStickyHeader,
 | 
					    WorkItemStickyHeader,
 | 
				
			||||||
    WorkItemAncestors,
 | 
					    WorkItemAncestors,
 | 
				
			||||||
    WorkItemTitle,
 | 
					    WorkItemTitle,
 | 
				
			||||||
    WorkItemLoading,
 | 
					    WorkItemLoading,
 | 
				
			||||||
    WorkItemAbuseModal,
 | 
					    WorkItemAbuseModal,
 | 
				
			||||||
 | 
					    WorkItemDrawer,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mixins: [glFeatureFlagMixin()],
 | 
					  mixins: [glFeatureFlagMixin()],
 | 
				
			||||||
  inject: [
 | 
					  inject: [
 | 
				
			||||||
| 
						 | 
					@ -145,18 +144,11 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    let modalWorkItemId = getParameterByName(DETAIL_VIEW_QUERY_PARAM_NAME);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (modalWorkItemId) {
 | 
					 | 
				
			||||||
      modalWorkItemId = convertToGraphQLId(TYPENAME_WORK_ITEM, modalWorkItemId);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      error: undefined,
 | 
					      error: undefined,
 | 
				
			||||||
      updateError: undefined,
 | 
					      updateError: undefined,
 | 
				
			||||||
      workItem: {},
 | 
					      workItem: {},
 | 
				
			||||||
      updateInProgress: false,
 | 
					      updateInProgress: false,
 | 
				
			||||||
      modalWorkItemId,
 | 
					 | 
				
			||||||
      modalWorkItemIid: getParameterByName('work_item_iid'),
 | 
					      modalWorkItemIid: getParameterByName('work_item_iid'),
 | 
				
			||||||
      modalWorkItemNamespaceFullPath: '',
 | 
					      modalWorkItemNamespaceFullPath: '',
 | 
				
			||||||
      isReportModalOpen: false,
 | 
					      isReportModalOpen: false,
 | 
				
			||||||
| 
						 | 
					@ -171,6 +163,7 @@ export default {
 | 
				
			||||||
      designUploadError: null,
 | 
					      designUploadError: null,
 | 
				
			||||||
      designUploadErrorVariant: ALERT_VARIANTS.danger,
 | 
					      designUploadErrorVariant: ALERT_VARIANTS.danger,
 | 
				
			||||||
      workspacePermissions: defaultWorkspacePermissions,
 | 
					      workspacePermissions: defaultWorkspacePermissions,
 | 
				
			||||||
 | 
					      activeChildItem: null,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  apollo: {
 | 
					  apollo: {
 | 
				
			||||||
| 
						 | 
					@ -209,6 +202,7 @@ export default {
 | 
				
			||||||
        if (!res.data) {
 | 
					        if (!res.data) {
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        this.activeChildItem = null;
 | 
				
			||||||
        this.$emit('work-item-updated', this.workItem);
 | 
					        this.$emit('work-item-updated', this.workItem);
 | 
				
			||||||
        if (isEmpty(this.workItem)) {
 | 
					        if (isEmpty(this.workItem)) {
 | 
				
			||||||
          this.setEmptyState();
 | 
					          this.setEmptyState();
 | 
				
			||||||
| 
						 | 
					@ -431,14 +425,12 @@ export default {
 | 
				
			||||||
    iid() {
 | 
					    iid() {
 | 
				
			||||||
      return this.workItemIid || this.workItem.iid;
 | 
					      return this.workItemIid || this.workItem.iid;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					    isItemSelected() {
 | 
				
			||||||
  mounted() {
 | 
					      return !isEmpty(this.activeChildItem);
 | 
				
			||||||
    if (this.modalWorkItemId) {
 | 
					    },
 | 
				
			||||||
      this.openInModal({
 | 
					    activeChildItemType() {
 | 
				
			||||||
        event: undefined,
 | 
					      return this.activeChildItem?.workItemType?.name;
 | 
				
			||||||
        modalWorkItem: { id: this.modalWorkItemId },
 | 
					    },
 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    handleWorkItemCreated() {
 | 
					    handleWorkItemCreated() {
 | 
				
			||||||
| 
						 | 
					@ -489,23 +481,13 @@ export default {
 | 
				
			||||||
      this.error = this.$options.i18n.fetchError;
 | 
					      this.error = this.$options.i18n.fetchError;
 | 
				
			||||||
      document.title = s__('404|Not found');
 | 
					      document.title = s__('404|Not found');
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    updateUrl(modalWorkItem) {
 | 
					    openContextualView({ event, modalWorkItem, context }) {
 | 
				
			||||||
      updateHistory({
 | 
					 | 
				
			||||||
        url: setUrlParams({
 | 
					 | 
				
			||||||
          [DETAIL_VIEW_QUERY_PARAM_NAME]: getIdFromGraphQLId(modalWorkItem?.id),
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
        replace: true,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    openInModal({ event, modalWorkItem, context }) {
 | 
					 | 
				
			||||||
      if (!this.workItemsAlphaEnabled || context === LINKED_ITEMS_ANCHOR || this.isDrawer) {
 | 
					      if (!this.workItemsAlphaEnabled || context === LINKED_ITEMS_ANCHOR || this.isDrawer) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (event) {
 | 
					      if (event) {
 | 
				
			||||||
        event.preventDefault();
 | 
					        event.preventDefault();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.updateUrl(modalWorkItem);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.isModal) {
 | 
					      if (this.isModal) {
 | 
				
			||||||
| 
						 | 
					@ -513,13 +495,7 @@ export default {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.modalWorkItemId = modalWorkItem.id;
 | 
					      this.activeChildItem = modalWorkItem;
 | 
				
			||||||
      this.modalWorkItemIid = modalWorkItem.iid;
 | 
					 | 
				
			||||||
      this.modalWorkItemNamespaceFullPath = modalWorkItem?.reference?.replace(
 | 
					 | 
				
			||||||
        `#${modalWorkItem.iid}`,
 | 
					 | 
				
			||||||
        '',
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      this.$refs.modal.show();
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    openReportAbuseModal(reply) {
 | 
					    openReportAbuseModal(reply) {
 | 
				
			||||||
      if (this.isModal) {
 | 
					      if (this.isModal) {
 | 
				
			||||||
| 
						 | 
					@ -655,6 +631,19 @@ export default {
 | 
				
			||||||
        iid: this.iid,
 | 
					        iid: this.iid,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    async deleteChildItem({ id }) {
 | 
				
			||||||
 | 
					      this.activeChildItem = null;
 | 
				
			||||||
 | 
					      await this.$nextTick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const { cache } = this.$apollo.provider.clients.defaultClient;
 | 
				
			||||||
 | 
					      cache.evict({
 | 
				
			||||||
 | 
					        id: cache.identify({
 | 
				
			||||||
 | 
					          __typename: 'WorkItem',
 | 
				
			||||||
 | 
					          id,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      cache.gc();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  WORK_ITEM_TYPE_VALUE_OBJECTIVE,
 | 
					  WORK_ITEM_TYPE_VALUE_OBJECTIVE,
 | 
				
			||||||
  WORKSPACE_PROJECT,
 | 
					  WORKSPACE_PROJECT,
 | 
				
			||||||
| 
						 | 
					@ -887,7 +876,7 @@ export default {
 | 
				
			||||||
              :confidential="workItem.confidential"
 | 
					              :confidential="workItem.confidential"
 | 
				
			||||||
              :allowed-child-types="allowedChildTypes"
 | 
					              :allowed-child-types="allowedChildTypes"
 | 
				
			||||||
              :is-drawer="isDrawer"
 | 
					              :is-drawer="isDrawer"
 | 
				
			||||||
              @show-modal="openInModal"
 | 
					              @show-modal="openContextualView"
 | 
				
			||||||
              @addChild="$emit('addChild')"
 | 
					              @addChild="$emit('addChild')"
 | 
				
			||||||
              @childrenLoaded="hasChildren = $event"
 | 
					              @childrenLoaded="hasChildren = $event"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
| 
						 | 
					@ -899,7 +888,7 @@ export default {
 | 
				
			||||||
              :work-item-full-path="workItemFullPath"
 | 
					              :work-item-full-path="workItemFullPath"
 | 
				
			||||||
              :work-item-type="workItem.workItemType.name"
 | 
					              :work-item-type="workItem.workItemType.name"
 | 
				
			||||||
              :can-admin-work-item-link="canAdminWorkItemLink"
 | 
					              :can-admin-work-item-link="canAdminWorkItemLink"
 | 
				
			||||||
              @showModal="openInModal"
 | 
					              @showModal="openContextualView"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <work-item-notes
 | 
					            <work-item-notes
 | 
				
			||||||
              v-if="workItemNotes"
 | 
					              v-if="workItemNotes"
 | 
				
			||||||
| 
						 | 
					@ -922,16 +911,14 @@ export default {
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </section>
 | 
					      </section>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
    <work-item-detail-modal
 | 
					    <work-item-drawer
 | 
				
			||||||
      v-if="!isModal && !isDrawer"
 | 
					      v-if="workItemsAlphaEnabled && !isDrawer"
 | 
				
			||||||
      ref="modal"
 | 
					      :active-item="activeChildItem"
 | 
				
			||||||
      :parent-id="workItem.id"
 | 
					      :open="isItemSelected"
 | 
				
			||||||
      :work-item-id="modalWorkItemId"
 | 
					      :issuable-type="activeChildItemType"
 | 
				
			||||||
      :work-item-iid="modalWorkItemIid"
 | 
					      click-outside-exclude-selector=".issuable-list"
 | 
				
			||||||
      :work-item-full-path="modalWorkItemNamespaceFullPath"
 | 
					      @close="activeChildItem = null"
 | 
				
			||||||
      :show="true"
 | 
					      @workItemDeleted="deleteChildItem"
 | 
				
			||||||
      @close="updateUrl"
 | 
					 | 
				
			||||||
      @openReportAbuse="toggleReportAbuseModal(true, $event)"
 | 
					 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <work-item-abuse-modal
 | 
					    <work-item-abuse-modal
 | 
				
			||||||
      v-if="isReportModalOpen"
 | 
					      v-if="isReportModalOpen"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,7 @@ export default {
 | 
				
			||||||
        if (data.workItemDelete.errors?.length) {
 | 
					        if (data.workItemDelete.errors?.length) {
 | 
				
			||||||
          throw new Error(data.workItemDelete.errors[0]);
 | 
					          throw new Error(data.workItemDelete.errors[0]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.$emit('workItemDeleted');
 | 
					        this.$emit('workItemDeleted', { id: workItemId });
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        this.$emit('deleteWorkItemError');
 | 
					        this.$emit('deleteWorkItemError');
 | 
				
			||||||
        Sentry.captureException(error);
 | 
					        Sentry.captureException(error);
 | 
				
			||||||
| 
						 | 
					@ -119,6 +119,7 @@ export default {
 | 
				
			||||||
      e.preventDefault();
 | 
					      e.preventDefault();
 | 
				
			||||||
      const shouldRouterNav =
 | 
					      const shouldRouterNav =
 | 
				
			||||||
        !this.preventRouterNav &&
 | 
					        !this.preventRouterNav &&
 | 
				
			||||||
 | 
					        this.$router &&
 | 
				
			||||||
        canRouterNav({
 | 
					        canRouterNav({
 | 
				
			||||||
          fullPath: this.fullPath,
 | 
					          fullPath: this.fullPath,
 | 
				
			||||||
          webUrl: workItem.webUrl,
 | 
					          webUrl: workItem.webUrl,
 | 
				
			||||||
| 
						 | 
					@ -189,11 +190,14 @@ export default {
 | 
				
			||||||
    '[id^="insert-comment-template-modal"]',
 | 
					    '[id^="insert-comment-template-modal"]',
 | 
				
			||||||
    '.pika-single',
 | 
					    '.pika-single',
 | 
				
			||||||
    '.atwho-container',
 | 
					    '.atwho-container',
 | 
				
			||||||
 | 
					    '.item-title',
 | 
				
			||||||
    '.tippy-content .gl-new-dropdown-panel',
 | 
					    '.tippy-content .gl-new-dropdown-panel',
 | 
				
			||||||
    '#blocked-by-issues-modal',
 | 
					    '#blocked-by-issues-modal',
 | 
				
			||||||
    '#open-children-warning-modal',
 | 
					    '#open-children-warning-modal',
 | 
				
			||||||
    '#create-work-item-modal',
 | 
					    '#create-work-item-modal',
 | 
				
			||||||
    '#work-item-confirm-delete',
 | 
					    '#work-item-confirm-delete',
 | 
				
			||||||
 | 
					    '.work-item-link-child',
 | 
				
			||||||
 | 
					    '.modal-content',
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -202,6 +206,7 @@ export default {
 | 
				
			||||||
  <gl-drawer
 | 
					  <gl-drawer
 | 
				
			||||||
    v-gl-outside="handleClickOutside"
 | 
					    v-gl-outside="handleClickOutside"
 | 
				
			||||||
    :open="open"
 | 
					    :open="open"
 | 
				
			||||||
 | 
					    :z-index="200"
 | 
				
			||||||
    data-testid="work-item-drawer"
 | 
					    data-testid="work-item-drawer"
 | 
				
			||||||
    header-sticky
 | 
					    header-sticky
 | 
				
			||||||
    header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
 | 
					    header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -554,6 +554,7 @@ export default {
 | 
				
			||||||
      @removeChild="removeChild"
 | 
					      @removeChild="removeChild"
 | 
				
			||||||
      @error="$emit('error', $event)"
 | 
					      @error="$emit('error', $event)"
 | 
				
			||||||
      @click="onClick($event, child)"
 | 
					      @click="onClick($event, child)"
 | 
				
			||||||
 | 
					      @click.native="onClick($event, child)"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
  </component>
 | 
					  </component>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -267,7 +267,7 @@ export default {
 | 
				
			||||||
          :loading="isLoadingChildren && !fetchNextPageInProgress"
 | 
					          :loading="isLoadingChildren && !fetchNextPageInProgress"
 | 
				
			||||||
          class="!gl-px-0 !gl-py-3"
 | 
					          class="!gl-px-0 !gl-py-3"
 | 
				
			||||||
          data-testid="expand-child"
 | 
					          data-testid="expand-child"
 | 
				
			||||||
          @click="toggleItem"
 | 
					          @click.stop="toggleItem"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,8 @@ import { sprintf, s__ } from '~/locale';
 | 
				
			||||||
import { createAlert } from '~/alert';
 | 
					import { createAlert } from '~/alert';
 | 
				
			||||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
 | 
					import CrudComponent from '~/vue_shared/components/crud_component.vue';
 | 
				
			||||||
import { findWidget } from '~/issues/list/utils';
 | 
					import { findWidget } from '~/issues/list/utils';
 | 
				
			||||||
 | 
					import { getParameterByName } from '~/lib/utils/url_utility';
 | 
				
			||||||
 | 
					import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  FORM_TYPES,
 | 
					  FORM_TYPES,
 | 
				
			||||||
  WORK_ITEMS_TREE_TEXT,
 | 
					  WORK_ITEMS_TREE_TEXT,
 | 
				
			||||||
| 
						 | 
					@ -18,6 +20,7 @@ import {
 | 
				
			||||||
  WORK_ITEM_TYPE_VALUE_EPIC,
 | 
					  WORK_ITEM_TYPE_VALUE_EPIC,
 | 
				
			||||||
  WIDGET_TYPE_HIERARCHY,
 | 
					  WIDGET_TYPE_HIERARCHY,
 | 
				
			||||||
  INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
 | 
					  INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
 | 
				
			||||||
 | 
					  DETAIL_VIEW_QUERY_PARAM_NAME,
 | 
				
			||||||
} from '../../constants';
 | 
					} from '../../constants';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  findHierarchyWidgets,
 | 
					  findHierarchyWidgets,
 | 
				
			||||||
| 
						 | 
					@ -156,6 +159,7 @@ export default {
 | 
				
			||||||
        if (this.hasNextPage && this.children.length === 0) {
 | 
					        if (this.hasNextPage && this.children.length === 0) {
 | 
				
			||||||
          this.fetchNextPage();
 | 
					          this.fetchNextPage();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        this.checkDrawerParams();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    workItemTypes: {
 | 
					    workItemTypes: {
 | 
				
			||||||
| 
						 | 
					@ -328,6 +332,21 @@ export default {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    checkDrawerParams() {
 | 
				
			||||||
 | 
					      const queryParam = getParameterByName(DETAIL_VIEW_QUERY_PARAM_NAME);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!queryParam) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const params = JSON.parse(atob(queryParam));
 | 
				
			||||||
 | 
					      if (params.id) {
 | 
				
			||||||
 | 
					        const modalWorkItem = this.children.find((i) => getIdFromGraphQLId(i.id) === params.id);
 | 
				
			||||||
 | 
					        if (modalWorkItem) {
 | 
				
			||||||
 | 
					          this.$emit('show-modal', { modalWorkItem });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  i18n: {
 | 
					  i18n: {
 | 
				
			||||||
    noChildItemsOpen: s__('WorkItem|No child items are currently open.'),
 | 
					    noChildItemsOpen: s__('WorkItem|No child items are currently open.'),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,6 @@ export default {
 | 
				
			||||||
      'TimeTracking|Add an %{estimateStart}estimate%{estimateEnd} or %{timeSpentStart}time spent%{timeSpentEnd}.',
 | 
					      'TimeTracking|Add an %{estimateStart}estimate%{estimateEnd} or %{timeSpentStart}time spent%{timeSpentEnd}.',
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  createTimelogModalId: CREATE_TIMELOG_MODAL_ID,
 | 
					 | 
				
			||||||
  setTimeEstimateModalId: SET_TIME_ESTIMATE_MODAL_ID,
 | 
					 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    TimeTrackingReport,
 | 
					    TimeTrackingReport,
 | 
				
			||||||
    CreateTimelogForm,
 | 
					    CreateTimelogForm,
 | 
				
			||||||
| 
						 | 
					@ -101,6 +99,15 @@ export default {
 | 
				
			||||||
    timeRemainingPercent() {
 | 
					    timeRemainingPercent() {
 | 
				
			||||||
      return Math.floor((this.totalTimeSpent / this.timeEstimate) * 100);
 | 
					      return Math.floor((this.totalTimeSpent / this.timeEstimate) * 100);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    createTimelogModalId() {
 | 
				
			||||||
 | 
					      return `${CREATE_TIMELOG_MODAL_ID}-${this.workItemId}`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setTimeEstimateModalId() {
 | 
				
			||||||
 | 
					      return `${SET_TIME_ESTIMATE_MODAL_ID}-${this.workItemId}`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    timeTrackingModalId() {
 | 
				
			||||||
 | 
					      return `time-tracking-modal-${this.workItemId}`;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -113,7 +120,7 @@ export default {
 | 
				
			||||||
      </h3>
 | 
					      </h3>
 | 
				
			||||||
      <gl-button
 | 
					      <gl-button
 | 
				
			||||||
        v-if="canUpdate"
 | 
					        v-if="canUpdate"
 | 
				
			||||||
        v-gl-modal="$options.createTimelogModalId"
 | 
					        v-gl-modal="createTimelogModalId"
 | 
				
			||||||
        v-gl-tooltip.top
 | 
					        v-gl-tooltip.top
 | 
				
			||||||
        category="tertiary"
 | 
					        category="tertiary"
 | 
				
			||||||
        icon="plus"
 | 
					        icon="plus"
 | 
				
			||||||
| 
						 | 
					@ -129,7 +136,7 @@ export default {
 | 
				
			||||||
        <span class="gl-text-subtle">{{ s__('TimeTracking|Spent') }}</span>
 | 
					        <span class="gl-text-subtle">{{ s__('TimeTracking|Spent') }}</span>
 | 
				
			||||||
        <gl-button
 | 
					        <gl-button
 | 
				
			||||||
          v-if="canUpdate"
 | 
					          v-if="canUpdate"
 | 
				
			||||||
          v-gl-modal="'time-tracking-report'"
 | 
					          v-gl-modal="timeTrackingModalId"
 | 
				
			||||||
          v-gl-tooltip="s__('TimeTracking|View time tracking report')"
 | 
					          v-gl-tooltip="s__('TimeTracking|View time tracking report')"
 | 
				
			||||||
          variant="link"
 | 
					          variant="link"
 | 
				
			||||||
          class="!gl-text-sm"
 | 
					          class="!gl-text-sm"
 | 
				
			||||||
| 
						 | 
					@ -150,7 +157,7 @@ export default {
 | 
				
			||||||
          <span class="gl-text-subtle">{{ s__('TimeTracking|Estimate') }}</span>
 | 
					          <span class="gl-text-subtle">{{ s__('TimeTracking|Estimate') }}</span>
 | 
				
			||||||
          <gl-button
 | 
					          <gl-button
 | 
				
			||||||
            v-if="canUpdate"
 | 
					            v-if="canUpdate"
 | 
				
			||||||
            v-gl-modal="$options.setTimeEstimateModalId"
 | 
					            v-gl-modal="setTimeEstimateModalId"
 | 
				
			||||||
            v-gl-tooltip="s__('TimeTracking|Set estimate')"
 | 
					            v-gl-tooltip="s__('TimeTracking|Set estimate')"
 | 
				
			||||||
            variant="link"
 | 
					            variant="link"
 | 
				
			||||||
            class="!gl-text-sm"
 | 
					            class="!gl-text-sm"
 | 
				
			||||||
| 
						 | 
					@ -164,7 +171,7 @@ export default {
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
        <gl-button
 | 
					        <gl-button
 | 
				
			||||||
          v-else-if="canUpdate"
 | 
					          v-else-if="canUpdate"
 | 
				
			||||||
          v-gl-modal="$options.setTimeEstimateModalId"
 | 
					          v-gl-modal="setTimeEstimateModalId"
 | 
				
			||||||
          class="gl-ml-auto !gl-text-sm"
 | 
					          class="gl-ml-auto !gl-text-sm"
 | 
				
			||||||
          variant="link"
 | 
					          variant="link"
 | 
				
			||||||
          data-testid="add-estimate-button"
 | 
					          data-testid="add-estimate-button"
 | 
				
			||||||
| 
						 | 
					@ -176,7 +183,7 @@ export default {
 | 
				
			||||||
        <gl-sprintf :message="$options.i18n.addTimeTrackingMessage">
 | 
					        <gl-sprintf :message="$options.i18n.addTimeTrackingMessage">
 | 
				
			||||||
          <template #estimate="{ content }">
 | 
					          <template #estimate="{ content }">
 | 
				
			||||||
            <gl-button
 | 
					            <gl-button
 | 
				
			||||||
              v-gl-modal="$options.setTimeEstimateModalId"
 | 
					              v-gl-modal="setTimeEstimateModalId"
 | 
				
			||||||
              class="gl-align-baseline !gl-text-sm"
 | 
					              class="gl-align-baseline !gl-text-sm"
 | 
				
			||||||
              variant="link"
 | 
					              variant="link"
 | 
				
			||||||
              data-testid="add-estimate-button"
 | 
					              data-testid="add-estimate-button"
 | 
				
			||||||
| 
						 | 
					@ -186,7 +193,7 @@ export default {
 | 
				
			||||||
          </template>
 | 
					          </template>
 | 
				
			||||||
          <template #timeSpent="{ content }">
 | 
					          <template #timeSpent="{ content }">
 | 
				
			||||||
            <gl-button
 | 
					            <gl-button
 | 
				
			||||||
              v-gl-modal="$options.createTimelogModalId"
 | 
					              v-gl-modal="createTimelogModalId"
 | 
				
			||||||
              class="gl-align-baseline !gl-text-sm"
 | 
					              class="gl-align-baseline !gl-text-sm"
 | 
				
			||||||
              variant="link"
 | 
					              variant="link"
 | 
				
			||||||
              data-testid="add-time-spent-button"
 | 
					              data-testid="add-time-spent-button"
 | 
				
			||||||
| 
						 | 
					@ -210,7 +217,7 @@ export default {
 | 
				
			||||||
    <create-timelog-form :work-item-id="workItemId" :work-item-type="workItemType" />
 | 
					    <create-timelog-form :work-item-id="workItemId" :work-item-type="workItemType" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <gl-modal
 | 
					    <gl-modal
 | 
				
			||||||
      modal-id="time-tracking-report"
 | 
					      :modal-id="timeTrackingModalId"
 | 
				
			||||||
      data-testid="time-tracking-report-modal"
 | 
					      data-testid="time-tracking-report-modal"
 | 
				
			||||||
      hide-footer
 | 
					      hide-footer
 | 
				
			||||||
      size="lg"
 | 
					      size="lg"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -281,6 +281,10 @@ export const makeDrawerItemFullPath = (activeItem, fullPath, issuableType = TYPE
 | 
				
			||||||
  if (activeItem?.fullPath) {
 | 
					  if (activeItem?.fullPath) {
 | 
				
			||||||
    return activeItem.fullPath;
 | 
					    return activeItem.fullPath;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  if (activeItem?.namespace?.fullPath) {
 | 
				
			||||||
 | 
					    return activeItem.namespace.fullPath;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const delimiter = issuableType === TYPE_EPIC ? '&' : '#';
 | 
					  const delimiter = issuableType === TYPE_EPIC ? '&' : '#';
 | 
				
			||||||
  if (!activeItem?.referencePath) {
 | 
					  if (!activeItem?.referencePath) {
 | 
				
			||||||
    return fullPath;
 | 
					    return fullPath;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ module Organizations
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def organization_index_app_data
 | 
					    def organization_index_app_data
 | 
				
			||||||
      shared_organization_index_app_data
 | 
					      shared_organization_index_app_data.to_json
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def organization_user_app_data(organization)
 | 
					    def organization_user_app_data(organization)
 | 
				
			||||||
| 
						 | 
					@ -111,7 +111,9 @@ module Organizations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def shared_organization_index_app_data
 | 
					    def shared_organization_index_app_data
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        new_organization_url: new_organization_path
 | 
					        new_organization_url: new_organization_path,
 | 
				
			||||||
 | 
					        can_create_organization: Feature.enabled?(:allow_organization_creation, current_user) &&
 | 
				
			||||||
 | 
					          can?(current_user, :create_organization)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,4 +2,4 @@
 | 
				
			||||||
- page_title s_('Organization|Organizations')
 | 
					- page_title s_('Organization|Organizations')
 | 
				
			||||||
- header_title _("Your work"), root_path
 | 
					- header_title _("Your work"), root_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#js-organizations-index{ data: organization_index_app_data }
 | 
					#js-organizations-index{ data: { app_data: organization_index_app_data } }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,7 +65,19 @@ class SecretsInitializer
 | 
				
			||||||
      secret_key_base: generate_new_secure_token,
 | 
					      secret_key_base: generate_new_secure_token,
 | 
				
			||||||
      otp_key_base: generate_new_secure_token,
 | 
					      otp_key_base: generate_new_secure_token,
 | 
				
			||||||
      db_key_base: generate_new_secure_token,
 | 
					      db_key_base: generate_new_secure_token,
 | 
				
			||||||
      openid_connect_signing_key: generate_new_rsa_private_key
 | 
					      openid_connect_signing_key: generate_new_rsa_private_key,
 | 
				
			||||||
 | 
					      # 1. We set the following two keys as an array to support keys rotation.
 | 
				
			||||||
 | 
					      #    The last key in the array is always used to encrypt data:
 | 
				
			||||||
 | 
					      #    https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/encryption/key_provider.rb#L21
 | 
				
			||||||
 | 
					      #    while all the keys are used (in the order they're defined) to decrypt data:
 | 
				
			||||||
 | 
					      #    https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/encryption/cipher.rb#L26.
 | 
				
			||||||
 | 
					      #    This allows to rotate keys by adding a new key as the last key, and start a re-encryption process that
 | 
				
			||||||
 | 
					      #    runs in the background: https://gitlab.com/gitlab-org/gitlab/-/issues/494976
 | 
				
			||||||
 | 
					      # 2. We use the same method and length as Rails' defaults:
 | 
				
			||||||
 | 
					      #    https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/railties/databases.rake#L537-L540
 | 
				
			||||||
 | 
					      active_record_encryption_primary_key: [generate_new_secure_random_alphanumeric(32)],
 | 
				
			||||||
 | 
					      active_record_encryption_deterministic_key: [generate_new_secure_random_alphanumeric(32)],
 | 
				
			||||||
 | 
					      active_record_encryption_key_derivation_salt: generate_new_secure_random_alphanumeric(32)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # encrypted_settings_key_base is optional for now
 | 
					    # encrypted_settings_key_base is optional for now
 | 
				
			||||||
| 
						 | 
					@ -85,6 +97,10 @@ class SecretsInitializer
 | 
				
			||||||
    OpenSSL::PKey::RSA.new(2048).to_pem
 | 
					    OpenSSL::PKey::RSA.new(2048).to_pem
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def generate_new_secure_random_alphanumeric(chars)
 | 
				
			||||||
 | 
					    SecureRandom.alphanumeric(chars)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def warn_missing_secret(secret)
 | 
					  def warn_missing_secret(secret)
 | 
				
			||||||
    return if rails_env.test?
 | 
					    return if rails_env.test?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,10 +109,11 @@ class SecretsInitializer
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_missing_keys(defaults)
 | 
					  def set_missing_keys(defaults)
 | 
				
			||||||
    defaults.stringify_keys.each_with_object({}) do |(key, default), missing|
 | 
					    defaults.each_with_object({}) do |(key, default), missing|
 | 
				
			||||||
      next if Rails.application.credentials.public_send(key).present?
 | 
					      next if Rails.application.credentials.public_send(key).present?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      warn_missing_secret(key)
 | 
					      warn_missing_secret(key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      missing[key] = Rails.application.credentials[key] = default
 | 
					      missing[key] = Rails.application.credentials[key] = default
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -113,7 +130,7 @@ class SecretsInitializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    File.write(
 | 
					    File.write(
 | 
				
			||||||
      secrets_file_path,
 | 
					      secrets_file_path,
 | 
				
			||||||
      YAML.dump(secrets_from_file),
 | 
					      YAML.dump(secrets_from_file.deep_stringify_keys),
 | 
				
			||||||
      mode: 'w', perm: 0o600
 | 
					      mode: 'w', perm: 0o600
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Normally, this would automatically be setup by `ActiveRecord::Encryption` initializer, see
 | 
				
			||||||
 | 
					# https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/railtie.rb#L331-L335,
 | 
				
			||||||
 | 
					# but since we're setting `Rails.application.credentials.active_record_encryption` manually in
 | 
				
			||||||
 | 
					# `config/initializers/01_secret_token.rb`, the `ActiveRecord::Encryption` initializer runs prior
 | 
				
			||||||
 | 
					# to that. We don't want to mess up with the initializer chain, so we configure
 | 
				
			||||||
 | 
					# `ActiveRecord::Encryption` here instead.
 | 
				
			||||||
 | 
					ActiveRecord::Encryption.configure(
 | 
				
			||||||
 | 
					  primary_key: Rails.application.credentials[:active_record_encryption_primary_key],
 | 
				
			||||||
 | 
					  deterministic_key: Rails.application.credentials[:active_record_encryption_deterministic_key],
 | 
				
			||||||
 | 
					  key_derivation_salt: Rails.application.credentials[:active_record_encryption_key_derivation_salt],
 | 
				
			||||||
 | 
					  store_key_references: true # this is very important to know what key was used to encrypt a given attribute
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					migration_job_name: BackfillFreeSharedRunnersMinutesLimit
 | 
				
			||||||
 | 
					description: Backfills namespace shared_runners_minutes_limit values for free tier namespaces
 | 
				
			||||||
 | 
					feature_category: consumables_cost_management
 | 
				
			||||||
 | 
					introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161485
 | 
				
			||||||
 | 
					milestone: '17.7'
 | 
				
			||||||
 | 
					queued_migration_version: 20241125125332
 | 
				
			||||||
 | 
					finalize_after: '2025-01-01'
 | 
				
			||||||
 | 
					finalized_by: # version of the migration that finalized this BBM
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddMetadataToZoektEnabledNamespaces < Gitlab::Database::Migration[2.2]
 | 
				
			||||||
 | 
					  milestone '17.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :zoekt_enabled_namespaces, :metadata, :jsonb, default: {}, null: false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddCustomRolesToScanResultPolicies < Gitlab::Database::Migration[2.2]
 | 
				
			||||||
 | 
					  enable_lock_retries!
 | 
				
			||||||
 | 
					  milestone '17.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :scan_result_policies, :custom_roles, :bigint, array: true, default: [], null: false
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddCustomRolesConstraintToScanResultPolicies < Gitlab::Database::Migration[2.2]
 | 
				
			||||||
 | 
					  disable_ddl_transaction!
 | 
				
			||||||
 | 
					  milestone '17.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  CONSTRAINT_NAME = 'custom_roles_array_check'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def up
 | 
				
			||||||
 | 
					    add_check_constraint(:scan_result_policies, "ARRAY_POSITION(custom_roles, null) IS null", CONSTRAINT_NAME)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def down
 | 
				
			||||||
 | 
					    remove_check_constraint :scan_result_policies, CONSTRAINT_NAME
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class QueueBackfillFreeSharedRunnersMinutesLimit < Gitlab::Database::Migration[2.2]
 | 
				
			||||||
 | 
					  milestone '17.7'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  restrict_gitlab_migration gitlab_schema: :gitlab_main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MIGRATION = "BackfillFreeSharedRunnersMinutesLimit"
 | 
				
			||||||
 | 
					  DELAY_INTERVAL = 2.minutes
 | 
				
			||||||
 | 
					  BATCH_SIZE = 5000
 | 
				
			||||||
 | 
					  SUB_BATCH_SIZE = 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def up
 | 
				
			||||||
 | 
					    return unless Gitlab.dev_or_test_env? || Gitlab.com_except_jh?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queue_batched_background_migration(
 | 
				
			||||||
 | 
					      MIGRATION,
 | 
				
			||||||
 | 
					      :namespaces,
 | 
				
			||||||
 | 
					      :id,
 | 
				
			||||||
 | 
					      job_interval: DELAY_INTERVAL,
 | 
				
			||||||
 | 
					      batch_size: BATCH_SIZE,
 | 
				
			||||||
 | 
					      sub_batch_size: SUB_BATCH_SIZE
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def down
 | 
				
			||||||
 | 
					    return unless Gitlab.dev_or_test_env? || Gitlab.com_except_jh?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    delete_batched_background_migration(MIGRATION, :namespaces, :id, [])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					85b7e84f225aaa1fdd76115c88df56cc8cfb297f50ed0c8434906d2d76f66c68
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					97cb14f7d08a5301d274f2f1a54dd6b40c655ac9d2936bacd46187e687efbdb5
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					a8c9f960933989678f4a35b5ef48cea7c172d748e971fbf96e4d03d7038c0428
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					ad2953fc7be16a7bc66aa7513ec452c9e0d6bda1bf3700507c78af63feaed1f1
 | 
				
			||||||
| 
						 | 
					@ -19616,8 +19616,10 @@ CREATE TABLE scan_result_policies (
 | 
				
			||||||
    fallback_behavior jsonb DEFAULT '{}'::jsonb NOT NULL,
 | 
					    fallback_behavior jsonb DEFAULT '{}'::jsonb NOT NULL,
 | 
				
			||||||
    policy_tuning jsonb DEFAULT '{}'::jsonb NOT NULL,
 | 
					    policy_tuning jsonb DEFAULT '{}'::jsonb NOT NULL,
 | 
				
			||||||
    action_idx smallint DEFAULT 0 NOT NULL,
 | 
					    action_idx smallint DEFAULT 0 NOT NULL,
 | 
				
			||||||
 | 
					    custom_roles bigint[] DEFAULT '{}'::bigint[] NOT NULL,
 | 
				
			||||||
    CONSTRAINT age_value_null_or_positive CHECK (((age_value IS NULL) OR (age_value >= 0))),
 | 
					    CONSTRAINT age_value_null_or_positive CHECK (((age_value IS NULL) OR (age_value >= 0))),
 | 
				
			||||||
    CONSTRAINT check_scan_result_policies_rule_idx_positive CHECK (((rule_idx IS NULL) OR (rule_idx >= 0)))
 | 
					    CONSTRAINT check_scan_result_policies_rule_idx_positive CHECK (((rule_idx IS NULL) OR (rule_idx >= 0))),
 | 
				
			||||||
 | 
					    CONSTRAINT custom_roles_array_check CHECK ((array_position(custom_roles, NULL::bigint) IS NULL))
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE SEQUENCE scan_result_policies_id_seq
 | 
					CREATE SEQUENCE scan_result_policies_id_seq
 | 
				
			||||||
| 
						 | 
					@ -22723,7 +22725,8 @@ CREATE TABLE zoekt_enabled_namespaces (
 | 
				
			||||||
    root_namespace_id bigint NOT NULL,
 | 
					    root_namespace_id bigint NOT NULL,
 | 
				
			||||||
    created_at timestamp with time zone NOT NULL,
 | 
					    created_at timestamp with time zone NOT NULL,
 | 
				
			||||||
    updated_at timestamp with time zone NOT NULL,
 | 
					    updated_at timestamp with time zone NOT NULL,
 | 
				
			||||||
    search boolean DEFAULT true NOT NULL
 | 
					    search boolean DEFAULT true NOT NULL,
 | 
				
			||||||
 | 
					    metadata jsonb DEFAULT '{}'::jsonb NOT NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CREATE SEQUENCE zoekt_enabled_namespaces_id_seq
 | 
					CREATE SEQUENCE zoekt_enabled_namespaces_id_seq
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,15 +2,78 @@
 | 
				
			||||||
stage: Verify
 | 
					stage: Verify
 | 
				
			||||||
group: Runner
 | 
					group: Runner
 | 
				
			||||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
 | 
					info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
 | 
				
			||||||
remove_date: '2025-02-22'
 | 
					 | 
				
			||||||
redirect_to: '../../user/workspace/index.md'
 | 
					 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Interactive web terminals (removed)
 | 
					# Interactive web terminals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DETAILS:
 | 
					DETAILS:
 | 
				
			||||||
**Tier:** Free, Premium, Ultimate
 | 
					**Tier:** Free, Premium, Ultimate
 | 
				
			||||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
 | 
					**Offering:** GitLab.com, Self-managed, GitLab Dedicated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This feature was [deprecated and removed](https://gitlab.com/gitlab-org/gitlab/-/issues/444551) in GitLab 17.7.
 | 
					Interactive web terminals give the user access to a terminal in GitLab for
 | 
				
			||||||
Use [workspaces](../../user/workspace/index.md) instead.
 | 
					running one-off commands for their CI pipeline. You can think of it like a method for
 | 
				
			||||||
 | 
					debugging with SSH, but done directly from the job page. Since this is giving the user
 | 
				
			||||||
 | 
					shell access to the environment where [GitLab Runner](https://docs.gitlab.com/runner/)
 | 
				
			||||||
 | 
					is deployed, some [security precautions](../../administration/integration/terminal.md#security) were
 | 
				
			||||||
 | 
					taken to protect the users.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NOTE:
 | 
				
			||||||
 | 
					[Instance runners on GitLab.com](../runners/index.md) do not
 | 
				
			||||||
 | 
					provide an interactive web terminal. Follow
 | 
				
			||||||
 | 
					[this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/24674) for progress on
 | 
				
			||||||
 | 
					adding support. For groups and projects hosted on GitLab.com, interactive web
 | 
				
			||||||
 | 
					terminals are available when using your own group or project runner.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Two things need to be configured for the interactive web terminal to work:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- The runner needs to have
 | 
				
			||||||
 | 
					  [`[session_server]` configured properly](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section)
 | 
				
			||||||
 | 
					- If you are using a reverse proxy with your GitLab instance, web terminals need to be
 | 
				
			||||||
 | 
					  [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Partial support for Helm chart
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Interactive web terminals are partially supported in `gitlab-runner` Helm chart.
 | 
				
			||||||
 | 
					They are enabled when:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- The number of replica is one
 | 
				
			||||||
 | 
					- You use the `loadBalancer` service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Support for fixing these limitations is tracked in the following issues:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [Support of more than one replica](https://gitlab.com/gitlab-org/charts/gitlab-runner/-/issues/323)
 | 
				
			||||||
 | 
					- [Support of more service types](https://gitlab.com/gitlab-org/charts/gitlab-runner/-/issues/324)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Debugging a running job
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NOTE:
 | 
				
			||||||
 | 
					Not all executors are
 | 
				
			||||||
 | 
					[supported](https://docs.gitlab.com/runner/executors/#compatibility-chart).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NOTE:
 | 
				
			||||||
 | 
					The `docker` executor does not keep running
 | 
				
			||||||
 | 
					after the build script is finished. At that point, the terminal automatically
 | 
				
			||||||
 | 
					disconnects and does not wait for the user to finish. Follow
 | 
				
			||||||
 | 
					[this issue](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3605) for updates on
 | 
				
			||||||
 | 
					improving this behavior.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Sometimes, when a job is running, things don't go as you would expect, and it
 | 
				
			||||||
 | 
					would be helpful if one can have a shell to aid debugging. When a job is
 | 
				
			||||||
 | 
					running, on the right panel, you can see a `debug` button (**{external-link}**) that opens the terminal
 | 
				
			||||||
 | 
					for the current job. Only the person who started a job can debug it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When selected, a new tab opens to the terminal page where you can access
 | 
				
			||||||
 | 
					the terminal and type commands like in a standard shell.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you have the terminal open and the job has finished with its tasks, the
 | 
				
			||||||
 | 
					terminal blocks the job from finishing for the duration configured in
 | 
				
			||||||
 | 
					[`[session_server].session_timeout`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) until you
 | 
				
			||||||
 | 
					close the terminal window.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -357,6 +357,7 @@ module API
 | 
				
			||||||
        mount ::API::UserCounts
 | 
					        mount ::API::UserCounts
 | 
				
			||||||
        mount ::API::UserRunners
 | 
					        mount ::API::UserRunners
 | 
				
			||||||
        mount ::API::VirtualRegistries::Packages::Maven::Registries
 | 
					        mount ::API::VirtualRegistries::Packages::Maven::Registries
 | 
				
			||||||
 | 
					        mount ::API::VirtualRegistries::Packages::Maven::Upstreams
 | 
				
			||||||
        mount ::API::VirtualRegistries::Packages::Maven::Endpoints
 | 
					        mount ::API::VirtualRegistries::Packages::Maven::Endpoints
 | 
				
			||||||
        mount ::API::WebCommits
 | 
					        mount ::API::WebCommits
 | 
				
			||||||
        mount ::API::Wikis
 | 
					        mount ::API::Wikis
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,157 +0,0 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module API
 | 
					 | 
				
			||||||
  module Concerns
 | 
					 | 
				
			||||||
    module VirtualRegistries
 | 
					 | 
				
			||||||
      module Packages
 | 
					 | 
				
			||||||
        module Maven
 | 
					 | 
				
			||||||
          module UpstreamEndpoints
 | 
					 | 
				
			||||||
            extend ActiveSupport::Concern
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            included do
 | 
					 | 
				
			||||||
              desc 'List all maven virtual registry upstreams' do
 | 
					 | 
				
			||||||
                detail 'This feature was introduced in GitLab 17.4. \
 | 
					 | 
				
			||||||
                      This feature is currently in experiment state. \
 | 
					 | 
				
			||||||
                      This feature behind the `virtual_registry_maven` feature flag.'
 | 
					 | 
				
			||||||
                success code: 200
 | 
					 | 
				
			||||||
                failure [
 | 
					 | 
				
			||||||
                  { code: 400, message: 'Bad Request' },
 | 
					 | 
				
			||||||
                  { code: 401, message: 'Unauthorized' },
 | 
					 | 
				
			||||||
                  { code: 403, message: 'Forbidden' },
 | 
					 | 
				
			||||||
                  { code: 404, message: 'Not found' }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                tags %w[maven_virtual_registries]
 | 
					 | 
				
			||||||
                hidden true
 | 
					 | 
				
			||||||
              end
 | 
					 | 
				
			||||||
              get do
 | 
					 | 
				
			||||||
                authorize! :read_virtual_registry, registry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                present [upstream].compact, with: Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
					 | 
				
			||||||
              end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              desc 'Add a maven virtual registry upstream' do
 | 
					 | 
				
			||||||
                detail 'This feature was introduced in GitLab 17.4. \
 | 
					 | 
				
			||||||
                      This feature is currently in experiment state. \
 | 
					 | 
				
			||||||
                      This feature behind the `virtual_registry_maven` feature flag.'
 | 
					 | 
				
			||||||
                success code: 201
 | 
					 | 
				
			||||||
                failure [
 | 
					 | 
				
			||||||
                  { code: 400, message: 'Bad Request' },
 | 
					 | 
				
			||||||
                  { code: 401, message: 'Unauthorized' },
 | 
					 | 
				
			||||||
                  { code: 403, message: 'Forbidden' },
 | 
					 | 
				
			||||||
                  { code: 404, message: 'Not found' },
 | 
					 | 
				
			||||||
                  { code: 409, message: 'Conflict' }
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                tags %w[maven_virtual_registries]
 | 
					 | 
				
			||||||
                hidden true
 | 
					 | 
				
			||||||
              end
 | 
					 | 
				
			||||||
              params do
 | 
					 | 
				
			||||||
                requires :url, type: String, desc: 'The URL of the maven virtual registry upstream', allow_blank: false
 | 
					 | 
				
			||||||
                optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
 | 
					 | 
				
			||||||
                optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
 | 
					 | 
				
			||||||
                optional :cache_validity_hours, type: Integer, desc: 'The cache validity in hours. Defaults to 24'
 | 
					 | 
				
			||||||
                all_or_none_of :username, :password
 | 
					 | 
				
			||||||
              end
 | 
					 | 
				
			||||||
              post do
 | 
					 | 
				
			||||||
                authorize! :create_virtual_registry, registry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                conflict!(_('Upstream already exists')) if upstream
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                registry.build_upstream(declared_params(include_missing: false).merge(group: group))
 | 
					 | 
				
			||||||
                registry_upstream.group = group
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                ApplicationRecord.transaction do
 | 
					 | 
				
			||||||
                  render_validation_error!(upstream) unless upstream.save
 | 
					 | 
				
			||||||
                  render_validation_error!(registry_upstream) unless registry_upstream.save
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                created!
 | 
					 | 
				
			||||||
              end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              route_param :upstream_id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
 | 
					 | 
				
			||||||
                desc 'Get a specific maven virtual registry upstream' do
 | 
					 | 
				
			||||||
                  detail 'This feature was introduced in GitLab 17.4. \
 | 
					 | 
				
			||||||
                        This feature is currently in experiment state. \
 | 
					 | 
				
			||||||
                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
					 | 
				
			||||||
                  success code: 200
 | 
					 | 
				
			||||||
                  failure [
 | 
					 | 
				
			||||||
                    { code: 400, message: 'Bad Request' },
 | 
					 | 
				
			||||||
                    { code: 401, message: 'Unauthorized' },
 | 
					 | 
				
			||||||
                    { code: 403, message: 'Forbidden' },
 | 
					 | 
				
			||||||
                    { code: 404, message: 'Not found' }
 | 
					 | 
				
			||||||
                  ]
 | 
					 | 
				
			||||||
                  tags %w[maven_virtual_registries]
 | 
					 | 
				
			||||||
                  hidden true
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
                get do
 | 
					 | 
				
			||||||
                  authorize! :read_virtual_registry, registry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  # TODO: refactor this when we support multiple upstreams.
 | 
					 | 
				
			||||||
                  # https://gitlab.com/gitlab-org/gitlab/-/issues/480461
 | 
					 | 
				
			||||||
                  not_found! if upstream&.id != params[:upstream_id]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  present upstream, with: Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                desc 'Update a maven virtual registry upstream' do
 | 
					 | 
				
			||||||
                  detail 'This feature was introduced in GitLab 17.4. \
 | 
					 | 
				
			||||||
                        This feature is currently in experiment state. \
 | 
					 | 
				
			||||||
                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
					 | 
				
			||||||
                  success code: 200
 | 
					 | 
				
			||||||
                  failure [
 | 
					 | 
				
			||||||
                    { code: 400, message: 'Bad Request' },
 | 
					 | 
				
			||||||
                    { code: 401, message: 'Unauthorized' },
 | 
					 | 
				
			||||||
                    { code: 403, message: 'Forbidden' },
 | 
					 | 
				
			||||||
                    { code: 404, message: 'Not found' }
 | 
					 | 
				
			||||||
                  ]
 | 
					 | 
				
			||||||
                  tags %w[maven_virtual_registries]
 | 
					 | 
				
			||||||
                  hidden true
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
                params do
 | 
					 | 
				
			||||||
                  with(allow_blank: false) do
 | 
					 | 
				
			||||||
                    optional :url, type: String, desc: 'The URL of the maven virtual registry upstream'
 | 
					 | 
				
			||||||
                    optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
 | 
					 | 
				
			||||||
                    optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
 | 
					 | 
				
			||||||
                    optional :cache_validity_hours, type: Integer, desc: 'The validity of the cache in hours'
 | 
					 | 
				
			||||||
                  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  at_least_one_of :url, :username, :password, :cache_validity_hours
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
                patch do
 | 
					 | 
				
			||||||
                  authorize! :update_virtual_registry, registry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  render_validation_error!(upstream) unless upstream.update(declared_params(include_missing: false))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  status :ok
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                desc 'Delete a maven virtual registry upstream' do
 | 
					 | 
				
			||||||
                  detail 'This feature was introduced in GitLab 17.4. \
 | 
					 | 
				
			||||||
                        This feature is currently in experiment state. \
 | 
					 | 
				
			||||||
                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
					 | 
				
			||||||
                  success code: 204
 | 
					 | 
				
			||||||
                  failure [
 | 
					 | 
				
			||||||
                    { code: 400, message: 'Bad Request' },
 | 
					 | 
				
			||||||
                    { code: 401, message: 'Unauthorized' },
 | 
					 | 
				
			||||||
                    { code: 403, message: 'Forbidden' },
 | 
					 | 
				
			||||||
                    { code: 404, message: 'Not found' }
 | 
					 | 
				
			||||||
                  ]
 | 
					 | 
				
			||||||
                  tags %w[maven_virtual_registries]
 | 
					 | 
				
			||||||
                  hidden true
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
                delete do
 | 
					 | 
				
			||||||
                  authorize! :destroy_virtual_registry, registry
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  # TODO: refactor this when we support multiple upstreams.
 | 
					 | 
				
			||||||
                  # https://gitlab.com/gitlab-org/gitlab/-/issues/480461
 | 
					 | 
				
			||||||
                  not_found! if upstream&.id != params[:upstream_id]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  destroy_conditionally!(upstream)
 | 
					 | 
				
			||||||
                end
 | 
					 | 
				
			||||||
              end
 | 
					 | 
				
			||||||
            end
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
| 
						 | 
					@ -63,8 +63,6 @@ module API
 | 
				
			||||||
            namespace :registries do
 | 
					            namespace :registries do
 | 
				
			||||||
              route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' do
 | 
					              route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' do
 | 
				
			||||||
                namespace :upstreams do
 | 
					                namespace :upstreams do
 | 
				
			||||||
                  include ::API::Concerns::VirtualRegistries::Packages::Maven::UpstreamEndpoints
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  route_param :upstream_id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
 | 
					                  route_param :upstream_id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
 | 
				
			||||||
                    namespace :cached_responses do
 | 
					                    namespace :cached_responses do
 | 
				
			||||||
                      include ::API::Concerns::VirtualRegistries::Packages::Maven::CachedResponseEndpoints
 | 
					                      include ::API::Concerns::VirtualRegistries::Packages::Maven::CachedResponseEndpoints
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,193 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module API
 | 
				
			||||||
 | 
					  module VirtualRegistries
 | 
				
			||||||
 | 
					    module Packages
 | 
				
			||||||
 | 
					      module Maven
 | 
				
			||||||
 | 
					        class Upstreams < ::API::Base
 | 
				
			||||||
 | 
					          include ::API::Helpers::Authentication
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          feature_category :virtual_registry
 | 
				
			||||||
 | 
					          urgency :low
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          authenticate_with do |accept|
 | 
				
			||||||
 | 
					            accept.token_types(:personal_access_token).sent_through(:http_private_token_header)
 | 
				
			||||||
 | 
					            accept.token_types(:deploy_token).sent_through(:http_deploy_token_header)
 | 
				
			||||||
 | 
					            accept.token_types(:job_token).sent_through(:http_job_token_header)
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          helpers do
 | 
				
			||||||
 | 
					            include ::Gitlab::Utils::StrongMemoize
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            delegate :group, :registry_upstream, to: :registry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def require_dependency_proxy_enabled!
 | 
				
			||||||
 | 
					              not_found! unless Gitlab.config.dependency_proxy.enabled
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def registry
 | 
				
			||||||
 | 
					              ::VirtualRegistries::Packages::Maven::Registry.find(params[:id])
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					            strong_memoize_attr :registry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            def upstream
 | 
				
			||||||
 | 
					              ::VirtualRegistries::Packages::Maven::Upstream.find(params[:id])
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					            strong_memoize_attr :upstream
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          after_validation do
 | 
				
			||||||
 | 
					            not_found! unless Feature.enabled?(:virtual_registry_maven, current_user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            require_dependency_proxy_enabled!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            authenticate!
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          namespace 'virtual_registries/packages/maven' do
 | 
				
			||||||
 | 
					            namespace :registries do
 | 
				
			||||||
 | 
					              route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' do
 | 
				
			||||||
 | 
					                namespace :upstreams do
 | 
				
			||||||
 | 
					                  desc 'List all maven virtual registry upstreams' do
 | 
				
			||||||
 | 
					                    detail 'This feature was introduced in GitLab 17.4. \
 | 
				
			||||||
 | 
					                        This feature is currently in experiment state. \
 | 
				
			||||||
 | 
					                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
				
			||||||
 | 
					                    success code: 200
 | 
				
			||||||
 | 
					                    failure [
 | 
				
			||||||
 | 
					                      { code: 400, message: 'Bad Request' },
 | 
				
			||||||
 | 
					                      { code: 401, message: 'Unauthorized' },
 | 
				
			||||||
 | 
					                      { code: 403, message: 'Forbidden' },
 | 
				
			||||||
 | 
					                      { code: 404, message: 'Not found' }
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                    tags %w[maven_virtual_registries]
 | 
				
			||||||
 | 
					                    hidden true
 | 
				
			||||||
 | 
					                  end
 | 
				
			||||||
 | 
					                  get do
 | 
				
			||||||
 | 
					                    authorize! :read_virtual_registry, registry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    present [registry.upstream].compact, with: Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
				
			||||||
 | 
					                  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  desc 'Add a maven virtual registry upstream' do
 | 
				
			||||||
 | 
					                    detail 'This feature was introduced in GitLab 17.4. \
 | 
				
			||||||
 | 
					                        This feature is currently in experiment state. \
 | 
				
			||||||
 | 
					                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
				
			||||||
 | 
					                    success code: 201, model: ::API::Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
				
			||||||
 | 
					                    failure [
 | 
				
			||||||
 | 
					                      { code: 400, message: 'Bad Request' },
 | 
				
			||||||
 | 
					                      { code: 401, message: 'Unauthorized' },
 | 
				
			||||||
 | 
					                      { code: 403, message: 'Forbidden' },
 | 
				
			||||||
 | 
					                      { code: 404, message: 'Not found' },
 | 
				
			||||||
 | 
					                      { code: 409, message: 'Conflict' }
 | 
				
			||||||
 | 
					                    ]
 | 
				
			||||||
 | 
					                    tags %w[maven_virtual_registries]
 | 
				
			||||||
 | 
					                    hidden true
 | 
				
			||||||
 | 
					                  end
 | 
				
			||||||
 | 
					                  params do
 | 
				
			||||||
 | 
					                    requires :url, type: String, desc: 'The URL of the maven virtual registry upstream',
 | 
				
			||||||
 | 
					                      allow_blank: false
 | 
				
			||||||
 | 
					                    optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
 | 
				
			||||||
 | 
					                    optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
 | 
				
			||||||
 | 
					                    optional :cache_validity_hours, type: Integer, desc: 'The cache validity in hours. Defaults to 24'
 | 
				
			||||||
 | 
					                    all_or_none_of :username, :password
 | 
				
			||||||
 | 
					                  end
 | 
				
			||||||
 | 
					                  post do
 | 
				
			||||||
 | 
					                    authorize! :create_virtual_registry, registry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    conflict!(_('Upstream already exists')) if registry.upstream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    new_upstream = registry.build_upstream(declared_params(include_missing: false).merge(group:))
 | 
				
			||||||
 | 
					                    registry_upstream.group = group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    ApplicationRecord.transaction do
 | 
				
			||||||
 | 
					                      render_validation_error!(new_upstream) unless new_upstream.save
 | 
				
			||||||
 | 
					                      render_validation_error!(registry_upstream) unless registry_upstream.save
 | 
				
			||||||
 | 
					                    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    present new_upstream, with: Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
				
			||||||
 | 
					                  end
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					              end
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            namespace :upstreams do
 | 
				
			||||||
 | 
					              route_param :id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
 | 
				
			||||||
 | 
					                desc 'Get a specific maven virtual registry upstream' do
 | 
				
			||||||
 | 
					                  detail 'This feature was introduced in GitLab 17.4. \
 | 
				
			||||||
 | 
					                        This feature is currently in experiment state. \
 | 
				
			||||||
 | 
					                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
				
			||||||
 | 
					                  success ::API::Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
				
			||||||
 | 
					                  failure [
 | 
				
			||||||
 | 
					                    { code: 400, message: 'Bad Request' },
 | 
				
			||||||
 | 
					                    { code: 401, message: 'Unauthorized' },
 | 
				
			||||||
 | 
					                    { code: 403, message: 'Forbidden' },
 | 
				
			||||||
 | 
					                    { code: 404, message: 'Not found' }
 | 
				
			||||||
 | 
					                  ]
 | 
				
			||||||
 | 
					                  tags %w[maven_virtual_registries]
 | 
				
			||||||
 | 
					                  hidden true
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					                get do
 | 
				
			||||||
 | 
					                  authorize! :read_virtual_registry, upstream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  present upstream, with: ::API::Entities::VirtualRegistries::Packages::Maven::Upstream
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                desc 'Update a maven virtual registry upstream' do
 | 
				
			||||||
 | 
					                  detail 'This feature was introduced in GitLab 17.4. \
 | 
				
			||||||
 | 
					                        This feature is currently in experiment state. \
 | 
				
			||||||
 | 
					                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
				
			||||||
 | 
					                  success code: 200
 | 
				
			||||||
 | 
					                  failure [
 | 
				
			||||||
 | 
					                    { code: 400, message: 'Bad Request' },
 | 
				
			||||||
 | 
					                    { code: 401, message: 'Unauthorized' },
 | 
				
			||||||
 | 
					                    { code: 403, message: 'Forbidden' },
 | 
				
			||||||
 | 
					                    { code: 404, message: 'Not found' }
 | 
				
			||||||
 | 
					                  ]
 | 
				
			||||||
 | 
					                  tags %w[maven_virtual_registries]
 | 
				
			||||||
 | 
					                  hidden true
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					                params do
 | 
				
			||||||
 | 
					                  with(allow_blank: false) do
 | 
				
			||||||
 | 
					                    optional :url, type: String, desc: 'The URL of the maven virtual registry upstream'
 | 
				
			||||||
 | 
					                    optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
 | 
				
			||||||
 | 
					                    optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
 | 
				
			||||||
 | 
					                    optional :cache_validity_hours, type: Integer, desc: 'The validity of the cache in hours'
 | 
				
			||||||
 | 
					                  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  at_least_one_of :url, :username, :password, :cache_validity_hours
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					                patch do
 | 
				
			||||||
 | 
					                  authorize! :update_virtual_registry, upstream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  render_validation_error!(upstream) unless upstream.update(declared_params(include_missing: false))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  status :ok
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                desc 'Delete a maven virtual registry upstream' do
 | 
				
			||||||
 | 
					                  detail 'This feature was introduced in GitLab 17.4. \
 | 
				
			||||||
 | 
					                        This feature is currently in experiment state. \
 | 
				
			||||||
 | 
					                        This feature behind the `virtual_registry_maven` feature flag.'
 | 
				
			||||||
 | 
					                  success code: 204
 | 
				
			||||||
 | 
					                  failure [
 | 
				
			||||||
 | 
					                    { code: 400, message: 'Bad Request' },
 | 
				
			||||||
 | 
					                    { code: 401, message: 'Unauthorized' },
 | 
				
			||||||
 | 
					                    { code: 403, message: 'Forbidden' },
 | 
				
			||||||
 | 
					                    { code: 404, message: 'Not found' }
 | 
				
			||||||
 | 
					                  ]
 | 
				
			||||||
 | 
					                  tags %w[maven_virtual_registries]
 | 
				
			||||||
 | 
					                  hidden true
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					                delete do
 | 
				
			||||||
 | 
					                  authorize! :destroy_virtual_registry, upstream
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  destroy_conditionally!(upstream)
 | 
				
			||||||
 | 
					                end
 | 
				
			||||||
 | 
					              end
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Gitlab
 | 
				
			||||||
 | 
					  module BackgroundMigration
 | 
				
			||||||
 | 
					    class BackfillFreeSharedRunnersMinutesLimit < BatchedMigrationJob
 | 
				
			||||||
 | 
					      feature_category :consumables_cost_management
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      def perform; end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Gitlab::BackgroundMigration::BackfillFreeSharedRunnersMinutesLimit.prepend_mod
 | 
				
			||||||
| 
						 | 
					@ -107,6 +107,9 @@ function bundle_install_script() {
 | 
				
			||||||
  run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS} ${extra_install_args}"
 | 
					  run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS} ${extra_install_args}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if [[ $(bundle info pg) ]]; then
 | 
					  if [[ $(bundle info pg) ]]; then
 | 
				
			||||||
 | 
					    # Bundler will complain about replacing gems in world-writeable directories, so lock down access.
 | 
				
			||||||
 | 
					    # This appears to happen when the gems are uncached, since the Runner uses a restrictive umask.
 | 
				
			||||||
 | 
					    find vendor -type d -exec chmod 700 {} +
 | 
				
			||||||
    # When we test multiple versions of PG in the same pipeline, we have a single `setup-test-env`
 | 
					    # When we test multiple versions of PG in the same pipeline, we have a single `setup-test-env`
 | 
				
			||||||
    # job but the `pg` gem needs to be rebuilt since it includes extensions (https://guides.rubygems.org/gems-with-extensions).
 | 
					    # job but the `pg` gem needs to be rebuilt since it includes extensions (https://guides.rubygems.org/gems-with-extensions).
 | 
				
			||||||
    # Uncomment the following line if multiple versions of PG are tested in the same pipeline.
 | 
					    # Uncomment the following line if multiple versions of PG are tested in the same pipeline.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,13 +32,15 @@ describe('AdminOrganizationsIndexApp', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const successHandler = jest.fn().mockResolvedValue(organizationsGraphQlResponse);
 | 
					  const successHandler = jest.fn().mockResolvedValue(organizationsGraphQlResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = (handler = successHandler) => {
 | 
					  const createComponent = ({ handler = successHandler, provide = {} } = {}) => {
 | 
				
			||||||
    mockApollo = createMockApollo([[organizationsQuery, handler]]);
 | 
					    mockApollo = createMockApollo([[organizationsQuery, handler]]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    wrapper = shallowMountExtended(OrganizationsIndexApp, {
 | 
					    wrapper = shallowMountExtended(OrganizationsIndexApp, {
 | 
				
			||||||
      apolloProvider: mockApollo,
 | 
					      apolloProvider: mockApollo,
 | 
				
			||||||
      provide: {
 | 
					      provide: {
 | 
				
			||||||
        newOrganizationUrl: MOCK_NEW_ORG_URL,
 | 
					        newOrganizationUrl: MOCK_NEW_ORG_URL,
 | 
				
			||||||
 | 
					        canCreateOrganization: true,
 | 
				
			||||||
 | 
					        ...provide,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					@ -89,7 +91,7 @@ describe('AdminOrganizationsIndexApp', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('when API call is loading', () => {
 | 
					  describe('when API call is loading', () => {
 | 
				
			||||||
    beforeEach(() => {
 | 
					    beforeEach(() => {
 | 
				
			||||||
      createComponent(jest.fn().mockReturnValue(new Promise(() => {})));
 | 
					      createComponent({ handler: jest.fn().mockReturnValue(new Promise(() => {})) });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    itRendersHeaderText();
 | 
					    itRendersHeaderText();
 | 
				
			||||||
| 
						 | 
					@ -119,11 +121,9 @@ describe('AdminOrganizationsIndexApp', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('when `allowOrganizationCreation` feature flag is disabled', () => {
 | 
					  describe('when `canCreateOrganization` is false', () => {
 | 
				
			||||||
    beforeEach(() => {
 | 
					    beforeEach(() => {
 | 
				
			||||||
      gon.features = { allowOrganizationCreation: false };
 | 
					      createComponent({ provide: { canCreateOrganization: false } });
 | 
				
			||||||
 | 
					 | 
				
			||||||
      createComponent();
 | 
					 | 
				
			||||||
      return waitForPromises();
 | 
					      return waitForPromises();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -132,13 +132,13 @@ describe('AdminOrganizationsIndexApp', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('when API call is successful and returns no organizations', () => {
 | 
					  describe('when API call is successful and returns no organizations', () => {
 | 
				
			||||||
    beforeEach(async () => {
 | 
					    beforeEach(async () => {
 | 
				
			||||||
      createComponent(
 | 
					      createComponent({
 | 
				
			||||||
        jest.fn().mockResolvedValue({
 | 
					        handler: jest.fn().mockResolvedValue({
 | 
				
			||||||
          data: {
 | 
					          data: {
 | 
				
			||||||
            organizations: organizationEmpty,
 | 
					            organizations: organizationEmpty,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      });
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -158,7 +158,7 @@ describe('AdminOrganizationsIndexApp', () => {
 | 
				
			||||||
    const error = new Error();
 | 
					    const error = new Error();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    beforeEach(async () => {
 | 
					    beforeEach(async () => {
 | 
				
			||||||
      createComponent(jest.fn().mockRejectedValue(error));
 | 
					      createComponent({ handler: jest.fn().mockRejectedValue(error) });
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,13 +34,15 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const successHandler = jest.fn().mockResolvedValue(currentUserOrganizationsGraphQlResponse);
 | 
					  const successHandler = jest.fn().mockResolvedValue(currentUserOrganizationsGraphQlResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = (handler = successHandler) => {
 | 
					  const createComponent = ({ handler = successHandler, provide = {} } = {}) => {
 | 
				
			||||||
    mockApollo = createMockApollo([[currentUserOrganizationsQuery, handler]]);
 | 
					    mockApollo = createMockApollo([[currentUserOrganizationsQuery, handler]]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    wrapper = shallowMountExtended(OrganizationsIndexApp, {
 | 
					    wrapper = shallowMountExtended(OrganizationsIndexApp, {
 | 
				
			||||||
      apolloProvider: mockApollo,
 | 
					      apolloProvider: mockApollo,
 | 
				
			||||||
      provide: {
 | 
					      provide: {
 | 
				
			||||||
        newOrganizationUrl: MOCK_NEW_ORG_URL,
 | 
					        newOrganizationUrl: MOCK_NEW_ORG_URL,
 | 
				
			||||||
 | 
					        canCreateOrganization: true,
 | 
				
			||||||
 | 
					        ...provide,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					@ -85,14 +87,10 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('`allowOrganizationCreation` is enabled', () => {
 | 
					  describe('`canCreateOrganization` is true', () => {
 | 
				
			||||||
    beforeEach(() => {
 | 
					 | 
				
			||||||
      gon.features = { allowOrganizationCreation: true };
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    describe('when API call is loading', () => {
 | 
					    describe('when API call is loading', () => {
 | 
				
			||||||
      beforeEach(() => {
 | 
					      beforeEach(() => {
 | 
				
			||||||
        createComponent(jest.fn().mockResolvedValue({}));
 | 
					        createComponent({ handler: jest.fn().mockResolvedValue({}) });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      itRendersHeaderText();
 | 
					      itRendersHeaderText();
 | 
				
			||||||
| 
						 | 
					@ -122,14 +120,13 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('`allowOrganizationCreation` is disabled', () => {
 | 
					  describe('`canCreateOrganization` is false', () => {
 | 
				
			||||||
    beforeEach(() => {
 | 
					 | 
				
			||||||
      gon.features = { allowOrganizationCreation: false };
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    describe('when API call is loading', () => {
 | 
					    describe('when API call is loading', () => {
 | 
				
			||||||
      beforeEach(() => {
 | 
					      beforeEach(() => {
 | 
				
			||||||
        createComponent(jest.fn().mockResolvedValue({}));
 | 
					        createComponent({
 | 
				
			||||||
 | 
					          handler: jest.fn().mockResolvedValue({}),
 | 
				
			||||||
 | 
					          provide: { canCreateOrganization: false },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      itRendersHeaderText();
 | 
					      itRendersHeaderText();
 | 
				
			||||||
| 
						 | 
					@ -142,7 +139,7 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    describe('when API call is successful', () => {
 | 
					    describe('when API call is successful', () => {
 | 
				
			||||||
      beforeEach(() => {
 | 
					      beforeEach(() => {
 | 
				
			||||||
        createComponent();
 | 
					        createComponent({ provide: { canCreateOrganization: false } });
 | 
				
			||||||
        return waitForPromises();
 | 
					        return waitForPromises();
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -161,8 +158,8 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('when API call is successful and returns no organizations', () => {
 | 
					  describe('when API call is successful and returns no organizations', () => {
 | 
				
			||||||
    beforeEach(async () => {
 | 
					    beforeEach(async () => {
 | 
				
			||||||
      createComponent(
 | 
					      createComponent({
 | 
				
			||||||
        jest.fn().mockResolvedValue({
 | 
					        handler: jest.fn().mockResolvedValue({
 | 
				
			||||||
          data: {
 | 
					          data: {
 | 
				
			||||||
            currentUser: {
 | 
					            currentUser: {
 | 
				
			||||||
              id: 'gid://gitlab/User/1',
 | 
					              id: 'gid://gitlab/User/1',
 | 
				
			||||||
| 
						 | 
					@ -170,7 +167,7 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      );
 | 
					      });
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -190,7 +187,7 @@ describe('OrganizationsIndexApp', () => {
 | 
				
			||||||
    const error = new Error();
 | 
					    const error = new Error();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    beforeEach(async () => {
 | 
					    beforeEach(async () => {
 | 
				
			||||||
      createComponent(jest.fn().mockRejectedValue(error));
 | 
					      createComponent({ handler: jest.fn().mockRejectedValue(error) });
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,13 +21,15 @@ describe('OrganizationsView', () => {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  } = currentUserOrganizationsGraphQlResponse;
 | 
					  } = currentUserOrganizationsGraphQlResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = (props = {}) => {
 | 
					  const createComponent = (props = {}, provide = {}) => {
 | 
				
			||||||
    wrapper = shallowMount(OrganizationsView, {
 | 
					    wrapper = shallowMount(OrganizationsView, {
 | 
				
			||||||
      propsData: {
 | 
					      propsData: {
 | 
				
			||||||
        ...props,
 | 
					        ...props,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      provide: {
 | 
					      provide: {
 | 
				
			||||||
        newOrganizationUrl: MOCK_NEW_ORG_URL,
 | 
					        newOrganizationUrl: MOCK_NEW_ORG_URL,
 | 
				
			||||||
 | 
					        canCreateOrganization: true,
 | 
				
			||||||
 | 
					        ...provide,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					@ -36,10 +38,6 @@ describe('OrganizationsView', () => {
 | 
				
			||||||
  const findOrganizationsList = () => wrapper.findComponent(OrganizationsList);
 | 
					  const findOrganizationsList = () => wrapper.findComponent(OrganizationsList);
 | 
				
			||||||
  const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
 | 
					  const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					 | 
				
			||||||
    gon.features = { allowOrganizationCreation: true };
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe.each`
 | 
					  describe.each`
 | 
				
			||||||
    description                                    | loading  | orgsData         | emptyStateSvg                   | emptyStateUrl
 | 
					    description                                    | loading  | orgsData         | emptyStateSvg                   | emptyStateUrl
 | 
				
			||||||
    ${'when loading'}                              | ${true}  | ${[]}            | ${false}                        | ${false}
 | 
					    ${'when loading'}                              | ${true}  | ${[]}            | ${false}                        | ${false}
 | 
				
			||||||
| 
						 | 
					@ -71,10 +69,12 @@ describe('OrganizationsView', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('when `allowOrganizationCreation` feature flag is disabled', () => {
 | 
					  describe('when `canCreateOrganization` feature flag is false', () => {
 | 
				
			||||||
    beforeEach(() => {
 | 
					    beforeEach(() => {
 | 
				
			||||||
      gon.features = { allowOrganizationCreation: false };
 | 
					      createComponent(
 | 
				
			||||||
      createComponent({ loading: false, organizations: { nodes: [], pageInfo: {} } });
 | 
					        { loading: false, organizations: { nodes: [], pageInfo: {} } },
 | 
				
			||||||
 | 
					        { canCreateOrganization: false },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('does not render `New organization` button in empty state', () => {
 | 
					    it('does not render `New organization` button in empty state', () => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -137,6 +137,7 @@ describe('WorkItemLinkChildContents', () => {
 | 
				
			||||||
    it('emits click event with correct parameters on clicking title', () => {
 | 
					    it('emits click event with correct parameters on clicking title', () => {
 | 
				
			||||||
      const eventObj = {
 | 
					      const eventObj = {
 | 
				
			||||||
        preventDefault: jest.fn(),
 | 
					        preventDefault: jest.fn(),
 | 
				
			||||||
 | 
					        stopPropagation: jest.fn(),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      findTitleEl().vm.$emit('click', eventObj);
 | 
					      findTitleEl().vm.$emit('click', eventObj);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -152,7 +153,7 @@ describe('WorkItemLinkChildContents', () => {
 | 
				
			||||||
          workItemFullPath: 'gitlab-org/gitlab-test',
 | 
					          workItemFullPath: 'gitlab-org/gitlab-test',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        findTitleEl().vm.$emit('click', { preventDefault });
 | 
					        findTitleEl().vm.$emit('click', { preventDefault, stopPropagation: jest.fn() });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it('pushes a new router state', () => {
 | 
					      it('pushes a new router state', () => {
 | 
				
			||||||
| 
						 | 
					@ -218,7 +219,7 @@ describe('WorkItemLinkChildContents', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('removeChild event on menu triggers `click-remove-child` event', () => {
 | 
					    it('removeChild event on menu triggers `click-remove-child` event', () => {
 | 
				
			||||||
      findRemoveButton().vm.$emit('click');
 | 
					      findRemoveButton().vm.$emit('click', { stopPropagation: jest.fn() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
 | 
					      expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,6 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
 | 
				
			||||||
import createMockApollo from 'helpers/mock_apollo_helper';
 | 
					import createMockApollo from 'helpers/mock_apollo_helper';
 | 
				
			||||||
import waitForPromises from 'helpers/wait_for_promises';
 | 
					import waitForPromises from 'helpers/wait_for_promises';
 | 
				
			||||||
import setWindowLocation from 'helpers/set_window_location_helper';
 | 
					import setWindowLocation from 'helpers/set_window_location_helper';
 | 
				
			||||||
import { stubComponent } from 'helpers/stub_component';
 | 
					 | 
				
			||||||
import WorkItemLoading from '~/work_items/components/work_item_loading.vue';
 | 
					import WorkItemLoading from '~/work_items/components/work_item_loading.vue';
 | 
				
			||||||
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
 | 
					import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
 | 
				
			||||||
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
 | 
					import WorkItemActions from '~/work_items/components/work_item_actions.vue';
 | 
				
			||||||
| 
						 | 
					@ -17,10 +16,10 @@ import WorkItemAttributesWrapper from '~/work_items/components/work_item_attribu
 | 
				
			||||||
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
 | 
					import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
 | 
				
			||||||
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
 | 
					import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
 | 
				
			||||||
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
 | 
					import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
 | 
				
			||||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
 | 
					 | 
				
			||||||
import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
 | 
					import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue';
 | 
				
			||||||
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
 | 
					import WorkItemTitle from '~/work_items/components/work_item_title.vue';
 | 
				
			||||||
import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
 | 
					import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue';
 | 
				
			||||||
 | 
					import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
 | 
				
			||||||
import TodosToggle from '~/work_items/components/shared/todos_toggle.vue';
 | 
					import TodosToggle from '~/work_items/components/shared/todos_toggle.vue';
 | 
				
			||||||
import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue';
 | 
					import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue';
 | 
				
			||||||
import DesignUploadButton from '~/work_items/components//design_management/upload_button.vue';
 | 
					import DesignUploadButton from '~/work_items/components//design_management/upload_button.vue';
 | 
				
			||||||
| 
						 | 
					@ -41,7 +40,6 @@ import {
 | 
				
			||||||
  workItemLinkedItemsResponse,
 | 
					  workItemLinkedItemsResponse,
 | 
				
			||||||
  objectiveType,
 | 
					  objectiveType,
 | 
				
			||||||
  epicType,
 | 
					  epicType,
 | 
				
			||||||
  mockWorkItemCommentNote,
 | 
					 | 
				
			||||||
  mockBlockingLinkedItem,
 | 
					  mockBlockingLinkedItem,
 | 
				
			||||||
  allowedChildrenTypesResponse,
 | 
					  allowedChildrenTypesResponse,
 | 
				
			||||||
  mockProjectPermissionsQueryResponse,
 | 
					  mockProjectPermissionsQueryResponse,
 | 
				
			||||||
| 
						 | 
					@ -76,7 +74,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
  const successHandlerWithNoPermissions = jest
 | 
					  const successHandlerWithNoPermissions = jest
 | 
				
			||||||
    .fn()
 | 
					    .fn()
 | 
				
			||||||
    .mockResolvedValue(workItemQueryResponseWithNoPermissions);
 | 
					    .mockResolvedValue(workItemQueryResponseWithNoPermissions);
 | 
				
			||||||
  const showModalHandler = jest.fn();
 | 
					 | 
				
			||||||
  const { id } = workItemByIidQueryResponse.data.workspace.workItem;
 | 
					  const { id } = workItemByIidQueryResponse.data.workspace.workItem;
 | 
				
			||||||
  const workItemUpdatedSubscriptionHandler = jest
 | 
					  const workItemUpdatedSubscriptionHandler = jest
 | 
				
			||||||
    .fn()
 | 
					    .fn()
 | 
				
			||||||
| 
						 | 
					@ -117,7 +114,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
  const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
 | 
					  const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
 | 
				
			||||||
  const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships);
 | 
					  const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships);
 | 
				
			||||||
  const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
 | 
					  const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
 | 
				
			||||||
  const findModal = () => wrapper.findComponent(WorkItemDetailModal);
 | 
					 | 
				
			||||||
  const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
 | 
					  const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
 | 
				
			||||||
  const findTodosToggle = () => wrapper.findComponent(TodosToggle);
 | 
					  const findTodosToggle = () => wrapper.findComponent(TodosToggle);
 | 
				
			||||||
  const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
 | 
					  const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
 | 
				
			||||||
| 
						 | 
					@ -127,6 +123,7 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
  const findWorkItemDesigns = () => wrapper.findComponent(DesignWidget);
 | 
					  const findWorkItemDesigns = () => wrapper.findComponent(DesignWidget);
 | 
				
			||||||
  const findDesignUploadButton = () => wrapper.findComponent(DesignUploadButton);
 | 
					  const findDesignUploadButton = () => wrapper.findComponent(DesignUploadButton);
 | 
				
			||||||
  const findDetailWrapper = () => wrapper.findByTestId('detail-wrapper');
 | 
					  const findDetailWrapper = () => wrapper.findByTestId('detail-wrapper');
 | 
				
			||||||
 | 
					  const findDrawer = () => wrapper.findComponent(WorkItemDrawer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const createComponent = ({
 | 
					  const createComponent = ({
 | 
				
			||||||
    isModal = false,
 | 
					    isModal = false,
 | 
				
			||||||
| 
						 | 
					@ -188,11 +185,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
        WorkItemWeight: true,
 | 
					        WorkItemWeight: true,
 | 
				
			||||||
        WorkItemIteration: true,
 | 
					        WorkItemIteration: true,
 | 
				
			||||||
        WorkItemHealthStatus: true,
 | 
					        WorkItemHealthStatus: true,
 | 
				
			||||||
        WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
 | 
					 | 
				
			||||||
          methods: {
 | 
					 | 
				
			||||||
            show: showModalHandler,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      mocks: {
 | 
					      mocks: {
 | 
				
			||||||
        $router: router,
 | 
					        $router: router,
 | 
				
			||||||
| 
						 | 
					@ -564,17 +556,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
    expect(successHandler).not.toHaveBeenCalled();
 | 
					    expect(successHandler).not.toHaveBeenCalled();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('shows work item modal if "show" query param set', async () => {
 | 
					 | 
				
			||||||
    const workItemId = workItemQueryResponse.data.workItem.id;
 | 
					 | 
				
			||||||
    setWindowLocation(`?show=${workItemId}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    createComponent();
 | 
					 | 
				
			||||||
    await waitForPromises();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    expect(findModal().exists()).toBe(true);
 | 
					 | 
				
			||||||
    expect(findModal().props('workItemId')).toBe(workItemId);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  it('skips calling the work item query when there is no workItemIid and no workItemId', async () => {
 | 
					  it('skips calling the work item query when there is no workItemIid and no workItemId', async () => {
 | 
				
			||||||
    createComponent({ workItemIid: null, workItemId: null });
 | 
					    createComponent({ workItemIid: null, workItemId: null });
 | 
				
			||||||
    await waitForPromises();
 | 
					    await waitForPromises();
 | 
				
			||||||
| 
						 | 
					@ -637,31 +618,22 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it('renders a modal', async () => {
 | 
					      it('opens the drawer with the child when `show-modal` is emitted', async () => {
 | 
				
			||||||
        createComponent({ handler: objectiveHandler });
 | 
					 | 
				
			||||||
        await waitForPromises();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expect(findModal().exists()).toBe(true);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      it('opens the modal with the child when `show-modal` is emitted', async () => {
 | 
					 | 
				
			||||||
        createComponent({ handler: objectiveHandler, workItemsAlphaEnabled: true });
 | 
					        createComponent({ handler: objectiveHandler, workItemsAlphaEnabled: true });
 | 
				
			||||||
        await waitForPromises();
 | 
					        await waitForPromises();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const event = {
 | 
					        const event = {
 | 
				
			||||||
          preventDefault: jest.fn(),
 | 
					          preventDefault: jest.fn(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        const modalWorkItem = { id: 'childWorkItemId' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        findHierarchyTree().vm.$emit('show-modal', {
 | 
					        findHierarchyTree().vm.$emit('show-modal', {
 | 
				
			||||||
          event,
 | 
					          event,
 | 
				
			||||||
          modalWorkItem: { id: 'childWorkItemId' },
 | 
					          modalWorkItem,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        await waitForPromises();
 | 
					        await waitForPromises();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        expect(wrapper.findComponent(WorkItemDetailModal).props().workItemId).toBe(
 | 
					        expect(findDrawer().props('activeItem')).toEqual(modalWorkItem);
 | 
				
			||||||
          'childWorkItemId',
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        expect(showModalHandler).toHaveBeenCalled();
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      describe('work item is rendered in a modal and has children', () => {
 | 
					      describe('work item is rendered in a modal and has children', () => {
 | 
				
			||||||
| 
						 | 
					@ -675,10 +647,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
          await waitForPromises();
 | 
					          await waitForPromises();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it('does not render a new modal', () => {
 | 
					 | 
				
			||||||
          expect(findModal().exists()).toBe(false);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        it('emits `update-modal` when `show-modal` is emitted', async () => {
 | 
					        it('emits `update-modal` when `show-modal` is emitted', async () => {
 | 
				
			||||||
          const event = {
 | 
					          const event = {
 | 
				
			||||||
            preventDefault: jest.fn(),
 | 
					            preventDefault: jest.fn(),
 | 
				
			||||||
| 
						 | 
					@ -746,15 +714,15 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
        const event = {
 | 
					        const event = {
 | 
				
			||||||
          preventDefault: jest.fn(),
 | 
					          preventDefault: jest.fn(),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        const modalWorkItem = { id: 'childWorkItemId' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        findWorkItemRelationships().vm.$emit('showModal', {
 | 
					        findWorkItemRelationships().vm.$emit('showModal', {
 | 
				
			||||||
          event,
 | 
					          event,
 | 
				
			||||||
          modalWorkItem: { id: 'childWorkItemId' },
 | 
					          modalWorkItem,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        await waitForPromises();
 | 
					        await waitForPromises();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        expect(findModal().props().workItemId).toBe('childWorkItemId');
 | 
					        expect(findDrawer().props('activeItem')).toEqual(modalWorkItem);
 | 
				
			||||||
        expect(showModalHandler).toHaveBeenCalled();
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      describe('linked work item is rendered in a modal and has linked items', () => {
 | 
					      describe('linked work item is rendered in a modal and has linked items', () => {
 | 
				
			||||||
| 
						 | 
					@ -768,10 +736,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
          await waitForPromises();
 | 
					          await waitForPromises();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it('does not render a new modal', () => {
 | 
					 | 
				
			||||||
          expect(findModal().exists()).toBe(false);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        it('emits `update-modal` when `show-modal` is emitted', async () => {
 | 
					        it('emits `update-modal` when `show-modal` is emitted', async () => {
 | 
				
			||||||
          const event = {
 | 
					          const event = {
 | 
				
			||||||
            preventDefault: jest.fn(),
 | 
					            preventDefault: jest.fn(),
 | 
				
			||||||
| 
						 | 
					@ -819,20 +783,6 @@ describe('WorkItemDetail component', () => {
 | 
				
			||||||
      expect(findWorkItemAbuseModal().exists()).toBe(false);
 | 
					      expect(findWorkItemAbuseModal().exists()).toBe(false);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
 | 
					 | 
				
			||||||
      findModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await nextTick();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(findWorkItemAbuseModal().exists()).toBe(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      findWorkItemAbuseModal().vm.$emit('close-modal');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      await nextTick();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(findWorkItemAbuseModal().exists()).toBe(false);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('should be visible when the work item actions button emits `toggleReportAbuseModal` event', async () => {
 | 
					    it('should be visible when the work item actions button emits `toggleReportAbuseModal` event', async () => {
 | 
				
			||||||
      findWorkItemActions().vm.$emit('toggleReportAbuseModal', true);
 | 
					      findWorkItemActions().vm.$emit('toggleReportAbuseModal', true);
 | 
				
			||||||
      await nextTick();
 | 
					      await nextTick();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,7 @@ describe('WorkItemLinkChild', () => {
 | 
				
			||||||
  describe('when clicking on expand button', () => {
 | 
					  describe('when clicking on expand button', () => {
 | 
				
			||||||
    it('fetches and displays children of item when clicking on expand button', async () => {
 | 
					    it('fetches and displays children of item when clicking on expand button', async () => {
 | 
				
			||||||
      createComponent();
 | 
					      createComponent();
 | 
				
			||||||
      await findExpandButton().vm.$emit('click');
 | 
					      await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(findExpandButton().props('loading')).toBe(true);
 | 
					      expect(findExpandButton().props('loading')).toBe(true);
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
| 
						 | 
					@ -117,7 +117,7 @@ describe('WorkItemLinkChild', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('does not render border on `WorkItemLinkChildContents` container', async () => {
 | 
					    it('does not render border on `WorkItemLinkChildContents` container', async () => {
 | 
				
			||||||
      createComponent();
 | 
					      createComponent();
 | 
				
			||||||
      await findExpandButton().vm.$emit('click');
 | 
					      await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(findWorkItemLinkChildContentsContainer().classes()).not.toContain('!gl-border-b-1');
 | 
					      expect(findWorkItemLinkChildContentsContainer().classes()).not.toContain('!gl-border-b-1');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					@ -134,8 +134,8 @@ describe('WorkItemLinkChild', () => {
 | 
				
			||||||
      const childrenNodes = getChildrenNodes();
 | 
					      const childrenNodes = getChildrenNodes();
 | 
				
			||||||
      expect(findTreeChildren().props('children')).toEqual(childrenNodes);
 | 
					      expect(findTreeChildren().props('children')).toEqual(childrenNodes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await findExpandButton().vm.$emit('click'); // Collapse
 | 
					      await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() }); // Collapse
 | 
				
			||||||
      findExpandButton().vm.$emit('click'); // Expand again
 | 
					      await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() }); // Expand again
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once.
 | 
					      expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once.
 | 
				
			||||||
| 
						 | 
					@ -190,7 +190,7 @@ describe('WorkItemLinkChild', () => {
 | 
				
			||||||
        workItemTreeQueryHandler: getWorkItemTreeQueryFailureHandler,
 | 
					        workItemTreeQueryHandler: getWorkItemTreeQueryFailureHandler,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      findExpandButton().vm.$emit('click');
 | 
					      await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(createAlert).toHaveBeenCalledWith({
 | 
					      expect(createAlert).toHaveBeenCalledWith({
 | 
				
			||||||
| 
						 | 
					@ -258,7 +258,7 @@ describe('WorkItemLinkChild', () => {
 | 
				
			||||||
        workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
 | 
					        workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
 | 
				
			||||||
        isExpanded: true,
 | 
					        isExpanded: true,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      await findExpandButton().vm.$emit('click');
 | 
					      await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await waitForPromises();
 | 
					      await waitForPromises();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_item
 | 
				
			||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | 
					import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 | 
				
			||||||
import createMockApollo from 'helpers/mock_apollo_helper';
 | 
					import createMockApollo from 'helpers/mock_apollo_helper';
 | 
				
			||||||
import waitForPromises from 'helpers/wait_for_promises';
 | 
					import waitForPromises from 'helpers/wait_for_promises';
 | 
				
			||||||
 | 
					import setWindowLocation from 'helpers/set_window_location_helper';
 | 
				
			||||||
import { createAlert } from '~/alert';
 | 
					import { createAlert } from '~/alert';
 | 
				
			||||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
 | 
					import CrudComponent from '~/vue_shared/components/crud_component.vue';
 | 
				
			||||||
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
 | 
					import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
 | 
				
			||||||
| 
						 | 
					@ -436,4 +437,24 @@ describe('WorkItemTree', () => {
 | 
				
			||||||
      'No child items are currently open.',
 | 
					      'No child items are currently open.',
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe('when there is show URL parameter', () => {
 | 
				
			||||||
 | 
					    it('emits `show-modal` event when child work item id is encoded in the URL', async () => {
 | 
				
			||||||
 | 
					      const encodedWorkItemId = btoa(JSON.stringify({ id: 31 }));
 | 
				
			||||||
 | 
					      setWindowLocation(`?show=${encodedWorkItemId}`);
 | 
				
			||||||
 | 
					      await createComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(wrapper.emitted('show-modal')).toEqual([
 | 
				
			||||||
 | 
					        [{ modalWorkItem: expect.objectContaining({ id: 'gid://gitlab/WorkItem/31' }) }],
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('does not emit `show-modal` event when child work item id is not encoded in the URL', async () => {
 | 
				
			||||||
 | 
					      const encodedWorkItemId = btoa(JSON.stringify({ id: 1 }));
 | 
				
			||||||
 | 
					      setWindowLocation(`?show=${encodedWorkItemId}`);
 | 
				
			||||||
 | 
					      await createComponent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(wrapper.emitted('show-modal')).toBeUndefined();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,8 +61,8 @@ describe('WorkItemTimeTracking component', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('has a modal directive', () => {
 | 
					    it('has a modal directive', () => {
 | 
				
			||||||
      expect(getBinding(findAddTimeEntryButton().element, 'gl-modal').value).toBe(
 | 
					      expect(getBinding(findAddTimeEntryButton().element, 'gl-modal').value).toEqual(
 | 
				
			||||||
        'create-timelog-modal',
 | 
					        expect.stringContaining('create-timelog-modal'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -81,15 +81,15 @@ describe('WorkItemTimeTracking component', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('allows user to add an estimate by clicking "estimate"', () => {
 | 
					    it('allows user to add an estimate by clicking "estimate"', () => {
 | 
				
			||||||
      expect(findEstimateButton().props('variant')).toBe('link');
 | 
					      expect(findEstimateButton().props('variant')).toBe('link');
 | 
				
			||||||
      expect(getBinding(findEstimateButton().element, 'gl-modal').value).toBe(
 | 
					      expect(getBinding(findEstimateButton().element, 'gl-modal').value).toEqual(
 | 
				
			||||||
        'set-time-estimate-modal',
 | 
					        expect.stringContaining('set-time-estimate-modal'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('allows user to add a time entry by clicking "time spent"', () => {
 | 
					    it('allows user to add a time entry by clicking "time spent"', () => {
 | 
				
			||||||
      expect(findAddTimeSpentButton().props('variant')).toBe('link');
 | 
					      expect(findAddTimeSpentButton().props('variant')).toBe('link');
 | 
				
			||||||
      expect(getBinding(findAddTimeSpentButton().element, 'gl-modal').value).toBe(
 | 
					      expect(getBinding(findAddTimeSpentButton().element, 'gl-modal').value).toEqual(
 | 
				
			||||||
        'create-timelog-modal',
 | 
					        expect.stringContaining('create-timelog-modal'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -106,8 +106,8 @@ describe('WorkItemTimeTracking component', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('time spent links to time tracking report', () => {
 | 
					    it('time spent links to time tracking report', () => {
 | 
				
			||||||
      expect(findViewTimeSpentButton().props('variant')).toBe('link');
 | 
					      expect(findViewTimeSpentButton().props('variant')).toBe('link');
 | 
				
			||||||
      expect(getBinding(findViewTimeSpentButton().element, 'gl-modal').value).toBe(
 | 
					      expect(getBinding(findViewTimeSpentButton().element, 'gl-modal').value).toEqual(
 | 
				
			||||||
        'time-tracking-report',
 | 
					        expect.stringContaining('time-tracking-modal'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      expect(getBinding(findViewTimeSpentButton().element, 'gl-tooltip').value).toBe(
 | 
					      expect(getBinding(findViewTimeSpentButton().element, 'gl-tooltip').value).toBe(
 | 
				
			||||||
        'View time tracking report',
 | 
					        'View time tracking report',
 | 
				
			||||||
| 
						 | 
					@ -116,8 +116,8 @@ describe('WorkItemTimeTracking component', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('shows "Add estimate" button to add estimate', () => {
 | 
					    it('shows "Add estimate" button to add estimate', () => {
 | 
				
			||||||
      expect(findAddEstimateButton().props('variant')).toBe('link');
 | 
					      expect(findAddEstimateButton().props('variant')).toBe('link');
 | 
				
			||||||
      expect(getBinding(findAddEstimateButton().element, 'gl-modal').value).toBe(
 | 
					      expect(getBinding(findAddEstimateButton().element, 'gl-modal').value).toEqual(
 | 
				
			||||||
        'set-time-estimate-modal',
 | 
					        expect.stringContaining('set-time-estimate-modal'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -141,8 +141,8 @@ describe('WorkItemTimeTracking component', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('estimate links to "Add estimate" modal', () => {
 | 
					    it('estimate links to "Add estimate" modal', () => {
 | 
				
			||||||
      expect(findSetEstimateButton().props('variant')).toBe('link');
 | 
					      expect(findSetEstimateButton().props('variant')).toBe('link');
 | 
				
			||||||
      expect(getBinding(findSetEstimateButton().element, 'gl-modal').value).toBe(
 | 
					      expect(getBinding(findSetEstimateButton().element, 'gl-modal').value).toEqual(
 | 
				
			||||||
        'set-time-estimate-modal',
 | 
					        expect.stringContaining('set-time-estimate-modal'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      expect(getBinding(findSetEstimateButton().element, 'gl-tooltip').value).toBe('Set estimate');
 | 
					      expect(getBinding(findSetEstimateButton().element, 'gl-tooltip').value).toBe('Set estimate');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,37 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  shared_examples 'index app data' do
 | 
				
			||||||
 | 
					    it 'returns expected data object' do
 | 
				
			||||||
 | 
					      expect(data).to eq(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          'new_organization_url' => new_organization_path,
 | 
				
			||||||
 | 
					          'can_create_organization' => true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when can_create_organization admin setting is disabled' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        stub_application_setting(can_create_organization: false)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns false for can_create_organization' do
 | 
				
			||||||
 | 
					        expect(data['can_create_organization']).to be(false)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when allow_organization_creation feature flag is disabled' do
 | 
				
			||||||
 | 
					      before do
 | 
				
			||||||
 | 
					        stub_feature_flags(allow_organization_creation: false)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns false for can_create_organization' do
 | 
				
			||||||
 | 
					        expect(data['can_create_organization']).to be(false)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#organization_layout_nav' do
 | 
					  describe '#organization_layout_nav' do
 | 
				
			||||||
    context 'when current controller is not organizations' do
 | 
					    context 'when current controller is not organizations' do
 | 
				
			||||||
      it 'returns organization' do
 | 
					      it 'returns organization' do
 | 
				
			||||||
| 
						 | 
					@ -183,13 +214,9 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#organization_index_app_data' do
 | 
					  describe '#organization_index_app_data' do
 | 
				
			||||||
    it 'returns expected data object' do
 | 
					    subject(:data) { Gitlab::Json.parse(helper.organization_index_app_data) }
 | 
				
			||||||
      expect(helper.organization_index_app_data).to eq(
 | 
					
 | 
				
			||||||
        {
 | 
					    it_behaves_like 'index app data'
 | 
				
			||||||
          new_organization_url: new_organization_path
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#organization_new_app_data' do
 | 
					  describe '#organization_new_app_data' do
 | 
				
			||||||
| 
						 | 
					@ -307,13 +334,9 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#admin_organizations_index_app_data' do
 | 
					  describe '#admin_organizations_index_app_data' do
 | 
				
			||||||
    it 'returns expected json' do
 | 
					    subject(:data) { Gitlab::Json.parse(helper.admin_organizations_index_app_data) }
 | 
				
			||||||
      expect(Gitlab::Json.parse(helper.admin_organizations_index_app_data)).to eq(
 | 
					
 | 
				
			||||||
        {
 | 
					    it_behaves_like 'index app data'
 | 
				
			||||||
          'new_organization_url' => new_organization_path
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#organization_projects_edit_app_data' do
 | 
					  describe '#organization_projects_edit_app_data' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,8 @@ RSpec.describe SecretsInitializer do
 | 
				
			||||||
  describe 'ensure acknowledged secrets in any installations' do
 | 
					  describe 'ensure acknowledged secrets in any installations' do
 | 
				
			||||||
    let(:acknowledged_secrets) do
 | 
					    let(:acknowledged_secrets) do
 | 
				
			||||||
      %w[secret_key_base otp_key_base db_key_base openid_connect_signing_key encrypted_settings_key_base
 | 
					      %w[secret_key_base otp_key_base db_key_base openid_connect_signing_key encrypted_settings_key_base
 | 
				
			||||||
        rotated_encrypted_settings_key_base]
 | 
					        rotated_encrypted_settings_key_base active_record_encryption_primary_key
 | 
				
			||||||
 | 
					        active_record_encryption_deterministic_key active_record_encryption_key_derivation_salt]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'does not allow to add a new secret without a proper handling' do
 | 
					    it 'does not allow to add a new secret without a proper handling' do
 | 
				
			||||||
| 
						 | 
					@ -84,11 +85,15 @@ RSpec.describe SecretsInitializer do
 | 
				
			||||||
        db_key_base
 | 
					        db_key_base
 | 
				
			||||||
        otp_key_base
 | 
					        otp_key_base
 | 
				
			||||||
        openid_connect_signing_key
 | 
					        openid_connect_signing_key
 | 
				
			||||||
 | 
					        active_record_encryption_primary_key
 | 
				
			||||||
 | 
					        active_record_encryption_deterministic_key
 | 
				
			||||||
 | 
					        active_record_encryption_key_derivation_salt
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let(:hex_key) { /\h{128}/ }
 | 
					    let(:hex_key) { /\A\h{128}\z/ }
 | 
				
			||||||
    let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m }
 | 
					    let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\z/m }
 | 
				
			||||||
 | 
					    let(:alphanumeric_key) { /\A[A-Za-z0-9]{32}\z/m }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    around do |example|
 | 
					    around do |example|
 | 
				
			||||||
      # We store Rails.application.credentials as a hash so that we can revert to the original
 | 
					      # We store Rails.application.credentials as a hash so that we can revert to the original
 | 
				
			||||||
| 
						 | 
					@ -134,9 +139,17 @@ RSpec.describe SecretsInitializer do
 | 
				
			||||||
        expect(keys).to all(match(rsa_key))
 | 
					        expect(keys).to all(match(rsa_key))
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'generates alphanumeric keys for active_record_encryption items' do
 | 
				
			||||||
 | 
					        initializer.execute!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(Rails.application.credentials.active_record_encryption_primary_key).to all(match(alphanumeric_key))
 | 
				
			||||||
 | 
					        expect(Rails.application.credentials.active_record_encryption_deterministic_key).to all(match(alphanumeric_key))
 | 
				
			||||||
 | 
					        expect(Rails.application.credentials.active_record_encryption_key_derivation_salt).to match(alphanumeric_key)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it 'warns about the secrets to add to secrets.yml' do
 | 
					      it 'warns about the secrets to add to secrets.yml' do
 | 
				
			||||||
        allowed_keys.each do |key|
 | 
					        allowed_keys.each do |key|
 | 
				
			||||||
          expect(initializer).to receive(:warn_missing_secret).with(key)
 | 
					          expect(initializer).to receive(:warn_missing_secret).with(key.to_sym)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        initializer.execute!
 | 
					        initializer.execute!
 | 
				
			||||||
| 
						 | 
					@ -166,7 +179,7 @@ RSpec.describe SecretsInitializer do
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it 'writes the encrypted_settings_key_base secret' do
 | 
					        it 'writes the encrypted_settings_key_base secret' do
 | 
				
			||||||
          expect(initializer).to receive(:warn_missing_secret).with('encrypted_settings_key_base')
 | 
					          expect(initializer).to receive(:warn_missing_secret).with(:encrypted_settings_key_base)
 | 
				
			||||||
          expect(File).to receive(:write).with(fake_secret_file.path, any_args) do |_filename, contents, _options|
 | 
					          expect(File).to receive(:write).with(fake_secret_file.path, any_args) do |_filename, contents, _options|
 | 
				
			||||||
            new_secrets = YAML.safe_load(contents)[rails_env_name]
 | 
					            new_secrets = YAML.safe_load(contents)[rails_env_name]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -240,7 +253,16 @@ RSpec.describe SecretsInitializer do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context 'with some secrets missing, some in ENV, some in Rails.application.credentials, some in secrets.yml' do
 | 
					    context 'with some secrets missing, some in ENV, some in Rails.application.credentials, some in secrets.yml' do
 | 
				
			||||||
      let(:rails_env_name) { 'foo' }
 | 
					      let(:rails_env_name) { 'foo' }
 | 
				
			||||||
      let(:secrets_hash) { { rails_env_name => { 'otp_key_base' => 'otp_key_base' } } }
 | 
					      let(:secrets_hash) do
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          rails_env_name => {
 | 
				
			||||||
 | 
					            'otp_key_base' => 'otp_key_base',
 | 
				
			||||||
 | 
					            'active_record_encryption_primary_key' => ['primary_key'],
 | 
				
			||||||
 | 
					            'active_record_encryption_deterministic_key' => ['deterministic_key'],
 | 
				
			||||||
 | 
					            'active_record_encryption_key_derivation_salt' => 'key_derivation_salt'
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      before do
 | 
					      before do
 | 
				
			||||||
        stub_env('SECRET_KEY_BASE', 'env_key')
 | 
					        stub_env('SECRET_KEY_BASE', 'env_key')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -150,12 +150,14 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger, feature_category: :continuous_int
 | 
				
			||||||
    subject(:commit) { logger.commit(pipeline: pipeline, caller: 'source') }
 | 
					    subject(:commit) { logger.commit(pipeline: pipeline, caller: 'source') }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    before do
 | 
					    before do
 | 
				
			||||||
      stub_feature_flags(ci_pipeline_creation_logger: flag)
 | 
					      freeze_time do
 | 
				
			||||||
      allow(logger).to receive(:current_monotonic_time) { Time.current.to_i }
 | 
					        stub_feature_flags(ci_pipeline_creation_logger: flag)
 | 
				
			||||||
 | 
					        allow(logger).to receive(:current_monotonic_time) { Time.current.to_i }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      logger.instrument(:pipeline_save) { travel(60.seconds) }
 | 
					        logger.instrument(:pipeline_save) { travel(60.seconds) }
 | 
				
			||||||
      logger.observe(:pipeline_creation_duration_s, 30)
 | 
					        logger.observe(:pipeline_creation_duration_s, 30)
 | 
				
			||||||
      logger.observe(:pipeline_creation_duration_s, 10)
 | 
					        logger.observe(:pipeline_creation_duration_s, 10)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    context 'when the feature flag is enabled' do
 | 
					    context 'when the feature flag is enabled' do
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require 'spec_helper'
 | 
				
			||||||
 | 
					require_migration!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RSpec.describe QueueBackfillFreeSharedRunnersMinutesLimit, feature_category: :consumables_cost_management do
 | 
				
			||||||
 | 
					  let!(:batched_migration) { described_class::MIGRATION }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it 'does not schedule the background job when Gitlab.com_except_jh? is false' do
 | 
				
			||||||
 | 
					    allow(Gitlab).to receive_messages(dev_or_test_env?: false, com_except_jh?: false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reversible_migration do |migration|
 | 
				
			||||||
 | 
					      migration.before -> {
 | 
				
			||||||
 | 
					        expect(batched_migration).not_to have_scheduled_batched_migration
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      migration.after -> {
 | 
				
			||||||
 | 
					        expect(batched_migration).not_to have_scheduled_batched_migration
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it 'schedules a new batched migration when Gitlab.com_except_jh? is true' do
 | 
				
			||||||
 | 
					    allow(Gitlab).to receive_messages(dev_or_test_env?: true, com_except_jh?: true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reversible_migration do |migration|
 | 
				
			||||||
 | 
					      migration.before -> {
 | 
				
			||||||
 | 
					        expect(batched_migration).not_to have_scheduled_batched_migration
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      migration.after -> {
 | 
				
			||||||
 | 
					        expect(batched_migration).to have_scheduled_batched_migration(
 | 
				
			||||||
 | 
					          table_name: :namespaces,
 | 
				
			||||||
 | 
					          column_name: :id,
 | 
				
			||||||
 | 
					          interval: described_class::DELAY_INTERVAL,
 | 
				
			||||||
 | 
					          batch_size: described_class::BATCH_SIZE,
 | 
				
			||||||
 | 
					          sub_batch_size: described_class::SUB_BATCH_SIZE,
 | 
				
			||||||
 | 
					          gitlab_schema: :gitlab_main
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -69,9 +69,7 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Registries, :aggregate_f
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      with_them do
 | 
					      with_them do
 | 
				
			||||||
        let(:headers) do
 | 
					        let(:headers) { token_header(token) }
 | 
				
			||||||
          token_header(token)
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it_behaves_like 'returning response status', params[:status]
 | 
					        it_behaves_like 'returning response status', params[:status]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require 'spec_helper'
 | 
					require 'spec_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_failures, feature_category: :virtual_registry do
 | 
					RSpec.describe API::VirtualRegistries::Packages::Maven::Upstreams, :aggregate_failures, feature_category: :virtual_registry do
 | 
				
			||||||
  using RSpec::Parameterized::TableSyntax
 | 
					  using RSpec::Parameterized::TableSyntax
 | 
				
			||||||
  include_context 'for maven virtual registry api setup'
 | 
					  include_context 'for maven virtual registry api setup'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,22 +64,12 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
    context 'for authentication' do
 | 
					    context 'for authentication' do
 | 
				
			||||||
      where(:token, :sent_as, :status) do
 | 
					      where(:token, :sent_as, :status) do
 | 
				
			||||||
        :personal_access_token | :header     | :ok
 | 
					        :personal_access_token | :header     | :ok
 | 
				
			||||||
        :personal_access_token | :basic_auth | :ok
 | 
					 | 
				
			||||||
        :deploy_token          | :header     | :ok
 | 
					        :deploy_token          | :header     | :ok
 | 
				
			||||||
        :deploy_token          | :basic_auth | :ok
 | 
					 | 
				
			||||||
        :job_token             | :header     | :ok
 | 
					        :job_token             | :header     | :ok
 | 
				
			||||||
        :job_token             | :basic_auth | :ok
 | 
					 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      with_them do
 | 
					      with_them do
 | 
				
			||||||
        let(:headers) do
 | 
					        let(:headers) { token_header(token) }
 | 
				
			||||||
          case sent_as
 | 
					 | 
				
			||||||
          when :header
 | 
					 | 
				
			||||||
            token_header(token)
 | 
					 | 
				
			||||||
          when :basic_auth
 | 
					 | 
				
			||||||
            token_basic_auth(token)
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it_behaves_like 'returning response status', params[:status]
 | 
					        it_behaves_like 'returning response status', params[:status]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					@ -94,12 +84,16 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
    subject(:api_request) { post api(url), headers: headers, params: params }
 | 
					    subject(:api_request) { post api(url), headers: headers, params: params }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    shared_examples 'successful response' do
 | 
					    shared_examples 'successful response' do
 | 
				
			||||||
 | 
					      let(:upstream_model) { ::VirtualRegistries::Packages::Maven::Upstream }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it 'returns a successful response' do
 | 
					      it 'returns a successful response' do
 | 
				
			||||||
        expect { api_request }.to change { ::VirtualRegistries::Packages::Maven::Upstream.count }.by(1)
 | 
					        expect { api_request }.to change { upstream_model.count }.by(1)
 | 
				
			||||||
          .and change { ::VirtualRegistries::Packages::Maven::RegistryUpstream.count }.by(1)
 | 
					          .and change { ::VirtualRegistries::Packages::Maven::RegistryUpstream.count }.by(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        expect(::VirtualRegistries::Packages::Maven::Upstream.last.cache_validity_hours).to eq(
 | 
					        expect(response).to have_gitlab_http_status(:created)
 | 
				
			||||||
          params[:cache_validity_hours] || ::VirtualRegistries::Packages::Maven::Upstream.new.cache_validity_hours
 | 
					        expect(Gitlab::Json.parse(response.body)).to eq(upstream_model.last.as_json)
 | 
				
			||||||
 | 
					        expect(upstream_model.last.cache_validity_hours).to eq(
 | 
				
			||||||
 | 
					          params[:cache_validity_hours] || upstream_model.new.cache_validity_hours
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					@ -191,22 +185,12 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      where(:token, :sent_as, :status) do
 | 
					      where(:token, :sent_as, :status) do
 | 
				
			||||||
        :personal_access_token | :header     | :created
 | 
					        :personal_access_token | :header     | :created
 | 
				
			||||||
        :personal_access_token | :basic_auth | :created
 | 
					 | 
				
			||||||
        :deploy_token          | :header     | :forbidden
 | 
					        :deploy_token          | :header     | :forbidden
 | 
				
			||||||
        :deploy_token          | :basic_auth | :forbidden
 | 
					 | 
				
			||||||
        :job_token             | :header     | :created
 | 
					        :job_token             | :header     | :created
 | 
				
			||||||
        :job_token             | :basic_auth | :created
 | 
					 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      with_them do
 | 
					      with_them do
 | 
				
			||||||
        let(:headers) do
 | 
					        let(:headers) { token_header(token) }
 | 
				
			||||||
          case sent_as
 | 
					 | 
				
			||||||
          when :header
 | 
					 | 
				
			||||||
            token_header(token)
 | 
					 | 
				
			||||||
          when :basic_auth
 | 
					 | 
				
			||||||
            token_basic_auth(token)
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if params[:status] == :created
 | 
					        if params[:status] == :created
 | 
				
			||||||
          it_behaves_like 'successful response'
 | 
					          it_behaves_like 'successful response'
 | 
				
			||||||
| 
						 | 
					@ -217,8 +201,8 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'GET /api/v4/virtual_registries/packages/maven/registries/:id/upstreams/:upstream_id' do
 | 
					  describe 'GET /api/v4/virtual_registries/packages/maven/upstreams/:id' do
 | 
				
			||||||
    let(:url) { "/virtual_registries/packages/maven/registries/#{registry.id}/upstreams/#{upstream.id}" }
 | 
					    let(:url) { "/virtual_registries/packages/maven/upstreams/#{upstream.id}" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subject(:api_request) { get api(url), headers: headers }
 | 
					    subject(:api_request) { get api(url), headers: headers }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -262,30 +246,20 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
    context 'for authentication' do
 | 
					    context 'for authentication' do
 | 
				
			||||||
      where(:token, :sent_as, :status) do
 | 
					      where(:token, :sent_as, :status) do
 | 
				
			||||||
        :personal_access_token | :header     | :ok
 | 
					        :personal_access_token | :header     | :ok
 | 
				
			||||||
        :personal_access_token | :basic_auth | :ok
 | 
					 | 
				
			||||||
        :deploy_token          | :header     | :ok
 | 
					        :deploy_token          | :header     | :ok
 | 
				
			||||||
        :deploy_token          | :basic_auth | :ok
 | 
					 | 
				
			||||||
        :job_token             | :header     | :ok
 | 
					        :job_token             | :header     | :ok
 | 
				
			||||||
        :job_token             | :basic_auth | :ok
 | 
					 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      with_them do
 | 
					      with_them do
 | 
				
			||||||
        let(:headers) do
 | 
					        let(:headers) { token_header(token) }
 | 
				
			||||||
          case sent_as
 | 
					 | 
				
			||||||
          when :header
 | 
					 | 
				
			||||||
            token_header(token)
 | 
					 | 
				
			||||||
          when :basic_auth
 | 
					 | 
				
			||||||
            token_basic_auth(token)
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it_behaves_like 'returning response status', params[:status]
 | 
					        it_behaves_like 'returning response status', params[:status]
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'PATCH /api/v4/virtual_registries/packages/maven/registries/:id/upstreams/:upstream_id' do
 | 
					  describe 'PATCH /api/v4/virtual_registries/packages/maven/upstreams/:id' do
 | 
				
			||||||
    let(:url) { "/virtual_registries/packages/maven/registries/#{registry.id}/upstreams/#{upstream.id}" }
 | 
					    let(:url) { "/virtual_registries/packages/maven/upstreams/#{upstream.id}" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subject(:api_request) { patch api(url), params: params, headers: headers }
 | 
					    subject(:api_request) { patch api(url), params: params, headers: headers }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -321,22 +295,12 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        where(:token, :sent_as, :status) do
 | 
					        where(:token, :sent_as, :status) do
 | 
				
			||||||
          :personal_access_token | :header     | :ok
 | 
					          :personal_access_token | :header     | :ok
 | 
				
			||||||
          :personal_access_token | :basic_auth | :ok
 | 
					 | 
				
			||||||
          :deploy_token          | :header     | :forbidden
 | 
					          :deploy_token          | :header     | :forbidden
 | 
				
			||||||
          :deploy_token          | :basic_auth | :forbidden
 | 
					 | 
				
			||||||
          :job_token             | :header     | :ok
 | 
					          :job_token             | :header     | :ok
 | 
				
			||||||
          :job_token             | :basic_auth | :ok
 | 
					 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with_them do
 | 
					        with_them do
 | 
				
			||||||
          let(:headers) do
 | 
					          let(:headers) { token_header(token) }
 | 
				
			||||||
            case sent_as
 | 
					 | 
				
			||||||
            when :header
 | 
					 | 
				
			||||||
              token_header(token)
 | 
					 | 
				
			||||||
            when :basic_auth
 | 
					 | 
				
			||||||
              token_basic_auth(token)
 | 
					 | 
				
			||||||
            end
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          it_behaves_like 'returning response status', params[:status]
 | 
					          it_behaves_like 'returning response status', params[:status]
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
| 
						 | 
					@ -372,8 +336,8 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'DELETE /api/v4/virtual_registries/packages/maven/registries/:id/upstreams/:upstream_id' do
 | 
					  describe 'DELETE /api/v4/virtual_registries/packages/maven/upstreams/:id' do
 | 
				
			||||||
    let(:url) { "/virtual_registries/packages/maven/registries/#{registry.id}/upstreams/#{upstream.id}" }
 | 
					    let(:url) { "/virtual_registries/packages/maven/upstreams/#{upstream.id}" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    subject(:api_request) { delete api(url), headers: headers }
 | 
					    subject(:api_request) { delete api(url), headers: headers }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -419,22 +383,12 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      where(:token, :sent_as, :status) do
 | 
					      where(:token, :sent_as, :status) do
 | 
				
			||||||
        :personal_access_token | :header     | :no_content
 | 
					        :personal_access_token | :header     | :no_content
 | 
				
			||||||
        :personal_access_token | :basic_auth | :no_content
 | 
					 | 
				
			||||||
        :deploy_token          | :header     | :forbidden
 | 
					        :deploy_token          | :header     | :forbidden
 | 
				
			||||||
        :deploy_token          | :basic_auth | :forbidden
 | 
					 | 
				
			||||||
        :job_token             | :header     | :no_content
 | 
					        :job_token             | :header     | :no_content
 | 
				
			||||||
        :job_token             | :basic_auth | :no_content
 | 
					 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      with_them do
 | 
					      with_them do
 | 
				
			||||||
        let(:headers) do
 | 
					        let(:headers) { token_header(token) }
 | 
				
			||||||
          case sent_as
 | 
					 | 
				
			||||||
          when :header
 | 
					 | 
				
			||||||
            token_header(token)
 | 
					 | 
				
			||||||
          when :basic_auth
 | 
					 | 
				
			||||||
            token_basic_auth(token)
 | 
					 | 
				
			||||||
          end
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if params[:status] == :no_content
 | 
					        if params[:status] == :no_content
 | 
				
			||||||
          it_behaves_like 'successful response'
 | 
					          it_behaves_like 'successful response'
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ RSpec.shared_examples 'work items rolled up dates' do
 | 
				
			||||||
          wait_for_all_requests
 | 
					          wait_for_all_requests
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        within_testid('work-item-detail-modal') do
 | 
					        within_testid('work-item-drawer') do
 | 
				
			||||||
          find_and_click_edit work_item_rolledup_dates_selector
 | 
					          find_and_click_edit work_item_rolledup_dates_selector
 | 
				
			||||||
          # set empty value before the value to ensure
 | 
					          # set empty value before the value to ensure
 | 
				
			||||||
          # the current value don't mess with the new value input
 | 
					          # the current value don't mess with the new value input
 | 
				
			||||||
| 
						 | 
					@ -64,10 +64,10 @@ RSpec.shared_examples 'work items rolled up dates' do
 | 
				
			||||||
          fill_in 'Start', with: start_date
 | 
					          fill_in 'Start', with: start_date
 | 
				
			||||||
          fill_in 'Due', with: "" # ensure to reset the input first to avoid wrong date values
 | 
					          fill_in 'Due', with: "" # ensure to reset the input first to avoid wrong date values
 | 
				
			||||||
          fill_in 'Due', with: due_date
 | 
					          fill_in 'Due', with: due_date
 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        find_by_testid('work-item-close').click
 | 
					          find_by_testid('close-icon').click
 | 
				
			||||||
        wait_for_all_requests
 | 
					          wait_for_all_requests
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        page.refresh
 | 
					        page.refresh
 | 
				
			||||||
        wait_for_all_requests
 | 
					        wait_for_all_requests
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue