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