Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dd33e91737
commit
8432be20de
73 changed files with 2670 additions and 280 deletions
|
@ -5,9 +5,6 @@ GraphQL/FieldDefinitions:
|
|||
- app/graphql/types/group_type.rb
|
||||
- app/graphql/types/issue_type.rb
|
||||
- app/graphql/types/label_type.rb
|
||||
- app/graphql/types/project_type.rb
|
||||
- app/graphql/types/projects/topic_type.rb
|
||||
- app/graphql/types/release_type.rb
|
||||
- ee/app/graphql/types/ci/code_quality_degradation_type.rb
|
||||
- ee/app/graphql/types/epic_type.rb
|
||||
- ee/app/graphql/types/group_release_stats_type.rb
|
||||
|
|
|
@ -18,21 +18,6 @@ Rails/SaveBang:
|
|||
- ee/spec/models/visible_approvable_spec.rb
|
||||
- ee/spec/models/vulnerabilities/feedback_spec.rb
|
||||
- ee/spec/models/vulnerabilities/issue_link_spec.rb
|
||||
- ee/spec/services/geo/blob_verification_secondary_service_spec.rb
|
||||
- ee/spec/services/geo/files_expire_service_spec.rb
|
||||
- ee/spec/services/geo/metrics_update_service_spec.rb
|
||||
- ee/spec/services/geo/registry_consistency_service_spec.rb
|
||||
- ee/spec/services/geo/repository_verification_secondary_service_spec.rb
|
||||
- ee/spec/services/groups/autocomplete_service_spec.rb
|
||||
- ee/spec/services/ldap_group_reset_service_spec.rb
|
||||
- ee/spec/services/lfs/unlock_file_service_spec.rb
|
||||
- ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb
|
||||
- ee/spec/services/quick_actions/interpret_service_spec.rb
|
||||
- ee/spec/services/slash_commands/global_slack_handler_spec.rb
|
||||
- ee/spec/services/start_pull_mirroring_service_spec.rb
|
||||
- ee/spec/services/status_page/trigger_publish_service_spec.rb
|
||||
- ee/spec/services/todo_service_spec.rb
|
||||
- ee/spec/services/vulnerability_feedback/create_service_spec.rb
|
||||
- spec/lib/backup/manager_spec.rb
|
||||
- spec/lib/gitlab/alerting/alert_spec.rb
|
||||
- spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
|
||||
|
|
|
@ -1 +1 @@
|
|||
501a7a9b19eb80ec039caeb8019cab7a8cfcbb44
|
||||
7ce0d18ad44686865aa0dbf5f1b47d9cc05988be
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
|
||||
|
||||
initWorkItemsHierarchy();
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
|
||||
|
||||
import { GlButton, GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
|
||||
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
|
||||
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
|
||||
import { __, s__ } from '~/locale';
|
||||
import {
|
||||
|
@ -41,16 +41,19 @@ export default {
|
|||
pucWarningHelpText: s__(
|
||||
'ProjectSettings|Highlight the usage of hidden unicode characters. These have innocent uses for right-to-left languages, but can also be used in potential exploits.',
|
||||
),
|
||||
confirmButtonText: __('Save changes'),
|
||||
},
|
||||
|
||||
components: {
|
||||
projectFeatureSetting,
|
||||
projectSettingRow,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
GlFormCheckbox,
|
||||
GlToggle,
|
||||
ConfirmDanger,
|
||||
},
|
||||
mixins: [settingsMixin],
|
||||
|
||||
|
@ -163,6 +166,15 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
confirmationPhrase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showVisibilityConfirmModal: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const defaults = {
|
||||
|
@ -274,6 +286,12 @@ export default {
|
|||
cveIdRequestIsDisabled() {
|
||||
return this.visibilityLevel !== visibilityOptions.PUBLIC;
|
||||
},
|
||||
isVisibilityReduced() {
|
||||
return (
|
||||
this.showVisibilityConfirmModal &&
|
||||
this.visibilityLevel < this.currentSettings.visibilityLevel
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -774,5 +792,24 @@ export default {
|
|||
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
|
||||
</gl-form-checkbox>
|
||||
</project-setting-row>
|
||||
<confirm-danger
|
||||
v-if="isVisibilityReduced"
|
||||
button-class="qa-visibility-features-permissions-save-button"
|
||||
button-variant="confirm"
|
||||
:disabled="false"
|
||||
:phrase="confirmationPhrase"
|
||||
:button-text="$options.i18n.confirmButtonText"
|
||||
data-testid="project-features-save-button"
|
||||
@confirm="$emit('confirm')"
|
||||
/>
|
||||
<gl-button
|
||||
v-else
|
||||
type="submit"
|
||||
variant="confirm"
|
||||
data-testid="project-features-save-button"
|
||||
button-class="qa-visibility-features-permissions-save-button"
|
||||
>
|
||||
{{ $options.i18n.confirmButtonText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import settingsPanel from './components/settings_panel.vue';
|
||||
|
||||
export default function initProjectPermissionsSettings() {
|
||||
|
@ -6,8 +7,36 @@ export default function initProjectPermissionsSettings() {
|
|||
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
|
||||
const componentProps = JSON.parse(componentPropsEl.innerHTML);
|
||||
|
||||
const {
|
||||
targetFormId,
|
||||
additionalInformation,
|
||||
confirmDangerMessage,
|
||||
confirmButtonText,
|
||||
showVisibilityConfirmModal,
|
||||
htmlConfirmationMessage,
|
||||
phrase: confirmationPhrase,
|
||||
} = mountPoint.dataset;
|
||||
|
||||
return new Vue({
|
||||
el: mountPoint,
|
||||
render: (createElement) => createElement(settingsPanel, { props: { ...componentProps } }),
|
||||
provide: {
|
||||
additionalInformation,
|
||||
confirmDangerMessage,
|
||||
confirmButtonText,
|
||||
htmlConfirmationMessage: parseBoolean(htmlConfirmationMessage),
|
||||
},
|
||||
render: (createElement) =>
|
||||
createElement(settingsPanel, {
|
||||
props: {
|
||||
...componentProps,
|
||||
confirmationPhrase,
|
||||
showVisibilityConfirmModal: parseBoolean(showVisibilityConfirmModal),
|
||||
},
|
||||
on: {
|
||||
confirm: () => {
|
||||
if (targetFormId) document.getElementById(targetFormId)?.submit();
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,54 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const widgetTypes = {
|
||||
title: 'TITLE',
|
||||
};
|
||||
|
||||
export const WI_TITLE_TRACK_LABEL = 'item_title';
|
||||
|
||||
export const workItemTypes = {
|
||||
EPIC: {
|
||||
title: __('Epic'),
|
||||
icon: 'epic',
|
||||
color: '#694CC0',
|
||||
backgroundColor: '#E1D8F9',
|
||||
},
|
||||
ISSUE: {
|
||||
title: __('Issue'),
|
||||
icon: 'issues',
|
||||
color: '#1068BF',
|
||||
backgroundColor: '#CBE2F9',
|
||||
},
|
||||
TASK: {
|
||||
title: __('Task'),
|
||||
icon: 'task-done',
|
||||
color: '#217645',
|
||||
backgroundColor: '#C3E6CD',
|
||||
},
|
||||
INCIDENT: {
|
||||
title: __('Incident'),
|
||||
icon: 'issue-type-incident',
|
||||
backgroundColor: '#db2a0f',
|
||||
color: '#FDD4CD',
|
||||
iconSize: 16,
|
||||
},
|
||||
SUB_EPIC: {
|
||||
title: __('Child epic'),
|
||||
icon: 'epic',
|
||||
color: '#AB6100',
|
||||
backgroundColor: '#F5D9A8',
|
||||
},
|
||||
REQUIREMENT: {
|
||||
title: __('Requirement'),
|
||||
icon: 'requirements',
|
||||
color: '#0068c5',
|
||||
backgroundColor: '#c5e3fb',
|
||||
},
|
||||
TEST_CASE: {
|
||||
title: __('Test case'),
|
||||
icon: 'issue-type-test-case',
|
||||
backgroundColor: '#007a3f',
|
||||
color: '#bae8cb',
|
||||
iconSize: 16,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<script>
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import Cookies from 'js-cookie';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { workItemTypes } from '~/work_items/constants';
|
||||
import RESPONSE from '../static_response';
|
||||
import { WORK_ITEMS_SURVEY_COOKIE_NAME } from '../constants';
|
||||
import Hierarchy from './hierarchy.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBanner,
|
||||
Hierarchy,
|
||||
},
|
||||
inject: ['illustrationPath', 'licensePlan'],
|
||||
data() {
|
||||
return {
|
||||
bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
|
||||
workItemHierarchy: RESPONSE[this.licensePlan],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasUnavailableStructure() {
|
||||
return this.workItemTypes.unavailable.length > 0;
|
||||
},
|
||||
workItemTypes() {
|
||||
return this.workItemHierarchy.reduce(
|
||||
(itemTypes, item) => {
|
||||
const key = item.available ? 'available' : 'unavailable';
|
||||
itemTypes[key].push({
|
||||
...item,
|
||||
...workItemTypes[item.type],
|
||||
nestedTypes: item.nestedTypes
|
||||
? item.nestedTypes.map((type) => workItemTypes[type])
|
||||
: null,
|
||||
});
|
||||
return itemTypes;
|
||||
},
|
||||
{ available: [], unavailable: [] },
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
|
||||
this.bannerVisible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-banner
|
||||
v-if="bannerVisible"
|
||||
class="gl-mt-4 gl-px-5!"
|
||||
:title="s__('Hierarchy|Help us improve work items in GitLab!')"
|
||||
:button-text="s__('Hierarchy|Take the work items survey')"
|
||||
button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
|
||||
:svg-path="illustrationPath"
|
||||
@close="handleClose"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</gl-banner>
|
||||
<h3 class="gl-mt-5!">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div class="gl-font-weight-bold gl-mb-2">{{ s__('Hierarchy|Current structure') }}</div>
|
||||
<p class="gl-mb-3!">{{ s__('Hierarchy|You can start using these items now.') }}</p>
|
||||
<hierarchy :work-item-types="workItemTypes.available" />
|
||||
|
||||
<div
|
||||
v-if="hasUnavailableStructure"
|
||||
data-testid="unavailable-structure"
|
||||
class="gl-font-weight-bold gl-mt-5 gl-mb-2"
|
||||
>
|
||||
{{ s__('Hierarchy|Unavailable structure') }}
|
||||
</div>
|
||||
<p v-if="hasUnavailableStructure" class="gl-mb-3!">
|
||||
{{ s__('Hierarchy|These items are unavailable in the current structure.') }}
|
||||
</p>
|
||||
<hierarchy :work-item-types="workItemTypes.unavailable" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,119 @@
|
|||
<script>
|
||||
import { GlIcon, GlBadge } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlBadge,
|
||||
},
|
||||
props: {
|
||||
workItemTypes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isLastItem(index, workItem) {
|
||||
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
|
||||
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
|
||||
|
||||
return isLastItemInArray && hasMoreThanOneItem;
|
||||
},
|
||||
nestedWorkItemTypeMargin(index, workItem) {
|
||||
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
|
||||
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
|
||||
|
||||
if (isLastItemInArray && hasMoreThanOneItem) {
|
||||
return 'gl-ml-0';
|
||||
}
|
||||
|
||||
return 'gl-ml-6';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="workItem in workItemTypes"
|
||||
:key="workItem.id"
|
||||
class="gl-mb-3"
|
||||
:class="{ flex: !workItem.available }"
|
||||
>
|
||||
<span
|
||||
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal"
|
||||
data-testid="work-item-wrapper"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
backgroundColor: workItem.backgroundColor,
|
||||
color: workItem.color,
|
||||
}"
|
||||
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
|
||||
>
|
||||
<gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
|
||||
</span>
|
||||
|
||||
{{ workItem.title }}
|
||||
</span>
|
||||
|
||||
<gl-badge
|
||||
v-if="!workItem.available"
|
||||
variant="info"
|
||||
icon="license"
|
||||
size="sm"
|
||||
class="gl-ml-3 gl-align-self-center"
|
||||
>{{ workItem.license }}</gl-badge
|
||||
>
|
||||
|
||||
<div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
|
||||
<svg
|
||||
v-if="workItem.nestedTypes.length > 1"
|
||||
class="hierarchy-rounded-arrow-tail gl-text-gray-400"
|
||||
data-testid="hierarchy-rounded-arrow-tail"
|
||||
width="2"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="0.75"
|
||||
y1="1"
|
||||
x2="0.75"
|
||||
y2="100%"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
|
||||
<div :key="nestedWorkItem.id" class="gl-display-block gl-mt-2 gl-ml-6">
|
||||
<gl-icon name="arrow-down" class="gl-text-gray-400" />
|
||||
</div>
|
||||
<gl-icon
|
||||
v-if="isLastItem(index, workItem)"
|
||||
:key="nestedWorkItem.id"
|
||||
name="level-up"
|
||||
class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow"
|
||||
/>
|
||||
<span
|
||||
:key="nestedWorkItem.id"
|
||||
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal"
|
||||
:class="nestedWorkItemTypeMargin(index, workItem)"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
backgroundColor: nestedWorkItem.backgroundColor,
|
||||
color: nestedWorkItem.color,
|
||||
}"
|
||||
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
|
||||
>
|
||||
<gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
|
||||
</span>
|
||||
|
||||
{{ nestedWorkItem.title }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
12
app/assets/javascripts/work_items_hierarchy/constants.js
Normal file
12
app/assets/javascripts/work_items_hierarchy/constants.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
|
||||
|
||||
/**
|
||||
* Hard-coded strings since we're rendering hierarchy
|
||||
* items from mock responses. Remove this when we
|
||||
* have a real hierarchy endpoint.
|
||||
*/
|
||||
export const LICENSE_PLAN = {
|
||||
FREE: 'free',
|
||||
PREMIUM: 'premium',
|
||||
ULTIMATE: 'ultimate',
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { LICENSE_PLAN } from './constants';
|
||||
|
||||
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
|
||||
if (hasSubEpics) {
|
||||
return LICENSE_PLAN.ULTIMATE;
|
||||
} else if (hasEpics) {
|
||||
return LICENSE_PLAN.PREMIUM;
|
||||
}
|
||||
return LICENSE_PLAN.FREE;
|
||||
}
|
142
app/assets/javascripts/work_items_hierarchy/static_response.js
Normal file
142
app/assets/javascripts/work_items_hierarchy/static_response.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
const FREE_TIER = 'free';
|
||||
const ULTIMATE_TIER = 'ultimate';
|
||||
const PREMIUM_TIER = 'premium';
|
||||
|
||||
const RESPONSE = {
|
||||
[FREE_TIER]: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'ISSUE',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TASK',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'INCIDENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'EPIC',
|
||||
available: false,
|
||||
license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'SUB_EPIC',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'REQUIREMENT',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'TEST_CASE',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
],
|
||||
|
||||
[PREMIUM_TIER]: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'EPIC',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: ['ISSUE'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TASK',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'INCIDENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'SUB_EPIC',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'REQUIREMENT',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'TEST_CASE',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
],
|
||||
|
||||
[ULTIMATE_TIER]: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'EPIC',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: ['SUB_EPIC', 'ISSUE'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TASK',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'INCIDENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'REQUIREMENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'TEST_CASE',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default RESPONSE;
|
|
@ -0,0 +1,26 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import App from './components/app.vue';
|
||||
import { inferLicensePlan } from './hierarchy_util';
|
||||
|
||||
export const initWorkItemsHierarchy = () => {
|
||||
const el = document.querySelector('#js-work-items-hierarchy');
|
||||
|
||||
const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
|
||||
|
||||
const licensePlan = inferLicensePlan({
|
||||
hasEpics: parseBoolean(hasEpics),
|
||||
hasSubEpics: parseBoolean(hasSubEpics),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
illustrationPath,
|
||||
licensePlan,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -32,3 +32,4 @@
|
|||
@import './pages/storage_quota';
|
||||
@import './pages/tree';
|
||||
@import './pages/users';
|
||||
@import './pages/hierarchy';
|
||||
|
|
15
app/assets/stylesheets/pages/hierarchy.scss
Normal file
15
app/assets/stylesheets/pages/hierarchy.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
.hierarchy-rounded-arrow-tail {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 5px;
|
||||
height: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.hierarchy-icon-wrapper {
|
||||
height: $default-icon-size;
|
||||
width: $default-icon-size;
|
||||
}
|
||||
|
||||
.hierarchy-rounded-arrow {
|
||||
transform: scale(1, -1) rotate(90deg);
|
||||
}
|
|
@ -772,6 +772,9 @@ svg {
|
|||
.gl-mt-2 {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.gl-mt-5 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.gl-mb-3 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
|
15
app/controllers/concerns/work_items_hierarchy.rb
Normal file
15
app/controllers/concerns/work_items_hierarchy.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WorkItemsHierarchy
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def planning_hierarchy
|
||||
return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
|
||||
|
||||
render 'shared/planning_hierarchy'
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
|
||||
WorkItemsHierarchy.prepend_mod_with('WorkItemsHierarchy')
|
|
@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
include RecordUserLastActivity
|
||||
include ImportUrlParams
|
||||
include FiltersEvents
|
||||
include WorkItemsHierarchy
|
||||
|
||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
|
||||
|
@ -52,6 +53,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
|
||||
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
|
||||
feature_category :code_review, [:unfoldered_environment_names]
|
||||
feature_category :portfolio_management, [:planning_hierarchy]
|
||||
|
||||
urgency :low, [:refs]
|
||||
urgency :high, [:unfoldered_environment_names]
|
||||
|
|
|
@ -27,7 +27,6 @@ module Types
|
|||
|
||||
field :description, GraphQL::Types::String, null: true,
|
||||
description: 'Short description of the project.'
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
field :tag_list, GraphQL::Types::String, null: true,
|
||||
deprecated: { reason: 'Use `topics`', milestone: '13.12' },
|
||||
|
@ -75,21 +74,6 @@ module Types
|
|||
field :avatar_url, GraphQL::Types::String, null: true, calls_gitaly: true,
|
||||
description: 'URL to avatar image file of the project.'
|
||||
|
||||
{
|
||||
issues: "Issues are",
|
||||
merge_requests: "Merge Requests are",
|
||||
wiki: 'Wikis are',
|
||||
snippets: 'Snippets are',
|
||||
container_registry: 'Container Registry is'
|
||||
}.each do |feature, name_string|
|
||||
field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
|
||||
description: "Indicates if #{name_string} enabled for the current user"
|
||||
|
||||
define_method "#{feature}_enabled" do
|
||||
object.feature_available?(feature, context[:current_user])
|
||||
end
|
||||
end
|
||||
|
||||
field :jobs_enabled, GraphQL::Types::Boolean, null: true,
|
||||
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user.'
|
||||
|
||||
|
@ -391,15 +375,6 @@ module Types
|
|||
null: true,
|
||||
description: 'Template used to create squash commit message in merge requests.'
|
||||
|
||||
def label(title:)
|
||||
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
|
||||
LabelsFinder
|
||||
.new(current_user, project: args[:key], title: titles)
|
||||
.execute
|
||||
.each { |label| loader.call(label.title, label) }
|
||||
end
|
||||
end
|
||||
|
||||
field :labels,
|
||||
Types::LabelType.connection_type,
|
||||
null: true,
|
||||
|
@ -411,6 +386,32 @@ module Types
|
|||
description: 'Work item types available to the project.',
|
||||
feature_flag: :work_items
|
||||
|
||||
def label(title:)
|
||||
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
|
||||
LabelsFinder
|
||||
.new(current_user, project: args[:key], title: titles)
|
||||
.execute
|
||||
.each { |label| loader.call(label.title, label) }
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
issues: "Issues are",
|
||||
merge_requests: "Merge Requests are",
|
||||
wiki: 'Wikis are',
|
||||
snippets: 'Snippets are',
|
||||
container_registry: 'Container Registry is'
|
||||
}.each do |feature, name_string|
|
||||
field "#{feature}_enabled", GraphQL::Types::Boolean, null: true,
|
||||
description: "Indicates if #{name_string} enabled for the current user"
|
||||
|
||||
define_method "#{feature}_enabled" do
|
||||
object.feature_available?(feature, context[:current_user])
|
||||
end
|
||||
end
|
||||
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
def avatar_url
|
||||
object.avatar_url(only_path: false)
|
||||
end
|
||||
|
|
|
@ -14,11 +14,12 @@ module Types
|
|||
|
||||
field :description, GraphQL::Types::String, null: true,
|
||||
description: 'Description of the topic.'
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
field :avatar_url, GraphQL::Types::String, null: true,
|
||||
description: 'URL to avatar image file of the topic.'
|
||||
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
def avatar_url
|
||||
object.avatar_url(only_path: false)
|
||||
end
|
||||
|
|
|
@ -20,7 +20,6 @@ module Types
|
|||
authorize: :download_code
|
||||
field :description, GraphQL::Types::String, null: true,
|
||||
description: 'Description (also known as "release notes") of the release.'
|
||||
markdown_field :description_html, null: true
|
||||
field :name, GraphQL::Types::String, null: true,
|
||||
description: 'Name of the release.'
|
||||
field :created_at, Types::TimeType, null: true,
|
||||
|
@ -42,14 +41,16 @@ module Types
|
|||
field :author, Types::UserType, null: true,
|
||||
description: 'User that created the release.'
|
||||
|
||||
def author
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
|
||||
end
|
||||
|
||||
field :commit, Types::CommitType, null: true,
|
||||
complexity: 10, calls_gitaly: true,
|
||||
description: 'Commit associated with the release.'
|
||||
|
||||
markdown_field :description_html, null: true
|
||||
|
||||
def author
|
||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
|
||||
end
|
||||
|
||||
def commit
|
||||
return if release.sha.nil?
|
||||
|
||||
|
|
|
@ -672,18 +672,16 @@ module ProjectsHelper
|
|||
html_escape(message) % { strong_start: strong_start, strong_end: strong_end, project_name: project.name, group_name: project.group ? project.group.name : nil }
|
||||
end
|
||||
|
||||
def visibility_confirm_modal_data(project, remove_form_id = nil)
|
||||
def visibility_confirm_modal_data(project, target_form_id = nil)
|
||||
{
|
||||
remove_form_id: remove_form_id,
|
||||
qa_selector: 'visibility_features_permissions_save_button',
|
||||
button_text: _('Save changes'),
|
||||
target_form_id: target_form_id,
|
||||
button_testid: 'reduce-project-visibility-button',
|
||||
button_variant: 'confirm',
|
||||
confirm_button_text: _('Reduce project visibility'),
|
||||
confirm_danger_message: confirm_reduce_visibility_message(project),
|
||||
phrase: project.full_path,
|
||||
additional_information: _('Note: current forks will keep their visibility level.'),
|
||||
html_confirmation_message: true
|
||||
html_confirmation_message: true.to_s,
|
||||
show_visibility_confirm_modal: show_visibility_confirm_modal?(project).to_s
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class ProjectCiCdSetting < ApplicationRecord
|
||||
belongs_to :project, inverse_of: :ci_cd_settings
|
||||
|
||||
DEFAULT_GIT_DEPTH = 50
|
||||
DEFAULT_GIT_DEPTH = 20
|
||||
|
||||
before_create :set_default_git_depth
|
||||
|
||||
|
|
|
@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_wiki
|
||||
enable :read_issue
|
||||
enable :read_label
|
||||
enable :read_work_items_hierarchy
|
||||
enable :read_milestone
|
||||
enable :read_snippet
|
||||
enable :read_project_member
|
||||
|
@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_issue_board_list
|
||||
enable :read_wiki
|
||||
enable :read_label
|
||||
enable :read_work_items_hierarchy
|
||||
enable :read_milestone
|
||||
enable :read_snippet
|
||||
enable :read_project_member
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
%div
|
||||
- if show_recaptcha_sign_up?
|
||||
= recaptcha_tags nonce: content_security_policy_nonce
|
||||
.submit-container
|
||||
.submit-container.gl-mt-5
|
||||
= f.submit button_text, class: 'btn gl-button btn-confirm gl-display-block gl-w-full', data: { qa_selector: 'new_user_register_button' }
|
||||
= render 'devise/shared/terms_of_service_notice', button_text: button_text
|
||||
- if show_omniauth_providers && omniauth_providers_placement == :bottom
|
||||
|
|
|
@ -32,64 +32,5 @@
|
|||
type_plural: type_plural,
|
||||
active_tokens: @active_personal_access_tokens,
|
||||
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
|
||||
- if Feature.enabled?(:hide_access_tokens, default_enabled: :yaml)
|
||||
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
|
||||
- else
|
||||
- unless Gitlab::CurrentSettings.disable_feed_token
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Feed token')
|
||||
%p
|
||||
= s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8.feed-token-reset
|
||||
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
|
||||
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
|
||||
%p.form-text.text-muted
|
||||
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
|
||||
= reset_message.html_safe
|
||||
|
||||
- if incoming_email_token_enabled?
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Incoming email token')
|
||||
%p
|
||||
= s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8.incoming-email-token-reset
|
||||
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
|
||||
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
|
||||
%p.form-text.text-muted
|
||||
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
|
||||
= reset_message.html_safe
|
||||
|
||||
- if static_objects_external_storage_enabled?
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Static object token')
|
||||
%p
|
||||
= s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8
|
||||
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
|
||||
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
|
||||
%p.form-text.text-muted
|
||||
- reset_link = url_for [:reset, :static_object_token, :profile]
|
||||
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
|
||||
- reset_link_end = '</a>'.html_safe
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
|
||||
= reset_message.html_safe
|
||||
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- page_title _("General")
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- expanded = expanded_by_default?
|
||||
- remove_visibility_form_id = 'reduce-visibility-form'
|
||||
- reduce_visibility_form_id = 'reduce-visibility-form'
|
||||
|
||||
%section.settings.general-settings.no-animate.expanded#js-general-settings
|
||||
.settings-header
|
||||
|
@ -18,11 +18,10 @@
|
|||
%p= _('Choose visibility level, enable/disable project features and their permissions, disable email notifications, and show default award emoji.')
|
||||
|
||||
.settings-content
|
||||
= form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: remove_visibility_form_id }, authenticity_token: true do |f|
|
||||
= form_for @project, html: { multipart: true, class: "sharing-permissions-form", id: reduce_visibility_form_id }, authenticity_token: true do |f|
|
||||
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
|
||||
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
|
||||
.js-project-permissions-form
|
||||
= f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: visibility_confirm_modal_data(@project, remove_visibility_form_id)
|
||||
.js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
|
||||
|
||||
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
|
||||
.settings-header
|
||||
|
|
5
app/views/shared/planning_hierarchy.html.haml
Normal file
5
app/views/shared/planning_hierarchy.html.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
- page_title _("Planning hierarchy")
|
||||
- has_sub_epics = Gitlab.ee? && @project&.feature_available?(:subepics)
|
||||
- has_epics = Gitlab.ee? && @project&.feature_available?(:epics)
|
||||
|
||||
#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }
|
|
@ -1,22 +1,23 @@
|
|||
- sslStatus = hook.enable_ssl_verification ? _('enabled') : _('disabled')
|
||||
- sslBadgeText = _('SSL Verification:') + ' ' + sslStatus
|
||||
|
||||
%li
|
||||
.row
|
||||
.col-md-8.col-lg-7
|
||||
%strong.light-header
|
||||
= hook.url
|
||||
- if hook.rate_limited?
|
||||
%span.gl-badge.badge-danger.badge-pill.sm= _('Disabled')
|
||||
= gl_badge_tag(_('Disabled'), variant: :danger, size: :sm)
|
||||
- elsif hook.permanently_disabled?
|
||||
%span.gl-badge.badge-danger.badge-pill.sm= s_('Webhooks|Failed to connect')
|
||||
= gl_badge_tag(s_('Webhooks|Failed to connect'), variant: :danger, size: :sm)
|
||||
- elsif hook.temporarily_disabled?
|
||||
%span.gl-badge.badge-warning.badge-pill.sm= s_('Webhooks|Fails to connect')
|
||||
= gl_badge_tag(s_('Webhooks|Fails to connect'), variant: :warning, size: :sm)
|
||||
|
||||
%div
|
||||
- hook.class.triggers.each_value do |trigger|
|
||||
- if hook.public_send(trigger)
|
||||
%span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2.deploy-project-label= trigger.to_s.titleize
|
||||
%span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2
|
||||
= _('SSL Verification:')
|
||||
= hook.enable_ssl_verification ? _('enabled') : _('disabled')
|
||||
= gl_badge_tag(trigger.to_s.titleize, size: :sm)
|
||||
= gl_badge_tag(sslBadgeText, size: :sm)
|
||||
|
||||
.col-md-4.col-lg-5.text-right-md.gl-mt-2
|
||||
%span>= render 'shared/web_hooks/test_button', hook: hook, button_class: 'btn-sm btn-default gl-mr-3'
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: hide_access_tokens
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76280
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347490
|
||||
milestone: '14.6'
|
||||
name: work_items_hierarchy
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76720
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
|
||||
milestone: '14.7'
|
||||
type: development
|
||||
group: group::access
|
||||
default_enabled: true
|
||||
group: group::product planning
|
||||
default_enabled: false
|
|
@ -641,6 +641,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
post :generate_new_export
|
||||
get :download_export
|
||||
get :activity
|
||||
get :planning_hierarchy
|
||||
get :refs
|
||||
put :new_issuable_address
|
||||
get :unfoldered_environment_names
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
- name: "Pseudonymizer" # The name of the feature to be deprecated
|
||||
announcement_milestone: "14.7" # The milestone when this feature was first announced as deprecated.
|
||||
announcement_date: "2021-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||
announcement_date: "2022-01-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
|
||||
removal_milestone: "15.0" # The milestone when this feature is planned to be removed
|
||||
removal_date: "2021-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
|
||||
removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed.
|
||||
body: | # Do not modify this line, instead modify the lines below.
|
||||
The Pseudonymizer feature is generally unused,
|
||||
can cause production issues with large databases,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
- name: "Sidekiq metrics and health checks configuration"
|
||||
announcement_milestone: "14.7"
|
||||
announcement_date: "2021-01-22"
|
||||
removal_milestone: "15.0"
|
||||
removal_date: "2022-05-22"
|
||||
breaking_change: true
|
||||
body: | # Do not modify this line, instead modify the lines below.
|
||||
Exporting Sidekiq metrics and health checks using a single process and port is deprecated.
|
||||
Support will be removed in 15.0.
|
||||
|
||||
We have updated Sidekiq to export [metrics and health checks from two separate processes](https://gitlab.com/groups/gitlab-org/-/epics/6409)
|
||||
to improve stability and availability and prevent data loss in edge cases.
|
||||
As those are two separate servers, a configuration change will be required in 15.0
|
||||
to explicitly set separate ports for metrics and health-checks.
|
||||
The newly introduced settings for `sidekiq['health_checks_*']`
|
||||
should always be set in `gitlab.rb`.
|
||||
For more information, check the documentation for [configuring Sidekiq](https://docs.gitlab.com/ee/administration/sidekiq.html).
|
||||
|
||||
These changes also require updates in either Prometheus to scrape the new endpoint or k8s health-checks to target the new
|
||||
health-check port to work properly, otherwise either metrics or health-checks will disappear.
|
||||
|
||||
For the deprecation period those settings are optional
|
||||
and GitLab will default the Sidekiq health-checks port to the same port as `sidekiq_exporter`
|
||||
and only run one server (not changing the current behaviour).
|
||||
Only if they are both set and a different port is provided, a separate metrics server will spin up
|
||||
to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
|
||||
stage: Enablement
|
||||
tiers: [Free, Premium, Ultimate]
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347509
|
||||
documentation_url: https://docs.gitlab.com/ee/administration/sidekiq.html
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexesForPrimaryEmailCleanupMigration < Gitlab::Database::Migration[1.0]
|
||||
USERS_INDEX = :index_users_on_id_for_primary_email_migration
|
||||
EMAIL_INDEX = :index_emails_on_email_user_id
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
unless index_exists_by_name?(:users, USERS_INDEX)
|
||||
|
||||
disable_statement_timeout do
|
||||
execute <<~SQL
|
||||
CREATE INDEX CONCURRENTLY #{USERS_INDEX}
|
||||
ON users (id) INCLUDE (email, confirmed_at)
|
||||
WHERE confirmed_at IS NOT NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :users, USERS_INDEX
|
||||
remove_concurrent_index_by_name :emails, EMAIL_INDEX
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed < Gitlab::Database::Migration[1.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
MIGRATION_NAME = 'AddPrimaryEmailToEmailsIfUserConfirmed'
|
||||
BATCH_SIZE = 10_000
|
||||
|
||||
# Stubbed class to access the User table
|
||||
class User < ActiveRecord::Base
|
||||
include ::EachBatch
|
||||
|
||||
self.table_name = 'users'
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||
|
||||
has_many :emails
|
||||
end
|
||||
|
||||
# Stubbed class to access the Emails table
|
||||
class Email < ActiveRecord::Base
|
||||
self.table_name = 'emails'
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
belongs_to :user
|
||||
end
|
||||
|
||||
def up
|
||||
finalize_background_migration(MIGRATION_NAME)
|
||||
|
||||
# Select confirmed users that do not have their primary email in the emails table,
|
||||
# and create the email record. There should be none if the background migration
|
||||
# completed, but in case there is any leftover, we deal with it synchronously.
|
||||
not_exists_condition = 'NOT EXISTS (SELECT 1 FROM emails WHERE emails.email = users.email AND emails.user_id = users.id)'
|
||||
|
||||
User.confirmed.each_batch(of: BATCH_SIZE) do |user_batch|
|
||||
user_batch.select(:id, :email, :confirmed_at).where(not_exists_condition).each do |user|
|
||||
current_time = Time.now.utc
|
||||
|
||||
begin
|
||||
Email.create(
|
||||
user_id: user.id,
|
||||
email: user.email,
|
||||
confirmed_at: user.confirmed_at,
|
||||
created_at: current_time,
|
||||
updated_at: current_time
|
||||
)
|
||||
rescue StandardError => error
|
||||
Gitlab::AppLogger.error("Could not add primary email #{user.email} to emails for user with ID #{user.id} due to #{error}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# Intentionally left blank
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropTemporaryIndexesForPrimaryEmailMigration < Gitlab::Database::Migration[1.0]
|
||||
USERS_INDEX = :index_users_on_id_for_primary_email_migration
|
||||
EMAIL_INDEX = :index_emails_on_email_user_id
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :users, USERS_INDEX
|
||||
remove_concurrent_index_by_name :emails, EMAIL_INDEX
|
||||
end
|
||||
|
||||
def down
|
||||
unless index_exists_by_name?(:users, USERS_INDEX)
|
||||
|
||||
disable_statement_timeout do
|
||||
execute <<~SQL
|
||||
CREATE INDEX CONCURRENTLY #{USERS_INDEX}
|
||||
ON users (id) INCLUDE (email, confirmed_at)
|
||||
WHERE confirmed_at IS NOT NULL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
add_concurrent_index :emails, [:email, :user_id], name: EMAIL_INDEX
|
||||
end
|
||||
end
|
1
db/schema_migrations/20211206161271
Normal file
1
db/schema_migrations/20211206161271
Normal file
|
@ -0,0 +1 @@
|
|||
fa4a39c3bea70d31e8144f8830ef0353f22a7a663a891d9043e79f362058fbde
|
1
db/schema_migrations/20211206162601
Normal file
1
db/schema_migrations/20211206162601
Normal file
|
@ -0,0 +1 @@
|
|||
529c7ea38bbaa0c29491c2dfdb654a4a6adba93122d9bc23d6632526ff7fdb05
|
1
db/schema_migrations/20211220064757
Normal file
1
db/schema_migrations/20211220064757
Normal file
|
@ -0,0 +1 @@
|
|||
34bfe07fff59a415540ca2c5c96b33dc9030c15b2ffbb30cb7deedeb939ae132
|
|
@ -28,14 +28,14 @@ A logger emits a log message only if its log level is equal to or above the mini
|
|||
|
||||
The following log levels are supported:
|
||||
|
||||
| Level | Name |
|
||||
|-------|---------|
|
||||
| 0 | DEBUG |
|
||||
| 1 | INFO |
|
||||
| 2 | WARN |
|
||||
| 3 | ERROR |
|
||||
| 4 | FATAL |
|
||||
| 5 | UNKNOWN |
|
||||
| Level | Name |
|
||||
|:------|:----------|
|
||||
| 0 | `DEBUG` |
|
||||
| 1 | `INFO` |
|
||||
| 2 | `WARN` |
|
||||
| 3 | `ERROR` |
|
||||
| 4 | `FATAL` |
|
||||
| 5 | `UNKNOWN` |
|
||||
|
||||
GitLab loggers emit all log messages because they are set to `DEBUG` by default.
|
||||
|
||||
|
@ -53,8 +53,8 @@ GITLAB_LOG_LEVEL=info
|
|||
For some services, other log levels are in place that are not affected by this setting.
|
||||
Some of these services have their own environment variables to override the log level. For example:
|
||||
|
||||
| Service | Log Level | Environment variable |
|
||||
|----------------------|-----------|----------------------|
|
||||
| Service | Log level | Environment variable |
|
||||
|:---------------------|:----------|:---------------------|
|
||||
| GitLab API | `INFO` | |
|
||||
| GitLab Cleanup | `INFO` | `DEBUG` |
|
||||
| GitLab Doctor | `INFO` | `VERBOSE` |
|
||||
|
@ -84,26 +84,26 @@ are written to a file called `current`. The `logrotate` service built into GitLa
|
|||
[manages all logs](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
|
||||
except those captured by `runit`.
|
||||
|
||||
| Log type | Managed by logrotate | Managed by svlogd/runit |
|
||||
|-------------------------------------------------|------------------------|-------------------------|
|
||||
| [Alertmanager Logs](#alertmanager-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Crond Logs](#crond-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Gitaly](#gitaly-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [GitLab Exporter for Omnibus](#gitlab-exporter) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [GitLab Pages Logs](#pages-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| GitLab Rails | **{check-circle}** Yes | **{dotted-circle}** No |
|
||||
| [GitLab Shell Logs](#gitlab-shelllog) | **{check-circle}** Yes | **{dotted-circle}** No |
|
||||
| [Grafana Logs](#grafana-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [LogRotate Logs](#logrotate-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Mailroom](#mail_room_jsonlog-default) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [NGINX](#nginx-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [PostgreSQL Logs](#postgresql-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Praefect Logs](#praefect-logs) | **{dotted-circle}** Yes| **{check-circle}** Yes |
|
||||
| [Prometheus Logs](#prometheus-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Puma](#puma-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [Redis Logs](#redis-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Registry Logs](#registry-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Workhorse Logs](#workhorse-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| Log type | Managed by logrotate | Managed by svlogd/runit |
|
||||
|:------------------------------------------------|:------------------------|:------------------------|
|
||||
| [Alertmanager Logs](#alertmanager-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Crond Logs](#crond-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Gitaly](#gitaly-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [GitLab Exporter for Omnibus](#gitlab-exporter) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [GitLab Pages Logs](#pages-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| GitLab Rails | **{check-circle}** Yes | **{dotted-circle}** No |
|
||||
| [GitLab Shell Logs](#gitlab-shelllog) | **{check-circle}** Yes | **{dotted-circle}** No |
|
||||
| [Grafana Logs](#grafana-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [LogRotate Logs](#logrotate-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Mailroom](#mail_room_jsonlog-default) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [NGINX](#nginx-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [PostgreSQL Logs](#postgresql-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Praefect Logs](#praefect-logs) | **{dotted-circle}** Yes | **{check-circle}** Yes |
|
||||
| [Prometheus Logs](#prometheus-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Puma](#puma-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [Redis Logs](#redis-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Registry Logs](#registry-logs) | **{dotted-circle}** No | **{check-circle}** Yes |
|
||||
| [Workhorse Logs](#workhorse-logs) | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
|
||||
## `production_json.log`
|
||||
|
||||
|
|
|
@ -27,13 +27,13 @@ lookup of authorized SSH keys.
|
|||
|
||||
WARNING:
|
||||
OpenSSH version 6.9+ is required because `AuthorizedKeysCommand` must be
|
||||
able to accept a fingerprint. Check the version of OpenSSH on your server.
|
||||
able to accept a fingerprint. Check the version of OpenSSH on your server with `sshd -V`.
|
||||
|
||||
## Fast lookup is required for Geo **(PREMIUM)**
|
||||
|
||||
By default, GitLab manages an `authorized_keys` file that is located in the
|
||||
`git` user's home directory. For most installations, this will be located under
|
||||
`/var/opt/gitlab/.ssh/authorized_keys`, but you can use the following command to locate the `authorized_keys` on your system.:
|
||||
`/var/opt/gitlab/.ssh/authorized_keys`, but you can use the following command to locate the `authorized_keys` on your system:
|
||||
|
||||
```shell
|
||||
getent passwd git | cut -d: -f6 | awk '{print $1"/.ssh/authorized_keys"}'
|
||||
|
@ -77,9 +77,13 @@ sudo service sshd reload
|
|||
```
|
||||
|
||||
Confirm that SSH is working by commenting out your user's key in the `authorized_keys`
|
||||
file (start the line with a `#` to comment it), and attempting to pull a repository.
|
||||
file (start the line with a `#` to comment it), and from your local machine, attempt to pull a repository or run:
|
||||
|
||||
A successful pull would mean that GitLab was able to find the key in the database,
|
||||
```shell
|
||||
ssh -T git@gitlab.example.com
|
||||
```
|
||||
|
||||
A successful pull or [welcome message](../../ssh/index.md#verify-that-you-can-connect) would mean that GitLab was able to find the key in the database,
|
||||
since it is not present in the file anymore.
|
||||
|
||||
NOTE:
|
||||
|
@ -114,7 +118,7 @@ adding a new one, and attempting to pull a repository.
|
|||
|
||||
Then you can backup and delete your `authorized_keys` file for best performance.
|
||||
The current users' keys are already present in the database, so there is no need for migration
|
||||
or for asking users to re-add their keys.
|
||||
or for users to re-add their keys.
|
||||
|
||||
## How to go back to using the `authorized_keys` file
|
||||
|
||||
|
|
|
@ -159,7 +159,8 @@ in the `.gitlab-ci.yml` file.
|
|||
|
||||
## Limit the number of changes fetched during clone
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28919) in GitLab 12.0.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28919) in GitLab 12.0.
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77576) `git depth` value in GitLab 14.7.
|
||||
|
||||
You can limit the number of changes that GitLab CI/CD fetches when it clones
|
||||
a repository.
|
||||
|
@ -171,8 +172,8 @@ a repository.
|
|||
The maximum value is `1000`. To disable shallow clone and make GitLab CI/CD
|
||||
fetch all branches and tags each time, keep the value empty or set to `0`.
|
||||
|
||||
In GitLab 12.0 and later, newly created projects automatically have a default
|
||||
`git depth` value of `50`.
|
||||
In GitLab versions 14.7 and later, newly created projects have a default `git depth`
|
||||
value of `20`. GitLab versions 14.6 and earlier have a default `git depth` value of `50`.
|
||||
|
||||
This value can be overridden by the [`GIT_DEPTH` variable](../large_repositories/index.md#shallow-cloning)
|
||||
in the `.gitlab-ci.yml` file.
|
||||
|
|
|
@ -335,7 +335,7 @@ can cause production issues with large databases,
|
|||
and can interfere with object storage development.
|
||||
It is now considered deprecated, and will be removed in GitLab 15.0.
|
||||
|
||||
Planned removal milestone: 15.0 (2021-05-22)
|
||||
Planned removal milestone: 15.0 (2022-05-22)
|
||||
|
||||
### Removal of Static Site Editor
|
||||
|
||||
|
@ -352,6 +352,30 @@ only supported report file in 15.0, but this is the first step towards GitLab su
|
|||
|
||||
Planned removal milestone: 15.0 (2022-05-22)
|
||||
|
||||
### Sidekiq metrics and health checks configuration
|
||||
|
||||
Exporting Sidekiq metrics and health checks using a single process and port is deprecated.
|
||||
Support will be removed in 15.0.
|
||||
|
||||
We have updated Sidekiq to export [metrics and health checks from two separate processes](https://gitlab.com/groups/gitlab-org/-/epics/6409)
|
||||
to improve stability and availability and prevent data loss in edge cases.
|
||||
As those are two separate servers, a configuration change will be required in 15.0
|
||||
to explicitly set separate ports for metrics and health-checks.
|
||||
The newly introduced settings for `sidekiq['health_checks_*']`
|
||||
should always be set in `gitlab.rb`.
|
||||
For more information, check the documentation for [configuring Sidekiq](https://docs.gitlab.com/ee/administration/sidekiq.html).
|
||||
|
||||
These changes also require updates in either Prometheus to scrape the new endpoint or k8s health-checks to target the new
|
||||
health-check port to work properly, otherwise either metrics or health-checks will disappear.
|
||||
|
||||
For the deprecation period those settings are optional
|
||||
and GitLab will default the Sidekiq health-checks port to the same port as `sidekiq_exporter`
|
||||
and only run one server (not changing the current behaviour).
|
||||
Only if they are both set and a different port is provided, a separate metrics server will spin up
|
||||
to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0.
|
||||
|
||||
Planned removal milestone: 15.0 (2022-05-22)
|
||||
|
||||
### Tracing in GitLab
|
||||
|
||||
Tracing in GitLab is an integration with Jaeger, an open-source end-to-end distributed tracing system. GitLab users can navigate to their Jaeger instance to gain insight into the performance of a deployed application, tracking each function or microservice that handles a given request. Tracing in GitLab is deprecated in GitLab 14.7, and scheduled for removal in 15.0. To track work on a possible replacement, see the issue for [Opstrace integration with GitLab](https://gitlab.com/groups/gitlab-org/-/epics/6976).
|
||||
|
|
|
@ -537,6 +537,31 @@ See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-g
|
|||
|
||||
- See [Maintenance mode issue in GitLab 13.9 to 14.4](#maintenance-mode-issue-in-gitlab-139-to-144).
|
||||
|
||||
- For GitLab Enterprise Edition customers, we noticed an issue when [subscription expiration is upcoming, and you create new subgroups and projects](https://gitlab.com/gitlab-org/gitlab/-/issues/322546). If you fall under that category and get 500 errors, you can work around this issue:
|
||||
|
||||
1. SSH into you GitLab server, and open a Rails console:
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails console
|
||||
```
|
||||
|
||||
1. Disable the following features:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:subscribable_subscription_banner)
|
||||
Feature.disable(:subscribable_license_banner)
|
||||
```
|
||||
|
||||
1. Restart Puma or Unicorn:
|
||||
|
||||
```shell
|
||||
#For installations using Puma
|
||||
sudo gitlab-ctl restart puma
|
||||
|
||||
#For installations using Unicorn
|
||||
sudo gitlab-ctl restart unicorn
|
||||
```
|
||||
|
||||
### 13.8.8
|
||||
|
||||
GitLab 13.8 includes a background migration to address [an issue with duplicate service records](https://gitlab.com/gitlab-org/gitlab/-/issues/290008). If duplicate services are present, this background migration must complete before a unique index is applied to the services table, which was [introduced in GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52563). Upgrades from GitLab 13.8 and earlier to later versions must include an intermediate upgrade to GitLab 13.8.8 and [must wait until the background migrations complete](#checking-for-background-migrations-before-upgrading) before proceeding.
|
||||
|
|
|
@ -63,6 +63,9 @@ vulnerability.
|
|||
|
||||
## Requirements
|
||||
|
||||
Dependency Scanning runs in the `test` stage, which is available by default. If you redefine the
|
||||
stages in the `.gitlab-ci.yml` file, the `test` stage is required.
|
||||
|
||||
To run dependency scanning jobs, by default, you need GitLab Runner with the
|
||||
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
|
||||
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
|
||||
|
|
|
@ -14,6 +14,8 @@ Currently, IaC scanning supports configuration files for Terraform, Ansible, AWS
|
|||
|
||||
## Requirements
|
||||
|
||||
IaC Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required.
|
||||
|
||||
To run IaC scanning jobs, by default, you need GitLab Runner with the
|
||||
[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
|
||||
[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
|
||||
|
|
|
@ -370,10 +370,10 @@ For information on this, see the [general Application Security troubleshooting s
|
|||
|
||||
### Error: `Couldn't run the gitleaks command: exit status 2`
|
||||
|
||||
If a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` variable
|
||||
is set to 50 (a [project default](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone)),
|
||||
the Secret Detection job fails as the clone is not deep enough to contain all of the
|
||||
relevant commits.
|
||||
If a pipeline is triggered from a Merge Request containing 60 commits while the `GIT_DEPTH` variable's
|
||||
value is less than that, the Secret Detection job fails as the clone is not deep enough to contain all of the
|
||||
relevant commits. For information on the current default value, see the
|
||||
[pipeline configuration documentation](../../../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone).
|
||||
|
||||
To confirm this as the cause of the error, set the
|
||||
[logging level](../../application_security/secret_detection/index.md#logging-level) to `debug`, then
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
|
@ -20,6 +20,20 @@ To learn about hierarchies in general, common frameworks, and using GitLab for
|
|||
portfolio management, see
|
||||
[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/).
|
||||
|
||||
## View planning hierarchies
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.7 and is behind the feature flag `work_items_hierarchy`.
|
||||
|
||||
To view the planning hierarchy in a project:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Project information > Planning hierarchy**.
|
||||
|
||||
Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy.
|
||||
The work items outside your subscription plan show up below **Unavailable structure**.
|
||||
|
||||
![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_7.png)
|
||||
|
||||
## Hierarchies with epics
|
||||
|
||||
With epics, you can achieve the following hierarchy:
|
||||
|
|
|
@ -14,6 +14,8 @@ variables:
|
|||
image: "$SECURE_ANALYZERS_PREFIX/secrets:$SECRETS_ANALYZER_VERSION"
|
||||
services: []
|
||||
allow_failure: true
|
||||
variables:
|
||||
GIT_DEPTH: "50"
|
||||
# `rules` must be overridden explicitly by each child job
|
||||
# see https://gitlab.com/gitlab-org/gitlab/-/issues/218444
|
||||
artifacts:
|
||||
|
|
27
lib/sidebars/concerns/work_item_hierarchy.rb
Normal file
27
lib/sidebars/concerns/work_item_hierarchy.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This module has the necessary methods to render
|
||||
# work items hierarchy menu
|
||||
module Sidebars
|
||||
module Concerns
|
||||
module WorkItemHierarchy
|
||||
def hierarchy_menu_item(container, url, path)
|
||||
unless show_hierarachy_menu_item?(container)
|
||||
return ::Sidebars::NilMenuItem.new(item_id: :hierarchy)
|
||||
end
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Planning hierarchy'),
|
||||
link: url,
|
||||
active_routes: { path: path },
|
||||
item_id: :hierarchy
|
||||
)
|
||||
end
|
||||
|
||||
def show_hierarachy_menu_item?(container)
|
||||
Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) &&
|
||||
can?(context.current_user, :read_work_items_hierarchy, container)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,10 +4,13 @@ module Sidebars
|
|||
module Projects
|
||||
module Menus
|
||||
class ProjectInformationMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::WorkItemHierarchy
|
||||
|
||||
override :configure_menu_items
|
||||
def configure_menu_items
|
||||
add_item(activity_menu_item)
|
||||
add_item(labels_menu_item)
|
||||
add_item(hierarchy_menu_item(context.project, planning_hierarchy_project_path(context.project), 'projects#planning_hierarchy'))
|
||||
add_item(members_menu_item)
|
||||
|
||||
true
|
||||
|
|
|
@ -1835,21 +1835,12 @@ msgstr ""
|
|||
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Personal Access Tokens"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1874,9 +1865,6 @@ msgstr ""
|
|||
msgid "AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|reset this token"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Learn more"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7083,6 +7071,9 @@ msgstr ""
|
|||
msgid "Child"
|
||||
msgstr ""
|
||||
|
||||
msgid "Child epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Child epic does not exist."
|
||||
msgstr ""
|
||||
|
||||
|
@ -17628,6 +17619,33 @@ msgstr[1] ""
|
|||
msgid "Hide values"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Current structure"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals."
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Help us improve work items in GitLab!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Planning hierarchy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Take the work items survey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|These items are unavailable in the current structure."
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Unavailable structure"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|You can start using these items now."
|
||||
msgstr ""
|
||||
|
||||
msgid "High or unknown vulnerabilities present"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26512,6 +26530,9 @@ msgstr ""
|
|||
msgid "Plan:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Planning hierarchy"
|
||||
msgstr ""
|
||||
|
||||
msgid "PlantUML"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30235,6 +30256,9 @@ msgstr ""
|
|||
msgid "Required only if you are not using role instance credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Requirement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Requirement %{reference} has been added"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34918,6 +34942,9 @@ msgstr ""
|
|||
msgid "Target-Branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task"
|
||||
msgstr ""
|
||||
|
||||
msgid "Task ID: %{elastic_task}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35163,6 +35190,9 @@ msgstr ""
|
|||
msgid "Test Cases"
|
||||
msgstr ""
|
||||
|
||||
msgid "Test case"
|
||||
msgstr ""
|
||||
|
||||
msgid "Test coverage parsing"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -5,12 +5,9 @@ module QA
|
|||
module Project
|
||||
module Settings
|
||||
class VisibilityFeaturesPermissions < Page::Base
|
||||
view 'app/helpers/projects_helper.rb' do
|
||||
element :visibility_features_permissions_save_button
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue' do
|
||||
element :project_visibility_dropdown
|
||||
element :visibility_features_permissions_save_button
|
||||
end
|
||||
|
||||
def set_project_visibility(visibility)
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
describe 'Merge request creation from fork' do
|
||||
describe 'Merge request creation from fork', quarantine: {
|
||||
only: { subdomain: %i[canary production] },
|
||||
issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/343801",
|
||||
type: :investigation
|
||||
} do
|
||||
let(:merge_request) do
|
||||
Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request|
|
||||
merge_request.fork_branch = 'feature-branch'
|
||||
|
|
39
spec/controllers/concerns/work_items_hierarchy_spec.rb
Normal file
39
spec/controllers/concerns/work_items_hierarchy_spec.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WorkItemsHierarchy do
|
||||
controller(ApplicationController) do
|
||||
include WorkItemsHierarchy
|
||||
end
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
render_views
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
routes.draw { get :planning_hierarchy, to: "anonymous#planning_hierarchy" }
|
||||
controller.instance_variable_set(:@project, project)
|
||||
end
|
||||
|
||||
it 'renders hierarchy' do
|
||||
stub_feature_flags(work_items_hierarchy: true)
|
||||
|
||||
get :planning_hierarchy
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to match(/id="js-work-items-hierarchy"/)
|
||||
end
|
||||
|
||||
it 'renders 404' do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
|
||||
get :planning_hierarchy
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
|
||||
end
|
||||
end
|
|
@ -62,66 +62,33 @@ RSpec.describe 'Profile account page', :js do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'when I reset feed token' do
|
||||
it 'resets feed token with `hide_access_tokens` feature flag enabled' do
|
||||
visit profile_personal_access_tokens_path
|
||||
it 'allows resetting of feed token' do
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('[data-testid="feed-token-container"]') do
|
||||
previous_token = find_field('Feed token').value
|
||||
within('[data-testid="feed-token-container"]') do
|
||||
previous_token = find_field('Feed token').value
|
||||
|
||||
accept_confirm { click_link('reset this token') }
|
||||
accept_confirm { click_link('reset this token') }
|
||||
|
||||
click_button('Click to reveal')
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(find_field('Feed token').value).not_to eq(previous_token)
|
||||
end
|
||||
end
|
||||
|
||||
it 'resets feed token with `hide_access_tokens` feature flag disabled' do
|
||||
stub_feature_flags(hide_access_tokens: false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('.feed-token-reset') do
|
||||
previous_token = find("#feed_token").value
|
||||
|
||||
accept_confirm { find('[data-testid="reset_feed_token_link"]').click }
|
||||
|
||||
expect(find('#feed_token').value).not_to eq(previous_token)
|
||||
end
|
||||
expect(find_field('Feed token').value).not_to eq(previous_token)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when I reset incoming email token' do
|
||||
before do
|
||||
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
end
|
||||
it 'allows resetting of incoming email token' do
|
||||
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
|
||||
|
||||
it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do
|
||||
visit profile_personal_access_tokens_path
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('[data-testid="incoming-email-token-container"]') do
|
||||
previous_token = find_field('Incoming email token').value
|
||||
within('[data-testid="incoming-email-token-container"]') do
|
||||
previous_token = find_field('Incoming email token').value
|
||||
|
||||
accept_confirm { click_link('reset this token') }
|
||||
accept_confirm { click_link('reset this token') }
|
||||
|
||||
click_button('Click to reveal')
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(find_field('Incoming email token').value).not_to eq(previous_token)
|
||||
end
|
||||
end
|
||||
|
||||
it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do
|
||||
stub_feature_flags(hide_access_tokens: false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('.incoming-email-token-reset') do
|
||||
previous_token = find('#incoming_email_token').value
|
||||
|
||||
accept_confirm { find('[data-testid="reset_email_token_link"]').click }
|
||||
|
||||
expect(find('#incoming_email_token').value).not_to eq(previous_token)
|
||||
end
|
||||
expect(find_field('Incoming email token').value).not_to eq(previous_token)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
|
||||
describe "feed token" do
|
||||
context "when enabled" do
|
||||
it "displays feed token with `hide_access_tokens` feature flag enabled" do
|
||||
it "displays feed token" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
|
@ -143,15 +143,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
expect(page).to have_content(feed_token_description)
|
||||
end
|
||||
end
|
||||
|
||||
it "displays feed token with `hide_access_tokens` feature flag disabled" do
|
||||
stub_feature_flags(hide_access_tokens: false)
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(page).to have_field('Feed token', with: user.feed_token)
|
||||
expect(page).to have_content(feed_token_description)
|
||||
end
|
||||
end
|
||||
|
||||
context "when disabled" do
|
||||
|
|
|
@ -24,7 +24,7 @@ RSpec.describe 'Edit Project Settings' do
|
|||
# disable by clicking toggle
|
||||
toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
|
||||
page.within('.sharing-permissions') do
|
||||
find('input[value="Save changes"]').click
|
||||
find('[data-testid="project-features-save-button"]').click
|
||||
end
|
||||
wait_for_requests
|
||||
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
|
||||
|
@ -32,7 +32,7 @@ RSpec.describe 'Edit Project Settings' do
|
|||
# re-enable by clicking toggle again
|
||||
toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
|
||||
page.within('.sharing-permissions') do
|
||||
find('input[value="Save changes"]').click
|
||||
find('[data-testid="project-features-save-button"]').click
|
||||
end
|
||||
wait_for_requests
|
||||
expect(page).to have_selector(".shortcuts-#{shortcut_name}")
|
||||
|
|
|
@ -47,7 +47,7 @@ RSpec.describe 'Projects settings' do
|
|||
# disable by clicking toggle
|
||||
forking_enabled_button.click
|
||||
page.within('.sharing-permissions') do
|
||||
find('input[value="Save changes"]').click
|
||||
find('[data-testid="project-features-save-button"]').click
|
||||
end
|
||||
wait_for_requests
|
||||
|
||||
|
@ -77,7 +77,7 @@ RSpec.describe 'Projects settings' do
|
|||
expect(default_award_emojis_input.value).to eq('false')
|
||||
|
||||
page.within('.sharing-permissions') do
|
||||
find('input[value="Save changes"]').click
|
||||
find('[data-testid="project-features-save-button"]').click
|
||||
end
|
||||
wait_for_requests
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
|
|||
|
||||
within('.sharing-permissions-form') do
|
||||
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
|
||||
find('input[value="Save changes"]').send_keys(:return)
|
||||
find('[data-testid="project-features-save-button"]').send_keys(:return)
|
||||
end
|
||||
|
||||
expect(page).not_to have_content 'Pipelines must succeed'
|
||||
|
@ -74,7 +74,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
|
|||
|
||||
within('.sharing-permissions-form') do
|
||||
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
|
||||
find('input[value="Save changes"]').send_keys(:return)
|
||||
find('[data-testid="project-features-save-button"]').send_keys(:return)
|
||||
end
|
||||
|
||||
expect(page).to have_content 'Pipelines must succeed'
|
||||
|
@ -95,7 +95,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
|
|||
|
||||
within('.sharing-permissions-form') do
|
||||
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
|
||||
find('input[value="Save changes"]').send_keys(:return)
|
||||
find('[data-testid="project-features-save-button"]').send_keys(:return)
|
||||
end
|
||||
|
||||
expect(page).to have_content 'Pipelines must succeed'
|
||||
|
|
|
@ -5,14 +5,6 @@ require 'spec_helper'
|
|||
RSpec.describe 'User changes public project visibility', :js do
|
||||
include ProjectForksHelper
|
||||
|
||||
before do
|
||||
fork_project(project, project.owner)
|
||||
|
||||
sign_in(project.owner)
|
||||
|
||||
visit edit_project_path(project)
|
||||
end
|
||||
|
||||
shared_examples 'changing visibility to private' do
|
||||
it 'requires confirmation' do
|
||||
visibility_select = first('.project-feature-controls .select-control')
|
||||
|
@ -34,15 +26,85 @@ RSpec.describe 'User changes public project visibility', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a project is public' do
|
||||
shared_examples 'does not require confirmation' do
|
||||
it 'saves without confirmation' do
|
||||
visibility_select = first('.project-feature-controls .select-control')
|
||||
visibility_select.select('Private')
|
||||
|
||||
page.within('#js-shared-permissions') do
|
||||
click_button 'Save changes'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(project.reload).to be_private
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has forks' do
|
||||
before do
|
||||
fork_project(project, project.owner)
|
||||
|
||||
sign_in(project.owner)
|
||||
|
||||
visit edit_project_path(project)
|
||||
end
|
||||
|
||||
context 'when a project is public' do
|
||||
let(:project) { create(:project, :empty_repo, :public) }
|
||||
|
||||
it_behaves_like 'changing visibility to private'
|
||||
end
|
||||
|
||||
context 'when the project is internal' do
|
||||
let(:project) { create(:project, :empty_repo, :internal) }
|
||||
|
||||
it_behaves_like 'changing visibility to private'
|
||||
end
|
||||
|
||||
context 'when the visibility level is untouched' do
|
||||
let(:project) { create(:project, :empty_repo, :public) }
|
||||
|
||||
it 'saves without confirmation' do
|
||||
expect(page).to have_selector('.js-emails-disabled', visible: true)
|
||||
find('.js-emails-disabled input[type="checkbox"]').click
|
||||
|
||||
page.within('#js-shared-permissions') do
|
||||
click_button 'Save changes'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(project.reload).to be_public
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project is not forked' do
|
||||
let(:project) { create(:project, :empty_repo, :public) }
|
||||
|
||||
it_behaves_like 'changing visibility to private'
|
||||
before do
|
||||
sign_in(project.owner)
|
||||
|
||||
visit edit_project_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not require confirmation'
|
||||
end
|
||||
|
||||
context 'when the project is internal' do
|
||||
let(:project) { create(:project, :empty_repo, :internal) }
|
||||
context 'with unlink_fork_network_upon_visibility_decrease = false' do
|
||||
let(:project) { create(:project, :empty_repo, :public) }
|
||||
|
||||
it_behaves_like 'changing visibility to private'
|
||||
before do
|
||||
stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false)
|
||||
|
||||
fork_project(project, project.owner)
|
||||
|
||||
sign_in(project.owner)
|
||||
|
||||
visit edit_project_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'does not require confirmation'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
visibilityLevelDescriptions,
|
||||
visibilityOptions,
|
||||
} from '~/pages/projects/shared/permissions/constants';
|
||||
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
|
||||
|
||||
const defaultProps = {
|
||||
currentSettings: {
|
||||
|
@ -47,6 +48,8 @@ const defaultProps = {
|
|||
packagesAvailable: false,
|
||||
packagesHelpPath: '/help/user/packages/index',
|
||||
requestCveAvailable: true,
|
||||
confirmationPhrase: 'my-fake-project',
|
||||
showVisibilityConfirmModal: false,
|
||||
};
|
||||
|
||||
describe('Settings Panel', () => {
|
||||
|
@ -104,6 +107,7 @@ describe('Settings Panel', () => {
|
|||
);
|
||||
const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' });
|
||||
const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' });
|
||||
const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -177,6 +181,44 @@ describe('Settings Panel', () => {
|
|||
|
||||
expect(findRequestAccessEnabledInput().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not require confirmation if the visibility is reduced', async () => {
|
||||
wrapper = mountComponent({
|
||||
currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
|
||||
});
|
||||
|
||||
expect(findConfirmDangerButton().exists()).toBe(false);
|
||||
|
||||
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
|
||||
|
||||
expect(findConfirmDangerButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('showVisibilityConfirmModal=true', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
currentSettings: { visibilityLevel: visibilityOptions.INTERNAL },
|
||||
showVisibilityConfirmModal: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('will render the confirmation dialog if the visibility is reduced', async () => {
|
||||
expect(findConfirmDangerButton().exists()).toBe(false);
|
||||
|
||||
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
|
||||
|
||||
expect(findConfirmDangerButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => {
|
||||
expect(wrapper.emitted('confirm')).toBeUndefined();
|
||||
|
||||
await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE);
|
||||
await findConfirmDangerButton().vm.$emit('confirm');
|
||||
|
||||
expect(wrapper.emitted('confirm')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issues settings', () => {
|
||||
|
|
File diff suppressed because it is too large
Load diff
78
spec/frontend/work_items_hierarchy/components/app_spec.js
Normal file
78
spec/frontend/work_items_hierarchy/components/app_spec.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import App from '~/work_items_hierarchy/components/app.vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
describe('WorkItemsHierarchy App', () => {
|
||||
let wrapper;
|
||||
const createComponent = (props = {}, data = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
mount(App, {
|
||||
localVue,
|
||||
provide: {
|
||||
illustrationPath: '/foo.svg',
|
||||
licensePlan: 'free',
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe.each`
|
||||
licensePlan
|
||||
${'free'}
|
||||
${'premium'}
|
||||
${'ultimate'}
|
||||
`('when licensePlan is $licensePlan', ({ licensePlan }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ licensePlan });
|
||||
});
|
||||
|
||||
it('matches the snapshot', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('survey banner', () => {
|
||||
it('shows when the banner is visible', () => {
|
||||
createComponent({}, { bannerVisible: true });
|
||||
|
||||
expect(wrapper.find(GlBanner).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hide when close is called', async () => {
|
||||
createComponent({}, { bannerVisible: true });
|
||||
|
||||
wrapper.findByTestId('close-icon').trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find(GlBanner).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unavailable structure', () => {
|
||||
it.each`
|
||||
licensePlan | visible
|
||||
${'free'} | ${true}
|
||||
${'premium'} | ${true}
|
||||
${'ultimate'} | ${false}
|
||||
`('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
|
||||
createComponent({ licensePlan });
|
||||
|
||||
expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
|
||||
});
|
||||
});
|
||||
});
|
118
spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
Normal file
118
spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import RESPONSE from '~/work_items_hierarchy/static_response';
|
||||
import { workItemTypes } from '~/work_items/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
describe('WorkItemsHierarchy Hierarchy', () => {
|
||||
let wrapper;
|
||||
|
||||
const workItemsFromResponse = (response) => {
|
||||
return response.reduce(
|
||||
(itemTypes, item) => {
|
||||
const key = item.available ? 'available' : 'unavailable';
|
||||
itemTypes[key].push({
|
||||
...item,
|
||||
...workItemTypes[item.type],
|
||||
nestedTypes: item.nestedTypes
|
||||
? item.nestedTypes.map((type) => workItemTypes[type])
|
||||
: null,
|
||||
});
|
||||
return itemTypes;
|
||||
},
|
||||
{ available: [], unavailable: [] },
|
||||
);
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
mount(Hierarchy, {
|
||||
localVue,
|
||||
propsData: {
|
||||
workItemTypes: props.workItemTypes,
|
||||
...props,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('available structure', () => {
|
||||
let items = [];
|
||||
|
||||
beforeEach(() => {
|
||||
items = workItemsFromResponse(RESPONSE.ultimate).available;
|
||||
createComponent({ workItemTypes: items });
|
||||
});
|
||||
|
||||
it('renders all work items', () => {
|
||||
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('does not render badges', () => {
|
||||
expect(wrapper.find(GlBadge).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unavailable structure', () => {
|
||||
let items = [];
|
||||
|
||||
beforeEach(() => {
|
||||
items = workItemsFromResponse(RESPONSE.premium).unavailable;
|
||||
createComponent({ workItemTypes: items });
|
||||
});
|
||||
|
||||
it('renders all work items', () => {
|
||||
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('renders license badges for all work items', () => {
|
||||
expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('does not render svg icon for linking', () => {
|
||||
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
|
||||
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested work items', () => {
|
||||
describe.each`
|
||||
licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
|
||||
${'ultimate'} | ${true} | ${true} | ${true}
|
||||
${'premium'} | ${false} | ${false} | ${true}
|
||||
${'free'} | ${false} | ${false} | ${false}
|
||||
`(
|
||||
'when $licensePlan license',
|
||||
({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
|
||||
let items = [];
|
||||
beforeEach(() => {
|
||||
items = workItemsFromResponse(RESPONSE[licensePlan]).available;
|
||||
createComponent({ workItemTypes: items });
|
||||
});
|
||||
|
||||
it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
|
||||
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
|
||||
arrowTailVisible,
|
||||
);
|
||||
});
|
||||
|
||||
it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
|
||||
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
|
||||
});
|
||||
|
||||
it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
|
||||
expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
16
spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
Normal file
16
spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
|
||||
import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
|
||||
|
||||
describe('inferLicensePlan', () => {
|
||||
it.each`
|
||||
epics | subEpics | licensePlan
|
||||
${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
|
||||
${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
|
||||
${false} | ${false} | ${LICENSE_PLAN.FREE}
|
||||
`(
|
||||
'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
|
||||
({ epics, subEpics, licensePlan }) => {
|
||||
expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
|
||||
},
|
||||
);
|
||||
});
|
36
spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
Normal file
36
spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
|
||||
shared_examples 'hierarchy menu' do
|
||||
let(:item_id) { :hierarchy }
|
||||
|
||||
context 'when the feature is disabled does not render' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
end
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when the feature is enabled does render' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: true)
|
||||
end
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Project hierarchy menu item' do
|
||||
let_it_be_with_reload(:project) { create(:project, :repository) }
|
||||
|
||||
let(:user) { project.owner }
|
||||
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||
|
||||
subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
|
||||
|
||||
it_behaves_like 'hierarchy menu'
|
||||
end
|
||||
end
|
|
@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
|
|||
specify { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Hierarchy' do
|
||||
let(:item_id) { :hierarchy }
|
||||
|
||||
context 'when the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
end
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when the feature is enabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: true)
|
||||
end
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do
|
||||
let(:migration) { described_class.new }
|
||||
let(:users) { table(:users) }
|
||||
let(:emails) { table(:emails) }
|
||||
|
||||
let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 3.days.ago, projects_limit: 100) }
|
||||
let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
|
||||
let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) }
|
||||
let!(:user_4) { users.create!(name: 'unconfirmed-user', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) }
|
||||
|
||||
let!(:email_1) { emails.create!(email: 'confirmed-1@example.com', user_id: user_1.id, confirmed_at: 1.day.ago) }
|
||||
let!(:email_2) { emails.create!(email: 'other_2@example.com', user_id: user_2.id, confirmed_at: 1.day.ago) }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class.name}::BATCH_SIZE", 2)
|
||||
end
|
||||
|
||||
it 'consume any pending background migration job' do
|
||||
expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
|
||||
expect(coordinator).to receive(:steal).with('AddPrimaryEmailToEmailsIfUserConfirmed').twice
|
||||
end
|
||||
|
||||
migration.up
|
||||
end
|
||||
|
||||
it 'adds the primary email to emails for leftover confirmed users that do not have their primary email in the emails table', :aggregate_failures do
|
||||
original_email_1_confirmed_at = email_1.reload.confirmed_at
|
||||
|
||||
expect { migration.up }.to change { emails.count }.by(2)
|
||||
|
||||
expect(emails.find_by(user_id: user_2.id, email: 'confirmed-2@example.com').confirmed_at).to eq(user_2.reload.confirmed_at)
|
||||
expect(emails.find_by(user_id: user_3.id, email: 'confirmed-3@example.com').confirmed_at).to eq(user_3.reload.confirmed_at)
|
||||
expect(email_1.reload.confirmed_at).to eq(original_email_1_confirmed_at)
|
||||
|
||||
expect(emails.exists?(user_id: user_4.id)).to be(false)
|
||||
end
|
||||
|
||||
it 'continues in case of errors with one email' do
|
||||
allow(Email).to receive(:create) { raise 'boom!' }
|
||||
|
||||
expect { migration.up }.not_to raise_error
|
||||
end
|
||||
end
|
|
@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do
|
|||
nav_sub_items: [
|
||||
_('Activity'),
|
||||
_('Labels'),
|
||||
_('Planning hierarchy'),
|
||||
_('Members')
|
||||
]
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
|
|||
%i[
|
||||
award_emoji create_issue create_merge_request_in create_note
|
||||
create_project read_issue_board read_issue read_issue_iid read_issue_link
|
||||
read_label read_issue_board_list read_milestone read_note read_project
|
||||
read_label read_work_items_hierarchy read_issue_board_list read_milestone read_note read_project
|
||||
read_project_for_iids read_project_member read_release read_snippet
|
||||
read_wiki upload_file
|
||||
]
|
||||
|
|
|
@ -48,7 +48,7 @@ class FindChanges # rubocop:disable Gitlab/NamespacedClass
|
|||
|
||||
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
|
||||
|
||||
mr_changes.changes.map { |change| change['new_path'] }
|
||||
mr_changes.changes.map { |change| change['new_path'] unless change['deleted_file'] }.compact
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue