Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
adeba64772
commit
6a4ec0399b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export const DESIGN_DETAIL_LAYOUT_CLASSLIST = [
|
||||
'design-detail-layout',
|
||||
'gl-overflow-hidden',
|
||||
'gl-m-0',
|
||||
];
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
fragment DesignFile on Design {
|
||||
id
|
||||
event
|
||||
filename
|
||||
notesCount
|
||||
image
|
||||
imageV432x230
|
||||
description
|
||||
descriptionHtml
|
||||
fullPath
|
||||
currentUserTodos(state: pending) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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__(
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ module Sidebars
|
|||
:dashboard,
|
||||
:vulnerability_report,
|
||||
:dependency_list,
|
||||
:license_compliance,
|
||||
:audit_events,
|
||||
:scan_policies,
|
||||
:on_demand_scans,
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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' } },
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -259,6 +259,9 @@
|
|||
"packageProtectionRuleExists": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"protectionRuleExists": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"_links": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue