Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-12-09 09:22:09 +00:00
parent c051381589
commit a0bb115d01
52 changed files with 743 additions and 468 deletions

View File

@ -1 +1 @@
52935d26b797c63c6aa31047cd1319cfddb5bb1c 7999217addae25ef054b769e74265cbd2ad28bad

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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);

View File

@ -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,

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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() {
return !isEmpty(this.activeChildItem);
},
activeChildItemType() {
return this.activeChildItem?.workItemType?.name;
}, },
mounted() {
if (this.modalWorkItemId) {
this.openInModal({
event: undefined,
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"

View File

@ -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))"

View File

@ -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>

View File

@ -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

View File

@ -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.'),

View File

@ -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"

View File

@ -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;

View File

@ -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

View File

@ -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 } }

View File

@ -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

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
85b7e84f225aaa1fdd76115c88df56cc8cfb297f50ed0c8434906d2d76f66c68

View File

@ -0,0 +1 @@
97cb14f7d08a5301d274f2f1a54dd6b40c655ac9d2936bacd46187e687efbdb5

View File

@ -0,0 +1 @@
a8c9f960933989678f4a35b5ef48cea7c172d748e971fbf96e4d03d7038c0428

View File

@ -0,0 +1 @@
ad2953fc7be16a7bc66aa7513ec452c9e0d6bda1bf3700507c78af63feaed1f1

View File

@ -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

View File

@ -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.
![Example of job running with terminal available](img/interactive_web_terminal_running_job_v17_3.png)
When selected, a new tab opens to the terminal page where you can access
the terminal and type commands like in a standard shell.
![terminal of the job](img/interactive_web_terminal_page_v11_1.png)
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.
![finished job with terminal open](img/finished_job_with_terminal_open_v11_2.png)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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();
}); });

View File

@ -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();
}); });

View File

@ -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', () => {

View File

@ -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]]);
}); });

View File

@ -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();

View File

@ -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();

View File

@ -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();
});
});
}); });

View File

@ -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');
}); });

View File

@ -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

View File

@ -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')

View File

@ -150,6 +150,7 @@ 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
freeze_time do
stub_feature_flags(ci_pipeline_creation_logger: flag) stub_feature_flags(ci_pipeline_creation_logger: flag)
allow(logger).to receive(:current_monotonic_time) { Time.current.to_i } allow(logger).to receive(:current_monotonic_time) { Time.current.to_i }
@ -157,6 +158,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger, feature_category: :continuous_int
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
let(:flag) { true } let(:flag) { true }

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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