Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-13 12:17:47 +00:00
parent adeba64772
commit 6a4ec0399b
122 changed files with 2321 additions and 293 deletions

View File

@ -176,10 +176,6 @@ Dangerfile
/ee/spec/policies/vulnerabilities/
/ee/spec/policies/vulnerability*.rb
^[Threat Insights frontend] @gitlab-org/govern/threat-insights-frontend-team
/ee/app/assets/javascripts/license_compliance/components/detected_licenses_table.vue
/ee/spec/frontend/license_compliance/components/detected_licenses_table_spec.js
^[Composition Analysis backend] @gitlab-org/secure/composition-analysis-be
/app/events/package_metadata/
/app/models/concerns/enums/package_metadata.rb

View File

@ -72,7 +72,7 @@ release-environments-qa:
stage: qa
extends:
- .qa-base
timeout: 3h
timeout: 30m
variables:
QA_SCENARIO: "Test::Instance::Smoke"
RELEASE: "${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_SHA}"

View File

@ -690,7 +690,6 @@ Layout/LineLength:
- 'ee/app/controllers/projects/audit_events_controller.rb'
- 'ee/app/controllers/projects/insights_controller.rb'
- 'ee/app/controllers/projects/integrations/zentao/issues_controller.rb'
- 'ee/app/controllers/projects/licenses_controller.rb'
- 'ee/app/controllers/projects/protected_environments_controller.rb'
- 'ee/app/controllers/projects/requirements_management/requirements_controller.rb'
- 'ee/app/controllers/projects/security/policies_controller.rb'
@ -1242,7 +1241,6 @@ Layout/LineLength:
- 'ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb'
- 'ee/spec/controllers/projects/integrations/zentao/issues_controller_spec.rb'
- 'ee/spec/controllers/projects/issues_controller_spec.rb'
- 'ee/spec/controllers/projects/licenses_controller_spec.rb'
- 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'
- 'ee/spec/controllers/projects/mirrors_controller_spec.rb'
- 'ee/spec/controllers/projects/pipelines_controller_spec.rb'
@ -1325,7 +1323,6 @@ Layout/LineLength:
- 'ee/spec/features/projects/feature_flags/user_updates_feature_flag_spec.rb'
- 'ee/spec/features/projects/iterations/iteration_cadences_list_spec.rb'
- 'ee/spec/features/projects/iterations/user_views_iteration_spec.rb'
- 'ee/spec/features/projects/licenses/maintainer_views_policies_spec.rb'
- 'ee/spec/features/projects/members/member_is_removed_from_project_spec.rb'
- 'ee/spec/features/projects/members/member_leaves_project_spec.rb'
- 'ee/spec/features/projects/new_project_spec.rb'
@ -2705,7 +2702,6 @@ Layout/LineLength:
- 'qa/qa/ee/page/group/settings/saml_sso.rb'
- 'qa/qa/ee/page/merge_request/show.rb'
- 'qa/qa/ee/page/project/job/show.rb'
- 'qa/qa/ee/page/project/secure/license_compliance.rb'
- 'qa/qa/ee/page/project/secure/security_dashboard.rb'
- 'qa/qa/ee/page/project/secure/show.rb'
- 'qa/qa/flow/sign_up.rb'

View File

@ -34,7 +34,6 @@ Rails/Pluck:
- 'ee/spec/controllers/operations_controller_spec.rb'
- 'ee/spec/controllers/projects/audit_events_controller_spec.rb'
- 'ee/spec/controllers/projects/feature_flag_issues_controller_spec.rb'
- 'ee/spec/controllers/projects/licenses_controller_spec.rb'
- 'ee/spec/features/projects/new_project_spec.rb'
- 'ee/spec/graphql/api/vulnerabilities_spec.rb'
- 'ee/spec/helpers/ee/geo_helper_spec.rb'

View File

@ -18,7 +18,6 @@ RSpec/AvoidConditionalStatements:
- 'ee/spec/features/labels_hierarchy_spec.rb'
- 'ee/spec/features/profiles/usage_quotas_spec.rb'
- 'ee/spec/features/projects/analytics/visualization_designer_spec.rb'
- 'ee/spec/features/projects/licenses/maintainer_views_policies_spec.rb'
- 'ee/spec/features/projects/merge_requests/user_approves_merge_request_spec.rb'
- 'ee/spec/features/projects/settings/issues_settings_spec.rb'
- 'ee/spec/features/projects_spec.rb'

View File

@ -38,7 +38,6 @@ RSpec/BeforeAllRoleAssignment:
- 'ee/spec/controllers/projects/issue_links_controller_spec.rb'
- 'ee/spec/controllers/projects/iterations_controller_spec.rb'
- 'ee/spec/controllers/projects/learn_gitlab_controller_spec.rb'
- 'ee/spec/controllers/projects/licenses_controller_spec.rb'
- 'ee/spec/controllers/projects/merge_requests_controller_spec.rb'
- 'ee/spec/controllers/projects/pipelines_controller_spec.rb'
- 'ee/spec/controllers/projects/repositories_controller_spec.rb'

View File

@ -3,33 +3,6 @@
Style/RedundantReturn:
Details: grace period
Exclude:
- 'app/controllers/concerns/hotlink_interceptor.rb'
- 'app/controllers/concerns/issuable_collections.rb'
- 'app/controllers/concerns/notes_actions.rb'
- 'app/controllers/concerns/snippet_authorizations.rb'
- 'app/controllers/groups/labels_controller.rb'
- 'app/controllers/groups/milestones_controller.rb'
- 'app/controllers/groups/registry/repositories_controller.rb'
- 'app/controllers/groups/settings/ci_cd_controller.rb'
- 'app/controllers/groups/variables_controller.rb'
- 'app/controllers/import/bitbucket_server_controller.rb'
- 'app/controllers/import/github_controller.rb'
- 'app/controllers/profiles_controller.rb'
- 'app/controllers/projects/application_controller.rb'
- 'app/controllers/projects/artifacts_controller.rb'
- 'app/controllers/projects/blob_controller.rb'
- 'app/controllers/projects/jobs_controller.rb'
- 'app/controllers/projects/labels_controller.rb'
- 'app/controllers/projects/merge_requests/conflicts_controller.rb'
- 'app/controllers/projects/merge_requests/diffs_controller.rb'
- 'app/controllers/projects/merge_requests_controller.rb'
- 'app/controllers/projects/milestones_controller.rb'
- 'app/controllers/projects/notes_controller.rb'
- 'app/controllers/projects/pipeline_schedules_controller.rb'
- 'app/controllers/projects/pipelines_controller.rb'
- 'app/controllers/projects/refs_controller.rb'
- 'app/controllers/projects/snippets/application_controller.rb'
- 'app/controllers/projects/web_ide_terminals_controller.rb'
- 'app/controllers/sent_notifications_controller.rb'
- 'app/controllers/snippets/notes_controller.rb'
- 'app/helpers/profiles_helper.rb'

View File

@ -925,6 +925,7 @@ export default {
<work-item-detail
:key="activeIssuable.iid"
:work-item-iid="activeIssuable.iid"
is-drawer
class="gl-pt-0! work-item-drawer"
@work-item-updated="updateIssuablesCache"
@work-item-emoji-updated="updateIssuableEmojis"

View File

@ -8,12 +8,23 @@ import {
} from '@gitlab/cluster-client';
import { connectionStatus } from '~/environments/graphql/resolvers/kubernetes/constants';
import { updateConnectionStatus } from '~/environments/graphql/resolvers/kubernetes/k8s_connection_status';
import { s__ } from '~/locale';
export const handleClusterError = async (err) => {
if (!err.response) {
throw err;
}
const contentType = err.response.headers.get('Content-Type');
if (contentType !== 'application/json') {
throw new Error(
s__(
'KubernetesDashboard|There was a problem fetching cluster information. Refresh the page and try again.',
),
);
}
const errorData = await err.response.json();
throw errorData;
};

View File

@ -64,7 +64,10 @@ export default {
</script>
<template>
<div data-testid="packages-and-registries-group-settings">
<div
data-testid="packages-and-registries-group-settings"
class="js-hide-when-nothing-matches-search"
>
<gl-alert v-if="alertMessage" variant="warning" class="gl-mt-4" @dismiss="dismissAlert">
{{ alertMessage }}
</gl-alert>

View File

@ -69,7 +69,10 @@ export default {
</script>
<template>
<div data-testid="packages-and-registries-project-settings">
<div
data-testid="packages-and-registries-project-settings"
class="js-hide-when-nothing-matches-search"
>
<metadata-database-alert v-if="!isContainerRegistryMetadataDatabaseEnabled" class="gl-mt-5" />
<gl-alert
v-if="showAlert"

View File

@ -44,6 +44,9 @@ export default {
newReleasePath: {
default: '',
},
atomFeedPath: {
default: '',
},
},
apollo: {
/**
@ -165,6 +168,9 @@ export default {
isFullRequestLoaded() {
return Boolean(!this.isFullRequestLoading && this.fullGraphqlResponse?.data.project);
},
atomFeedBtnTitle() {
return this.$options.i18n.atomFeedBtnTitle;
},
releaseBtnTitle() {
return this.isCatalogResource
? this.$options.i18n.catalogResourceReleaseBtnTitle
@ -289,6 +295,17 @@ export default {
<div v-else class="gl-align-self-end gl-display-flex gl-gap-3">
<releases-sort :value="sort" @input="onSortChanged" />
<gl-button
v-if="atomFeedPath"
v-gl-tooltip.hover
:title="atomFeedBtnTitle"
:href="atomFeedPath"
icon="rss"
class="gl-ml-2"
data-testid="atom-feed-btn"
:aria-label="atomFeedBtnTitle"
/>
<div
v-if="newReleasePath"
v-gl-tooltip.hover
@ -298,6 +315,7 @@ export default {
<gl-button
:disabled="isCatalogResource"
:href="newReleasePath"
class="gl-ml-2"
category="primary"
variant="confirm"
>{{ $options.i18n.newRelease }}</gl-button

View File

@ -56,6 +56,7 @@ export const i18n = {
),
alertInfoPublishMessage: s__('CiCatalog|How do I publish a component?'),
alertTitle: s__('CiCatalog|Publish the CI/CD components in this project to the CI/CD Catalog'),
atomFeedBtnTitle: __('Subscribe to releases RSS feed'),
catalogResourceReleaseBtnTitle: s__(
"CiCatalog|Use the 'release' keyword in a CI/CD job to publish to the CI/CD Catalog.",
),

View File

@ -1,5 +1,6 @@
<script>
import { GlEmptyState, GlSearchBoxByType } from '@gitlab/ui';
import EmptyStateSvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg';
import { escapeRegExp } from 'lodash';
import {
EXCLUDED_NODES,
@ -195,6 +196,7 @@ export default {
},
},
TYPING_DELAY,
EmptyStateSvg,
};
</script>
<template>
@ -210,6 +212,7 @@ export default {
v-if="!hasMatches"
:title="__('No results found')"
:description="__('Edit your search and try again')"
:svg-path="$options.EmptyStateSvg"
/>
</div>
</template>

View File

@ -0,0 +1,5 @@
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = [
'design-detail-layout',
'gl-overflow-hidden',
'gl-m-0',
];

View File

@ -2,6 +2,7 @@
import { GlLoadingIcon, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import { n__, __ } from '~/locale';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { DESIGN_ROUTE_NAME } from '../../constants';
export default {
components: {
@ -116,13 +117,21 @@ export default {
this.imageLoading = true;
},
},
DESIGN_ROUTE_NAME,
};
</script>
<template>
<div class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0">
<router-link
:to="{
name: $options.DESIGN_ROUTE_NAME,
params: { id: filename },
query: $route.query,
}"
class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0"
>
<div
class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base"
class="card-body gl-p-0 gl-flex gl-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base"
>
<div
v-if="icon.name"
@ -140,7 +149,7 @@ export default {
</span>
</div>
<gl-intersection-observer
class="gl-flex-grow-1"
class="gl-grow"
data-testid="design-image"
:data-qa-filename="filename"
@appear="onAppear"
@ -163,11 +172,8 @@ export default {
/>
</gl-intersection-observer>
</div>
<div class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4">
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
data-testid="design-file-name"
>
<div class="card-footer gl-flex gl-w-full gl-bg-white gl-py-3 gl-px-4">
<div class="gl-flex gl-flex-col str-truncated-100">
<span
v-gl-tooltip
class="gl-font-sm str-truncated-100"
@ -179,15 +185,12 @@ export default {
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div>
<div
v-if="notesCount"
class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
>
<div v-if="notesCount" class="gl-ml-auto gl-flex gl-items-center gl-text-gray-500">
<gl-icon name="comments" class="gl-ml-2" />
<span :aria-label="notesLabel" class="gl-font-sm gl-ml-2">
{{ notesCount }}
</span>
</div>
</div>
</div>
</router-link>
</template>

View File

@ -107,6 +107,7 @@ export default {
<design v-bind="design" class="gl-bg-white" :is-uploading="false" />
</li>
</ol>
<router-view :key="$route.fullPath" />
</template>
</widget-wrapper>
</template>

View File

@ -0,0 +1,178 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlAlert } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { fetchPolicies } from '~/lib/graphql';
import { Mousetrap } from '~/lib/mousetrap';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import { WORK_ITEM_ROUTE_NAME } from '../../../constants';
import getDesignQuery from '../graphql/design_details.query.graphql';
import { extractDesign, getPageLayoutElement } from '../utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
import { DESIGN_NOT_FOUND_ERROR, DESIGN_VERSION_NOT_EXIST_ERROR } from '../error_messages';
import DesignPresentation from './design_presentation.vue';
const DEFAULT_SCALE = 1;
const DEFAULT_MAX_SCALE = 2;
export default {
WORK_ITEM_ROUTE_NAME,
components: {
DesignPresentation,
GlAlert,
},
inject: ['fullPath'],
beforeRouteUpdate(to, from, next) {
// reset scale when the active design changes
this.scale = DEFAULT_SCALE;
next();
},
beforeRouteEnter(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
beforeRouteLeave(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
props: {
iid: {
type: String,
required: true,
},
},
data() {
return {
design: {},
annotationCoordinates: null,
errorMessage: '',
scale: DEFAULT_SCALE,
resolvedDiscussionsExpanded: false,
prevCurrentUserTodos: null,
maxScale: DEFAULT_MAX_SCALE,
discussions: [],
workItemId: '',
workItemTitle: '',
};
},
apollo: {
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
// We need this for handling loading state when using frontend cache
notifyOnNetworkStatusChange: true,
variables() {
return this.designVariables;
},
update: (data) => extractDesign(data),
result(res) {
this.onDesignQueryResult(res);
},
error() {
this.onQueryError(DESIGN_NOT_FOUND_ERROR);
},
},
},
computed: {
isLoading() {
return this.$apollo.queries.design.loading && !this.design.id;
},
designVariables() {
return {
fullPath: this.fullPath,
iid: this.iid,
filenames: [this.$route.params.id],
atVersion: this.designsVersion,
};
},
hasValidVersion() {
return this.$route.query.version;
},
designsVersion() {
return this.hasValidVersion
? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`
: null;
},
},
mounted() {
Mousetrap.bind(keysFor(ISSUE_CLOSE_DESIGN), this.closeDesign);
},
methods: {
onDesignQueryResult({ data, loading }) {
// On the initial load with cache-and-network policy data is undefined while loading is true
// To prevent throwing an error, we don't perform any logic until loading is false
if (loading) {
return;
}
if (!data || !extractDesign(data)) {
this.onQueryError(DESIGN_NOT_FOUND_ERROR);
} else if (this.$route.query.version && !this.hasValidVersion) {
this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR);
} else {
const workItem = data.project.workItems.nodes[0];
this.workItemId = workItem.id;
this.workItemTitle = workItem.title;
}
},
onQueryError(message) {
// because we redirect user to work item page,
// we want to create these alerts on the work item page
createAlert({ message });
this.$router.push({ name: this.$options.WORK_ITEM_ROUTE_NAME });
},
onError(message, e) {
this.errorMessage = message;
if (e) throw e;
},
closeDesign() {
this.$router.push({
name: this.$options.WORK_ITEM_ROUTE_NAME,
query: this.$route.query,
});
},
setMaxScale(event) {
this.maxScale = 1 / event;
},
},
};
</script>
<template>
<div
class="design-detail js-design-detail fixed-top gl-w-full gl-flex gl-justify-content-center gl-flex-col gl-lg-flex-direction-row gl-bg-gray-10"
>
<div class="gl-flex gl-overflow-hidden gl-grow gl-flex-col gl-relative">
<div
class="gl-flex gl-overflow-hidden gl-flex-col gl-lg-flex-direction-row gl-grow gl-relative"
>
<div class="gl-flex gl-overflow-hidden gl-flex-grow-2 gl-flex-col gl-relative">
<div v-if="errorMessage" class="gl-p-5">
<gl-alert variant="danger" @dismiss="errorMessage = null">
{{ errorMessage }}
</gl-alert>
</div>
<design-presentation
:image="design.image"
:image-name="design.filename"
:discussions="discussions"
:scale="scale"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:is-loading="isLoading"
disable-commenting
@setMaxScale="setMaxScale"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,321 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import DesignImage from './image.vue';
const CLICK_DRAG_BUFFER_PX = 2;
export default {
components: {
DesignImage,
GlLoadingIcon,
},
props: {
image: {
type: String,
required: false,
default: '',
},
imageName: {
type: String,
required: false,
default: '',
},
discussions: {
type: Array,
required: true,
},
isAnnotating: {
type: Boolean,
required: false,
default: false,
},
scale: {
type: Number,
required: false,
default: 1,
},
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
disableCommenting: {
type: Boolean,
required: true,
},
},
data() {
return {
overlayDimensions: null,
overlayPosition: null,
currentAnnotationPosition: null,
zoomFocalPoint: {
x: 0,
y: 0,
width: 0,
height: 0,
},
initialLoad: true,
lastDragPosition: null,
isDraggingDesign: false,
isLoggedIn: isLoggedIn(),
};
},
computed: {
discussionStartingNotes() {
return this.discussions.map((discussion) => ({
...discussion.notes[0],
index: discussion.index,
}));
},
currentCommentForm() {
return (this.isAnnotating && this.currentAnnotationPosition) || null;
},
presentationStyle() {
return {
cursor: this.isDraggingDesign ? 'grabbing' : undefined,
};
},
},
beforeDestroy() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
presentationViewport.removeEventListener('scroll', this.scrollThrottled, false);
},
mounted() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
this.scrollThrottled = throttle(() => {
this.shiftZoomFocalPoint();
}, 400);
presentationViewport.addEventListener('scroll', this.scrollThrottled, false);
},
methods: {
syncCurrentAnnotationPosition() {
if (!this.currentAnnotationPosition) return;
const widthRatio = this.overlayDimensions.width / this.currentAnnotationPosition.width;
const heightRatio = this.overlayDimensions.height / this.currentAnnotationPosition.height;
const x = this.currentAnnotationPosition.x * widthRatio;
const y = this.currentAnnotationPosition.y * heightRatio;
this.currentAnnotationPosition = this.getAnnotationPosition({ x, y });
},
setOverlayDimensions(overlayDimensions) {
this.overlayDimensions = overlayDimensions;
// every time we set overlay dimensions, we need to
// update the current annotation as well
this.syncCurrentAnnotationPosition();
},
setOverlayPosition() {
if (!this.overlayDimensions) {
this.overlayPosition = {};
}
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
// default to center
this.overlayPosition = {
left: `calc(50% - ${this.overlayDimensions.width / 2}px)`,
top: `calc(50% - ${this.overlayDimensions.height / 2}px)`,
};
// if the overlay overflows, then don't center
if (this.overlayDimensions.width > presentationViewport.offsetWidth) {
this.overlayPosition.left = '0';
}
if (this.overlayDimensions.height > presentationViewport.offsetHeight) {
this.overlayPosition.top = '0';
}
},
/**
* Return a point that represents the center of an
* overflowing child element w.r.t it's parent
*/
getViewportCenter() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return {};
// get height of scroll bars (i.e. the max values for scrollTop, scrollLeft)
const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth;
const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight;
// determine how many child pixels have been scrolled
const xScrollRatio =
presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0;
const yScrollRatio =
presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0;
const xScrollOffset =
(presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio;
const yScrollOffset =
(presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio;
const viewportCenterX = presentationViewport.offsetWidth / 2;
const viewportCenterY = presentationViewport.offsetHeight / 2;
const focalPointX = viewportCenterX + xScrollOffset;
const focalPointY = viewportCenterY + yScrollOffset;
return {
x: focalPointX,
y: focalPointY,
};
},
/**
* Scroll the viewport such that the focal point is positioned centrally
*/
scrollToFocalPoint() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return;
const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2;
const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2;
presentationViewport.scrollTo(scrollX, scrollY);
},
scaleZoomFocalPoint() {
const { x, y, width, height } = this.zoomFocalPoint;
const widthRatio = this.overlayDimensions.width / width;
const heightRatio = this.overlayDimensions.height / height;
this.zoomFocalPoint = {
x: Math.round(x * widthRatio * 100) / 100,
y: Math.round(y * heightRatio * 100) / 100,
...this.overlayDimensions,
};
},
shiftZoomFocalPoint() {
this.zoomFocalPoint = {
...this.getViewportCenter(),
...this.overlayDimensions,
};
},
onImageResize(imageDimensions) {
this.setOverlayDimensions(imageDimensions);
this.setOverlayPosition();
this.$nextTick(() => {
if (this.initialLoad) {
// set focal point on initial load
this.shiftZoomFocalPoint();
this.initialLoad = false;
} else {
this.scaleZoomFocalPoint();
this.scrollToFocalPoint();
}
});
},
getAnnotationPosition(coordinates) {
const { x, y } = coordinates;
const { width, height } = this.overlayDimensions;
return {
x: Math.round(x),
y: Math.round(y),
width: Math.round(width),
height: Math.round(height),
};
},
openCommentForm(coordinates) {
this.currentAnnotationPosition = this.getAnnotationPosition(coordinates);
this.$emit('openCommentForm', this.currentAnnotationPosition);
},
closeCommentForm() {
this.currentAnnotationPosition = null;
this.$emit('closeCommentForm');
},
moveNote({ noteId, discussionId, coordinates }) {
const position = this.getAnnotationPosition(coordinates);
this.$emit('moveNote', { noteId, discussionId, position });
},
onPresentationMousedown({ clientX, clientY }) {
if (!this.isDesignOverflowing()) return;
this.lastDragPosition = {
x: clientX,
y: clientY,
};
},
getDragDelta(clientX, clientY) {
return {
deltaX: this.lastDragPosition.x - clientX,
deltaY: this.lastDragPosition.y - clientY,
};
},
exceedsDragThreshold(clientX, clientY) {
const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
return Math.abs(deltaX) > CLICK_DRAG_BUFFER_PX || Math.abs(deltaY) > CLICK_DRAG_BUFFER_PX;
},
shouldDragDesign(clientX, clientY) {
return (
this.lastDragPosition &&
(this.isDraggingDesign || this.exceedsDragThreshold(clientX, clientY))
);
},
onPresentationMousemove({ clientX, clientY }) {
const { presentationViewport } = this.$refs;
if (!presentationViewport || !this.shouldDragDesign(clientX, clientY)) return;
this.isDraggingDesign = true;
const { scrollLeft, scrollTop } = presentationViewport;
const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY);
this.lastDragPosition = {
x: clientX,
y: clientY,
};
},
onPresentationMouseup() {
this.lastDragPosition = null;
this.isDraggingDesign = false;
},
isDesignOverflowing() {
const { presentationViewport } = this.$refs;
if (!presentationViewport) return false;
return (
presentationViewport.scrollWidth > presentationViewport.offsetWidth ||
presentationViewport.scrollHeight > presentationViewport.offsetHeight
);
},
},
};
</script>
<template>
<div
ref="presentationViewport"
class="gl-h-full gl-w-full gl-p-5 overflow-auto gl-relative"
:style="presentationStyle"
@mousedown="onPresentationMousedown"
@mousemove="onPresentationMousemove"
@mouseup="onPresentationMouseup"
@mouseleave="onPresentationMouseup"
@touchstart="onPresentationMousedown"
@touchmove="onPresentationMousemove"
@touchend="onPresentationMouseup"
@touchcancel="onPresentationMouseup"
>
<gl-loading-icon v-if="isLoading" size="xl" class="gl-flex gl-h-full gl-items-center" />
<div v-else class="gl-h-full gl-w-full gl-flex gl-items-center gl-relative">
<design-image
v-if="image"
:image="image"
:name="imageName"
:scale="scale"
@resize="onImageResize"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,157 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
import { DESIGN_MARK_APP_START, DESIGN_MAIN_IMAGE_OUTPUT } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
export default {
components: {
GlIcon,
},
props: {
image: {
type: String,
required: false,
default: '',
},
name: {
type: String,
required: false,
default: '',
},
scale: {
type: Number,
required: false,
default: 1,
},
},
data() {
return {
baseImageSize: null,
imageStyle: null,
imageError: false,
};
},
watch: {
scale(val) {
this.zoom(val);
},
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
},
mounted() {
if (!this.image) {
this.onImgLoad();
}
this.resizeThrottled = throttle(() => {
// NOTE: if imageStyle is set, then baseImageSize
// won't change due to resize. We must still emit a
// `resize` event so that the parent can handle
// resizes appropriately (e.g. for design_overlay)
this.setBaseImageSize();
}, 400);
window.addEventListener('resize', this.resizeThrottled, false);
},
methods: {
onImgLoad() {
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
requestIdleCallback(this.setImageNaturalScale, { timeout: 1000 });
performanceMarkAndMeasure({
measures: [
{
name: DESIGN_MAIN_IMAGE_OUTPUT,
start: DESIGN_MARK_APP_START,
},
],
});
},
onImgError() {
this.imageError = true;
},
setBaseImageSize() {
const { contentImg } = this.$refs;
if (!contentImg) return;
if (contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) {
this.baseImageSize = {
height: contentImg.naturalHeight,
width: contentImg.naturalWidth,
};
} else {
this.baseImageSize = {
height: contentImg.offsetHeight,
width: contentImg.offsetWidth,
};
}
this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
},
setImageNaturalScale() {
const { contentImg } = this.$refs;
if (!contentImg) {
return;
}
const { naturalHeight, naturalWidth } = contentImg;
// In case image 404s
if (naturalHeight === 0 || naturalWidth === 0) {
return;
}
const { height, width } = this.baseImageSize;
this.imageStyle = {
width: `${width}px`,
height: `${height}px`,
};
this.$parent.$emit(
'setMaxScale',
Math.round(((height + width) / (naturalHeight + naturalWidth)) * 100) / 100,
);
},
onResize({ width, height }) {
this.$emit('resize', { width, height });
},
zoom(amount) {
if (amount === 1) {
this.imageStyle = null;
this.$nextTick(() => {
this.setBaseImageSize();
});
return;
}
const width = this.baseImageSize.width * amount;
const height = this.baseImageSize.height * amount;
this.imageStyle = {
width: `${width}px`,
height: `${height}px`,
};
this.onResize({ width, height });
},
},
};
</script>
<template>
<div class="gl-mx-auto gl-my-auto js-design-image">
<gl-icon v-if="imageError" class="gl-text-gray-200" name="media-broken" :size="48" />
<img
v-show="!imageError"
ref="contentImg"
class="gl-max-h-full gl-border"
:src="image"
:alt="name"
:style="imageStyle"
:class="{ 'img-fluid': !imageStyle }"
@error="onImgError"
@load="onImgLoad"
/>
</div>
</template>

View File

@ -115,9 +115,9 @@ export default {
@select="routeToVersion"
>
<template #list-item="{ item }">
<span class="gl-display-flex gl-align-items-center">
<span class="gl-flex gl-items-center">
<gl-avatar :alt="getAuthorName(item.author)" :size="32" :src="getAvatarUrl(item)" />
<span class="gl-display-flex gl-flex-direction-column">
<span class="gl-flex gl-flex-col">
<span class="gl-font-weight-bold">{{ versionText(item) }}</span>
<span v-if="item.author" class="gl-text-gray-600 gl-mt-1">
<span class="gl-display-block">{{ getAuthorName(item.author) }}</span>

View File

@ -0,0 +1,5 @@
import { __ } from '~/locale';
export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.');
export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.');

View File

@ -0,0 +1,30 @@
#import "./fragments/design_file.fragment.graphql"
query getDesignDetails(
$fullPath: ID!
$iid: String!
$atVersion: DesignManagementVersionID
$filenames: [String!]
) {
project(fullPath: $fullPath) {
id
workItems(iid: $iid) {
nodes {
id
title
widgets {
... on WorkItemWidgetDesigns {
type
designCollection {
designs(atVersion: $atVersion, filenames: $filenames) {
nodes {
...DesignFile
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,16 @@
fragment DesignFile on Design {
id
event
filename
notesCount
image
imageV432x230
description
descriptionHtml
fullPath
currentUserTodos(state: pending) {
nodes {
id
}
}
}

View File

@ -1 +1,10 @@
import { findDesignWidget } from '../../utils';
export const findVersionId = (id) => (id.match('::Version/(.+$)') || [])[1];
export const extractDesigns = (data) =>
findDesignWidget(data.project.workItems.nodes[0].widgets).designCollection.designs.nodes;
export const extractDesign = (data) => (extractDesigns(data) || [])[0];
export const getPageLayoutElement = () => document.querySelector('.layout-page');

View File

@ -95,6 +95,11 @@ export default {
required: false,
default: '',
},
isDrawer: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@ -561,6 +566,7 @@ export default {
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@promotedToObjective="$emit('promotedToObjective', workItemIid)"
@toggleEditMode="enableEditMode"
/>
<div data-testid="work-item-overview" class="work-item-overview">
<section>
@ -585,7 +591,10 @@ export default {
@error="updateError = $event"
@emoji-updated="$emit('work-item-emoji-updated', $event)"
/>
<design-widget v-if="!workItemLoading && hasDesignWidget" :work-item-id="workItem.id" />
<design-widget
v-if="!workItemLoading && !isDrawer && hasDesignWidget"
:work-item-id="workItem.id"
/>
</section>
<aside
data-testid="work-item-overview-right-sidebar"

View File

@ -1,11 +1,12 @@
<script>
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import { GlLoadingIcon, GlIntersectionObserver, GlButton, GlLink } from '@gitlab/ui';
import LockedBadge from '~/issuable/components/locked_badge.vue';
import { WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { isNotesWidget } from '../utils';
import WorkItemActions from './work_item_actions.vue';
import WorkItemTodos from './work_item_todos.vue';
import WorkItemStateBadge from './work_item_state_badge.vue';
export default {
components: {
@ -15,6 +16,9 @@ export default {
WorkItemActions,
WorkItemTodos,
ConfidentialityBadge,
WorkItemStateBadge,
GlButton,
GlLink,
},
props: {
workItem: {
@ -78,6 +82,9 @@ export default {
projectFullPath() {
return this.workItem.namespace?.fullPath;
},
workItemState() {
return this.workItem.state;
},
},
WORKSPACE_PROJECT,
};
@ -95,18 +102,33 @@ export default {
data-testid="work-item-sticky-header"
>
<div
class="work-item-sticky-header-text gl-align-items-center gl-mx-auto gl-px-6 gl-display-flex gl-gap-3"
class="work-item-sticky-header-text gl-items-center gl-mx-auto gl-px-5 xl:gl-px-6 gl-flex gl-gap-3"
>
<span class="gl-text-truncate gl-font-weight-bold gl-pr-3 gl-mr-auto">
{{ workItem.title }}
</span>
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
<gl-loading-icon v-if="updateInProgress" />
<confidentiality-badge
v-if="workItem.confidential"
:issuable-type="workItemType"
:workspace-type="$options.WORKSPACE_PROJECT"
hide-text-in-small-screens
/>
<locked-badge v-if="isDiscussionLocked" :issuable-type="workItemType" />
<gl-link
class="gl-truncate gl-block gl-font-bold gl-pr-3 gl-mr-auto gl-text-black"
href="#top"
:title="workItem.title"
>
{{ workItem.title }}
</gl-link>
<gl-button
v-if="canUpdate"
category="secondary"
data-testid="work-item-edit-button-sticky"
class="shortcut-edit-wi-description"
@click="$emit('toggleEditMode')"
>
{{ __('Edit') }}
</gl-button>
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"

View File

@ -50,6 +50,9 @@ export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
export const WORK_ITEM_TITLE_MAX_LENGTH = 255;
export const WORK_ITEM_ROUTE_NAME = 'workItem';
export const DESIGN_ROUTE_NAME = 'design';
export const i18n = {
fetchErrorTitle: s__('WorkItem|Work item not found'),
fetchError: s__(

View File

@ -1,5 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { DESIGN_MARK_APP_START, DESIGN_MEASURE_BEFORE_APP } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items';
@ -56,6 +58,16 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => {
newCommentTemplatePaths: JSON.parse(newCommentTemplatePaths),
reportAbusePath,
},
mounted() {
performanceMarkAndMeasure({
mark: DESIGN_MARK_APP_START,
measures: [
{
name: DESIGN_MEASURE_BEFORE_APP,
},
],
});
},
render(createElement) {
return createElement(App, {
props: {

View File

@ -1,3 +1,6 @@
import DesignDetail from '../components/design_management/design_preview/design_details.vue';
import { DESIGN_ROUTE_NAME } from '../constants';
function getRoutes() {
const routes = [
{
@ -5,6 +8,19 @@ function getRoutes() {
name: 'workItem',
component: () => import('../pages/work_item_root.vue'),
props: true,
children: [
{
name: DESIGN_ROUTE_NAME,
path: 'designs/:id',
component: DesignDetail,
beforeEnter({ params: { id } }, _, next) {
if (typeof id === 'string') {
next();
}
},
props: ({ params: { id, iid } }) => ({ id, iid }),
},
],
},
];

View File

@ -4,7 +4,7 @@ module HotlinkInterceptor
extend ActiveSupport::Concern
def intercept_hotlinking!
return render_406 if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
render_406 if Gitlab::HotlinkingDetector.intercept_hotlinking?(request)
end
private

View File

@ -19,7 +19,7 @@ module IssuableCollections
@issuables = issuables_collection
set_pagination
return if redirect_out_of_range(@issuables, @total_pages)
nil if redirect_out_of_range(@issuables, @total_pages)
end
def set_pagination

View File

@ -211,7 +211,7 @@ module NotesActions
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
access_denied! unless can?(current_user, :admin_note, note)
end
def create_note_params

View File

@ -6,18 +6,18 @@ module SnippetAuthorizations
private
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_snippet, snippet)
render_404 unless can?(current_user, :read_snippet, snippet)
end
def authorize_update_snippet!
return render_404 unless can?(current_user, :update_snippet, snippet)
render_404 unless can?(current_user, :update_snippet, snippet)
end
def authorize_admin_snippet!
return render_404 unless can?(current_user, :admin_snippet, snippet)
render_404 unless can?(current_user, :admin_snippet, snippet)
end
def authorize_create_snippet!
return render_404 unless can?(current_user, :create_snippet)
render_404 unless can?(current_user, :create_snippet)
end
end

View File

@ -77,15 +77,15 @@ class Groups::LabelsController < Groups::ApplicationController
protected
def authorize_group_for_admin_labels!
return render_404 unless can?(current_user, :admin_label, @group)
render_404 unless can?(current_user, :admin_label, @group)
end
def authorize_label_for_admin_label!
return render_404 unless can?(current_user, :admin_label, @label)
render_404 unless can?(current_user, :admin_label, @label)
end
def authorize_read_labels!
return render_404 unless can?(current_user, :read_label, @group)
render_404 unless can?(current_user, :read_label, @group)
end
def label

View File

@ -93,7 +93,7 @@ class Groups::MilestonesController < Groups::ApplicationController
private
def authorize_admin_milestones!
return render_404 unless can?(current_user, :admin_milestone, group)
render_404 unless can?(current_user, :admin_milestone, group)
end
def milestone_params

View File

@ -46,7 +46,7 @@ module Groups
end
def authorize_read_container_image!
return render_404 unless can?(current_user, :read_container_image, group)
render_404 unless can?(current_user, :read_container_image, group)
end
end
end

View File

@ -104,7 +104,8 @@ class Import::BitbucketServerController < Import::BaseController
return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present?
return render_validation_error('Missing repository slug') unless @repo_slug.present?
return render_validation_error('Invalid project key') unless VALID_BITBUCKET_PROJECT_CHARS.match?(@project_key)
return render_validation_error('Invalid repository slug') unless VALID_BITBUCKET_CHARS.match?(@repo_slug)
render_validation_error('Invalid repository slug') unless VALID_BITBUCKET_CHARS.match?(@repo_slug)
end
def render_validation_error(message)

View File

@ -166,7 +166,7 @@ class Import::GithubController < Import::BaseController
end
def authorize_owner_access!
return render_404 unless current_user.can?(:owner_access, project)
render_404 unless current_user.can?(:owner_access, project)
end
def import_params

View File

@ -67,7 +67,7 @@ class ProfilesController < Profiles::ApplicationController
end
def authorize_change_username!
return render_404 unless @user.can_change_username?
render_404 unless @user.can_change_username?
end
def username_param

View File

@ -91,7 +91,7 @@ class Projects::ApplicationController < ApplicationController
end
def check_issues_available!
return render_404 unless @project.feature_available?(:issues, current_user)
render_404 unless @project.feature_available?(:issues, current_user)
end
def set_is_ambiguous_ref

View File

@ -187,7 +187,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def authorize_read_job_artifacts!
return access_denied! unless can?(current_user, :read_job_artifacts, job_artifact)
access_denied! unless can?(current_user, :read_job_artifacts, job_artifact)
end
end

View File

@ -161,7 +161,7 @@ class Projects::BlobController < Projects::ApplicationController
def commit
@commit ||= @repository.commit(@ref)
return render_404 unless @commit
render_404 unless @commit
end
def redirect_renamed_default_branch?

View File

@ -188,27 +188,27 @@ class Projects::JobsController < Projects::ApplicationController
attr_reader :build
def authorize_read_build_report_results!
return access_denied! unless can?(current_user, :read_build_report_results, build)
access_denied! unless can?(current_user, :read_build_report_results, build)
end
def authorize_update_build!
return access_denied! unless can?(current_user, :update_build, @build)
access_denied! unless can?(current_user, :update_build, @build)
end
def authorize_cancel_build!
return access_denied! unless can?(current_user, :cancel_build, @build)
access_denied! unless can?(current_user, :cancel_build, @build)
end
def authorize_erase_build!
return access_denied! unless can?(current_user, :erase_build, @build)
access_denied! unless can?(current_user, :erase_build, @build)
end
def authorize_use_build_terminal!
return access_denied! unless can?(current_user, :create_build_terminal, @build)
access_denied! unless can?(current_user, :create_build_terminal, @build)
end
def authorize_create_proxy_build!
return access_denied! unless can?(current_user, :create_build_service_proxy, @build)
access_denied! unless can?(current_user, :create_build_service_proxy, @build)
end
def verify_api_request!

View File

@ -185,10 +185,10 @@ class Projects::LabelsController < Projects::ApplicationController
end
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
render_404 unless can?(current_user, :admin_label, @project)
end
def authorize_admin_group_labels!
return render_404 unless can?(current_user, :admin_label, @project.group)
render_404 unless can?(current_user, :admin_label, @project.group)
end
end

View File

@ -80,7 +80,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
def authorize_can_resolve_conflicts!
@conflicts_list = ::MergeRequests::Conflicts::ListService.new(@merge_request)
return render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
render_404 unless @conflicts_list.can_be_resolved_by?(current_user)
end
def serializer

View File

@ -133,7 +133,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def define_diff_vars
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
@compare = commit || find_merge_request_diff_compare
return render_404 unless @compare
render_404 unless @compare
end
# rubocop: disable CodeReuse/ActiveRecord

View File

@ -171,11 +171,11 @@ class Projects::MilestonesController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def authorize_admin_milestone!
return render_404 unless can?(current_user, :admin_milestone, @project)
render_404 unless can?(current_user, :admin_milestone, @project)
end
def authorize_promote_milestone!
return render_404 unless can?(current_user, :admin_milestone, project_group)
render_404 unless can?(current_user, :admin_milestone, project_group)
end
def milestone_params

View File

@ -104,11 +104,11 @@ class Projects::NotesController < Projects::ApplicationController
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
access_denied! unless can?(current_user, :admin_note, note)
end
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
access_denied! unless can?(current_user, :resolve_note, note)
end
def authorize_create_note!

View File

@ -105,18 +105,18 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
end
def authorize_create_pipeline_schedule!
return access_denied! unless can?(current_user, :create_pipeline_schedule, new_schedule)
access_denied! unless can?(current_user, :create_pipeline_schedule, new_schedule)
end
def authorize_play_pipeline_schedule!
return access_denied! unless can?(current_user, :play_pipeline_schedule, schedule)
access_denied! unless can?(current_user, :play_pipeline_schedule, schedule)
end
def authorize_update_pipeline_schedule!
return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
access_denied! unless can?(current_user, :update_pipeline_schedule, schedule)
end
def authorize_admin_pipeline_schedule!
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end
end

View File

@ -315,15 +315,15 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def authorize_update_pipeline!
return access_denied! unless can?(current_user, :update_pipeline, @pipeline)
access_denied! unless can?(current_user, :update_pipeline, @pipeline)
end
def authorize_cancel_pipeline!
return access_denied! unless can?(current_user, :cancel_pipeline, @pipeline)
access_denied! unless can?(current_user, :cancel_pipeline, @pipeline)
end
def authorize_read_build_on_pipeline!
return access_denied! unless can?(current_user, :read_build, @pipeline)
access_denied! unless can?(current_user, :read_build, @pipeline)
end
def limited_pipelines_count(project, scope = nil)

View File

@ -68,7 +68,7 @@ class Projects::RefsController < Projects::ApplicationController
end
def validate_ref_id
return not_found if permitted_params[:id].present? && permitted_params[:id] !~ Gitlab::PathRegex.git_reference_regex
not_found if permitted_params[:id].present? && permitted_params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
def permitted_params

View File

@ -9,6 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :authorize_create_release!, only: :new
before_action :validate_suffix_path, :fetch_latest_tag, only: :latest_permalink
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:downloads]) do
authenticate_sessionless_user!(:download)
end
@ -24,6 +25,10 @@ class Projects::ReleasesController < Projects::ApplicationController
format.json do
render json: ReleaseSerializer.new.represent(releases)
end
format.atom do
@releases = releases
render layout: 'xml'
end
end
end

View File

@ -12,7 +12,7 @@ class Projects::Snippets::ApplicationController < Projects::ApplicationControlle
# because ProjectSnippets are checked against the project rather
# than the user
def authorize_create_snippet!
return render_404 unless can?(current_user, :create_snippet, project)
render_404 unless can?(current_user, :create_snippet, project)
end
def snippet_klass

View File

@ -68,7 +68,7 @@ class Projects::WebIdeTerminalsController < Projects::ApplicationController
private
def authorize_create_web_ide_terminal!
return access_denied! unless can?(current_user, :create_web_ide_terminal, project)
access_denied! unless can?(current_user, :create_web_ide_terminal, project)
end
def authorize_read_web_ide_terminal!
@ -80,7 +80,7 @@ class Projects::WebIdeTerminalsController < Projects::ApplicationController
end
def authorize_build_ability!(ability)
return access_denied! unless can?(current_user, ability, build)
access_denied! unless can?(current_user, ability, build)
end
def build

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
module Resolvers
module Analytics
module CycleAnalytics
module ValueStreams
class StageMetricsResolver < BaseResolver
type ::Types::Analytics::CycleAnalytics::ValueStreams::StageMetricsType, null: true
argument :timeframe, Types::TimeframeInputType,
required: true,
description: 'Aggregation timeframe. Filters the issue or the merge request creation time for FOSS ' \
'projects, and the end event timestamp for licensed projects or groups.'
argument :assignee_usernames, [GraphQL::Types::String],
required: false,
description: 'Usernames of users assigned to the issue or the merge request.'
argument :author_username, GraphQL::Types::String,
required: false,
description: 'Username of the author of the issue or the merge request.'
argument :milestone_title, GraphQL::Types::String,
required: false,
description: 'Milestone applied to the issue or the merge request.'
argument :label_names, [GraphQL::Types::String],
required: false,
description: 'Labels applied to the issue or the merge request.'
def resolve(**args)
formatted_args = args.to_hash
timeframe = args.delete(:timeframe)
formatted_args[:created_after] = timeframe[:start]
formatted_args[:created_before] = timeframe[:end]
if formatted_args[:assignee_usernames].present?
formatted_args[:assignee_username] =
formatted_args.delete(:assignee_usernames)
end
formatted_args[:label_name] = formatted_args.delete(:label_names) if formatted_args[:label_names].present?
params = Gitlab::Analytics::CycleAnalytics::RequestParams.new(
namespace: object.namespace,
current_user: current_user,
**formatted_args.compact
)
Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: object, params: params.to_data_collector_params)
end
end
end
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module Types
module Analytics
module CycleAnalytics
module ValueStreams
# rubocop: disable Graphql/AuthorizeTypes -- # Already authorized in parent value stream type.
class StageMetricsType < BaseObject
graphql_name 'ValueStreamStageMetrics'
field :average,
::Types::Analytics::CycleAnalytics::MetricType,
description: 'Average duration in seconds.'
field :count,
::Types::Analytics::CycleAnalytics::MetricType,
description: 'Limited item count. The backend counts maximum 1000 items, ' \
'for free projects, and maximum 10,000 items for licensed ' \
'projects or licensed groups.'
field :median,
::Types::Analytics::CycleAnalytics::MetricType,
description: 'Median duration in seconds.'
def count
{
value: object.count,
identifier: 'value_stream_stage_count',
title: s_('CycleAnalytics|Item count')
}
end
def average
{
value: object.average.seconds,
identifier: 'value_stream_stage_average',
title: s_('CycleAnalytics|Average duration'),
unit: s_('CycleAnalytics|seconds')
}
end
def median
{
value: object.median.seconds,
identifier: 'value_stream_stage_median',
title: s_('CycleAnalytics|Median duration'),
unit: s_('CycleAnalytics|seconds')
}
end
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
end

View File

@ -48,6 +48,12 @@ module Types
null: false,
description: 'HTML description of the end event.'
field :metrics,
Types::Analytics::CycleAnalytics::ValueStreams::StageMetricsType,
null: false,
resolver: Resolvers::Analytics::CycleAnalytics::ValueStreams::StageMetricsResolver,
description: 'Aggregated metrics for the given stage'
def start_event_identifier
events_enum[object.start_event_identifier]
end

View File

@ -23,12 +23,18 @@ module Types
field :name, GraphQL::Types::String, null: false, description: 'Name of the package.'
field :package_protection_rule_exists, GraphQL::Types::Boolean,
null: false,
alpha: { milestone: '16.11' },
deprecated: { reason: 'Use `protectionRuleExists`', milestone: '17.0' },
description:
'Whether any matching package protection rule exists for this package. ' \
'Available only when feature flag `packages_protected_packages` is enabled.'
field :package_type, Types::Packages::PackageTypeEnum, null: false, description: 'Package type.'
field :project, Types::ProjectType, null: false, description: 'Project where the package is stored.'
field :protection_rule_exists, GraphQL::Types::Boolean,
null: false,
alpha: { milestone: '17.0' },
description:
'Whether any matching package protection rule exists for this package. ' \
'Available only when feature flag `packages_protected_packages` is enabled.'
field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
field :status_message, GraphQL::Types::String, null: true, description: 'Status message.'
field :tags, Types::Packages::PackageTagType.connection_type, null: true, description: 'Package tags.'
@ -39,12 +45,14 @@ module Types
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
def package_protection_rule_exists
def protection_rule_exists
return false if Feature.disabled?(:packages_protected_packages, object.project)
object.matching_package_protection_rules.exists?
end
alias_method :package_protection_rule_exists, :protection_rule_exists
# NOTE: This method must be kept in sync with the union
# type: `Types::Packages::MetadataType`.
#

View File

@ -21,7 +21,8 @@ module ReleasesHelper
project_id: @project.id,
project_path: @project.full_path,
illustration_path: illustration,
documentation_path: releases_help_page_path
documentation_path: releases_help_page_path,
atom_feed_path: project_releases_path(@project, rss_url_options)
}.tap do |data|
if can?(current_user, :create_release, @project)
data[:new_release_path] = new_project_release_path(@project)

View File

@ -3,7 +3,6 @@
class SentNotification < ApplicationRecord
include IgnorableColumns
ignore_column %i[id_convert_to_bigint], remove_with: '17.0', remove_after: '2024-04-19'
belongs_to :project
belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :recipient, class_name: "User"

View File

@ -23,28 +23,18 @@ module Issuable
issuable.assignees.each(&:invalidate_cache_counts)
end
def group_for(issuable)
if issuable.project.present?
issuable.project.group
else
issuable.namespace
end
end
def delete_associated_records(issuable)
actor = group_for(issuable)
delete_todos(actor, issuable)
delete_label_links(actor, issuable)
delete_todos(issuable)
delete_label_links(issuable)
end
def delete_todos(actor, issuable)
def delete_todos(issuable)
issuable.run_after_commit_or_now do
TodosDestroyer::DestroyedIssuableWorker.perform_async(issuable.id, issuable.class.name)
end
end
def delete_label_links(actor, issuable)
def delete_label_links(issuable)
issuable.run_after_commit_or_now do
Issuable::LabelLinksDestroyWorker.perform_async(issuable.id, issuable.class.name)
end

View File

@ -7,7 +7,7 @@
- if can?(current_user, :admin_project, @project)
= render Pajamas::AlertComponent.new(title: _('GitLab Pages has moved'),
alert_options: { class: 'gl-my-5', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
alert_options: { class: 'gl-my-5 js-hide-when-nothing-matches-search', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
- c.with_body do
= _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deploy > Pages', project_pages_path(@project)).html_safe}

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
release_url = project_release_url(@project, tag: release.tag)
author_email = Gitlab::SafeRequestStore.fetch([:release_author_email, release.author.email]) do
release.author&.public_email || release.author&.email
end
xml.entry do
xml.id release_url
xml.link href: release_url
xml.title truncate(release.name, length: 160)
xml.summary strip_signature(release.commit.message)
xml.content markdown_field(release, :description), type: 'html'
xml.updated release.updated_at.xmlschema
xml.published release.released_at.xmlschema
xml.author do
xml.name release.author&.name
xml.email author_email
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
xml.title "#{@project.name} releases"
xml.link href: project_releases_url(@project, rss_url_options), rel: "self", type: "application/atom+xml"
xml.link href: project_releases_url(@project), rel: "alternate", type: "text/html"
xml.id project_releases_url(@project)
xml.updated @releases.latest.updated_at.xmlschema if @releases.any?
xml << render(partial: 'release', collection: @releases) if @releases.any?

View File

@ -3,4 +3,7 @@
- if use_startup_query_for_index_page?
- add_page_startup_graphql_call('releases/all_releases', index_page_startup_query_variables)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_releases_path(@project, rss_url_options), title: "#{@project.name} releases")
#js-releases-page{ data: data_for_releases_page }

View File

@ -10,12 +10,12 @@
%span= _("in")
.gl-display-inline-block
#js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } }
%span= s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
%span= safe_format(s_('SearchCodeResults|of %{link_to_project}'), link_to_project: link_to_project)
- else
= _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
= safe_format(_("in project %{link_to_project}"), link_to_project: link_to_project)
- elsif @group
- link_to_group = link_to(@group.name, @group, class: 'ml-md-1')
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= safe_format(_("in group %{link_to_group}"), link_to_group: link_to_group)
.gl-flex.gl-gap-3.gl-mt-3.gl-sm-mt-0
= render Pajamas::ButtonComponent.new(category: 'primary', icon: 'filter', button_options: {id: 'js-open-mobile-filters', class: 'gl-lg-display-none gl-flex-grow-1 gl-md-flex-grow-0'}) do
= s_('GlobalSearch|Filters')

View File

@ -2,7 +2,7 @@
= render Pajamas::AlertComponent.new(title: _('Slack notifications integration is deprecated'),
variant: :warning,
dismissible: false,
alert_options: { class: 'gl-mt-5', data: { testid: "slack-notifications-deprecation" } }) do |c|
alert_options: { class: 'gl-mt-5 js-hide-when-nothing-matches-search', data: { testid: "slack-notifications-deprecation" } }) do |c|
- c.with_body do
- help_page_link = help_page_url('user/project/integrations/gitlab_slack_application')
- learn_more_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_link }
@ -12,7 +12,7 @@
= render Pajamas::AlertComponent.new(title: _('Slack notifications will be deprecated'),
variant: :warning,
dismissible: false,
alert_options: { class: 'gl-mt-5', data: { testid: "slack-notifications-deprecation" } }) do |c|
alert_options: { class: 'gl-mt-5 js-hide-when-nothing-matches-search', data: { testid: "slack-notifications-deprecation" } }) do |c|
- c.with_body do
- help_page_link = help_page_url('user/project/integrations/gitlab_slack_application')
- learn_more_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_link }

View File

@ -11,7 +11,12 @@ class ObjectStoreSettings
# endpoints. Technically dependency_proxy and terraform_state fall
# into this category, but they will likely be handled by Workhorse in
# the future.
WORKHORSE_ACCELERATED_TYPES = SUPPORTED_TYPES - %w[pages]
#
# ci_secure_files doesn't support Workhorse yet
# (https://gitlab.com/gitlab-org/gitlab/-/issues/461124), and it was
# introduced first as a storage-specific setting. To avoid breaking
# consolidated settings for other object types, exclude it here.
WORKHORSE_ACCELERATED_TYPES = SUPPORTED_TYPES - %w[pages ci_secure_files]
# pages and ci_secure_files may be enabled but use legacy disk storage
# we don't need to raise an error in that case

View File

@ -360,6 +360,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :import_csv
post 'import_csv/authorize', to: 'work_items#authorize'
end
member do
get '/designs(/*vueroute)', to: 'work_items#show', as: :designs, format: false
end
end
post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create'

View File

@ -1,15 +1,15 @@
- title: "Running a single database is deprecated"
removal_milestone: "18.0"
removal_milestone: "19.0"
announcement_milestone: "16.1"
breaking_change: true
reporter: lohrc
stage: data_stores
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/411239
body: |
From GitLab 18.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
From GitLab 19.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
We recommend running both databases on the same Postgres instance(s) due to ease of management for most deployments.
This change provides additional scalability for the largest of GitLab instances, like GitLab.com.
This change applies to all installation methods: Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
Before upgrading to GitLab 18.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
Before upgrading to GitLab 19.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
documentation_url: https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html

View File

@ -137,3 +137,13 @@ file and selecting the Git tag that correlates with your target GitLab version
(for example `15.0.5+ee.0`). If required by your load balancer, you can then define
[custom SSL ciphers](https://docs.gitlab.com/omnibus/settings/ssl/index.html#use-custom-ssl-ciphers)
for NGINX.
### Some pages and links are downloaded instead of rendered in the browser
Some GitLab features require the use of WebSockets. In some scenarios where WebSockets support is not enabled on your load balancer, you could experience some links or pages downloading instead of being rendered in the browser. The files downloaded may contain content that look like the following:
```plaintext
One or more reserved bits are on: reserved1 = 1, reserved2 = 0, reserved3 = 0
```
Your load balancer must be capable of supporting HTTP WebSocket requests. If links are downloading this way, check your load balancer configuration and ensure that HTTP WebSocket requests are enabled.

View File

@ -25904,10 +25904,11 @@ Represents a package with pipelines in the Package Registry.
| <a id="packageid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. |
| <a id="packagemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
| <a id="packagename"></a>`name` | [`String!`](#string) | Name of the package. |
| <a id="packagepackageprotectionruleexists"></a>`packageProtectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Whether any matching package protection rule exists for this package. Available only when feature flag `packages_protected_packages` is enabled. |
| <a id="packagepackageprotectionruleexists"></a>`packageProtectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in GitLab 17.0. Use `protectionRuleExists`. |
| <a id="packagepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. Max page size 20. (see [Connections](#connections)) |
| <a id="packageproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packageprotectionruleexists"></a>`protectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 17.0. **Status**: Experiment. Whether any matching package protection rule exists for this package. Available only when feature flag `packages_protected_packages` is enabled. |
| <a id="packagestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagestatusmessage"></a>`statusMessage` | [`String`](#string) | Status message. |
| <a id="packagetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
@ -25928,9 +25929,10 @@ Represents a package in the Package Registry.
| <a id="packagebaseid"></a>`id` | [`PackagesPackageID!`](#packagespackageid) | ID of the package. |
| <a id="packagebasemetadata"></a>`metadata` | [`PackageMetadata`](#packagemetadata) | Package metadata. |
| <a id="packagebasename"></a>`name` | [`String!`](#string) | Name of the package. |
| <a id="packagebasepackageprotectionruleexists"></a>`packageProtectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Whether any matching package protection rule exists for this package. Available only when feature flag `packages_protected_packages` is enabled. |
| <a id="packagebasepackageprotectionruleexists"></a>`packageProtectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in GitLab 17.0. Use `protectionRuleExists`. |
| <a id="packagebasepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagebaseproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagebaseprotectionruleexists"></a>`protectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 17.0. **Status**: Experiment. Whether any matching package protection rule exists for this package. Available only when feature flag `packages_protected_packages` is enabled. |
| <a id="packagebasestatus"></a>`status` | [`PackageStatus!`](#packagestatus) | Package status. |
| <a id="packagebasestatusmessage"></a>`statusMessage` | [`String`](#string) | Status message. |
| <a id="packagebasetags"></a>`tags` | [`PackageTagConnection`](#packagetagconnection) | Package tags. (see [Connections](#connections)) |
@ -25998,10 +26000,11 @@ Represents a package details in the Package Registry.
| <a id="packagedetailstypenpmurl"></a>`npmUrl` | [`String`](#string) | Url of the NPM project endpoint. |
| <a id="packagedetailstypenugeturl"></a>`nugetUrl` | [`String`](#string) | Url of the Nuget project endpoint. |
| <a id="packagedetailstypepackagefiles"></a>`packageFiles` | [`PackageFileConnection`](#packagefileconnection) | Package files. (see [Connections](#connections)) |
| <a id="packagedetailstypepackageprotectionruleexists"></a>`packageProtectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 16.11. **Status**: Experiment. Whether any matching package protection rule exists for this package. Available only when feature flag `packages_protected_packages` is enabled. |
| <a id="packagedetailstypepackageprotectionruleexists"></a>`packageProtectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in GitLab 17.0. Use `protectionRuleExists`. |
| <a id="packagedetailstypepackagetype"></a>`packageType` | [`PackageTypeEnum!`](#packagetypeenum) | Package type. |
| <a id="packagedetailstypepipelines"></a>`pipelines` | [`PipelineConnection`](#pipelineconnection) | Pipelines that built the package. Max page size 20. (see [Connections](#connections)) |
| <a id="packagedetailstypeproject"></a>`project` | [`Project!`](#project) | Project where the package is stored. |
| <a id="packagedetailstypeprotectionruleexists"></a>`protectionRuleExists` **{warning-solid}** | [`Boolean!`](#boolean) | **Introduced** in GitLab 17.0. **Status**: Experiment. Whether any matching package protection rule exists for this package. Available only when feature flag `packages_protected_packages` is enabled. |
| <a id="packagedetailstypepublicpackage"></a>`publicPackage` | [`Boolean`](#boolean) | Indicates if there is public access to the package. |
| <a id="packagedetailstypepypisetupurl"></a>`pypiSetupUrl` | [`String`](#string) | Url of the PyPi project setup endpoint. |
| <a id="packagedetailstypepypiurl"></a>`pypiUrl` | [`String`](#string) | Url of the PyPi project endpoint. |
@ -30987,6 +30990,34 @@ Represents a recorded measurement (object count) for the requested group.
| <a id="valuestreamstagestarteventidentifier"></a>`startEventIdentifier` | [`ValueStreamStageEvent!`](#valuestreamstageevent) | Start event identifier. |
| <a id="valuestreamstagestarteventlabel"></a>`startEventLabel` | [`Label`](#label) | Label associated with start event. |
#### Fields with arguments
##### `ValueStreamStage.metrics`
Aggregated metrics for the given stage.
Returns [`ValueStreamStageMetrics!`](#valuestreamstagemetrics).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="valuestreamstagemetricsassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue or the merge request. |
| <a id="valuestreamstagemetricsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue or the merge request. |
| <a id="valuestreamstagemetricslabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue or the merge request. |
| <a id="valuestreamstagemetricsmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue or the merge request. |
| <a id="valuestreamstagemetricstimeframe"></a>`timeframe` | [`Timeframe!`](#timeframe) | Aggregation timeframe. Filters the issue or the merge request creation time for FOSS projects, and the end event timestamp for licensed projects or groups. |
### `ValueStreamStageMetrics`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="valuestreamstagemetricsaverage"></a>`average` | [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric) | Average duration in seconds. |
| <a id="valuestreamstagemetricscount"></a>`count` | [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric) | Limited item count. The backend counts maximum 1000 items, for free projects, and maximum 10,000 items for licensed projects or licensed groups. |
| <a id="valuestreamstagemetricsmedian"></a>`median` | [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric) | Median duration in seconds. |
### `VulnerabilitiesCountByDay`
Represents the count of vulnerabilities by severity on a particular day. This data is retained for 365 days.
@ -33834,7 +33865,7 @@ Member role permission.
| <a id="memberrolepermissionmanage_group_access_tokens"></a>`MANAGE_GROUP_ACCESS_TOKENS` | Create, read, update, and delete group access tokens. When creating a token, users with this custom permission must select a role for that token that has the same or fewer permissions as the default role used as the base for the custom role. |
| <a id="memberrolepermissionmanage_project_access_tokens"></a>`MANAGE_PROJECT_ACCESS_TOKENS` | Create, read, update, and delete project access tokens. When creating a token, users with this custom permission must select a role for that token that has the same or fewer permissions as the default role used as the base for the custom role. |
| <a id="memberrolepermissionmanage_security_policy_link"></a>`MANAGE_SECURITY_POLICY_LINK` | Allows linking security policy projects. |
| <a id="memberrolepermissionread_code"></a>`READ_CODE` | Allows read-only access to the source code. |
| <a id="memberrolepermissionread_code"></a>`READ_CODE` | Allows read-only access to the source code in the user interface. Does not allow users to edit or download files, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. |
| <a id="memberrolepermissionread_dependency"></a>`READ_DEPENDENCY` | Allows read-only access to the dependencies and licenses. |
| <a id="memberrolepermissionread_vulnerability"></a>`READ_VULNERABILITY` | Read vulnerability reports and security dashboards. |
| <a id="memberrolepermissionremove_group"></a>`REMOVE_GROUP` | Ability to delete or restore a group. This ability does not allow deleting top level groups. Review the Retention period settings to prevent accidental deletion. |

View File

@ -1629,8 +1629,8 @@ POST /groups/:id/hooks
| -----------------------------| -------------- |----------| ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) |
| `url` | string | yes | The hook URL |
| `name` | string | no | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0) |
| `description` | string | no | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0) |
| `name` | string | no | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1) |
| `description` | string | no | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1) |
| `push_events` | boolean | no | Trigger hook on push events |
| `push_events_branch_filter` | string | no | Trigger hook on push events for matching branches only |
| `issues_events` | boolean | no | Trigger hook on issues events |
@ -1664,8 +1664,8 @@ PUT /groups/:id/hooks/:hook_id
| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding). |
| `hook_id` | integer | yes | The ID of the group hook. |
| `url` | string | yes | The hook URL. |
| `name` | string | no | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0). |
| `description` | string | no | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0). |
| `name` | string | no | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1). |
| `description` | string | no | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1). |
| `push_events` | boolean | no | Trigger hook on push events. |
| `push_events_branch_filter` | string | no | Trigger hook on push events for matching branches only. |
| `issues_events` | boolean | no | Trigger hook on issues events. |

View File

@ -2808,8 +2808,8 @@ POST /projects/:id/hooks
|------------------------------|-------------------|----------|-------------|
| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
| `url` | string | Yes | The hook URL. |
| `name` | string | No | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0). |
| `description` | string | No | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0). |
| `name` | string | No | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1). |
| `description` | string | No | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1). |
| `confidential_issues_events` | boolean | No | Trigger hook on confidential issues events. |
| `confidential_note_events` | boolean | No | Trigger hook on confidential note events. |
| `deployment_events` | boolean | No | Trigger hook on deployment events. |
@ -2841,8 +2841,8 @@ PUT /projects/:id/hooks/:hook_id
| `hook_id` | integer | Yes | The ID of the project hook. |
| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
| `url` | string | Yes | The hook URL. |
| `name` | string | No | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0). |
| `description` | string | No | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0). |
| `name` | string | No | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1). |
| `description` | string | No | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1). |
| `confidential_issues_events` | boolean | No | Trigger hook on confidential issues events. |
| `confidential_note_events` | boolean | No | Trigger hook on confidential note events. |
| `deployment_events` | boolean | No | Trigger hook on deployment events. |

View File

@ -100,8 +100,8 @@ POST /hooks
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `url` | string | yes | The hook URL |
| `name` | string | no | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0) |
| `description` | string | no | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.0) |
| `name` | string | no | Name of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1) |
| `description` | string | no | Description of the hook ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/460887) in GitLab 17.1) |
| `token` | string | no | Secret token to validate received payloads; this isn't returned in the response |
| `push_events` | boolean | no | When true, the hook fires on push events |
| `tag_push_events` | boolean | no | When true, the hook fires on new tags being pushed |

View File

@ -237,12 +237,16 @@ Try to avoid **below** when referring to an example or table in a documentation
- In the following example, the dog has fleas.
## Beta
## beta
Use uppercase for **Beta**. For example: **The XYZ feature is in Beta.** or **This Beta release is ready to test.**
Use lowercase for **beta**. For example:
- The feature is in beta.
- This is a beta feature.
- This beta release is ready to test.
You might also want to link to [this topic](../../../policy/experiment-beta-support.md#beta)
when writing about Beta features.
when writing about beta features.
## blacklist
@ -699,12 +703,18 @@ Instead of:
Use **expand** instead of **open** when you are talking about expanding or collapsing a section in the UI.
## Experiment
## experiment
Use uppercase for **Experiment**. For example: **The XYZ feature is an Experiment.** or **This Experiment is ready to test.**
Use lowercase for **experiment**. For example:
- This feature is an experiment.
- These features are experiments.
- This experiment is ready to test.
If you must, you can use **experimental**.
You might also want to link to [this topic](../../../policy/experiment-beta-support.md#experiment)
when writing about Experiment features.
when writing about experimental features.
## export
@ -809,6 +819,20 @@ For **GB** and **MB**, follow the [Microsoft guidance](https://learn.microsoft.c
Use title case for **Geo**.
## generally available, general availability
Use lowercase for **generally available** and **general availability**.
For example:
- This feature is generally available.
Use **generally available** more often. For example,
do not say:
- This feature has reached general availability.
You can use **GA** to indicate general availability if you spell it out on first use.
## Git suggestions
Use sentence case for **Git suggestions**.

View File

@ -970,7 +970,7 @@ This is the template for the example component which is tested in the
:key="todo.id"
:class="{ 'gl-strike': todo.isDone }"
data-testid="todo-item"
>{{ toddo.text }}</div>
>{{ todo.text }}</div>
<footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
<gl-form-input
type="text"

View File

@ -51,13 +51,20 @@ For example, use `self.table_name=` when the model name diverges from the table
We can allow exceptions only when renaming is challenging. For example, when the naming is used
for STI, exposed to the user, or if it would be a breaking change.
## Use namespaces to define bounded contexts
## Bounded contexts
A healthy application is divided into macro and sub components that represent the contexts at play,
whether they are related to business domain or infrastructure code.
See the [Bounded Contexts working group](https://handbook.gitlab.com/handbook/company/working-groups/bounded-contexts/) and
[GitLab Modular Monolith blueprint](../architecture/blueprints/modular_monolith/index.md) for more context on the
goals, motivations, and direction related to Bounded Contexts.
### Use namespaces to define bounded contexts
A healthy application is divided into macro and sub components that represent the bounded contexts at play.
As GitLab code has so many features and components, it's hard to see what contexts are involved.
These components can be related to business domain or infrastructure code.
As GitLab code has so many features and components it's hard to see what contexts are involved.
We should expect any class to be defined inside a module/namespace that represents the contexts where it operates.
We maintain a [list of allowed namespaces](#how-to-define-bounded-contexts) to define these contexts.
When we namespace classes inside their domain:
@ -66,32 +73,49 @@ When we namespace classes inside their domain:
- Top-level namespaces could be associated to one or more groups identified as domain experts.
- We can better identify the interactions and coupling between components.
For example, several classes inside `MergeRequests::` domain interact more with `Ci::`
domain and less with `ImportExport::`.
domain and less with `Import::`.
```ruby
# bad
class JobArtifact ... end
# good
module Ci
class JobArtifact ... end
end
```
### How to define bounded contexts
Allowed bounded contexts are defined in `config/bounded_contexts.yml` which contains namespaces for the
domain layer and infrastructure layer.
For **domain layer** we refer to:
1. Code in `app`, excluding the **application adapters** (controllers, API endpoints and views).
1. Code in `lib` that specifically relates to domain logic.
This includes `ActiveRecord` models, service objects, workers, and domain-specific Plain Old Ruby Objects.
For now we exclude application adapters from the modularization in order to keep the effort smaller and because
a given endpoint don't always match to a single domain (e.g. settings, merge request view, project view, etc.).
For **infrastructure layer** we refer to code in `lib` that is for generic purposes, not containing GitLab business concepts,
and that could be extracted into Ruby gems.
A good guideline for naming a top-level namespace (bounded context) is to use the related
[feature category](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/categories.yml).
For example, `Continuous Integration` feature category maps to `Ci::` namespace.
```ruby
# bad
class JobArtifact
end
# good
module Ci
class JobArtifact
end
end
```
Projects and Groups are generally container concepts because they identify tenants.
They allow features to exist at the project or group level, like repositories or runners,
but do not nest such features under `Projects::` or `Groups::`.
While features exist at the project or group level, like repositories or runners, we must not nest such features
under `Projects::` or `Groups::` but under their relative bounded context.
`Projects::` and `Groups::` namespaces should be used only for concepts that are strictly related to them:
for example `Project::CreateService` or `Groups::TransferService`.
For controllers we allow `app/controllers/projects` and `app/controllers/groups` to be exceptions.
For controllers we allow `app/controllers/projects` and `app/controllers/groups` to be exceptions, also because
bounded contexts are not applied to application layer.
We use this convention to indicate the scope of a given web endpoint.
Do not use the [stage or group name](https://handbook.gitlab.com/handbook/product/categories/#devops-stages)
@ -100,14 +124,12 @@ because a feature category could be reassigned to a different group in the futur
```ruby
# bad
module Create
class Commit
end
class Commit ... end
end
# good
module Repositories
class Commit
end
class Commit ... end
end
```
@ -125,15 +147,12 @@ For example, instead of having separate and granular bounded contexts like: `Con
`ContainerHostSecurity::`, `ContainerNetworkSecurity::`, we could have:
```ruby
module ContainerSecurity
module HostSecurity
end
module Security::Container
module Scanning ... end
module NetworkSecurity
end
module NetworkSecurity ... end
module Scanning
end
module HostSecurity ... end
end
```

View File

@ -45,7 +45,7 @@ AWS Services that are supported directly by a CodeStar Connection in an AWS acco
- **AWS Service Catalog** directly inherits CodeStar Connections, there is not any specific documentation about GitLab because it just uses any GitLab CodeStar Connection that has been created in the account. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Built]`
- **AWS Proton** directly inherits CodeStar Connections, there is not any specific documentation about GitLab since it just uses any GitLab CodeStar Connection that has been created in the account. ([12/28/2023](https://aws.amazon.com/about-aws/whats-new/2023/12/codepipeline-gitlab-self-managed/)) `[AWS Built]`
- **AWS CodeBuild** - [for GitLab.com, self-managed and dedicated - click documentation tabs here](https://docs.aws.amazon.com/codebuild/latest/userguide/create-project-console.html#create-project-console-source). ([03/26/2023](https://aws.amazon.com/about-aws/whats-new/2024/03/aws-codebuild-gitlab-gitlab-self-managed/)) `[AWS Built]`
- **AWS CodeBuild** - [for GitLab.com, self-managed and dedicated - click documentation tabs here](https://docs.aws.amazon.com/codebuild/latest/userguide/create-project-console.html#create-project-console-source). ([03/26/2024](https://aws.amazon.com/about-aws/whats-new/2024/03/aws-codebuild-gitlab-gitlab-self-managed/)) `[AWS Built]`
Documentation and References:

View File

@ -50,6 +50,30 @@ For deprecation reviewers (Technical Writers only):
{::options parse_block_html="true" /}
<div class="js-deprecation-filters"></div>
<div class="milestone-wrapper" data-milestone="19.0">
## GitLab 19.0
<div class="deprecation breaking-change" data-milestone="19.0">
### Running a single database is deprecated
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">16.1</span>
- Removal in GitLab <span class="milestone">19.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411239).
</div>
From GitLab 19.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
We recommend running both databases on the same Postgres instance(s) due to ease of management for most deployments.
This change provides additional scalability for the largest of GitLab instances, like GitLab.com.
This change applies to all installation methods: Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
Before upgrading to GitLab 19.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
</div>
</div>
<div class="milestone-wrapper" data-milestone="18.0">
## GitLab 18.0
@ -409,25 +433,6 @@ Occurrences of the `active` identifier in the GitLab GraphQL API endpoints will
<div class="deprecation breaking-change" data-milestone="18.0">
### Running a single database is deprecated
<div class="deprecation-notes">
- Announced in GitLab <span class="milestone">16.1</span>
- Removal in GitLab <span class="milestone">18.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/411239).
</div>
From GitLab 18.0, we will require a [separate database for CI features](https://gitlab.com/groups/gitlab-org/-/epics/7509).
We recommend running both databases on the same Postgres instance(s) due to ease of management for most deployments.
This change provides additional scalability for the largest of GitLab instances, like GitLab.com.
This change applies to all installation methods: Omnibus GitLab, GitLab Helm chart, GitLab Operator, GitLab Docker images, and installation from source.
Before upgrading to GitLab 18.0, please ensure you have [migrated](https://docs.gitlab.com/ee/administration/postgresql/multiple_databases.html) to two databases.
</div>
<div class="deprecation breaking-change" data-milestone="18.0">
### Self-managed certificate-based integration with Kubernetes
<div class="deprecation-notes">

View File

@ -68,7 +68,7 @@ These requirements are documented in the `Required permission` column in the fol
|:-----|:------------|:------------------|:---------|:--------------|:---------|
| [`admin_merge_request`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128302) | | Allows approval of merge requests. | GitLab [16.4](https://gitlab.com/gitlab-org/gitlab/-/issues/412708) | | |
| [`admin_push_rules`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147872) | | Configure push rules for repositories at the group or project level. | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/421786) | `custom_ability_admin_push_rules` | |
| [`read_code`](https://gitlab.com/gitlab-org/gitlab/-/issues/376180) | | Allows read-only access to the source code. | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/20277) | `customizable_roles` | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) |
| [`read_code`](https://gitlab.com/gitlab-org/gitlab/-/issues/376180) | | Allows read-only access to the source code in the user interface. Does not allow users to edit or download files, clone or pull repositories, view source code in an IDE, or view merge requests for private projects. | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/20277) | `customizable_roles` | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110810) |
## System access

View File

@ -205,6 +205,22 @@ this:
1. If the update returns a status code `204`, have the user attempt to sign in
using SAML SSO.
## 403 Forbidden response for disable action
If you [restrict group access by IP address](../access_and_permissions.md#restrict-group-access-by-ip-address),
SCIM deprovisioning might fail with the error response:
```plaintext
{"message":"403 Forbidden"}
```
This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/429607) when restricting group access by IP
address.
To work around this issue, use the Group SCIM API to
[update a single SCIM provisioned user](../../../development/internal_api/index.md#update-a-single-scim-provisioned-user)
to set the user's `active` state to `false`.
## Azure Active Directory
The following troubleshooting information is specifically for SCIM provisioned through Azure Active Directory.

View File

@ -15,6 +15,7 @@ built-in CI/CD to deploy your app.
Projects can be available [publicly, internally, or privately](../public_access.md).
GitLab does not limit the number of private projects you can create.
- [Getting started](../../user/get_started/get_started_projects.md)
- [Create a project](index.md)
- [Manage projects](working_with_projects.md)
- [Project visibility](../public_access.md)

View File

@ -49,7 +49,7 @@ The following languages are supported:
| Java | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| JavaScript | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Kotlin | **{check-circle}** Yes <br><br>(Requires third-party extension providing Kotlin support) | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Markdown | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | **{check-circle}** Yes |
| Markdown | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| PHP | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Python | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |
| Ruby | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes |

View File

@ -53,6 +53,7 @@ module Gitlab
add_parent_model_params!(finder_params)
add_time_range_params!(finder_params, params[:from], params[:to])
finder_params.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
end
end

View File

@ -21,7 +21,6 @@ module Sidebars
:dashboard,
:vulnerability_report,
:dependency_list,
:license_compliance,
:audit_events,
:scan_policies,
:on_demand_scans,

View File

@ -15879,6 +15879,9 @@ msgstr[1] ""
msgid "CycleAnalytics|'%{name}' is collecting the data. This can take a few minutes."
msgstr ""
msgid "CycleAnalytics|Average duration"
msgstr ""
msgid "CycleAnalytics|Average time to completion"
msgstr ""
@ -15903,9 +15906,15 @@ msgstr ""
msgid "CycleAnalytics|If you have recently upgraded your GitLab license from a tier without this feature, it can take up to 30 minutes for data to collect and display."
msgstr ""
msgid "CycleAnalytics|Item count"
msgstr ""
msgid "CycleAnalytics|Lead time for changes"
msgstr ""
msgid "CycleAnalytics|Median duration"
msgstr ""
msgid "CycleAnalytics|New value stream…"
msgstr ""
@ -15959,6 +15968,9 @@ msgstr ""
msgid "CycleAnalytics|project dropdown filter"
msgstr ""
msgid "CycleAnalytics|seconds"
msgstr ""
msgid "DAG visualization requires at least 3 dependent jobs."
msgstr ""
@ -29850,6 +29862,9 @@ msgstr ""
msgid "KubernetesDashboard|Suspended"
msgstr ""
msgid "KubernetesDashboard|There was a problem fetching cluster information. Refresh the page and try again."
msgstr ""
msgid "KubernetesDashboard|View projects"
msgstr ""
@ -30411,9 +30426,6 @@ msgstr ""
msgid "License Compliance| Used by %{dependencies}"
msgstr ""
msgid "License compliance"
msgstr ""
msgid "License key"
msgstr ""
@ -30546,57 +30558,30 @@ msgstr ""
msgid "Licenses"
msgstr ""
msgid "Licenses|%{remainingComponentsCount} more"
msgstr ""
msgid "Licenses|Acceptable license to be used in the project"
msgstr ""
msgid "Licenses|Component"
msgstr ""
msgid "Licenses|Components"
msgstr ""
msgid "Licenses|Displays licenses detected in the project based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr ""
msgid "Licenses|Drag your license file here or %{linkStart}click to upload%{linkEnd}."
msgstr ""
msgid "Licenses|Drop your license file to start the upload."
msgstr ""
msgid "Licenses|Error fetching the license list. Please check your network connection and try again."
msgstr ""
msgid "Licenses|Error: You are trying to upload something other than a file"
msgstr ""
msgid "Licenses|License Compliance"
msgstr ""
msgid "Licenses|Name"
msgstr ""
msgid "Licenses|Policy"
msgstr ""
msgid "Licenses|Policy violation: denied"
msgstr ""
msgid "Licenses|The file could not be uploaded."
msgstr ""
msgid "Licenses|The license list details information about the licenses used within your project."
msgstr ""
msgid "Licenses|Unacceptable license, if detected it will disallow a merge request until it's removed"
msgstr ""
msgid "Licenses|View license details for your project"
msgstr ""
msgid "Limit display of time tracking units to hours."
msgstr ""
@ -34097,9 +34082,6 @@ msgstr ""
msgid "No email participants were removed. Either none were provided, or they don't exist."
msgstr ""
msgid "No endpoint provided"
msgstr ""
msgid "No errors to display."
msgstr ""
@ -42104,9 +42086,15 @@ msgstr ""
msgid "PurchaseStep|An error occurred in the purchase step. If the problem persists please contact support at https://support.gitlab.com."
msgstr ""
msgid "Purchase|%{stripe3dsLinkStart}3D Secure authentication%{stripe3dsLinkEnd} failed. Please try the credit card again, or %{salesLinkStart}contact our sales team%{salesLinkEnd} to purchase."
msgstr ""
msgid "Purchase|%{stripe3dsLinkStart}3D Secure authentication%{stripe3dsLinkEnd} is not supported. Please %{salesLinkStart}contact our sales team%{salesLinkEnd} to purchase, or try a different credit card."
msgstr ""
msgid "Purchase|3D Secure authentication failed"
msgstr ""
msgid "Purchase|A full name in your profile is required to make a purchase. Check that the full name field in your %{userProfileLinkStart}user profile%{userProfileLinkEnd} has both a first and last name, then retry the purchase. If the problem persists, %{supportLinkStart}contact support%{supportLinkEnd}."
msgstr ""
@ -50420,6 +50408,9 @@ msgstr ""
msgid "Subscribe to calendar"
msgstr ""
msgid "Subscribe to releases RSS feed"
msgstr ""
msgid "Subscribed"
msgstr ""

View File

@ -19,6 +19,8 @@ gitlab:
bootsnap: false
hostname: gdk.test
application_settings_cache_seconds: 0
puma:
threads_max: 6
gitlab_k8s_agent:
enabled: false
gitlab_pages:

View File

@ -17,7 +17,7 @@ module QA
merge_request.fork.remove_via_api!
end
it 'can merge source branch from fork into upstream repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818' do
it 'can merge source branch from fork into upstream repository', :blocking, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818' do
merge_request.visit!
Page::MergeRequest::Show.perform do |merge_request|

View File

@ -3,7 +3,7 @@
module QA
RSpec.describe 'Create', product_group: :source_code do
describe 'Push mirror a repository over HTTP' do
it 'configures and syncs LFS objects for a (push) mirrored repository', :aggregate_failures,
it 'configures and syncs LFS objects for a (push) mirrored repository', :blocking, :aggregate_failures,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847',
quarantine: {
only: { condition: -> { ENV['QA_RUN_TYPE'] == 'e2e-package-and-test-ce' } },

View File

@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
describe 'Push mirror a repository over HTTP', product_group: :source_code do
describe 'Push mirror a repository over HTTP', :blocking, product_group: :source_code do
it('configures and syncs a (push) mirrored repository',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347741',
quarantine: {

View File

@ -53,7 +53,7 @@ module QA
it_behaves_like 'upload a file'
end
context 'when the file is an image',
context 'when the file is an image', :blocking,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/390007' do
let(:file_name) { 'dk.png' }

View File

@ -25,7 +25,7 @@ module QA
end
context(
'when using HTTP endpoint integration',
'when using HTTP endpoint integration', :blocking,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/393589',
quarantine: {
only: { pipeline: :nightly },

View File

@ -55,6 +55,8 @@ RSpec.describe ObjectStoreSettings, feature_category: :shared do
shared_examples 'consolidated settings for objects accelerated by Workhorse' do
it 'consolidates active object storage settings' do
expect(subject).to be_present
described_class::WORKHORSE_ACCELERATED_TYPES.each do |object_type|
# Use to_h to avoid https://gitlab.com/gitlab-org/gitlab/-/issues/286873
section = subject.try(object_type).to_h
@ -160,6 +162,29 @@ RSpec.describe ObjectStoreSettings, feature_category: :shared do
end
end
context 'CI secure files' do
let(:ci_secure_files_connection) { { 'provider' => 'Google', 'google_application_default' => true } }
before do
config['ci_secure_files'] = {
'enabled' => true,
'object_store' => {
'enabled' => true,
'connection' => ci_secure_files_connection
}
}
end
it_behaves_like 'consolidated settings for objects accelerated by Workhorse'
it 'allows CI secure files to define its own connection' do
expect { subject }.not_to raise_error
expect(settings.ci_secure_files['object_store']['connection'].to_hash).to eq(ci_secure_files_connection)
expect(settings.ci_secure_files['object_store']['consolidated_settings']).to be_falsey
end
end
context 'with Google CDN enabled' do
let(:cdn_config) do
{

View File

@ -259,6 +259,9 @@
"packageProtectionRuleExists": {
"type": "boolean"
},
"protectionRuleExists": {
"type": "boolean"
},
"_links": {
"type": "object",
"additionalProperties": false,

View File

@ -117,6 +117,20 @@ describe('~/frontend/environments/graphql/resolvers', () => {
mockResolvers.Query.k8sDashboardPods(null, { configuration }, { client }),
).rejects.toThrow('API error');
});
it('should return a generic error message if the error response is not of JSON type', async () => {
jest.spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces').mockRejectedValue({
response: {
headers: new Headers({ 'Content-Type': 'application/pdf' }),
},
});
await expect(
mockResolvers.Query.k8sDashboardPods(null, { configuration }, { client }),
).rejects.toThrow(
'There was a problem fetching cluster information. Refresh the page and try again.',
);
});
});
describe('k8sDeployments', () => {

View File

@ -39,6 +39,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('app_index.vue', () => {
const projectPath = 'project/path';
const atomFeedPath = 'project/path.atom';
const newReleasePath = 'path/to/new/release/page';
const before = 'beforeCursor';
const after = 'afterCursor';
@ -73,6 +74,7 @@ describe('app_index.vue', () => {
provide: {
newReleasePath,
projectPath,
atomFeedPath,
},
mocks: {
$toast: { show: toast },
@ -100,6 +102,7 @@ describe('app_index.vue', () => {
// Finders
const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
const findAtomFeedButton = () => wrapper.findByTestId('atom-feed-btn');
const findNewReleaseButton = () => wrapper.findByText(ReleasesIndexApp.i18n.newRelease);
const findAllReleaseBlocks = () => wrapper.findAllComponents(ReleaseBlock);
const findPagination = () => wrapper.findComponent(ReleasesPagination);
@ -299,6 +302,21 @@ describe('app_index.vue', () => {
});
});
describe('RSS feed button', () => {
beforeEach(() => {
createComponent();
});
it('renders the RSS feed button with the correct href', () => {
expect(findAtomFeedButton().attributes().href).toBe(atomFeedPath);
});
it('sets the correct tooltip text', () => {
expect(findAtomFeedButton().exists()).toBe(true);
expect(findAtomFeedButton().attributes('title')).toBe(i18n.atomFeedBtnTitle);
});
});
describe('pagination', () => {
beforeEach(() => {
mockQueryParams = { before };

View File

@ -9,14 +9,14 @@ exports[`Design item component when item appears in view after image is loaded r
`;
exports[`Design item component with notes renders item with multiple comments 1`] = `
<div
<a
class="card design-list-item gl-cursor-pointer gl-mb-0 js-design-list-item text-plain"
>
<div
class="card-body gl-align-items-center gl-display-flex gl-justify-content-center gl-overflow-hidden gl-p-0 gl-relative gl-rounded-top-base"
class="card-body gl-flex gl-items-center gl-justify-content-center gl-overflow-hidden gl-p-0 gl-relative gl-rounded-top-base"
>
<gl-intersection-observer-stub
class="gl-flex-grow-1"
class="gl-grow"
data-qa-filename="test"
data-testid="design-image"
>
@ -29,11 +29,10 @@ exports[`Design item component with notes renders item with multiple comments 1`
</gl-intersection-observer-stub>
</div>
<div
class="card-footer gl-bg-white gl-display-flex gl-px-4 gl-py-3 gl-w-full"
class="card-footer gl-bg-white gl-flex gl-px-4 gl-py-3 gl-w-full"
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
data-testid="design-file-name"
class="gl-flex gl-flex-col str-truncated-100"
>
<span
class="gl-font-sm str-truncated-100"
@ -55,7 +54,7 @@ exports[`Design item component with notes renders item with multiple comments 1`
</span>
</div>
<div
class="gl-align-items-center gl-display-flex gl-ml-auto gl-text-gray-500"
class="gl-flex gl-items-center gl-ml-auto gl-text-gray-500"
>
<gl-icon-stub
class="gl-ml-2"
@ -70,18 +69,18 @@ exports[`Design item component with notes renders item with multiple comments 1`
</span>
</div>
</div>
</div>
</a>
`;
exports[`Design item component with notes renders item with single comment 1`] = `
<div
<a
class="card design-list-item gl-cursor-pointer gl-mb-0 js-design-list-item text-plain"
>
<div
class="card-body gl-align-items-center gl-display-flex gl-justify-content-center gl-overflow-hidden gl-p-0 gl-relative gl-rounded-top-base"
class="card-body gl-flex gl-items-center gl-justify-content-center gl-overflow-hidden gl-p-0 gl-relative gl-rounded-top-base"
>
<gl-intersection-observer-stub
class="gl-flex-grow-1"
class="gl-grow"
data-qa-filename="test"
data-testid="design-image"
>
@ -94,11 +93,10 @@ exports[`Design item component with notes renders item with single comment 1`] =
</gl-intersection-observer-stub>
</div>
<div
class="card-footer gl-bg-white gl-display-flex gl-px-4 gl-py-3 gl-w-full"
class="card-footer gl-bg-white gl-flex gl-px-4 gl-py-3 gl-w-full"
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
data-testid="design-file-name"
class="gl-flex gl-flex-col str-truncated-100"
>
<span
class="gl-font-sm str-truncated-100"
@ -120,7 +118,7 @@ exports[`Design item component with notes renders item with single comment 1`] =
</span>
</div>
<div
class="gl-align-items-center gl-display-flex gl-ml-auto gl-text-gray-500"
class="gl-flex gl-items-center gl-ml-auto gl-text-gray-500"
>
<gl-icon-stub
class="gl-ml-2"
@ -135,5 +133,5 @@ exports[`Design item component with notes renders item with single comment 1`] =
</span>
</div>
</div>
</div>
</a>
`;

View File

@ -56,6 +56,9 @@ describe('DesignWidget', () => {
provide: {
fullPath: 'gitlab-org/gitlab-shell',
},
stubs: {
RouterView: true,
},
});
}

Some files were not shown because too many files have changed in this diff Show More