Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c051381589
commit
a0bb115d01
|
|
@ -1 +1 @@
|
|||
52935d26b797c63c6aa31047cd1319cfddb5bb1c
|
||||
7999217addae25ef054b769e74265cbd2ad28bad
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default {
|
|||
),
|
||||
},
|
||||
components: { GlButton, OrganizationsView },
|
||||
inject: ['newOrganizationUrl'],
|
||||
inject: ['newOrganizationUrl', 'canCreateOrganization'],
|
||||
data() {
|
||||
return {
|
||||
organizations: {},
|
||||
|
|
@ -46,9 +46,6 @@ export default {
|
|||
showHeader() {
|
||||
return this.loading || this.organizations.nodes?.length;
|
||||
},
|
||||
showNewOrganizationButton() {
|
||||
return gon.features?.allowOrganizationCreation;
|
||||
},
|
||||
loading() {
|
||||
return this.$apollo.queries.organizations.loading;
|
||||
},
|
||||
|
|
@ -78,7 +75,7 @@ export default {
|
|||
<div class="gl-py-6">
|
||||
<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>
|
||||
<gl-button v-if="showNewOrganizationButton" :href="newOrganizationUrl" variant="confirm">{{
|
||||
<gl-button v-if="canCreateOrganization" :href="newOrganizationUrl" variant="confirm">{{
|
||||
$options.i18n.newOrganization
|
||||
}}</gl-button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ export const initAdminOrganizationsIndex = () => {
|
|||
const {
|
||||
dataset: { appData },
|
||||
} = el;
|
||||
const { newOrganizationUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
|
||||
const { newOrganizationUrl, canCreateOrganization } = convertObjectPropsToCamelCase(
|
||||
JSON.parse(appData),
|
||||
);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
|
|
@ -24,6 +26,7 @@ export const initAdminOrganizationsIndex = () => {
|
|||
apolloProvider,
|
||||
provide: {
|
||||
newOrganizationUrl,
|
||||
canCreateOrganization,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default {
|
|||
GlButton,
|
||||
OrganizationsView,
|
||||
},
|
||||
inject: ['newOrganizationUrl'],
|
||||
inject: ['newOrganizationUrl', 'canCreateOrganization'],
|
||||
data() {
|
||||
return {
|
||||
organizations: {},
|
||||
|
|
@ -49,9 +49,6 @@ export default {
|
|||
showHeader() {
|
||||
return this.loading || this.organizations.nodes?.length;
|
||||
},
|
||||
showNewOrganizationButton() {
|
||||
return gon.features?.allowOrganizationCreation;
|
||||
},
|
||||
loading() {
|
||||
return this.$apollo.queries.organizations.loading;
|
||||
},
|
||||
|
|
@ -82,7 +79,7 @@ export default {
|
|||
<div v-if="showHeader" class="gl-flex gl-items-center">
|
||||
<h1 class="gl-my-4 gl-text-size-h-display">{{ $options.i18n.organizations }}</h1>
|
||||
<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
|
||||
}}</gl-button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ export const initOrganizationsIndex = () => {
|
|||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
const { newOrganizationUrl } = convertObjectPropsToCamelCase(el.dataset);
|
||||
const {
|
||||
dataset: { appData },
|
||||
} = el;
|
||||
const { newOrganizationUrl, canCreateOrganization } = convertObjectPropsToCamelCase(
|
||||
JSON.parse(appData),
|
||||
);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
|
|
@ -21,6 +26,7 @@ export const initOrganizationsIndex = () => {
|
|||
apolloProvider,
|
||||
provide: {
|
||||
newOrganizationUrl,
|
||||
canCreateOrganization,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(OrganizationsIndexApp);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default {
|
|||
OrganizationsList,
|
||||
GlEmptyState,
|
||||
},
|
||||
inject: ['newOrganizationUrl'],
|
||||
inject: ['newOrganizationUrl', 'canCreateOrganization'],
|
||||
props: {
|
||||
organizations: {
|
||||
type: Object,
|
||||
|
|
@ -43,7 +43,7 @@ export default {
|
|||
description: this.$options.i18n.emptyStateDescription,
|
||||
};
|
||||
|
||||
if (gon.features?.allowOrganizationCreation) {
|
||||
if (this.canCreateOrganization) {
|
||||
return {
|
||||
...baseProps,
|
||||
primaryButtonLink: this.newOrganizationUrl,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,11 @@ export default {
|
|||
timeTrackingDocsPath() {
|
||||
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: {
|
||||
resetModal() {
|
||||
|
|
@ -182,7 +187,6 @@ export default {
|
|||
return convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId);
|
||||
},
|
||||
},
|
||||
CREATE_TIMELOG_MODAL_ID,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -190,7 +194,7 @@ export default {
|
|||
<gl-modal
|
||||
ref="modal"
|
||||
:title="s__('CreateTimelogForm|Add time entry')"
|
||||
:modal-id="$options.CREATE_TIMELOG_MODAL_ID"
|
||||
:modal-id="createTimelogModalId"
|
||||
size="sm"
|
||||
data-testid="create-timelog-modal"
|
||||
:action-primary="primaryProps"
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ export default {
|
|||
issuableTypeName,
|
||||
});
|
||||
},
|
||||
setTimeEstimateModalId() {
|
||||
return this.workItemId
|
||||
? `${SET_TIME_ESTIMATE_MODAL_ID}-${this.workItemId}`
|
||||
: SET_TIME_ESTIMATE_MODAL_ID;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
timeTracking() {
|
||||
|
|
@ -212,7 +217,6 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
SET_TIME_ESTIMATE_MODAL_ID,
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -220,7 +224,7 @@ export default {
|
|||
<gl-modal
|
||||
ref="modal"
|
||||
:title="modalTitle"
|
||||
:modal-id="$options.SET_TIME_ESTIMATE_MODAL_ID"
|
||||
:modal-id="setTimeEstimateModalId"
|
||||
size="sm"
|
||||
data-testid="set-time-estimate-modal"
|
||||
:action-primary="primaryProps"
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ export default {
|
|||
:href="childItemWebUrl"
|
||||
:class="{ '!gl-text-subtle': !isChildItemOpen }"
|
||||
class="gl-hyphens-auto gl-break-words gl-font-semibold"
|
||||
@click.exact="handleTitleClick"
|
||||
@click.exact.stop="handleTitleClick"
|
||||
@mouseover="$emit('mouseover')"
|
||||
@mouseout="$emit('mouseout')"
|
||||
>
|
||||
|
|
@ -241,7 +241,7 @@ export default {
|
|||
>
|
||||
<template #avatar="{ avatar }">
|
||||
<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>
|
||||
</template>
|
||||
</gl-avatars-inline>
|
||||
|
|
@ -286,6 +286,7 @@ export default {
|
|||
:scoped="showScopedLabel(label)"
|
||||
class="gl-mb-auto gl-mr-2 gl-mt-2"
|
||||
tooltip-placement="top"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -299,7 +300,7 @@ export default {
|
|||
:aria-label="$options.i18n.remove"
|
||||
:title="$options.i18n.remove"
|
||||
data-testid="remove-work-item-link"
|
||||
@click="$emit('removeChild', childItem)"
|
||||
@click.stop="$emit('removeChild', childItem)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@ import { GlAlert, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui'
|
|||
import noAccessSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
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 { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_GROUP, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPENAME_GROUP } from '~/graphql_shared/constants';
|
||||
import { isLoggedIn } from '~/lib/utils/common_utils';
|
||||
import { WORKSPACE_PROJECT } from '~/issues/constants';
|
||||
import {
|
||||
i18n,
|
||||
DETAIL_VIEW_QUERY_PARAM_NAME,
|
||||
WIDGET_TYPE_ASSIGNEES,
|
||||
WIDGET_TYPE_NOTIFICATIONS,
|
||||
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 WorkItemDescription from './work_item_description.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 WorkItemRelationships from './work_item_relationships/work_item_relationships.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 WorkItemLoading from './work_item_loading.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 DesignUploadButton from './design_management/upload_button.vue';
|
||||
|
||||
|
|
@ -96,13 +95,13 @@ export default {
|
|||
WorkItemAttributesWrapper,
|
||||
WorkItemTree,
|
||||
WorkItemNotes,
|
||||
WorkItemDetailModal,
|
||||
WorkItemRelationships,
|
||||
WorkItemStickyHeader,
|
||||
WorkItemAncestors,
|
||||
WorkItemTitle,
|
||||
WorkItemLoading,
|
||||
WorkItemAbuseModal,
|
||||
WorkItemDrawer,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: [
|
||||
|
|
@ -145,18 +144,11 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
let modalWorkItemId = getParameterByName(DETAIL_VIEW_QUERY_PARAM_NAME);
|
||||
|
||||
if (modalWorkItemId) {
|
||||
modalWorkItemId = convertToGraphQLId(TYPENAME_WORK_ITEM, modalWorkItemId);
|
||||
}
|
||||
|
||||
return {
|
||||
error: undefined,
|
||||
updateError: undefined,
|
||||
workItem: {},
|
||||
updateInProgress: false,
|
||||
modalWorkItemId,
|
||||
modalWorkItemIid: getParameterByName('work_item_iid'),
|
||||
modalWorkItemNamespaceFullPath: '',
|
||||
isReportModalOpen: false,
|
||||
|
|
@ -171,6 +163,7 @@ export default {
|
|||
designUploadError: null,
|
||||
designUploadErrorVariant: ALERT_VARIANTS.danger,
|
||||
workspacePermissions: defaultWorkspacePermissions,
|
||||
activeChildItem: null,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
|
@ -209,6 +202,7 @@ export default {
|
|||
if (!res.data) {
|
||||
return;
|
||||
}
|
||||
this.activeChildItem = null;
|
||||
this.$emit('work-item-updated', this.workItem);
|
||||
if (isEmpty(this.workItem)) {
|
||||
this.setEmptyState();
|
||||
|
|
@ -431,14 +425,12 @@ export default {
|
|||
iid() {
|
||||
return this.workItemIid || this.workItem.iid;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.modalWorkItemId) {
|
||||
this.openInModal({
|
||||
event: undefined,
|
||||
modalWorkItem: { id: this.modalWorkItemId },
|
||||
});
|
||||
}
|
||||
isItemSelected() {
|
||||
return !isEmpty(this.activeChildItem);
|
||||
},
|
||||
activeChildItemType() {
|
||||
return this.activeChildItem?.workItemType?.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleWorkItemCreated() {
|
||||
|
|
@ -489,23 +481,13 @@ export default {
|
|||
this.error = this.$options.i18n.fetchError;
|
||||
document.title = s__('404|Not found');
|
||||
},
|
||||
updateUrl(modalWorkItem) {
|
||||
updateHistory({
|
||||
url: setUrlParams({
|
||||
[DETAIL_VIEW_QUERY_PARAM_NAME]: getIdFromGraphQLId(modalWorkItem?.id),
|
||||
}),
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
openInModal({ event, modalWorkItem, context }) {
|
||||
openContextualView({ event, modalWorkItem, context }) {
|
||||
if (!this.workItemsAlphaEnabled || context === LINKED_ITEMS_ANCHOR || this.isDrawer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.updateUrl(modalWorkItem);
|
||||
}
|
||||
|
||||
if (this.isModal) {
|
||||
|
|
@ -513,13 +495,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
this.modalWorkItemId = modalWorkItem.id;
|
||||
this.modalWorkItemIid = modalWorkItem.iid;
|
||||
this.modalWorkItemNamespaceFullPath = modalWorkItem?.reference?.replace(
|
||||
`#${modalWorkItem.iid}`,
|
||||
'',
|
||||
);
|
||||
this.$refs.modal.show();
|
||||
this.activeChildItem = modalWorkItem;
|
||||
},
|
||||
openReportAbuseModal(reply) {
|
||||
if (this.isModal) {
|
||||
|
|
@ -655,6 +631,19 @@ export default {
|
|||
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,
|
||||
WORKSPACE_PROJECT,
|
||||
|
|
@ -887,7 +876,7 @@ export default {
|
|||
:confidential="workItem.confidential"
|
||||
:allowed-child-types="allowedChildTypes"
|
||||
:is-drawer="isDrawer"
|
||||
@show-modal="openInModal"
|
||||
@show-modal="openContextualView"
|
||||
@addChild="$emit('addChild')"
|
||||
@childrenLoaded="hasChildren = $event"
|
||||
/>
|
||||
|
|
@ -899,7 +888,7 @@ export default {
|
|||
:work-item-full-path="workItemFullPath"
|
||||
:work-item-type="workItem.workItemType.name"
|
||||
:can-admin-work-item-link="canAdminWorkItemLink"
|
||||
@showModal="openInModal"
|
||||
@showModal="openContextualView"
|
||||
/>
|
||||
<work-item-notes
|
||||
v-if="workItemNotes"
|
||||
|
|
@ -922,16 +911,14 @@ export default {
|
|||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<work-item-detail-modal
|
||||
v-if="!isModal && !isDrawer"
|
||||
ref="modal"
|
||||
:parent-id="workItem.id"
|
||||
:work-item-id="modalWorkItemId"
|
||||
:work-item-iid="modalWorkItemIid"
|
||||
:work-item-full-path="modalWorkItemNamespaceFullPath"
|
||||
:show="true"
|
||||
@close="updateUrl"
|
||||
@openReportAbuse="toggleReportAbuseModal(true, $event)"
|
||||
<work-item-drawer
|
||||
v-if="workItemsAlphaEnabled && !isDrawer"
|
||||
:active-item="activeChildItem"
|
||||
:open="isItemSelected"
|
||||
:issuable-type="activeChildItemType"
|
||||
click-outside-exclude-selector=".issuable-list"
|
||||
@close="activeChildItem = null"
|
||||
@workItemDeleted="deleteChildItem"
|
||||
/>
|
||||
<work-item-abuse-modal
|
||||
v-if="isReportModalOpen"
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export default {
|
|||
if (data.workItemDelete.errors?.length) {
|
||||
throw new Error(data.workItemDelete.errors[0]);
|
||||
}
|
||||
this.$emit('workItemDeleted');
|
||||
this.$emit('workItemDeleted', { id: workItemId });
|
||||
} catch (error) {
|
||||
this.$emit('deleteWorkItemError');
|
||||
Sentry.captureException(error);
|
||||
|
|
@ -119,6 +119,7 @@ export default {
|
|||
e.preventDefault();
|
||||
const shouldRouterNav =
|
||||
!this.preventRouterNav &&
|
||||
this.$router &&
|
||||
canRouterNav({
|
||||
fullPath: this.fullPath,
|
||||
webUrl: workItem.webUrl,
|
||||
|
|
@ -189,11 +190,14 @@ export default {
|
|||
'[id^="insert-comment-template-modal"]',
|
||||
'.pika-single',
|
||||
'.atwho-container',
|
||||
'.item-title',
|
||||
'.tippy-content .gl-new-dropdown-panel',
|
||||
'#blocked-by-issues-modal',
|
||||
'#open-children-warning-modal',
|
||||
'#create-work-item-modal',
|
||||
'#work-item-confirm-delete',
|
||||
'.work-item-link-child',
|
||||
'.modal-content',
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
|
@ -202,6 +206,7 @@ export default {
|
|||
<gl-drawer
|
||||
v-gl-outside="handleClickOutside"
|
||||
:open="open"
|
||||
:z-index="200"
|
||||
data-testid="work-item-drawer"
|
||||
header-sticky
|
||||
header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ export default {
|
|||
@removeChild="removeChild"
|
||||
@error="$emit('error', $event)"
|
||||
@click="onClick($event, child)"
|
||||
@click.native="onClick($event, child)"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ export default {
|
|||
:loading="isLoadingChildren && !fetchNextPageInProgress"
|
||||
class="!gl-px-0 !gl-py-3"
|
||||
data-testid="expand-child"
|
||||
@click="toggleItem"
|
||||
@click.stop="toggleItem"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { sprintf, s__ } from '~/locale';
|
|||
import { createAlert } from '~/alert';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.vue';
|
||||
import { findWidget } from '~/issues/list/utils';
|
||||
import { getParameterByName } from '~/lib/utils/url_utility';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import {
|
||||
FORM_TYPES,
|
||||
WORK_ITEMS_TREE_TEXT,
|
||||
|
|
@ -18,6 +20,7 @@ import {
|
|||
WORK_ITEM_TYPE_VALUE_EPIC,
|
||||
WIDGET_TYPE_HIERARCHY,
|
||||
INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
|
||||
DETAIL_VIEW_QUERY_PARAM_NAME,
|
||||
} from '../../constants';
|
||||
import {
|
||||
findHierarchyWidgets,
|
||||
|
|
@ -156,6 +159,7 @@ export default {
|
|||
if (this.hasNextPage && this.children.length === 0) {
|
||||
this.fetchNextPage();
|
||||
}
|
||||
this.checkDrawerParams();
|
||||
},
|
||||
},
|
||||
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: {
|
||||
noChildItemsOpen: s__('WorkItem|No child items are currently open.'),
|
||||
|
|
|
|||
|
|
@ -26,8 +26,6 @@ export default {
|
|||
'TimeTracking|Add an %{estimateStart}estimate%{estimateEnd} or %{timeSpentStart}time spent%{timeSpentEnd}.',
|
||||
),
|
||||
},
|
||||
createTimelogModalId: CREATE_TIMELOG_MODAL_ID,
|
||||
setTimeEstimateModalId: SET_TIME_ESTIMATE_MODAL_ID,
|
||||
components: {
|
||||
TimeTrackingReport,
|
||||
CreateTimelogForm,
|
||||
|
|
@ -101,6 +99,15 @@ export default {
|
|||
timeRemainingPercent() {
|
||||
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>
|
||||
|
|
@ -113,7 +120,7 @@ export default {
|
|||
</h3>
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
v-gl-modal="$options.createTimelogModalId"
|
||||
v-gl-modal="createTimelogModalId"
|
||||
v-gl-tooltip.top
|
||||
category="tertiary"
|
||||
icon="plus"
|
||||
|
|
@ -129,7 +136,7 @@ export default {
|
|||
<span class="gl-text-subtle">{{ s__('TimeTracking|Spent') }}</span>
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
v-gl-modal="'time-tracking-report'"
|
||||
v-gl-modal="timeTrackingModalId"
|
||||
v-gl-tooltip="s__('TimeTracking|View time tracking report')"
|
||||
variant="link"
|
||||
class="!gl-text-sm"
|
||||
|
|
@ -150,7 +157,7 @@ export default {
|
|||
<span class="gl-text-subtle">{{ s__('TimeTracking|Estimate') }}</span>
|
||||
<gl-button
|
||||
v-if="canUpdate"
|
||||
v-gl-modal="$options.setTimeEstimateModalId"
|
||||
v-gl-modal="setTimeEstimateModalId"
|
||||
v-gl-tooltip="s__('TimeTracking|Set estimate')"
|
||||
variant="link"
|
||||
class="!gl-text-sm"
|
||||
|
|
@ -164,7 +171,7 @@ export default {
|
|||
</template>
|
||||
<gl-button
|
||||
v-else-if="canUpdate"
|
||||
v-gl-modal="$options.setTimeEstimateModalId"
|
||||
v-gl-modal="setTimeEstimateModalId"
|
||||
class="gl-ml-auto !gl-text-sm"
|
||||
variant="link"
|
||||
data-testid="add-estimate-button"
|
||||
|
|
@ -176,7 +183,7 @@ export default {
|
|||
<gl-sprintf :message="$options.i18n.addTimeTrackingMessage">
|
||||
<template #estimate="{ content }">
|
||||
<gl-button
|
||||
v-gl-modal="$options.setTimeEstimateModalId"
|
||||
v-gl-modal="setTimeEstimateModalId"
|
||||
class="gl-align-baseline !gl-text-sm"
|
||||
variant="link"
|
||||
data-testid="add-estimate-button"
|
||||
|
|
@ -186,7 +193,7 @@ export default {
|
|||
</template>
|
||||
<template #timeSpent="{ content }">
|
||||
<gl-button
|
||||
v-gl-modal="$options.createTimelogModalId"
|
||||
v-gl-modal="createTimelogModalId"
|
||||
class="gl-align-baseline !gl-text-sm"
|
||||
variant="link"
|
||||
data-testid="add-time-spent-button"
|
||||
|
|
@ -210,7 +217,7 @@ export default {
|
|||
<create-timelog-form :work-item-id="workItemId" :work-item-type="workItemType" />
|
||||
|
||||
<gl-modal
|
||||
modal-id="time-tracking-report"
|
||||
:modal-id="timeTrackingModalId"
|
||||
data-testid="time-tracking-report-modal"
|
||||
hide-footer
|
||||
size="lg"
|
||||
|
|
|
|||
|
|
@ -281,6 +281,10 @@ export const makeDrawerItemFullPath = (activeItem, fullPath, issuableType = TYPE
|
|||
if (activeItem?.fullPath) {
|
||||
return activeItem.fullPath;
|
||||
}
|
||||
if (activeItem?.namespace?.fullPath) {
|
||||
return activeItem.namespace.fullPath;
|
||||
}
|
||||
|
||||
const delimiter = issuableType === TYPE_EPIC ? '&' : '#';
|
||||
if (!activeItem?.referencePath) {
|
||||
return fullPath;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ module Organizations
|
|||
end
|
||||
|
||||
def organization_index_app_data
|
||||
shared_organization_index_app_data
|
||||
shared_organization_index_app_data.to_json
|
||||
end
|
||||
|
||||
def organization_user_app_data(organization)
|
||||
|
|
@ -111,7 +111,9 @@ module Organizations
|
|||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
- page_title s_('Organization|Organizations')
|
||||
- header_title _("Your work"), root_path
|
||||
|
||||
#js-organizations-index{ data: organization_index_app_data }
|
||||
#js-organizations-index{ data: { app_data: organization_index_app_data } }
|
||||
|
|
|
|||
|
|
@ -65,7 +65,19 @@ class SecretsInitializer
|
|||
secret_key_base: generate_new_secure_token,
|
||||
otp_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
|
||||
|
|
@ -85,6 +97,10 @@ class SecretsInitializer
|
|||
OpenSSL::PKey::RSA.new(2048).to_pem
|
||||
end
|
||||
|
||||
def generate_new_secure_random_alphanumeric(chars)
|
||||
SecureRandom.alphanumeric(chars)
|
||||
end
|
||||
|
||||
def warn_missing_secret(secret)
|
||||
return if rails_env.test?
|
||||
|
||||
|
|
@ -93,10 +109,11 @@ class SecretsInitializer
|
|||
end
|
||||
|
||||
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?
|
||||
|
||||
warn_missing_secret(key)
|
||||
|
||||
missing[key] = Rails.application.credentials[key] = default
|
||||
end
|
||||
end
|
||||
|
|
@ -113,7 +130,7 @@ class SecretsInitializer
|
|||
|
||||
File.write(
|
||||
secrets_file_path,
|
||||
YAML.dump(secrets_from_file),
|
||||
YAML.dump(secrets_from_file.deep_stringify_keys),
|
||||
mode: 'w', perm: 0o600
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Normally, this would automatically be setup by `ActiveRecord::Encryption` initializer, see
|
||||
# https://github.com/rails/rails/blob/v7.0.8.4/activerecord/lib/active_record/railtie.rb#L331-L335,
|
||||
# but since we're setting `Rails.application.credentials.active_record_encryption` manually in
|
||||
# `config/initializers/01_secret_token.rb`, the `ActiveRecord::Encryption` initializer runs prior
|
||||
# to that. We don't want to mess up with the initializer chain, so we configure
|
||||
# `ActiveRecord::Encryption` here instead.
|
||||
ActiveRecord::Encryption.configure(
|
||||
primary_key: Rails.application.credentials[:active_record_encryption_primary_key],
|
||||
deterministic_key: Rails.application.credentials[:active_record_encryption_deterministic_key],
|
||||
key_derivation_salt: Rails.application.credentials[:active_record_encryption_key_derivation_salt],
|
||||
store_key_references: true # this is very important to know what key was used to encrypt a given attribute
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
migration_job_name: BackfillFreeSharedRunnersMinutesLimit
|
||||
description: Backfills namespace shared_runners_minutes_limit values for free tier namespaces
|
||||
feature_category: consumables_cost_management
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/161485
|
||||
milestone: '17.7'
|
||||
queued_migration_version: 20241125125332
|
||||
finalize_after: '2025-01-01'
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddMetadataToZoektEnabledNamespaces < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.7'
|
||||
|
||||
def change
|
||||
add_column :zoekt_enabled_namespaces, :metadata, :jsonb, default: {}, null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddCustomRolesToScanResultPolicies < Gitlab::Database::Migration[2.2]
|
||||
enable_lock_retries!
|
||||
milestone '17.7'
|
||||
|
||||
def change
|
||||
add_column :scan_result_policies, :custom_roles, :bigint, array: true, default: [], null: false
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddCustomRolesConstraintToScanResultPolicies < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
milestone '17.7'
|
||||
|
||||
CONSTRAINT_NAME = 'custom_roles_array_check'
|
||||
|
||||
def up
|
||||
add_check_constraint(:scan_result_policies, "ARRAY_POSITION(custom_roles, null) IS null", CONSTRAINT_NAME)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_check_constraint :scan_result_policies, CONSTRAINT_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueBackfillFreeSharedRunnersMinutesLimit < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.7'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = "BackfillFreeSharedRunnersMinutesLimit"
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 5000
|
||||
SUB_BATCH_SIZE = 100
|
||||
|
||||
def up
|
||||
return unless Gitlab.dev_or_test_env? || Gitlab.com_except_jh?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:namespaces,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.dev_or_test_env? || Gitlab.com_except_jh?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, :namespaces, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
85b7e84f225aaa1fdd76115c88df56cc8cfb297f50ed0c8434906d2d76f66c68
|
||||
|
|
@ -0,0 +1 @@
|
|||
97cb14f7d08a5301d274f2f1a54dd6b40c655ac9d2936bacd46187e687efbdb5
|
||||
|
|
@ -0,0 +1 @@
|
|||
a8c9f960933989678f4a35b5ef48cea7c172d748e971fbf96e4d03d7038c0428
|
||||
|
|
@ -0,0 +1 @@
|
|||
ad2953fc7be16a7bc66aa7513ec452c9e0d6bda1bf3700507c78af63feaed1f1
|
||||
|
|
@ -19616,8 +19616,10 @@ CREATE TABLE scan_result_policies (
|
|||
fallback_behavior jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
policy_tuning jsonb DEFAULT '{}'::jsonb 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 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
|
||||
|
|
@ -22723,7 +22725,8 @@ CREATE TABLE zoekt_enabled_namespaces (
|
|||
root_namespace_id bigint NOT NULL,
|
||||
created_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
|
||||
|
|
|
|||
|
|
@ -2,15 +2,78 @@
|
|||
stage: Verify
|
||||
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
|
||||
remove_date: '2025-02-22'
|
||||
redirect_to: '../../user/workspace/index.md'
|
||||
---
|
||||
|
||||
# Interactive web terminals (removed)
|
||||
# Interactive web terminals
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Free, Premium, Ultimate
|
||||
**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.
|
||||
Use [workspaces](../../user/workspace/index.md) instead.
|
||||
Interactive web terminals give the user access to a terminal in GitLab for
|
||||
running one-off commands for their CI pipeline. You can think of it like a method for
|
||||
debugging with SSH, but done directly from the job page. Since this is giving the user
|
||||
shell access to the environment where [GitLab Runner](https://docs.gitlab.com/runner/)
|
||||
is deployed, some [security precautions](../../administration/integration/terminal.md#security) were
|
||||
taken to protect the users.
|
||||
|
||||
NOTE:
|
||||
[Instance runners on GitLab.com](../runners/index.md) do not
|
||||
provide an interactive web terminal. Follow
|
||||
[this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/24674) for progress on
|
||||
adding support. For groups and projects hosted on GitLab.com, interactive web
|
||||
terminals are available when using your own group or project runner.
|
||||
|
||||
## Configuration
|
||||
|
||||
Two things need to be configured for the interactive web terminal to work:
|
||||
|
||||
- The runner needs to have
|
||||
[`[session_server]` configured properly](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section)
|
||||
- If you are using a reverse proxy with your GitLab instance, web terminals need to be
|
||||
[enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support)
|
||||
|
||||
### Partial support for Helm chart
|
||||
|
||||
Interactive web terminals are partially supported in `gitlab-runner` Helm chart.
|
||||
They are enabled when:
|
||||
|
||||
- The number of replica is one
|
||||
- You use the `loadBalancer` service
|
||||
|
||||
Support for fixing these limitations is tracked in the following issues:
|
||||
|
||||
- [Support of more than one replica](https://gitlab.com/gitlab-org/charts/gitlab-runner/-/issues/323)
|
||||
- [Support of more service types](https://gitlab.com/gitlab-org/charts/gitlab-runner/-/issues/324)
|
||||
|
||||
## Debugging a running job
|
||||
|
||||
NOTE:
|
||||
Not all executors are
|
||||
[supported](https://docs.gitlab.com/runner/executors/#compatibility-chart).
|
||||
|
||||
NOTE:
|
||||
The `docker` executor does not keep running
|
||||
after the build script is finished. At that point, the terminal automatically
|
||||
disconnects and does not wait for the user to finish. Follow
|
||||
[this issue](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3605) for updates on
|
||||
improving this behavior.
|
||||
|
||||
Sometimes, when a job is running, things don't go as you would expect, and it
|
||||
would be helpful if one can have a shell to aid debugging. When a job is
|
||||
running, on the right panel, you can see a `debug` button (**{external-link}**) that opens the terminal
|
||||
for the current job. Only the person who started a job can debug it.
|
||||
|
||||

|
||||
|
||||
When selected, a new tab opens to the terminal page where you can access
|
||||
the terminal and type commands like in a standard shell.
|
||||
|
||||

|
||||
|
||||
If you have the terminal open and the job has finished with its tasks, the
|
||||
terminal blocks the job from finishing for the duration configured in
|
||||
[`[session_server].session_timeout`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) until you
|
||||
close the terminal window.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -357,6 +357,7 @@ module API
|
|||
mount ::API::UserCounts
|
||||
mount ::API::UserRunners
|
||||
mount ::API::VirtualRegistries::Packages::Maven::Registries
|
||||
mount ::API::VirtualRegistries::Packages::Maven::Upstreams
|
||||
mount ::API::VirtualRegistries::Packages::Maven::Endpoints
|
||||
mount ::API::WebCommits
|
||||
mount ::API::Wikis
|
||||
|
|
|
|||
|
|
@ -1,157 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Concerns
|
||||
module VirtualRegistries
|
||||
module Packages
|
||||
module Maven
|
||||
module UpstreamEndpoints
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
desc 'List all maven virtual registry upstreams' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
get do
|
||||
authorize! :read_virtual_registry, registry
|
||||
|
||||
present [upstream].compact, with: Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
end
|
||||
|
||||
desc 'Add a maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 201
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' },
|
||||
{ code: 409, message: 'Conflict' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
params do
|
||||
requires :url, type: String, desc: 'The URL of the maven virtual registry upstream', allow_blank: false
|
||||
optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
|
||||
optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
|
||||
optional :cache_validity_hours, type: Integer, desc: 'The cache validity in hours. Defaults to 24'
|
||||
all_or_none_of :username, :password
|
||||
end
|
||||
post do
|
||||
authorize! :create_virtual_registry, registry
|
||||
|
||||
conflict!(_('Upstream already exists')) if upstream
|
||||
|
||||
registry.build_upstream(declared_params(include_missing: false).merge(group: group))
|
||||
registry_upstream.group = group
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
render_validation_error!(upstream) unless upstream.save
|
||||
render_validation_error!(registry_upstream) unless registry_upstream.save
|
||||
end
|
||||
|
||||
created!
|
||||
end
|
||||
|
||||
route_param :upstream_id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
|
||||
desc 'Get a specific maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
get do
|
||||
authorize! :read_virtual_registry, registry
|
||||
|
||||
# TODO: refactor this when we support multiple upstreams.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/480461
|
||||
not_found! if upstream&.id != params[:upstream_id]
|
||||
|
||||
present upstream, with: Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
end
|
||||
|
||||
desc 'Update a maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
params do
|
||||
with(allow_blank: false) do
|
||||
optional :url, type: String, desc: 'The URL of the maven virtual registry upstream'
|
||||
optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
|
||||
optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
|
||||
optional :cache_validity_hours, type: Integer, desc: 'The validity of the cache in hours'
|
||||
end
|
||||
|
||||
at_least_one_of :url, :username, :password, :cache_validity_hours
|
||||
end
|
||||
patch do
|
||||
authorize! :update_virtual_registry, registry
|
||||
|
||||
render_validation_error!(upstream) unless upstream.update(declared_params(include_missing: false))
|
||||
|
||||
status :ok
|
||||
end
|
||||
|
||||
desc 'Delete a maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 204
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
delete do
|
||||
authorize! :destroy_virtual_registry, registry
|
||||
|
||||
# TODO: refactor this when we support multiple upstreams.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/480461
|
||||
not_found! if upstream&.id != params[:upstream_id]
|
||||
|
||||
destroy_conditionally!(upstream)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -63,8 +63,6 @@ module API
|
|||
namespace :registries do
|
||||
route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' 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
|
||||
namespace :cached_responses do
|
||||
include ::API::Concerns::VirtualRegistries::Packages::Maven::CachedResponseEndpoints
|
||||
|
|
|
|||
|
|
@ -0,0 +1,193 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module VirtualRegistries
|
||||
module Packages
|
||||
module Maven
|
||||
class Upstreams < ::API::Base
|
||||
include ::API::Helpers::Authentication
|
||||
|
||||
feature_category :virtual_registry
|
||||
urgency :low
|
||||
|
||||
authenticate_with do |accept|
|
||||
accept.token_types(:personal_access_token).sent_through(:http_private_token_header)
|
||||
accept.token_types(:deploy_token).sent_through(:http_deploy_token_header)
|
||||
accept.token_types(:job_token).sent_through(:http_job_token_header)
|
||||
end
|
||||
|
||||
helpers do
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
delegate :group, :registry_upstream, to: :registry
|
||||
|
||||
def require_dependency_proxy_enabled!
|
||||
not_found! unless Gitlab.config.dependency_proxy.enabled
|
||||
end
|
||||
|
||||
def registry
|
||||
::VirtualRegistries::Packages::Maven::Registry.find(params[:id])
|
||||
end
|
||||
strong_memoize_attr :registry
|
||||
|
||||
def upstream
|
||||
::VirtualRegistries::Packages::Maven::Upstream.find(params[:id])
|
||||
end
|
||||
strong_memoize_attr :upstream
|
||||
end
|
||||
|
||||
after_validation do
|
||||
not_found! unless Feature.enabled?(:virtual_registry_maven, current_user)
|
||||
|
||||
require_dependency_proxy_enabled!
|
||||
|
||||
authenticate!
|
||||
end
|
||||
|
||||
namespace 'virtual_registries/packages/maven' do
|
||||
namespace :registries do
|
||||
route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' do
|
||||
namespace :upstreams do
|
||||
desc 'List all maven virtual registry upstreams' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
get do
|
||||
authorize! :read_virtual_registry, registry
|
||||
|
||||
present [registry.upstream].compact, with: Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
end
|
||||
|
||||
desc 'Add a maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 201, model: ::API::Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' },
|
||||
{ code: 409, message: 'Conflict' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
params do
|
||||
requires :url, type: String, desc: 'The URL of the maven virtual registry upstream',
|
||||
allow_blank: false
|
||||
optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
|
||||
optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
|
||||
optional :cache_validity_hours, type: Integer, desc: 'The cache validity in hours. Defaults to 24'
|
||||
all_or_none_of :username, :password
|
||||
end
|
||||
post do
|
||||
authorize! :create_virtual_registry, registry
|
||||
|
||||
conflict!(_('Upstream already exists')) if registry.upstream
|
||||
|
||||
new_upstream = registry.build_upstream(declared_params(include_missing: false).merge(group:))
|
||||
registry_upstream.group = group
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
render_validation_error!(new_upstream) unless new_upstream.save
|
||||
render_validation_error!(registry_upstream) unless registry_upstream.save
|
||||
end
|
||||
|
||||
present new_upstream, with: Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :upstreams do
|
||||
route_param :id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
|
||||
desc 'Get a specific maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success ::API::Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
get do
|
||||
authorize! :read_virtual_registry, upstream
|
||||
|
||||
present upstream, with: ::API::Entities::VirtualRegistries::Packages::Maven::Upstream
|
||||
end
|
||||
|
||||
desc 'Update a maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 200
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
params do
|
||||
with(allow_blank: false) do
|
||||
optional :url, type: String, desc: 'The URL of the maven virtual registry upstream'
|
||||
optional :username, type: String, desc: 'The username of the maven virtual registry upstream'
|
||||
optional :password, type: String, desc: 'The password of the maven virtual registry upstream'
|
||||
optional :cache_validity_hours, type: Integer, desc: 'The validity of the cache in hours'
|
||||
end
|
||||
|
||||
at_least_one_of :url, :username, :password, :cache_validity_hours
|
||||
end
|
||||
patch do
|
||||
authorize! :update_virtual_registry, upstream
|
||||
|
||||
render_validation_error!(upstream) unless upstream.update(declared_params(include_missing: false))
|
||||
|
||||
status :ok
|
||||
end
|
||||
|
||||
desc 'Delete a maven virtual registry upstream' do
|
||||
detail 'This feature was introduced in GitLab 17.4. \
|
||||
This feature is currently in experiment state. \
|
||||
This feature behind the `virtual_registry_maven` feature flag.'
|
||||
success code: 204
|
||||
failure [
|
||||
{ code: 400, message: 'Bad Request' },
|
||||
{ code: 401, message: 'Unauthorized' },
|
||||
{ code: 403, message: 'Forbidden' },
|
||||
{ code: 404, message: 'Not found' }
|
||||
]
|
||||
tags %w[maven_virtual_registries]
|
||||
hidden true
|
||||
end
|
||||
delete do
|
||||
authorize! :destroy_virtual_registry, upstream
|
||||
|
||||
destroy_conditionally!(upstream)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class BackfillFreeSharedRunnersMinutesLimit < BatchedMigrationJob
|
||||
feature_category :consumables_cost_management
|
||||
|
||||
def perform; end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Gitlab::BackgroundMigration::BackfillFreeSharedRunnersMinutesLimit.prepend_mod
|
||||
|
|
@ -107,6 +107,9 @@ function bundle_install_script() {
|
|||
run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS} ${extra_install_args}"
|
||||
|
||||
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`
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -32,13 +32,15 @@ describe('AdminOrganizationsIndexApp', () => {
|
|||
|
||||
const successHandler = jest.fn().mockResolvedValue(organizationsGraphQlResponse);
|
||||
|
||||
const createComponent = (handler = successHandler) => {
|
||||
const createComponent = ({ handler = successHandler, provide = {} } = {}) => {
|
||||
mockApollo = createMockApollo([[organizationsQuery, handler]]);
|
||||
|
||||
wrapper = shallowMountExtended(OrganizationsIndexApp, {
|
||||
apolloProvider: mockApollo,
|
||||
provide: {
|
||||
newOrganizationUrl: MOCK_NEW_ORG_URL,
|
||||
canCreateOrganization: true,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -89,7 +91,7 @@ describe('AdminOrganizationsIndexApp', () => {
|
|||
|
||||
describe('when API call is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(jest.fn().mockReturnValue(new Promise(() => {})));
|
||||
createComponent({ handler: jest.fn().mockReturnValue(new Promise(() => {})) });
|
||||
});
|
||||
|
||||
itRendersHeaderText();
|
||||
|
|
@ -119,11 +121,9 @@ describe('AdminOrganizationsIndexApp', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when `allowOrganizationCreation` feature flag is disabled', () => {
|
||||
describe('when `canCreateOrganization` is false', () => {
|
||||
beforeEach(() => {
|
||||
gon.features = { allowOrganizationCreation: false };
|
||||
|
||||
createComponent();
|
||||
createComponent({ provide: { canCreateOrganization: false } });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -132,13 +132,13 @@ describe('AdminOrganizationsIndexApp', () => {
|
|||
|
||||
describe('when API call is successful and returns no organizations', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent(
|
||||
jest.fn().mockResolvedValue({
|
||||
createComponent({
|
||||
handler: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
organizations: organizationEmpty,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ describe('AdminOrganizationsIndexApp', () => {
|
|||
const error = new Error();
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent(jest.fn().mockRejectedValue(error));
|
||||
createComponent({ handler: jest.fn().mockRejectedValue(error) });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -34,13 +34,15 @@ describe('OrganizationsIndexApp', () => {
|
|||
|
||||
const successHandler = jest.fn().mockResolvedValue(currentUserOrganizationsGraphQlResponse);
|
||||
|
||||
const createComponent = (handler = successHandler) => {
|
||||
const createComponent = ({ handler = successHandler, provide = {} } = {}) => {
|
||||
mockApollo = createMockApollo([[currentUserOrganizationsQuery, handler]]);
|
||||
|
||||
wrapper = shallowMountExtended(OrganizationsIndexApp, {
|
||||
apolloProvider: mockApollo,
|
||||
provide: {
|
||||
newOrganizationUrl: MOCK_NEW_ORG_URL,
|
||||
canCreateOrganization: true,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -85,14 +87,10 @@ describe('OrganizationsIndexApp', () => {
|
|||
});
|
||||
};
|
||||
|
||||
describe('`allowOrganizationCreation` is enabled', () => {
|
||||
beforeEach(() => {
|
||||
gon.features = { allowOrganizationCreation: true };
|
||||
});
|
||||
|
||||
describe('`canCreateOrganization` is true', () => {
|
||||
describe('when API call is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(jest.fn().mockResolvedValue({}));
|
||||
createComponent({ handler: jest.fn().mockResolvedValue({}) });
|
||||
});
|
||||
|
||||
itRendersHeaderText();
|
||||
|
|
@ -122,14 +120,13 @@ describe('OrganizationsIndexApp', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('`allowOrganizationCreation` is disabled', () => {
|
||||
beforeEach(() => {
|
||||
gon.features = { allowOrganizationCreation: false };
|
||||
});
|
||||
|
||||
describe('`canCreateOrganization` is false', () => {
|
||||
describe('when API call is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(jest.fn().mockResolvedValue({}));
|
||||
createComponent({
|
||||
handler: jest.fn().mockResolvedValue({}),
|
||||
provide: { canCreateOrganization: false },
|
||||
});
|
||||
});
|
||||
|
||||
itRendersHeaderText();
|
||||
|
|
@ -142,7 +139,7 @@ describe('OrganizationsIndexApp', () => {
|
|||
});
|
||||
describe('when API call is successful', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
createComponent({ provide: { canCreateOrganization: false } });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -161,8 +158,8 @@ describe('OrganizationsIndexApp', () => {
|
|||
|
||||
describe('when API call is successful and returns no organizations', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent(
|
||||
jest.fn().mockResolvedValue({
|
||||
createComponent({
|
||||
handler: jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
currentUser: {
|
||||
id: 'gid://gitlab/User/1',
|
||||
|
|
@ -170,7 +167,7 @@ describe('OrganizationsIndexApp', () => {
|
|||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
@ -190,7 +187,7 @@ describe('OrganizationsIndexApp', () => {
|
|||
const error = new Error();
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent(jest.fn().mockRejectedValue(error));
|
||||
createComponent({ handler: jest.fn().mockRejectedValue(error) });
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ describe('OrganizationsView', () => {
|
|||
},
|
||||
} = currentUserOrganizationsGraphQlResponse;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
const createComponent = (props = {}, provide = {}) => {
|
||||
wrapper = shallowMount(OrganizationsView, {
|
||||
propsData: {
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
newOrganizationUrl: MOCK_NEW_ORG_URL,
|
||||
canCreateOrganization: true,
|
||||
...provide,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -36,10 +38,6 @@ describe('OrganizationsView', () => {
|
|||
const findOrganizationsList = () => wrapper.findComponent(OrganizationsList);
|
||||
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
|
||||
beforeEach(() => {
|
||||
gon.features = { allowOrganizationCreation: true };
|
||||
});
|
||||
|
||||
describe.each`
|
||||
description | loading | orgsData | emptyStateSvg | emptyStateUrl
|
||||
${'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(() => {
|
||||
gon.features = { allowOrganizationCreation: false };
|
||||
createComponent({ loading: false, organizations: { nodes: [], pageInfo: {} } });
|
||||
createComponent(
|
||||
{ loading: false, organizations: { nodes: [], pageInfo: {} } },
|
||||
{ canCreateOrganization: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render `New organization` button in empty state', () => {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ describe('WorkItemLinkChildContents', () => {
|
|||
it('emits click event with correct parameters on clicking title', () => {
|
||||
const eventObj = {
|
||||
preventDefault: jest.fn(),
|
||||
stopPropagation: jest.fn(),
|
||||
};
|
||||
findTitleEl().vm.$emit('click', eventObj);
|
||||
|
||||
|
|
@ -152,7 +153,7 @@ describe('WorkItemLinkChildContents', () => {
|
|||
workItemFullPath: 'gitlab-org/gitlab-test',
|
||||
});
|
||||
|
||||
findTitleEl().vm.$emit('click', { preventDefault });
|
||||
findTitleEl().vm.$emit('click', { preventDefault, stopPropagation: jest.fn() });
|
||||
});
|
||||
|
||||
it('pushes a new router state', () => {
|
||||
|
|
@ -218,7 +219,7 @@ describe('WorkItemLinkChildContents', () => {
|
|||
});
|
||||
|
||||
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]]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
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 WorkItemDetail from '~/work_items/components/work_item_detail.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 WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.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 WorkItemTitle from '~/work_items/components/work_item_title.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 DesignWidget from '~/work_items/components/design_management/design_management_widget.vue';
|
||||
import DesignUploadButton from '~/work_items/components//design_management/upload_button.vue';
|
||||
|
|
@ -41,7 +40,6 @@ import {
|
|||
workItemLinkedItemsResponse,
|
||||
objectiveType,
|
||||
epicType,
|
||||
mockWorkItemCommentNote,
|
||||
mockBlockingLinkedItem,
|
||||
allowedChildrenTypesResponse,
|
||||
mockProjectPermissionsQueryResponse,
|
||||
|
|
@ -76,7 +74,6 @@ describe('WorkItemDetail component', () => {
|
|||
const successHandlerWithNoPermissions = jest
|
||||
.fn()
|
||||
.mockResolvedValue(workItemQueryResponseWithNoPermissions);
|
||||
const showModalHandler = jest.fn();
|
||||
const { id } = workItemByIidQueryResponse.data.workspace.workItem;
|
||||
const workItemUpdatedSubscriptionHandler = jest
|
||||
.fn()
|
||||
|
|
@ -117,7 +114,6 @@ describe('WorkItemDetail component', () => {
|
|||
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
|
||||
const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships);
|
||||
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
|
||||
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
|
||||
const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal);
|
||||
const findTodosToggle = () => wrapper.findComponent(TodosToggle);
|
||||
const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader);
|
||||
|
|
@ -127,6 +123,7 @@ describe('WorkItemDetail component', () => {
|
|||
const findWorkItemDesigns = () => wrapper.findComponent(DesignWidget);
|
||||
const findDesignUploadButton = () => wrapper.findComponent(DesignUploadButton);
|
||||
const findDetailWrapper = () => wrapper.findByTestId('detail-wrapper');
|
||||
const findDrawer = () => wrapper.findComponent(WorkItemDrawer);
|
||||
|
||||
const createComponent = ({
|
||||
isModal = false,
|
||||
|
|
@ -188,11 +185,6 @@ describe('WorkItemDetail component', () => {
|
|||
WorkItemWeight: true,
|
||||
WorkItemIteration: true,
|
||||
WorkItemHealthStatus: true,
|
||||
WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
|
||||
methods: {
|
||||
show: showModalHandler,
|
||||
},
|
||||
}),
|
||||
},
|
||||
mocks: {
|
||||
$router: router,
|
||||
|
|
@ -564,17 +556,6 @@ describe('WorkItemDetail component', () => {
|
|||
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 () => {
|
||||
createComponent({ workItemIid: null, workItemId: null });
|
||||
await waitForPromises();
|
||||
|
|
@ -637,31 +618,22 @@ describe('WorkItemDetail component', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('renders a modal', async () => {
|
||||
createComponent({ handler: objectiveHandler });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('opens the modal with the child when `show-modal` is emitted', async () => {
|
||||
it('opens the drawer with the child when `show-modal` is emitted', async () => {
|
||||
createComponent({ handler: objectiveHandler, workItemsAlphaEnabled: true });
|
||||
await waitForPromises();
|
||||
|
||||
const event = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
const modalWorkItem = { id: 'childWorkItemId' };
|
||||
|
||||
findHierarchyTree().vm.$emit('show-modal', {
|
||||
event,
|
||||
modalWorkItem: { id: 'childWorkItemId' },
|
||||
modalWorkItem,
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.findComponent(WorkItemDetailModal).props().workItemId).toBe(
|
||||
'childWorkItemId',
|
||||
);
|
||||
expect(showModalHandler).toHaveBeenCalled();
|
||||
expect(findDrawer().props('activeItem')).toEqual(modalWorkItem);
|
||||
});
|
||||
|
||||
describe('work item is rendered in a modal and has children', () => {
|
||||
|
|
@ -675,10 +647,6 @@ describe('WorkItemDetail component', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not render a new modal', () => {
|
||||
expect(findModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('emits `update-modal` when `show-modal` is emitted', async () => {
|
||||
const event = {
|
||||
preventDefault: jest.fn(),
|
||||
|
|
@ -746,15 +714,15 @@ describe('WorkItemDetail component', () => {
|
|||
const event = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
const modalWorkItem = { id: 'childWorkItemId' };
|
||||
|
||||
findWorkItemRelationships().vm.$emit('showModal', {
|
||||
event,
|
||||
modalWorkItem: { id: 'childWorkItemId' },
|
||||
modalWorkItem,
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findModal().props().workItemId).toBe('childWorkItemId');
|
||||
expect(showModalHandler).toHaveBeenCalled();
|
||||
expect(findDrawer().props('activeItem')).toEqual(modalWorkItem);
|
||||
});
|
||||
|
||||
describe('linked work item is rendered in a modal and has linked items', () => {
|
||||
|
|
@ -768,10 +736,6 @@ describe('WorkItemDetail component', () => {
|
|||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('does not render a new modal', () => {
|
||||
expect(findModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('emits `update-modal` when `show-modal` is emitted', async () => {
|
||||
const event = {
|
||||
preventDefault: jest.fn(),
|
||||
|
|
@ -819,20 +783,6 @@ describe('WorkItemDetail component', () => {
|
|||
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 () => {
|
||||
findWorkItemActions().vm.$emit('toggleReportAbuseModal', true);
|
||||
await nextTick();
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ describe('WorkItemLinkChild', () => {
|
|||
describe('when clicking on expand button', () => {
|
||||
it('fetches and displays children of item when clicking on expand button', async () => {
|
||||
createComponent();
|
||||
await findExpandButton().vm.$emit('click');
|
||||
await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
|
||||
|
||||
expect(findExpandButton().props('loading')).toBe(true);
|
||||
await waitForPromises();
|
||||
|
|
@ -117,7 +117,7 @@ describe('WorkItemLinkChild', () => {
|
|||
|
||||
it('does not render border on `WorkItemLinkChildContents` container', async () => {
|
||||
createComponent();
|
||||
await findExpandButton().vm.$emit('click');
|
||||
await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
|
||||
|
||||
expect(findWorkItemLinkChildContentsContainer().classes()).not.toContain('!gl-border-b-1');
|
||||
});
|
||||
|
|
@ -134,8 +134,8 @@ describe('WorkItemLinkChild', () => {
|
|||
const childrenNodes = getChildrenNodes();
|
||||
expect(findTreeChildren().props('children')).toEqual(childrenNodes);
|
||||
|
||||
await findExpandButton().vm.$emit('click'); // Collapse
|
||||
findExpandButton().vm.$emit('click'); // Expand again
|
||||
await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() }); // Collapse
|
||||
await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() }); // Expand again
|
||||
await waitForPromises();
|
||||
|
||||
expect(getWorkItemTreeQueryHandler).toHaveBeenCalledTimes(1); // ensure children were fetched only once.
|
||||
|
|
@ -190,7 +190,7 @@ describe('WorkItemLinkChild', () => {
|
|||
workItemTreeQueryHandler: getWorkItemTreeQueryFailureHandler,
|
||||
});
|
||||
|
||||
findExpandButton().vm.$emit('click');
|
||||
await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
|
|
@ -258,7 +258,7 @@ describe('WorkItemLinkChild', () => {
|
|||
workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
isExpanded: true,
|
||||
});
|
||||
await findExpandButton().vm.$emit('click');
|
||||
await findExpandButton().vm.$emit('click', { stopPropagation: jest.fn() });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_item
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { createAlert } from '~/alert';
|
||||
import CrudComponent from '~/vue_shared/components/crud_component.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.',
|
||||
);
|
||||
});
|
||||
|
||||
describe('when there is show URL parameter', () => {
|
||||
it('emits `show-modal` event when child work item id is encoded in the URL', async () => {
|
||||
const encodedWorkItemId = btoa(JSON.stringify({ id: 31 }));
|
||||
setWindowLocation(`?show=${encodedWorkItemId}`);
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.emitted('show-modal')).toEqual([
|
||||
[{ modalWorkItem: expect.objectContaining({ id: 'gid://gitlab/WorkItem/31' }) }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not emit `show-modal` event when child work item id is not encoded in the URL', async () => {
|
||||
const encodedWorkItemId = btoa(JSON.stringify({ id: 1 }));
|
||||
setWindowLocation(`?show=${encodedWorkItemId}`);
|
||||
await createComponent();
|
||||
|
||||
expect(wrapper.emitted('show-modal')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ describe('WorkItemTimeTracking component', () => {
|
|||
});
|
||||
|
||||
it('has a modal directive', () => {
|
||||
expect(getBinding(findAddTimeEntryButton().element, 'gl-modal').value).toBe(
|
||||
'create-timelog-modal',
|
||||
expect(getBinding(findAddTimeEntryButton().element, 'gl-modal').value).toEqual(
|
||||
expect.stringContaining('create-timelog-modal'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -81,15 +81,15 @@ describe('WorkItemTimeTracking component', () => {
|
|||
|
||||
it('allows user to add an estimate by clicking "estimate"', () => {
|
||||
expect(findEstimateButton().props('variant')).toBe('link');
|
||||
expect(getBinding(findEstimateButton().element, 'gl-modal').value).toBe(
|
||||
'set-time-estimate-modal',
|
||||
expect(getBinding(findEstimateButton().element, 'gl-modal').value).toEqual(
|
||||
expect.stringContaining('set-time-estimate-modal'),
|
||||
);
|
||||
});
|
||||
|
||||
it('allows user to add a time entry by clicking "time spent"', () => {
|
||||
expect(findAddTimeSpentButton().props('variant')).toBe('link');
|
||||
expect(getBinding(findAddTimeSpentButton().element, 'gl-modal').value).toBe(
|
||||
'create-timelog-modal',
|
||||
expect(getBinding(findAddTimeSpentButton().element, 'gl-modal').value).toEqual(
|
||||
expect.stringContaining('create-timelog-modal'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -106,8 +106,8 @@ describe('WorkItemTimeTracking component', () => {
|
|||
|
||||
it('time spent links to time tracking report', () => {
|
||||
expect(findViewTimeSpentButton().props('variant')).toBe('link');
|
||||
expect(getBinding(findViewTimeSpentButton().element, 'gl-modal').value).toBe(
|
||||
'time-tracking-report',
|
||||
expect(getBinding(findViewTimeSpentButton().element, 'gl-modal').value).toEqual(
|
||||
expect.stringContaining('time-tracking-modal'),
|
||||
);
|
||||
expect(getBinding(findViewTimeSpentButton().element, 'gl-tooltip').value).toBe(
|
||||
'View time tracking report',
|
||||
|
|
@ -116,8 +116,8 @@ describe('WorkItemTimeTracking component', () => {
|
|||
|
||||
it('shows "Add estimate" button to add estimate', () => {
|
||||
expect(findAddEstimateButton().props('variant')).toBe('link');
|
||||
expect(getBinding(findAddEstimateButton().element, 'gl-modal').value).toBe(
|
||||
'set-time-estimate-modal',
|
||||
expect(getBinding(findAddEstimateButton().element, 'gl-modal').value).toEqual(
|
||||
expect.stringContaining('set-time-estimate-modal'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -141,8 +141,8 @@ describe('WorkItemTimeTracking component', () => {
|
|||
|
||||
it('estimate links to "Add estimate" modal', () => {
|
||||
expect(findSetEstimateButton().props('variant')).toBe('link');
|
||||
expect(getBinding(findSetEstimateButton().element, 'gl-modal').value).toBe(
|
||||
'set-time-estimate-modal',
|
||||
expect(getBinding(findSetEstimateButton().element, 'gl-modal').value).toEqual(
|
||||
expect.stringContaining('set-time-estimate-modal'),
|
||||
);
|
||||
expect(getBinding(findSetEstimateButton().element, 'gl-tooltip').value).toBe('Set estimate');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,6 +50,37 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
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
|
||||
context 'when current controller is not organizations' do
|
||||
it 'returns organization' do
|
||||
|
|
@ -183,13 +214,9 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
end
|
||||
|
||||
describe '#organization_index_app_data' do
|
||||
it 'returns expected data object' do
|
||||
expect(helper.organization_index_app_data).to eq(
|
||||
{
|
||||
new_organization_url: new_organization_path
|
||||
}
|
||||
)
|
||||
end
|
||||
subject(:data) { Gitlab::Json.parse(helper.organization_index_app_data) }
|
||||
|
||||
it_behaves_like 'index app data'
|
||||
end
|
||||
|
||||
describe '#organization_new_app_data' do
|
||||
|
|
@ -307,13 +334,9 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do
|
|||
end
|
||||
|
||||
describe '#admin_organizations_index_app_data' do
|
||||
it 'returns expected json' do
|
||||
expect(Gitlab::Json.parse(helper.admin_organizations_index_app_data)).to eq(
|
||||
{
|
||||
'new_organization_url' => new_organization_path
|
||||
}
|
||||
)
|
||||
end
|
||||
subject(:data) { Gitlab::Json.parse(helper.admin_organizations_index_app_data) }
|
||||
|
||||
it_behaves_like 'index app data'
|
||||
end
|
||||
|
||||
describe '#organization_projects_edit_app_data' do
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ RSpec.describe SecretsInitializer do
|
|||
describe 'ensure acknowledged secrets in any installations' do
|
||||
let(:acknowledged_secrets) do
|
||||
%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
|
||||
|
||||
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
|
||||
otp_key_base
|
||||
openid_connect_signing_key
|
||||
active_record_encryption_primary_key
|
||||
active_record_encryption_deterministic_key
|
||||
active_record_encryption_key_derivation_salt
|
||||
]
|
||||
end
|
||||
|
||||
let(:hex_key) { /\h{128}/ }
|
||||
let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m }
|
||||
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(:alphanumeric_key) { /\A[A-Za-z0-9]{32}\z/m }
|
||||
|
||||
around do |example|
|
||||
# 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))
|
||||
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
|
||||
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
|
||||
|
||||
initializer.execute!
|
||||
|
|
@ -166,7 +179,7 @@ RSpec.describe SecretsInitializer do
|
|||
end
|
||||
|
||||
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|
|
||||
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
|
||||
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
|
||||
stub_env('SECRET_KEY_BASE', 'env_key')
|
||||
|
|
|
|||
|
|
@ -150,12 +150,14 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger, feature_category: :continuous_int
|
|||
subject(:commit) { logger.commit(pipeline: pipeline, caller: 'source') }
|
||||
|
||||
before do
|
||||
stub_feature_flags(ci_pipeline_creation_logger: flag)
|
||||
allow(logger).to receive(:current_monotonic_time) { Time.current.to_i }
|
||||
freeze_time do
|
||||
stub_feature_flags(ci_pipeline_creation_logger: flag)
|
||||
allow(logger).to receive(:current_monotonic_time) { Time.current.to_i }
|
||||
|
||||
logger.instrument(:pipeline_save) { travel(60.seconds) }
|
||||
logger.observe(:pipeline_creation_duration_s, 30)
|
||||
logger.observe(:pipeline_creation_duration_s, 10)
|
||||
logger.instrument(:pipeline_save) { travel(60.seconds) }
|
||||
logger.observe(:pipeline_creation_duration_s, 30)
|
||||
logger.observe(:pipeline_creation_duration_s, 10)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the feature flag is enabled' do
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueBackfillFreeSharedRunnersMinutesLimit, feature_category: :consumables_cost_management do
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'does not schedule the background job when Gitlab.com_except_jh? is false' do
|
||||
allow(Gitlab).to receive_messages(dev_or_test_env?: false, com_except_jh?: false)
|
||||
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
it 'schedules a new batched migration when Gitlab.com_except_jh? is true' do
|
||||
allow(Gitlab).to receive_messages(dev_or_test_env?: true, com_except_jh?: true)
|
||||
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :namespaces,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE,
|
||||
gitlab_schema: :gitlab_main
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -69,9 +69,7 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Registries, :aggregate_f
|
|||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
token_header(token)
|
||||
end
|
||||
let(:headers) { token_header(token) }
|
||||
|
||||
it_behaves_like 'returning response status', params[:status]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
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
|
||||
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
|
||||
where(:token, :sent_as, :status) do
|
||||
:personal_access_token | :header | :ok
|
||||
:personal_access_token | :basic_auth | :ok
|
||||
:deploy_token | :header | :ok
|
||||
:deploy_token | :basic_auth | :ok
|
||||
:job_token | :header | :ok
|
||||
:job_token | :basic_auth | :ok
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
case sent_as
|
||||
when :header
|
||||
token_header(token)
|
||||
when :basic_auth
|
||||
token_basic_auth(token)
|
||||
end
|
||||
end
|
||||
let(:headers) { token_header(token) }
|
||||
|
||||
it_behaves_like 'returning response status', params[:status]
|
||||
end
|
||||
|
|
@ -94,12 +84,16 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
|
|||
subject(:api_request) { post api(url), headers: headers, params: params }
|
||||
|
||||
shared_examples 'successful response' do
|
||||
let(:upstream_model) { ::VirtualRegistries::Packages::Maven::Upstream }
|
||||
|
||||
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)
|
||||
|
||||
expect(::VirtualRegistries::Packages::Maven::Upstream.last.cache_validity_hours).to eq(
|
||||
params[:cache_validity_hours] || ::VirtualRegistries::Packages::Maven::Upstream.new.cache_validity_hours
|
||||
expect(response).to have_gitlab_http_status(:created)
|
||||
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
|
||||
|
|
@ -191,22 +185,12 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
|
|||
|
||||
where(:token, :sent_as, :status) do
|
||||
:personal_access_token | :header | :created
|
||||
:personal_access_token | :basic_auth | :created
|
||||
:deploy_token | :header | :forbidden
|
||||
:deploy_token | :basic_auth | :forbidden
|
||||
:job_token | :header | :created
|
||||
:job_token | :basic_auth | :created
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
case sent_as
|
||||
when :header
|
||||
token_header(token)
|
||||
when :basic_auth
|
||||
token_basic_auth(token)
|
||||
end
|
||||
end
|
||||
let(:headers) { token_header(token) }
|
||||
|
||||
if params[:status] == :created
|
||||
it_behaves_like 'successful response'
|
||||
|
|
@ -217,8 +201,8 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/virtual_registries/packages/maven/registries/:id/upstreams/:upstream_id' do
|
||||
let(:url) { "/virtual_registries/packages/maven/registries/#{registry.id}/upstreams/#{upstream.id}" }
|
||||
describe 'GET /api/v4/virtual_registries/packages/maven/upstreams/:id' do
|
||||
let(:url) { "/virtual_registries/packages/maven/upstreams/#{upstream.id}" }
|
||||
|
||||
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
|
||||
where(:token, :sent_as, :status) do
|
||||
:personal_access_token | :header | :ok
|
||||
:personal_access_token | :basic_auth | :ok
|
||||
:deploy_token | :header | :ok
|
||||
:deploy_token | :basic_auth | :ok
|
||||
:job_token | :header | :ok
|
||||
:job_token | :basic_auth | :ok
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
case sent_as
|
||||
when :header
|
||||
token_header(token)
|
||||
when :basic_auth
|
||||
token_basic_auth(token)
|
||||
end
|
||||
end
|
||||
let(:headers) { token_header(token) }
|
||||
|
||||
it_behaves_like 'returning response status', params[:status]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /api/v4/virtual_registries/packages/maven/registries/:id/upstreams/:upstream_id' do
|
||||
let(:url) { "/virtual_registries/packages/maven/registries/#{registry.id}/upstreams/#{upstream.id}" }
|
||||
describe 'PATCH /api/v4/virtual_registries/packages/maven/upstreams/:id' do
|
||||
let(:url) { "/virtual_registries/packages/maven/upstreams/#{upstream.id}" }
|
||||
|
||||
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
|
||||
:personal_access_token | :header | :ok
|
||||
:personal_access_token | :basic_auth | :ok
|
||||
:deploy_token | :header | :forbidden
|
||||
:deploy_token | :basic_auth | :forbidden
|
||||
:job_token | :header | :ok
|
||||
:job_token | :basic_auth | :ok
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
case sent_as
|
||||
when :header
|
||||
token_header(token)
|
||||
when :basic_auth
|
||||
token_basic_auth(token)
|
||||
end
|
||||
end
|
||||
let(:headers) { token_header(token) }
|
||||
|
||||
it_behaves_like 'returning response status', params[:status]
|
||||
end
|
||||
|
|
@ -372,8 +336,8 @@ RSpec.describe API::VirtualRegistries::Packages::Maven::Endpoints, :aggregate_fa
|
|||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v4/virtual_registries/packages/maven/registries/:id/upstreams/:upstream_id' do
|
||||
let(:url) { "/virtual_registries/packages/maven/registries/#{registry.id}/upstreams/#{upstream.id}" }
|
||||
describe 'DELETE /api/v4/virtual_registries/packages/maven/upstreams/:id' do
|
||||
let(:url) { "/virtual_registries/packages/maven/upstreams/#{upstream.id}" }
|
||||
|
||||
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
|
||||
:personal_access_token | :header | :no_content
|
||||
:personal_access_token | :basic_auth | :no_content
|
||||
:deploy_token | :header | :forbidden
|
||||
:deploy_token | :basic_auth | :forbidden
|
||||
:job_token | :header | :no_content
|
||||
:job_token | :basic_auth | :no_content
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) do
|
||||
case sent_as
|
||||
when :header
|
||||
token_header(token)
|
||||
when :basic_auth
|
||||
token_basic_auth(token)
|
||||
end
|
||||
end
|
||||
let(:headers) { token_header(token) }
|
||||
|
||||
if params[:status] == :no_content
|
||||
it_behaves_like 'successful response'
|
||||
|
|
@ -56,7 +56,7 @@ RSpec.shared_examples 'work items rolled up dates' do
|
|||
wait_for_all_requests
|
||||
end
|
||||
|
||||
within_testid('work-item-detail-modal') do
|
||||
within_testid('work-item-drawer') do
|
||||
find_and_click_edit work_item_rolledup_dates_selector
|
||||
# set empty value before the value to ensure
|
||||
# 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 'Due', with: "" # ensure to reset the input first to avoid wrong date values
|
||||
fill_in 'Due', with: due_date
|
||||
end
|
||||
|
||||
find_by_testid('work-item-close').click
|
||||
wait_for_all_requests
|
||||
find_by_testid('close-icon').click
|
||||
wait_for_all_requests
|
||||
end
|
||||
|
||||
page.refresh
|
||||
wait_for_all_requests
|
||||
|
|
|
|||
Loading…
Reference in New Issue