Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
68c476dbd8
commit
49bb78aac3
|
@ -1,8 +1,9 @@
|
|||
include:
|
||||
- template: Jobs/Code-Quality.gitlab-ci.yml
|
||||
# - template: Security/SAST.gitlab-ci.yml
|
||||
# - template: Security/Dependency-Scanning.gitlab-ci.yml
|
||||
# - template: Security/DAST.gitlab-ci.yml
|
||||
- template: Security/SAST.gitlab-ci.yml
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
- template: Security/Dependency-Scanning.gitlab-ci.yml
|
||||
- template: Security/License-Scanning.gitlab-ci.yml
|
||||
|
||||
code_quality:
|
||||
extends:
|
||||
|
@ -13,85 +14,55 @@ code_quality:
|
|||
- gl-code-quality-report.json # GitLab-specific
|
||||
rules: !reference [".reports:rules:code_quality", rules]
|
||||
|
||||
# We need to duplicate this job's definition because the rules
|
||||
# defined in the extended jobs rely on local YAML anchors
|
||||
# (`*if-default-refs`)
|
||||
.sast:
|
||||
.sast-analyzer:
|
||||
# We need to re-`extends` from `sast` as the `extends` here overrides the one from the template.
|
||||
extends:
|
||||
- .default-retry
|
||||
- .reports:rules:sast
|
||||
stage: test
|
||||
# `needs: []` starts the job immediately in the pipeline
|
||||
# https://docs.gitlab.com/ee/ci/yaml/README.html#needs
|
||||
- sast
|
||||
needs: []
|
||||
artifacts:
|
||||
paths:
|
||||
- gl-sast-report.json # GitLab-specific
|
||||
reports:
|
||||
sast: gl-sast-report.json
|
||||
expire_in: 1 week # GitLab-specific
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
|
||||
SAST_ANALYZER_IMAGE_TAG: 2
|
||||
SAST_BRAKEMAN_LEVEL: 2 # GitLab-specific
|
||||
SAST_EXCLUDED_PATHS: qa,spec,doc,ee/spec,config/gitlab.yml.example # GitLab-specific
|
||||
SAST_EXCLUDED_PATHS: "qa, spec, doc, ee/spec, config/gitlab.yml.example, tmp" # GitLab-specific
|
||||
SAST_DISABLE_BABEL: "true"
|
||||
script:
|
||||
- /analyzer run
|
||||
|
||||
brakeman-sast:
|
||||
extends: .sast
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
|
||||
rules: !reference [".reports:rules:sast", rules]
|
||||
|
||||
eslint-sast:
|
||||
extends: .sast
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
|
||||
rules: !reference [".reports:rules:sast", rules]
|
||||
|
||||
nodejs-scan-sast:
|
||||
extends: .sast
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
|
||||
rules: !reference [".reports:rules:sast", rules]
|
||||
|
||||
secrets-sast:
|
||||
extends: .sast
|
||||
image:
|
||||
name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:3"
|
||||
.secret-analyzer:
|
||||
extends: .default-retry
|
||||
needs: []
|
||||
artifacts:
|
||||
paths:
|
||||
- gl-secret-detection-report.json # GitLab-specific
|
||||
reports:
|
||||
sast: gl-secret-detection-report.json
|
||||
expire_in: 1 week # GitLab-specific
|
||||
|
||||
# We need to duplicate this job's definition because the rules
|
||||
# defined in the extended jobs rely on local YAML anchors
|
||||
# (`*if-default-refs`)
|
||||
.dependency_scanning:
|
||||
secret_detection:
|
||||
rules: !reference [".reports:rules:secret_detection", rules]
|
||||
|
||||
.ds-analyzer:
|
||||
# We need to re-`extends` from `dependency_scanning` as the `extends` here overrides the one from the template.
|
||||
extends:
|
||||
- .default-retry
|
||||
- .reports:rules:dependency_scanning
|
||||
stage: test
|
||||
- dependency_scanning
|
||||
needs: []
|
||||
variables:
|
||||
DS_MAJOR_VERSION: 2
|
||||
DS_EXCLUDED_PATHS: "qa/qa/ee/fixtures/secure_premade_reports, spec, ee/spec" # GitLab-specific
|
||||
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
|
||||
DS_EXCLUDED_PATHS: "qa/qa/ee/fixtures/secure_premade_reports, spec, ee/spec, tmp" # GitLab-specific
|
||||
artifacts:
|
||||
paths:
|
||||
- gl-dependency-scanning-report.json # GitLab-specific
|
||||
reports:
|
||||
dependency_scanning: gl-dependency-scanning-report.json
|
||||
expire_in: 1 week # GitLab-specific
|
||||
script:
|
||||
- /analyzer run
|
||||
|
||||
dependency_scanning gemnasium:
|
||||
extends: .dependency_scanning
|
||||
image:
|
||||
name: "$SECURE_ANALYZERS_PREFIX/gemnasium:$DS_MAJOR_VERSION"
|
||||
gemnasium-dependency_scanning:
|
||||
before_script:
|
||||
# git-lfs is needed for auto-remediation
|
||||
- apk add git-lfs
|
||||
|
@ -100,26 +71,22 @@ dependency_scanning gemnasium:
|
|||
- apk add jq
|
||||
# Lower execa severity based on https://gitlab.com/gitlab-org/gitlab/-/issues/223859#note_452922390
|
||||
- jq '(.vulnerabilities[] | select (.cve == "yarn.lock:execa:gemnasium:05cfa2e8-2d0c-42c1-8894-638e2f12ff3d")).severity = "Medium"' gl-dependency-scanning-report.json > temp.json && mv temp.json gl-dependency-scanning-report.json
|
||||
rules: !reference [".reports:rules:dependency_scanning", rules]
|
||||
|
||||
dependency_scanning bundler-audit:
|
||||
extends: .dependency_scanning
|
||||
image:
|
||||
name: "$SECURE_ANALYZERS_PREFIX/bundler-audit:$DS_MAJOR_VERSION"
|
||||
bundler-audit-dependency_scanning:
|
||||
rules: !reference [".reports:rules:dependency_scanning", rules]
|
||||
|
||||
dependency_scanning retire-js:
|
||||
extends: .dependency_scanning
|
||||
image:
|
||||
name: "$SECURE_ANALYZERS_PREFIX/retire.js:$DS_MAJOR_VERSION"
|
||||
retire-js-dependency_scanning:
|
||||
rules: !reference [".reports:rules:dependency_scanning", rules]
|
||||
|
||||
dependency_scanning gemnasium-python:
|
||||
extends: .dependency_scanning
|
||||
image:
|
||||
name: "$SECURE_ANALYZERS_PREFIX/gemnasium-python:$DS_MAJOR_VERSION"
|
||||
gemnasium-python-dependency_scanning:
|
||||
rules: !reference [".reports:rules:dependency_scanning", rules]
|
||||
|
||||
# Analyze dependencies for malicious behavior
|
||||
# See https://gitlab.com/gitlab-com/gl-security/security-research/package-hunter
|
||||
package_hunter:
|
||||
extends:
|
||||
- .default-retry
|
||||
- .reports:rules:package_hunter
|
||||
stage: test
|
||||
image:
|
||||
|
@ -133,24 +100,14 @@ package_hunter:
|
|||
- DEBUG=* HTR_user=$PACKAGE_HUNTER_USER HTR_pass=$PACKAGE_HUNTER_PASS node /usr/src/app/cli.js analyze --format gitlab gitlab.tgz | tee $CI_PROJECT_DIR/gl-dependency-scanning-report.json
|
||||
artifacts:
|
||||
paths:
|
||||
- gl-dependency-scanning-report.json # GitLab-specific
|
||||
- gl-dependency-scanning-report.json
|
||||
reports:
|
||||
dependency_scanning: gl-dependency-scanning-report.json
|
||||
expire_in: 1 week # GitLab-specific
|
||||
expire_in: 1 week
|
||||
|
||||
license_scanning:
|
||||
extends:
|
||||
- .default-retry
|
||||
- .reports:rules:license_scanning
|
||||
stage: test
|
||||
image:
|
||||
name: "registry.gitlab.com/gitlab-org/security-products/analyzers/license-finder:3"
|
||||
entrypoint: [""]
|
||||
extends: .default-retry
|
||||
needs: []
|
||||
script:
|
||||
- /run.sh analyze .
|
||||
artifacts:
|
||||
reports:
|
||||
license_scanning: gl-license-scanning-report.json
|
||||
expire_in: 1 week # GitLab-specific
|
||||
dependencies: []
|
||||
rules: !reference [".reports:rules:license_scanning", rules]
|
||||
|
|
|
@ -1001,6 +1001,16 @@
|
|||
changes: *code-backstage-qa-patterns
|
||||
allow_failure: true
|
||||
|
||||
.reports:rules:secret_detection:
|
||||
rules:
|
||||
- if: '$SECRET_DETECTION_DISABLED'
|
||||
when: never
|
||||
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' # The Secret-Detection template already has a `secret_detection_default_branch` job
|
||||
when: never
|
||||
# - <<: *if-default-branch-refs # To be done in a later iteration: https://gitlab.com/gitlab-org/gitlab/issues/31160#note_278188255
|
||||
- changes: *code-backstage-qa-patterns
|
||||
allow_failure: true
|
||||
|
||||
.reports:rules:dependency_scanning:
|
||||
rules:
|
||||
- if: '$DEPENDENCY_SCANNING_DISABLED || $GITLAB_FEATURES !~ /\bdependency_scanning\b/'
|
||||
|
|
|
@ -8,8 +8,6 @@ const createAnchor = (href) => {
|
|||
const fragment = new DocumentFragment();
|
||||
const el = document.createElement('a');
|
||||
el.classList.add('link-anchor');
|
||||
el.setAttribute('data-qa-selector', 'line_link');
|
||||
el.setAttribute('data-qa-number', href);
|
||||
el.href = href;
|
||||
fragment.appendChild(el);
|
||||
el.addEventListener('contextmenu', (e) => {
|
||||
|
|
|
@ -37,15 +37,16 @@ export const TRANSLATION_KEYS = {
|
|||
},
|
||||
};
|
||||
|
||||
export const FREQUENT_ITEMS_DROPDOWNS = [
|
||||
{
|
||||
namespace: 'projects',
|
||||
key: 'project',
|
||||
vuexModule: 'frequentProjects',
|
||||
},
|
||||
{
|
||||
namespace: 'groups',
|
||||
key: 'group',
|
||||
vuexModule: 'frequentGroups',
|
||||
},
|
||||
];
|
||||
export const FREQUENT_ITEMS_PROJECTS = {
|
||||
namespace: 'projects',
|
||||
key: 'project',
|
||||
vuexModule: 'frequentProjects',
|
||||
};
|
||||
|
||||
export const FREQUENT_ITEMS_GROUPS = {
|
||||
namespace: 'groups',
|
||||
key: 'group',
|
||||
vuexModule: 'frequentGroups',
|
||||
};
|
||||
|
||||
export const FREQUENT_ITEMS_DROPDOWNS = [FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS];
|
||||
|
|
|
@ -13,14 +13,16 @@ export const createFrequentItemsModule = (initState = {}) => ({
|
|||
state: state(initState),
|
||||
});
|
||||
|
||||
export const createStoreOptions = () => ({
|
||||
modules: FREQUENT_ITEMS_DROPDOWNS.reduce(
|
||||
(acc, { namespace, vuexModule }) =>
|
||||
Object.assign(acc, {
|
||||
[vuexModule]: createFrequentItemsModule({ dropdownType: namespace }),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
|
||||
export const createStore = () => {
|
||||
return new Vuex.Store({
|
||||
modules: FREQUENT_ITEMS_DROPDOWNS.reduce(
|
||||
(acc, { namespace, vuexModule }) =>
|
||||
Object.assign(acc, {
|
||||
[vuexModule]: createFrequentItemsModule({ dropdownType: namespace }),
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
return new Vuex.Store(createStoreOptions());
|
||||
};
|
||||
|
|
|
@ -35,6 +35,7 @@ import initUsagePingConsent from './usage_ping_consent';
|
|||
import GlFieldErrors from './gl_field_errors';
|
||||
import initUserPopovers from './user_popovers';
|
||||
import initBroadcastNotifications from './broadcast_notification';
|
||||
import { initTopNav } from './nav';
|
||||
|
||||
import 'ee_else_ce/main_ee';
|
||||
|
||||
|
@ -80,6 +81,7 @@ initRails();
|
|||
function deferredInitialisation() {
|
||||
const $body = $('body');
|
||||
|
||||
initTopNav();
|
||||
initBreadcrumbs();
|
||||
initTodoToggle();
|
||||
initLogoAnimation();
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<script>
|
||||
import { GlNav, GlNavItemDropdown, GlDropdownForm, GlTooltip } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
|
||||
|
||||
const TOOLTIP = s__('TopNav|Switch to...');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlNav,
|
||||
GlNavItemDropdown,
|
||||
GlDropdownForm,
|
||||
GlTooltip,
|
||||
TopNavDropdownMenu,
|
||||
},
|
||||
props: {
|
||||
navData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
findTooltipTarget() {
|
||||
// ### Why use a target function instead of `v-gl-tooltip`?
|
||||
// To get the tooltip to align correctly, we need it to target the actual
|
||||
// toggle button which we don't directly render.
|
||||
return this.$el.querySelector('.js-top-nav-dropdown-toggle');
|
||||
},
|
||||
},
|
||||
TOOLTIP,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-nav class="navbar-sub-nav">
|
||||
<gl-nav-item-dropdown
|
||||
:text="navData.activeTitle"
|
||||
icon="dot-grid"
|
||||
menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto!"
|
||||
toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
|
||||
no-flip
|
||||
>
|
||||
<gl-dropdown-form>
|
||||
<top-nav-dropdown-menu
|
||||
:primary="navData.primary"
|
||||
:secondary="navData.secondary"
|
||||
:views="navData.views"
|
||||
/>
|
||||
</gl-dropdown-form>
|
||||
</gl-nav-item-dropdown>
|
||||
<gl-tooltip
|
||||
boundary="window"
|
||||
:boundary-padding="0"
|
||||
:target="findTooltipTarget"
|
||||
placement="right"
|
||||
:title="$options.TOOLTIP"
|
||||
/>
|
||||
</gl-nav>
|
||||
</template>
|
|
@ -0,0 +1,74 @@
|
|||
<script>
|
||||
import FrequentItemsApp from '~/frequent_items/components/app.vue';
|
||||
import eventHub from '~/frequent_items/event_hub';
|
||||
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
|
||||
import TopNavMenuItem from './top_nav_menu_item.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FrequentItemsApp,
|
||||
TopNavMenuItem,
|
||||
VuexModuleProvider,
|
||||
},
|
||||
props: {
|
||||
frequentItemsVuexModule: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
frequentItemsDropdownType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
linksPrimary: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
linksSecondary: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
linkGroups() {
|
||||
return [
|
||||
{ key: 'primary', links: this.linksPrimary },
|
||||
{ key: 'secondary', links: this.linksSecondary },
|
||||
].filter((x) => x.links?.length);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// For historic reasons, the frequent-items-app component requires this too start up.
|
||||
this.$nextTick(() => {
|
||||
eventHub.$emit(`${this.frequentItemsDropdownType}-dropdownOpen`);
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
|
||||
<div class="frequent-items-dropdown-container gl-w-auto">
|
||||
<div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
|
||||
<vuex-module-provider :vuex-module="frequentItemsVuexModule">
|
||||
<frequent-items-app v-bind="$attrs" />
|
||||
</vuex-module-provider>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="({ key, links }, groupIndex) in linkGroups"
|
||||
:key="key"
|
||||
:class="{ 'gl-mt-3': groupIndex !== 0 }"
|
||||
class="gl-mt-auto gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
|
||||
data-testid="menu-item-group"
|
||||
>
|
||||
<top-nav-menu-item
|
||||
v-for="(link, linkIndex) in links"
|
||||
:key="link.title"
|
||||
:menu-item="link"
|
||||
:class="{ 'gl-mt-1': linkIndex !== 0 }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,144 @@
|
|||
<script>
|
||||
import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
|
||||
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
|
||||
import TopNavContainerView from './top_nav_container_view.vue';
|
||||
import TopNavMenuItem from './top_nav_menu_item.vue';
|
||||
|
||||
const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
|
||||
const SECONDARY_GROUP_CLASS = 'gl-pt-3 gl-mt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
KeepAliveSlots,
|
||||
TopNavContainerView,
|
||||
TopNavMenuItem,
|
||||
},
|
||||
props: {
|
||||
primary: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
secondary: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
views: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeId: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
menuItemGroups() {
|
||||
return [
|
||||
{ key: 'primary', items: this.primary, classes: '' },
|
||||
{
|
||||
key: 'secondary',
|
||||
items: this.secondary,
|
||||
classes: SECONDARY_GROUP_CLASS,
|
||||
},
|
||||
].filter((x) => x.items?.length);
|
||||
},
|
||||
allMenuItems() {
|
||||
return this.menuItemGroups.flatMap((x) => x.items);
|
||||
},
|
||||
activeMenuItem() {
|
||||
return this.allMenuItems.find((x) => x.id === this.activeId);
|
||||
},
|
||||
activeView() {
|
||||
return this.activeMenuItem?.view;
|
||||
},
|
||||
menuClass() {
|
||||
if (!this.activeView) {
|
||||
return 'gl-w-full';
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// Initialize activeId based on initialization prop
|
||||
this.activeId = this.allMenuItems.find((x) => x.active)?.id;
|
||||
},
|
||||
methods: {
|
||||
onClick({ id, href }) {
|
||||
// If we're a link, let's just do the default behavior so the view won't change
|
||||
if (href) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeId = id;
|
||||
},
|
||||
menuItemClasses(menuItem) {
|
||||
if (menuItem.id === this.activeId) {
|
||||
return ACTIVE_CLASS;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
},
|
||||
FREQUENT_ITEMS_PROJECTS,
|
||||
FREQUENT_ITEMS_GROUPS,
|
||||
// expose for unit tests
|
||||
ACTIVE_CLASS,
|
||||
SECONDARY_GROUP_CLASS,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-stretch">
|
||||
<div
|
||||
class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10"
|
||||
:class="menuClass"
|
||||
data-testid="menu-sidebar"
|
||||
>
|
||||
<div
|
||||
class="gl-py-3 gl-px-5 gl-h-full gl-display-flex gl-align-items-stretch gl-flex-direction-column"
|
||||
>
|
||||
<div
|
||||
v-for="group in menuItemGroups"
|
||||
:key="group.key"
|
||||
:class="group.classes"
|
||||
data-testid="menu-item-group"
|
||||
>
|
||||
<top-nav-menu-item
|
||||
v-for="(menu, index) in group.items"
|
||||
:key="menu.id"
|
||||
data-testid="menu-item"
|
||||
:class="[{ 'gl-mt-1': index !== 0 }, menuItemClasses(menu)]"
|
||||
:menu-item="menu"
|
||||
@click="onClick(menu)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<keep-alive-slots
|
||||
v-show="activeView"
|
||||
:slot-key="activeView"
|
||||
class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
|
||||
data-testid="menu-subview"
|
||||
>
|
||||
<template #projects>
|
||||
<top-nav-container-view
|
||||
:frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
|
||||
:frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
|
||||
v-bind="views.projects"
|
||||
/>
|
||||
</template>
|
||||
<template #groups>
|
||||
<top-nav-container-view
|
||||
:frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
|
||||
:frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
|
||||
v-bind="views.groups"
|
||||
/>
|
||||
</template>
|
||||
</keep-alive-slots>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { GlButton, GlIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
:href="menuItem.href"
|
||||
class="top-nav-menu-item gl-display-block"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<span class="gl-display-flex">
|
||||
<gl-icon v-if="menuItem.icon" :name="menuItem.icon" class="gl-mr-2!" />
|
||||
{{ menuItem.title }}
|
||||
<gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
|
||||
</span>
|
||||
</gl-button>
|
||||
</template>
|
|
@ -0,0 +1,12 @@
|
|||
export const initTopNav = async () => {
|
||||
const el = document.getElementById('js-top-nav');
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
// With combined_menu feature flag, there's a benefit to splitting up the import
|
||||
const { mountTopNav } = await import(/* webpackChunkName: 'top_nav' */ './mount');
|
||||
|
||||
mountTopNav(el);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import App from './components/top_nav_app.vue';
|
||||
import { createStore } from './stores';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export const mountTopNav = (el) => {
|
||||
const viewModel = JSON.parse(el.dataset.viewModel);
|
||||
const store = createStore();
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
render(h) {
|
||||
return h(App, {
|
||||
props: {
|
||||
navData: viewModel,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
import Vuex from 'vuex';
|
||||
import { createStoreOptions } from '~/frequent_items/store';
|
||||
|
||||
export const createStore = () => new Vuex.Store(createStoreOptions());
|
|
@ -839,8 +839,52 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
|||
.frequent-items-dropdown-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 500px;
|
||||
height: 354px;
|
||||
height: $grid-size * 40;
|
||||
|
||||
&.with-deprecated-styles {
|
||||
width: 500px;
|
||||
height: 354px;
|
||||
|
||||
.section-header,
|
||||
.frequent-items-list-container li.section-empty {
|
||||
padding: 0 $gl-padding;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
position: relative;
|
||||
padding: 4px $gl-padding;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
right: 25px;
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
|
||||
.frequent-items-dropdown-sidebar,
|
||||
.frequent-items-dropdown-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.frequent-items-dropdown-sidebar {
|
||||
border-bottom: 1px solid $border-color;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.frequent-items-list-container {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.frequent-items-dropdown-sidebar,
|
||||
.frequent-items-dropdown-content {
|
||||
|
@ -861,26 +905,8 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
|||
width: 70%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
|
||||
.frequent-items-dropdown-sidebar,
|
||||
.frequent-items-dropdown-content {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.frequent-items-dropdown-sidebar {
|
||||
border-bottom: 1px solid $border-color;
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header,
|
||||
.frequent-items-list-container li.section-empty {
|
||||
padding: 0 $gl-padding;
|
||||
color: $gl-text-color-secondary;
|
||||
font-size: $gl-font-size;
|
||||
}
|
||||
|
@ -898,36 +924,16 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
|
|||
}
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
position: relative;
|
||||
padding: 4px $gl-padding;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
right: 25px;
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-weight: 700;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.frequent-items-list-container {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frequent-items-list-item-container {
|
||||
.frequent-items-item-avatar-container,
|
||||
.frequent-items-item-metadata-container {
|
||||
float: left;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.frequent-items-item-metadata-container {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
$top-nav-hover-bg: var(--indigo-900-alpha-008, $indigo-900-alpha-008) !important;
|
||||
|
||||
.navbar-gitlab {
|
||||
padding: 0 16px;
|
||||
z-index: $header-zindex;
|
||||
|
@ -254,6 +256,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.top-nav-toggle,
|
||||
> button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
|
@ -629,3 +632,36 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-nav-container-view {
|
||||
.gl-new-dropdown & .gl-search-box-by-type {
|
||||
@include gl-m-0;
|
||||
}
|
||||
|
||||
.frequent-items-list-item-container > a:hover {
|
||||
background-color: $top-nav-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.top-nav-toggle {
|
||||
.dropdown-icon {
|
||||
@include gl-mr-3;
|
||||
}
|
||||
|
||||
.dropdown-chevron {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.top-nav-menu-item {
|
||||
color: var(--indigo-900, $theme-indigo-900) !important;
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
background-color: $top-nav-hover-bg;
|
||||
}
|
||||
|
||||
.gl-icon {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -283,6 +283,8 @@ $indigo-700: #4b4ba3;
|
|||
$indigo-800: #393982;
|
||||
$indigo-900: #292961;
|
||||
$indigo-950: #1a1a40;
|
||||
// To do this variant right for darkmode, we need to create a variable for it.
|
||||
$indigo-900-alpha-008: rgba($indigo-900, 0.08);
|
||||
|
||||
$theme-blue-50: #f4f8fc;
|
||||
$theme-blue-100: #e6edf5;
|
||||
|
|
|
@ -70,6 +70,7 @@ $indigo-700: #a6a6de;
|
|||
$indigo-800: #d1d1f0;
|
||||
$indigo-900: #ebebfa;
|
||||
$indigo-950: #f7f7ff;
|
||||
$indigo-900-alpha-008: rgba($indigo-900, 0.08);
|
||||
|
||||
$gray-lightest: #222;
|
||||
$gray-light: $gray-50;
|
||||
|
@ -160,6 +161,7 @@ body.gl-dark {
|
|||
--indigo-800: #{$indigo-800};
|
||||
--indigo-900: #{$indigo-900};
|
||||
--indigo-950: #{$indigo-950};
|
||||
--indigo-900-alpha-008: #{$indigo-900-alpha-008};
|
||||
|
||||
--gl-text-color: #{$gray-900};
|
||||
--border-color: #{$border-color};
|
||||
|
|
|
@ -189,3 +189,21 @@ $gl-line-height-42: px-to-rem(42px);
|
|||
.gl-line-height-42 {
|
||||
line-height: $gl-line-height-42;
|
||||
}
|
||||
|
||||
.gl-w-grid-size-30 {
|
||||
width: $grid-size * 30;
|
||||
}
|
||||
|
||||
.gl-w-grid-size-40 {
|
||||
width: $grid-size * 40;
|
||||
}
|
||||
|
||||
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
|
||||
.gl-max-w-none\! {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
|
||||
.gl-max-h-none\! {
|
||||
max-height: none !important;
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ module Boards
|
|||
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
|
||||
issues = issues_from(list_service)
|
||||
|
||||
Issue.move_nulls_to_end(issues) if Gitlab::Database.read_write?
|
||||
if Gitlab::Database.read_write? && !board.disabled_for?(current_user)
|
||||
Issue.move_nulls_to_end(issues)
|
||||
end
|
||||
|
||||
render_issues(issues, list_service.metadata)
|
||||
end
|
||||
|
|
|
@ -176,7 +176,11 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def retry
|
||||
pipeline.retry_failed(current_user)
|
||||
if Gitlab::Ci::Features.background_pipeline_retry_endpoint?(@project)
|
||||
::Ci::RetryPipelineWorker.perform_async(pipeline.id, current_user.id) # rubocop:disable CodeReuse/Worker
|
||||
else
|
||||
pipeline.retry_failed(current_user)
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
@ -10,7 +10,7 @@ module BoardsHelper
|
|||
boards_endpoint: @boards_endpoint,
|
||||
lists_endpoint: board_lists_path(board),
|
||||
board_id: board.id,
|
||||
disabled: disabled?.to_s,
|
||||
disabled: board.disabled_for?(current_user).to_s,
|
||||
root_path: root_path,
|
||||
full_path: full_path,
|
||||
bulk_update_path: @bulk_issues_path,
|
||||
|
@ -105,10 +105,6 @@ module BoardsHelper
|
|||
can?(current_user, :admin_issue, current_board_parent)
|
||||
end
|
||||
|
||||
def disabled?
|
||||
!can?(current_user, :create_non_backlog_issues, board)
|
||||
end
|
||||
|
||||
def board_list_data
|
||||
include_descendant_groups = @group&.present?
|
||||
|
||||
|
|
|
@ -9,6 +9,22 @@ module IssuesHelper
|
|||
classes.join(' ')
|
||||
end
|
||||
|
||||
def issue_manual_ordering_class
|
||||
is_sorting_by_relative_position = @sort == 'relative_position'
|
||||
|
||||
if is_sorting_by_relative_position && !issue_repositioning_disabled?
|
||||
"manual-ordering"
|
||||
end
|
||||
end
|
||||
|
||||
def issue_repositioning_disabled?
|
||||
if @group
|
||||
@group.root_ancestor.issue_repositioning_disabled?
|
||||
elsif @project
|
||||
@project.root_namespace.issue_repositioning_disabled?
|
||||
end
|
||||
end
|
||||
|
||||
def status_box_class(item)
|
||||
if item.try(:expired?)
|
||||
'status-box-expired'
|
||||
|
|
|
@ -11,16 +11,24 @@ module VersionCheckHelper
|
|||
|
||||
def link_to_version
|
||||
if Gitlab.pre_release?
|
||||
commit_link = link_to(Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', source_code_project, Gitlab.revision))
|
||||
commit_link = link_to(Gitlab.revision, source_host_url + namespace_project_commits_path(source_code_group, source_code_project, Gitlab.revision))
|
||||
[Gitlab::VERSION, content_tag(:small, commit_link)].join(' ').html_safe
|
||||
else
|
||||
link_to Gitlab::VERSION, Gitlab::COM_URL + namespace_project_tag_path('gitlab-org', source_code_project, "v#{Gitlab::VERSION}")
|
||||
link_to Gitlab::VERSION, source_host_url + namespace_project_tag_path(source_code_group, source_code_project, "v#{Gitlab::VERSION}")
|
||||
end
|
||||
end
|
||||
|
||||
def source_host_url
|
||||
Gitlab::COM_URL
|
||||
end
|
||||
|
||||
def source_code_group
|
||||
'gitlab-org'
|
||||
end
|
||||
|
||||
def source_code_project
|
||||
'gitlab-foss'
|
||||
end
|
||||
end
|
||||
|
||||
VersionCheckHelper.prepend_mod_with('VersionCheckHelper')
|
||||
VersionCheckHelper.prepend_mod
|
||||
|
|
|
@ -45,6 +45,12 @@ class Board < ApplicationRecord
|
|||
def to_type
|
||||
self.class.to_type
|
||||
end
|
||||
|
||||
def disabled_for?(current_user)
|
||||
namespace = group_board? ? resource_parent.root_ancestor : resource_parent.root_namespace
|
||||
|
||||
namespace.issue_repositioning_disabled? || !Ability.allowed?(current_user, :create_non_backlog_issues, self)
|
||||
end
|
||||
end
|
||||
|
||||
Board.prepend_mod_with('Board')
|
||||
|
|
|
@ -23,6 +23,7 @@ module Enums
|
|||
user_blocked: 14,
|
||||
project_deleted: 15,
|
||||
ci_quota_exceeded: 16,
|
||||
pipeline_loop_detected: 17,
|
||||
insufficient_bridge_permissions: 1_001,
|
||||
downstream_bridge_project_not_found: 1_002,
|
||||
invalid_bridge_trigger: 1_003,
|
||||
|
|
|
@ -79,6 +79,8 @@ module RelativePositioning
|
|||
objects = objects.reject(&:relative_position)
|
||||
return 0 if objects.empty?
|
||||
|
||||
objects.first.check_repositioning_allowed!
|
||||
|
||||
number_of_gaps = objects.size # 1 to the nearest neighbour, and one between each
|
||||
representative = RelativePositioning.mover.context(objects.first)
|
||||
|
||||
|
@ -123,6 +125,12 @@ module RelativePositioning
|
|||
::Gitlab::RelativePositioning::Mover.new(START_POSITION, (MIN_POSITION..MAX_POSITION))
|
||||
end
|
||||
|
||||
# To be overriden on child classes whenever
|
||||
# blocking position updates is necessary.
|
||||
def check_repositioning_allowed!
|
||||
nil
|
||||
end
|
||||
|
||||
def move_between(before, after)
|
||||
before, after = [before, after].sort_by(&:relative_position) if before && after
|
||||
|
||||
|
|
|
@ -272,6 +272,18 @@ class Issue < ApplicationRecord
|
|||
"id DESC")
|
||||
end
|
||||
|
||||
# Temporary disable moving null elements because of performance problems
|
||||
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
|
||||
def check_repositioning_allowed!
|
||||
if blocked_for_repositioning?
|
||||
raise ::Gitlab::RelativePositioning::IssuePositioningDisabled, "Issue relative position changes temporarily disabled."
|
||||
end
|
||||
end
|
||||
|
||||
def blocked_for_repositioning?
|
||||
resource_parent.root_namespace&.issue_repositioning_disabled?
|
||||
end
|
||||
|
||||
def hook_attrs
|
||||
Gitlab::HookData::IssueBuilder.new(self).build
|
||||
end
|
||||
|
|
|
@ -37,6 +37,7 @@ class MergeRequest < ApplicationRecord
|
|||
SORTING_PREFERENCE_FIELD = :merge_requests_sort
|
||||
|
||||
ALLOWED_TO_USE_MERGE_BASE_PIPELINE_FOR_COMPARISON = {
|
||||
'Ci::CompareMetricsReportsService' => ->(project) { ::Gitlab::Ci::Features.merge_base_pipeline_for_metrics_comparison?(project) },
|
||||
'Ci::CompareCodequalityReportsService' => ->(project) { true }
|
||||
}.freeze
|
||||
|
||||
|
|
|
@ -425,6 +425,10 @@ class Namespace < ApplicationRecord
|
|||
created_at >= 90.days.ago
|
||||
end
|
||||
|
||||
def issue_repositioning_disabled?
|
||||
Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expire_child_caches
|
||||
|
|
|
@ -15,6 +15,7 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
|
|||
scheduler_failure: 'The scheduler failed to assign job to the runner, please try again or contact system administrator',
|
||||
data_integrity_failure: 'There has been a structural integrity problem detected, please contact system administrator',
|
||||
forward_deployment_failure: 'The deployment job is older than the previously succeeded deployment job, and therefore cannot be run',
|
||||
pipeline_loop_detected: 'This job could not be executed because it would create infinitely looping pipelines',
|
||||
invalid_bridge_trigger: 'This job could not be executed because downstream pipeline trigger definition is invalid',
|
||||
downstream_bridge_project_not_found: 'This job could not be executed because downstream bridge project could not be found',
|
||||
insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
|
||||
|
|
|
@ -85,6 +85,12 @@ module Ci
|
|||
return false
|
||||
end
|
||||
|
||||
if has_cyclic_dependency?
|
||||
@bridge.drop!(:pipeline_loop_detected)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -109,11 +115,24 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def has_cyclic_dependency?
|
||||
return false if @bridge.triggers_child_pipeline?
|
||||
|
||||
if Feature.enabled?(:ci_drop_cyclical_triggered_pipelines, @bridge.project, default_enabled: :yaml)
|
||||
checksums = @bridge.pipeline.base_and_ancestors.map { |pipeline| config_checksum(pipeline) }
|
||||
checksums.uniq.length != checksums.length
|
||||
end
|
||||
end
|
||||
|
||||
def has_max_descendants_depth?
|
||||
return false unless @bridge.triggers_child_pipeline?
|
||||
|
||||
ancestors_of_new_child = @bridge.pipeline.base_and_ancestors(same_project: true)
|
||||
ancestors_of_new_child.count > MAX_DESCENDANTS_DEPTH
|
||||
end
|
||||
|
||||
def config_checksum(pipeline)
|
||||
[pipeline.project_id, pipeline.ref].hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,6 +100,8 @@ module Issues
|
|||
end
|
||||
|
||||
def handle_move_between_ids(issue)
|
||||
issue.check_repositioning_allowed! if params[:move_between_ids]
|
||||
|
||||
super
|
||||
|
||||
rebalance_if_needed(issue)
|
||||
|
|
|
@ -73,3 +73,5 @@ class SubmitUsagePingService
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
SubmitUsagePingService.prepend_mod
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
= render 'shared/alerts/positioning_disabled'
|
||||
|
||||
= render "shared/boards/show", board: @board, group: true
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
= _('Next')
|
||||
|
||||
- if Feature.enabled?(:combined_menu, current_user, default_enabled: :yaml)
|
||||
= render "layouts/nav/combined_menu"
|
||||
= render "layouts/nav/top_nav"
|
||||
- else
|
||||
- if current_user
|
||||
= render "layouts/nav/dashboard"
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
%button{ type: 'button', data: { toggle: "dropdown" } }
|
||||
= sprite_icon('ellipsis_v')
|
||||
= _('Projects')
|
|
@ -0,0 +1,7 @@
|
|||
- view_model = top_nav_view_model(project: @project, group: @group)
|
||||
%ul.list-unstyled.navbar-sub-nav#js-top-nav{ data: { view_model: view_model.to_json } }
|
||||
%li
|
||||
%a.top-nav-toggle{ href: '#', type: 'button', data: { toggle: "dropdown" } }
|
||||
= sprite_icon('dot-grid', css_class: "dropdown-icon")
|
||||
= view_model[:activeTitle]
|
||||
= sprite_icon('chevron-down')
|
|
@ -3,7 +3,7 @@
|
|||
-# Please see [this MR][1] for more context.
|
||||
-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
|
||||
- group_meta = { id: @group.id, name: @group.name, namespace: @group.full_name, web_url: group_path(@group), avatar_url: @group.avatar_url } if @group&.persisted?
|
||||
.frequent-items-dropdown-container
|
||||
.frequent-items-dropdown-container.with-deprecated-styles
|
||||
.frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
|
||||
%ul
|
||||
= nav_link(path: 'dashboard/groups#index') do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
-# Please see [this MR][1] for more context.
|
||||
-# [1]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56587
|
||||
- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
|
||||
.frequent-items-dropdown-container
|
||||
.frequent-items-dropdown-container.with-deprecated-styles
|
||||
.frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
|
||||
%ul
|
||||
= nav_link(path: 'dashboard/projects#index') do
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
- is_project_overview = local_assigns.fetch(:is_project_overview, false)
|
||||
= render 'shared/alerts/positioning_disabled'
|
||||
|
||||
- if Feature.enabled?(:vue_issuables_list, @project) && !is_project_overview
|
||||
- data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
|
||||
|
@ -15,7 +16,7 @@
|
|||
'scoped-labels-available': scoped_labels_available?(@project).to_json } }
|
||||
- else
|
||||
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
|
||||
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
|
||||
%ul.content-list.issues-list.issuable-list{ class: issue_manual_ordering_class }
|
||||
= render partial: "projects/issues/issue", collection: @issues
|
||||
- if @issues.blank?
|
||||
= render empty_state_path
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
= render 'shared/alerts/positioning_disabled'
|
||||
|
||||
- if @issues.to_a.any?
|
||||
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
|
||||
%ul.content-list.issues-list.issuable-list{ class: issue_manual_ordering_class, data: { group_full_path: @group&.full_path } }
|
||||
= render partial: 'projects/issues/issue', collection: @issues
|
||||
= paginate @issues, theme: "gitlab"
|
||||
- else
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
- if issue_repositioning_disabled?
|
||||
= render 'shared/alert_info', body: _('Issues manual ordering is temporarily disabled for technical reasons.')
|
|
@ -7,6 +7,8 @@
|
|||
- breadcrumb_title _("Epic Boards")
|
||||
- else
|
||||
- breadcrumb_title _("Issue Boards")
|
||||
= render 'shared/alerts/positioning_disabled'
|
||||
|
||||
- page_title("#{board.name}", _("Boards"))
|
||||
- add_page_specific_style 'page_bundles/boards'
|
||||
|
||||
|
|
|
@ -1515,6 +1515,15 @@
|
|||
:weight: 3
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: pipeline_default:ci_retry_pipeline
|
||||
:worker_name: Ci::RetryPipelineWorker
|
||||
:feature_category: :continuous_integration
|
||||
:has_external_dependencies:
|
||||
:urgency: :high
|
||||
:resource_boundary: :cpu
|
||||
:weight: 3
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: pipeline_default:pipeline_metrics
|
||||
:worker_name: PipelineMetricsWorker
|
||||
:feature_category: :continuous_integration
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class RetryPipelineWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ::ApplicationWorker
|
||||
include ::PipelineQueue
|
||||
|
||||
urgency :high
|
||||
worker_resource_boundary :cpu
|
||||
|
||||
def perform(pipeline_id, user_id)
|
||||
::Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
|
||||
::User.find_by_id(user_id).try do |user|
|
||||
pipeline.retry_failed(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,10 @@ class IssuePlacementWorker
|
|||
issue = find_issue(issue_id, project_id)
|
||||
return unless issue
|
||||
|
||||
# Temporary disable moving null elements because of performance problems
|
||||
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
|
||||
return if issue.blocked_for_repositioning?
|
||||
|
||||
# Move the oldest 100 unpositioned items to the end.
|
||||
# This is to deal with out-of-order execution of the worker,
|
||||
# while preserving creation order.
|
||||
|
|
|
@ -14,6 +14,11 @@ class IssueRebalancingWorker
|
|||
return if project_id.nil?
|
||||
|
||||
project = Project.find(project_id)
|
||||
|
||||
# Temporary disable reabalancing for performance reasons
|
||||
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
|
||||
return if project.root_namespace&.issue_repositioning_disabled?
|
||||
|
||||
# All issues are equivalent as far as we are concerned
|
||||
issue = project.issues.take # rubocop: disable CodeReuse/ActiveRecord
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ module SshKeys
|
|||
def perform
|
||||
return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml)
|
||||
|
||||
User.with_ssh_key_expired_today.find_each do |user|
|
||||
# rubocop:disable CodeReuse/ActiveRecord
|
||||
User.with_ssh_key_expired_today.find_each(batch_size: 10_000) do |user|
|
||||
with_context(user: user) do
|
||||
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expired ssh key(s)"
|
||||
|
||||
|
@ -22,6 +23,7 @@ module SshKeys
|
|||
|
||||
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: false }).execute
|
||||
end
|
||||
# rubocop:enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,8 @@ module SshKeys
|
|||
def perform
|
||||
return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml)
|
||||
|
||||
User.with_ssh_key_expiring_soon.find_each do |user|
|
||||
# rubocop:disable CodeReuse/ActiveRecord
|
||||
User.with_ssh_key_expiring_soon.find_each(batch_size: 10_000) do |user|
|
||||
with_context(user: user) do
|
||||
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring soon ssh key(s)"
|
||||
|
||||
|
@ -23,6 +24,7 @@ module SshKeys
|
|||
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: true }).execute
|
||||
end
|
||||
end
|
||||
# rubocop:enable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Optimize merge request permission check for references
|
||||
merge_request: 61591
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Implement wildcard support for pipeline include file paths
|
||||
merge_request: 61507
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make find_remote_root_refs_inmemory feature flag enabled by default
|
||||
merge_request: 61824
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow migrating scheduled and retried Sidekiq jobs to new queues
|
||||
merge_request: 60724
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make pipeline retry endpoint async.
|
||||
merge_request: 61270
|
||||
author:
|
||||
type: changed
|
|
@ -56,8 +56,9 @@ module Gitlab
|
|||
|
||||
config.generators.templates.push("#{config.root}/generator_templates")
|
||||
|
||||
foss_eager_load_paths = config.eager_load_paths.dup.freeze
|
||||
load_paths = lambda do |dir:|
|
||||
ext_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
|
||||
ext_paths = foss_eager_load_paths.each_with_object([]) do |path, memo|
|
||||
ext_path = config.root.join(dir, Pathname.new(path).relative_path_from(config.root))
|
||||
memo << ext_path.to_s
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: background_pipeline_retry_endpoint
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61270
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330915
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_drop_cyclical_triggered_pipelines
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1195
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329390
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::continuous integration
|
||||
default_enabled: true
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327315
|
|||
milestone: '13.11'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329664
|
|||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::gitaly
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: merge_base_pipeline_for_metrics_comparison
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61282
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330809
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::testing
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: block_issue_repositioning
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60141
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329663
|
||||
milestone: '13.12'
|
||||
type: ops
|
||||
group: group::project management
|
||||
default_enabled: false
|
|
@ -9,7 +9,7 @@ value_type: number
|
|||
status: data_available
|
||||
time_frame: 28d
|
||||
data_source: database
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssuesMetric'
|
||||
instrumentation_class: CountUsersCreatingIssuesMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -9,7 +9,7 @@ value_type: number
|
|||
status: data_available
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersUsingApproveQuickActionMetric'
|
||||
instrumentation_class: CountUsersUsingApproveQuickActionMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -9,7 +9,7 @@ value_type: number
|
|||
status: data_available
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersUsingApproveQuickActionMetric'
|
||||
instrumentation_class: CountUsersUsingApproveQuickActionMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -9,7 +9,7 @@ value_type: number
|
|||
status: data_available
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountIssuesMetric'
|
||||
instrumentation_class: CountIssuesMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -9,7 +9,7 @@ value_type: number
|
|||
status: data_available
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountUsersCreatingIssuesMetric'
|
||||
instrumentation_class: CountUsersCreatingIssuesMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -9,7 +9,7 @@ value_type: number
|
|||
status: data_available
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountBoardsMetric'
|
||||
instrumentation_class: CountBoardsMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -11,7 +11,7 @@ milestone: "9.1"
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1521
|
||||
time_frame: none
|
||||
data_source: database
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::UuidMetric'
|
||||
instrumentation_class: UuidMetric
|
||||
distribution:
|
||||
- ee
|
||||
- ce
|
||||
|
|
|
@ -9,7 +9,7 @@ value_type: string
|
|||
status: data_available
|
||||
time_frame: none
|
||||
data_source: system
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::HostnameMetric'
|
||||
instrumentation_class: HostnameMetric
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
},
|
||||
"instrumentation_class": {
|
||||
"type": "string",
|
||||
"pattern": "^(Gitlab::Usage::Metrics::Instrumentations::)(([A-Z][a-z]+)+::)*(([A-Z][a-z]+)+)$"
|
||||
"pattern": "^(([A-Z][a-z]+)+::)*(([A-Z][a-z]+)+)$"
|
||||
},
|
||||
"distribution": {
|
||||
"type": "array",
|
||||
|
|
|
@ -482,10 +482,15 @@ Use local includes instead of symbolic links.
|
|||
##### `include:local` with wildcard file paths
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/25921) in GitLab 13.11.
|
||||
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to enable it. **(CORE ONLY)**
|
||||
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
|
||||
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/327315) in GitLab 13.12.
|
||||
> - Enabled on GitLab.com.
|
||||
> - Recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to disable it. **(CORE ONLY)**
|
||||
|
||||
There can be
|
||||
[risks when disabling released features](../../user/feature_flags.md#risks-when-disabling-released-features).
|
||||
Refer to this feature's version history for more details.
|
||||
|
||||
You can use wildcard paths (`*` and `**`) with `include:local`.
|
||||
|
||||
|
@ -509,10 +514,10 @@ When the pipeline runs, GitLab:
|
|||
include: 'configs/**/*.yml'
|
||||
```
|
||||
|
||||
The wildcard file paths feature is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
The wildcard file paths feature is under development but ready for production use.
|
||||
It is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
|
||||
can enable it.
|
||||
can opt to disable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
|
|
|
@ -45,6 +45,30 @@ ENTITY_TITLE
|
|||
|
||||
You can [disable comments](#disable-comments-on-jira-issues) on issues.
|
||||
|
||||
### Require associated Jira issue for merge requests to be merged
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280766) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.12 behind a feature flag, disabled by default.
|
||||
> - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
|
||||
> - Disabled on GitLab.com.
|
||||
> - Not recommended for production use.
|
||||
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-the-ability-to-require-an-associated-jira-issue-on-merge-requests). **(ULTIMATE SELF)**
|
||||
|
||||
This in-development feature might not be available for your use. There can be
|
||||
[risks when enabling features still in development](../../user/application_security/index.md#security-approvals-in-merge-requests).
|
||||
Refer to this feature's version history for more details.
|
||||
|
||||
You can prevent merge requests from being merged if they do not refer to a Jira issue.
|
||||
To enforce this:
|
||||
|
||||
1. Navigate to your project's **Settings > General** page.
|
||||
1. Expand the **Merge requests** section.
|
||||
1. Under **Merge checks**, select the **Require an associated issue from Jira** check box.
|
||||
1. Select **Save** for the changes to take effect.
|
||||
|
||||
After you enable this feature, a merge request that doesn't reference an associated
|
||||
Jira issue can't be merged. The merge request displays the message
|
||||
**To merge, a Jira issue key must be mentioned in the title or description.**
|
||||
|
||||
## Close Jira issues in GitLab
|
||||
|
||||
If you have configured GitLab transition IDs, you can close a Jira issue directly
|
||||
|
@ -160,3 +184,22 @@ adding a comment to the Jira issue:
|
|||
|
||||
1. Refer to the [Configure GitLab](development_panel.md#configure-gitlab) instructions.
|
||||
1. Clear the **Enable comments** check box.
|
||||
|
||||
## Enable or disable the ability to require an associated Jira issue on merge requests
|
||||
|
||||
The ability to require an associated Jira issue on merge requests is under development
|
||||
and not ready for production use. It is deployed behind a feature flag that is
|
||||
**disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:jira_issue_association_on_merge_request)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:jira_issue_association_on_merge_request)
|
||||
```
|
||||
|
|
|
@ -41,7 +41,8 @@ The following Rake tasks are available for use with GitLab:
|
|||
| [Praefect Rake tasks](../administration/raketasks/praefect.md) | [Praefect](../administration/gitaly/praefect.md)-related tasks. |
|
||||
| [Project import/export](../administration/raketasks/project_import_export.md) | Prepare for [project exports and imports](../user/project/settings/import_export.md). |
|
||||
| [Sample Prometheus data](generate_sample_prometheus_data.md) | Generate sample Prometheus data. |
|
||||
| [SPDX license list import](spdx.md) | Import a local copy of the [SPDX license list](https://spdx.org/licenses/) for matching [License Compliance policies](../user/compliance/license_compliance/index.md). | |
|
||||
| [Sidekiq job migration](sidekiq_job_migration.md) | Migrate Sidekiq jobs scheduled for future dates to a new queue. |
|
||||
| [SPDX license list import](spdx.md) | Import a local copy of the [SPDX license list](https://spdx.org/licenses/) for matching [License Compliance policies](../user/compliance/license_compliance/index.md). |
|
||||
| [Repository storage](../administration/raketasks/storage.md) | List and migrate existing projects and attachments from legacy storage to hashed storage. |
|
||||
| [Uploads migrate](../administration/raketasks/uploads/migrate.md) | Migrate uploads between local storage and object storage. |
|
||||
| [Uploads sanitize](../administration/raketasks/uploads/sanitize.md) | Remove EXIF data from images uploaded to earlier versions of GitLab. |
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
stage: none
|
||||
group: unassigned
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Sidekiq job migration **(FREE SELF)**
|
||||
|
||||
WARNING:
|
||||
This operation should be very uncommon. We do not recommend it for the vast majority of GitLab instances.
|
||||
|
||||
Sidekiq routing rules allow administrators to re-route certain background jobs from their regular queue to an alternative queue. By default, GitLab uses one queue per background job type. GitLab has over 400 background job types, and so correspondingly it has over 400 queues.
|
||||
|
||||
Most administrators will not need to change this setting. In some cases with particularly large background job processing workloads, Redis performance may suffer due to the number of queues that GitLab listens to.
|
||||
|
||||
If the Sidekiq routing rules are changed, administrators need to take care with the migration to avoid losing jobs entirely. The basic migration steps are:
|
||||
|
||||
1. Listen to both the old and new queues.
|
||||
1. Update the routing rules.
|
||||
1. Wait until there are no publishers dispatching jobs to the old queues.
|
||||
1. Run the [Rake tasks for future jobs](#future-jobs).
|
||||
1. Wait for the old queues to be empty.
|
||||
1. Stop listening to the old queues.
|
||||
|
||||
## Future jobs
|
||||
|
||||
Step 4 involves rewriting some Sidekiq job data for jobs that are already stored in Redis, but due to run in future. There are two sets of jobs to run in future: scheduled jobs and jobs to be retried. We provide a separate Rake task to migrate each set:
|
||||
|
||||
- `gitlab:sidekiq:migrate_jobs:retry` for jobs to be retried.
|
||||
- `gitlab:sidekiq:migrate_jobs:scheduled` for scheduled jobs.
|
||||
|
||||
Most of the time, running both at the same time is the correct choice. There are two separate tasks to allow for more fine-grained control where needed. To run both at once:
|
||||
|
||||
```shell
|
||||
# omnibus-gitlab
|
||||
sudo gitlab-rake gitlab:sidekiq:migrate_jobs:retry gitlab:sidekiq:migrate_jobs:schedule
|
||||
|
||||
# source installations
|
||||
bundle exec rake gitlab:sidekiq:migrate_jobs:retry gitlab:sidekiq:migrate_jobs:schedule RAILS_ENV=production
|
||||
```
|
|
@ -204,6 +204,13 @@ To make an epic confidential:
|
|||
This section collects instructions for all the things you can do with [issues](../../project/issues/index.md)
|
||||
in relation to epics.
|
||||
|
||||
### View count of issues in an epic
|
||||
|
||||
On the **Epics and Issues** tab, under each epic name, hover over the total counts.
|
||||
|
||||
The number indicates all epics associated with the project, including issues
|
||||
you might not have permission to.
|
||||
|
||||
### Add a new issue to an epic
|
||||
|
||||
You can add an existing issue to an epic, or create a new issue that's
|
||||
|
@ -231,13 +238,6 @@ To add a new issue to an epic:
|
|||
If there are multiple issues to be added, press <kbd>Space</kbd> and repeat this step.
|
||||
1. Select **Add**.
|
||||
|
||||
#### View count of issues in an epic
|
||||
|
||||
On the **Epics and Issues** tab, under each epic name, hover over the total counts.
|
||||
|
||||
The number indicates all epics associated with the project, including issues
|
||||
you might not have permission to.
|
||||
|
||||
#### Create an issue from an epic
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5419) in GitLab 12.7.
|
||||
|
|
|
@ -208,7 +208,7 @@ The repository graph displays the history of the repository network visually, in
|
|||
|
||||
Find it under your project's **Repository > Graph**.
|
||||
|
||||
## Repository Languages
|
||||
## Repository languages
|
||||
|
||||
For the default branch of each repository, GitLab determines what programming languages
|
||||
were used and displays this on the project's pages. If this information is missing, it's
|
||||
|
@ -268,7 +268,7 @@ All projects can be cloned into Visual Studio Code. To do that:
|
|||
|
||||
When VS Code has successfully cloned your project, it opens the folder.
|
||||
|
||||
## Download Source Code
|
||||
## Download source code
|
||||
|
||||
> - Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/24704) in GitLab 11.11.
|
||||
> - Support for [including Git LFS blobs](../../../topics/git/lfs#lfs-objects-in-project-archives) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5.
|
||||
|
|
|
@ -47,6 +47,8 @@ Note the following:
|
|||
- Imported users can be mapped by their primary email on self-managed instances, if an administrative user (not an owner) does the import.
|
||||
Otherwise, a supplementary comment is left to mention that the original author and
|
||||
the MRs, notes, or issues are owned by the importer.
|
||||
- For project migration imports performed over GitLab.com Groups, preserving author information is
|
||||
possible through a [professional services engagement](https://about.gitlab.com/services/migration/).
|
||||
- If an imported project contains merge requests originating from forks,
|
||||
then new branches associated with such merge requests are created
|
||||
within a project during the import/export. Thus, the number of branches
|
||||
|
|
|
@ -40,7 +40,7 @@ The project description also partially supports [standard Markdown](../../markdo
|
|||
You can create a framework label to identify that your project has certain compliance requirements or needs additional oversight.
|
||||
|
||||
Group owners can create, edit and delete compliance frameworks by going to **Settings** > **General** and expanding the **Compliance frameworks** section.
|
||||
Compliance frameworks created can then be assigned to any number of projects via the project settings page inside the group or subgroups.
|
||||
Compliance frameworks created can then be assigned to any number of projects via the project settings page inside the group or subgroups.
|
||||
|
||||
NOTE:
|
||||
Attempting to create compliance frameworks on subgroups via GraphQL will cause the framework to be created on the root ancestor if the user has the correct permissions.
|
||||
|
@ -193,8 +193,9 @@ Set up your project's merge request settings:
|
|||
- Enable [merge request approvals](../merge_requests/approvals/index.md).
|
||||
- Enable [merge only if pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
|
||||
- Enable [merge only when all threads are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved).
|
||||
- Enable [`delete source branch after merge` option by default](../merge_requests/getting_started.md#deleting-the-source-branch)
|
||||
- Configure [suggested changes commit messages](../merge_requests/reviews/suggestions.md#configure-the-commit-message-for-applied-suggestions)
|
||||
- Enable [require an associated issue from Jira](../../../integration/jira/issues.md#require-associated-jira-issue-for-merge-requests-to-be-merged).
|
||||
- Enable [`delete source branch after merge` option by default](../merge_requests/getting_started.md#deleting-the-source-branch).
|
||||
- Configure [suggested changes commit messages](../merge_requests/reviews/suggestions.md#configure-the-commit-message-for-applied-suggestions).
|
||||
- Configure [the default target project](../merge_requests/creating_merge_requests.md#set-the-default-target-project) for merge requests coming from forks.
|
||||
|
||||
### Service Desk
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
module Banzai
|
||||
module ReferenceParser
|
||||
class MergeRequestParser < IssuableParser
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
self.reference_type = :merge_request
|
||||
|
||||
def records_for_nodes(nodes)
|
||||
|
@ -27,6 +29,16 @@ module Banzai
|
|||
self.class.data_attribute
|
||||
)
|
||||
end
|
||||
|
||||
def can_read_reference?(user, merge_request)
|
||||
memo = strong_memoize(:can_read_reference) { {} }
|
||||
|
||||
project_id = merge_request.project_id
|
||||
|
||||
return memo[project_id] if memo.key?(project_id)
|
||||
|
||||
memo[project_id] = can?(user, :read_merge_request_iid, merge_request.project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,10 @@ module Gitlab
|
|||
Feature.enabled?(:ci_pipeline_status_omit_commit_sha_in_cache_key, project, default_enabled: true)
|
||||
end
|
||||
|
||||
def self.merge_base_pipeline_for_metrics_comparison?(project)
|
||||
Feature.enabled?(:merge_base_pipeline_for_metrics_comparison, project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
# Remove in https://gitlab.com/gitlab-org/gitlab/-/issues/224199
|
||||
def self.store_pipeline_messages?(project)
|
||||
::Feature.enabled?(:ci_store_pipeline_messages, project, default_enabled: true)
|
||||
|
@ -54,6 +58,10 @@ module Gitlab
|
|||
def self.gldropdown_tags_enabled?
|
||||
::Feature.enabled?(:gldropdown_tags, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def self.background_pipeline_retry_endpoint?(project)
|
||||
::Feature.enabled?(:background_pipeline_retry_endpoint, project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ module Gitlab
|
|||
scheduler_failure: 'scheduler failure',
|
||||
data_integrity_failure: 'data integrity failure',
|
||||
forward_deployment_failure: 'forward deployment failure',
|
||||
pipeline_loop_detected: 'job would create infinitely looping pipelines',
|
||||
invalid_bridge_trigger: 'downstream pipeline trigger definition is invalid',
|
||||
downstream_bridge_project_not_found: 'downstream project could not be found',
|
||||
insufficient_bridge_permissions: 'no permissions to trigger downstream pipeline',
|
||||
|
|
|
@ -15,6 +15,7 @@ module Gitlab
|
|||
NoSpaceLeft = Class.new(StandardError)
|
||||
InvalidPosition = Class.new(StandardError)
|
||||
IllegalRange = Class.new(ArgumentError)
|
||||
IssuePositioningDisabled = Class.new(StandardError)
|
||||
|
||||
def self.range(lhs, rhs)
|
||||
if lhs && rhs
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class SidekiqMigrateJobs
|
||||
LOG_FREQUENCY = 1_000
|
||||
|
||||
attr_reader :sidekiq_set, :logger
|
||||
|
||||
def initialize(sidekiq_set, logger: nil)
|
||||
@sidekiq_set = sidekiq_set
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
# mappings is a hash of WorkerClassName => target_queue_name
|
||||
def execute(mappings)
|
||||
source_queues_regex = Regexp.union(mappings.keys)
|
||||
cursor = 0
|
||||
scanned = 0
|
||||
migrated = 0
|
||||
|
||||
estimated_size = Sidekiq.redis { |c| c.zcard(sidekiq_set) }
|
||||
logger&.info("Processing #{sidekiq_set} set. Estimated size: #{estimated_size}.")
|
||||
|
||||
begin
|
||||
cursor, jobs = Sidekiq.redis { |c| c.zscan(sidekiq_set, cursor) }
|
||||
|
||||
jobs.each do |(job, score)|
|
||||
if scanned > 0 && scanned % LOG_FREQUENCY == 0
|
||||
logger&.info("In progress. Scanned records: #{scanned}. Migrated records: #{migrated}.")
|
||||
end
|
||||
|
||||
scanned += 1
|
||||
|
||||
next unless job.match?(source_queues_regex)
|
||||
|
||||
job_hash = Sidekiq.load_json(job)
|
||||
destination_queue = mappings[job_hash['class']]
|
||||
|
||||
next unless mappings.has_key?(job_hash['class'])
|
||||
next if job_hash['queue'] == destination_queue
|
||||
|
||||
job_hash['queue'] = destination_queue
|
||||
|
||||
migrated += migrate_job(job, score, job_hash)
|
||||
end
|
||||
end while cursor.to_i != 0
|
||||
|
||||
logger&.info("Done. Scanned records: #{scanned}. Migrated records: #{migrated}.")
|
||||
|
||||
{
|
||||
scanned: scanned,
|
||||
migrated: migrated
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def migrate_job(job, score, job_hash)
|
||||
Sidekiq.redis do |connection|
|
||||
removed = connection.zrem(sidekiq_set, job)
|
||||
|
||||
if removed
|
||||
connection.zadd(sidekiq_set, score, Sidekiq.dump_json(job_hash))
|
||||
|
||||
1
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
instrumentation_class = definition.attributes[:instrumentation_class]
|
||||
|
||||
if instrumentation_class.present?
|
||||
metric_value = instrumentation_class.constantize.new(time_frame: definition.attributes[:time_frame]).value
|
||||
metric_value = "Gitlab::Usage::Metrics::Instrumentations::#{instrumentation_class}".constantize.new(time_frame: definition.attributes[:time_frame]).value
|
||||
|
||||
metric_payload(definition.key_path, metric_value)
|
||||
else
|
||||
|
|
|
@ -8,6 +8,29 @@ namespace :gitlab do
|
|||
File.write(path, banner + YAML.dump(object).gsub(/ *$/m, ''))
|
||||
end
|
||||
|
||||
namespace :migrate_jobs do
|
||||
def mappings
|
||||
::Gitlab::SidekiqConfig
|
||||
.workers
|
||||
.reject { |worker| worker.klass.is_a?(Gitlab::SidekiqConfig::DummyWorker) }
|
||||
.to_h { |worker| [worker.klass.to_s, ::Gitlab::SidekiqConfig::WorkerRouter.global.route(worker.klass)] }
|
||||
end
|
||||
|
||||
desc 'GitLab | Sidekiq | Migrate jobs in the scheduled set to new queue names'
|
||||
task schedule: :environment do
|
||||
::Gitlab::SidekiqMigrateJobs
|
||||
.new('schedule', logger: Logger.new($stdout))
|
||||
.execute(mappings)
|
||||
end
|
||||
|
||||
desc 'GitLab | Sidekiq | Migrate jobs in the retry set to new queue names'
|
||||
task retry: :environment do
|
||||
::Gitlab::SidekiqMigrateJobs
|
||||
.new('retry', logger: Logger.new($stdout))
|
||||
.execute(mappings)
|
||||
end
|
||||
end
|
||||
|
||||
namespace :all_queues_yml do
|
||||
desc 'GitLab | Sidekiq | Generate all_queues.yml based on worker definitions'
|
||||
task generate: :environment do
|
||||
|
|
|
@ -12,10 +12,12 @@ class VersionCheck
|
|||
def self.url
|
||||
encoded_data = Base64.urlsafe_encode64(data.to_json)
|
||||
|
||||
"#{host}?gitlab_info=#{encoded_data}"
|
||||
"#{host}/check.svg?gitlab_info=#{encoded_data}"
|
||||
end
|
||||
|
||||
def self.host
|
||||
'https://version.gitlab.com/check.svg'
|
||||
'https://version.gitlab.com'
|
||||
end
|
||||
end
|
||||
|
||||
VersionCheck.prepend_mod
|
||||
|
|
|
@ -11342,9 +11342,6 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Adopted"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Adoption"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|An error occurred while removing the group. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -11381,6 +11378,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Deploys"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Dev"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin."
|
||||
msgstr ""
|
||||
|
||||
|
@ -11408,6 +11408,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Not adopted"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Ops"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11429,6 +11432,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Scanning"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Sec"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|There was an error enabling the current group. Please refresh the page."
|
||||
msgstr ""
|
||||
|
||||
|
@ -11441,9 +11447,6 @@ msgstr ""
|
|||
msgid "DevopsAdoption|You cannot remove the group you are currently in."
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsReport|Adoption"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsReport|DevOps Score"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18359,6 +18362,9 @@ msgstr ""
|
|||
msgid "Issues closed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues manual ordering is temporarily disabled for technical reasons."
|
||||
msgstr ""
|
||||
|
||||
msgid "Issues must match this scope to appear in this list."
|
||||
msgstr ""
|
||||
|
||||
|
@ -34130,6 +34136,9 @@ msgstr ""
|
|||
msgid "Too many projects enabled. You will need to manage them via the console or the API."
|
||||
msgstr ""
|
||||
|
||||
msgid "TopNav|Switch to..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Topics (optional)"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -108,10 +108,6 @@ module QA
|
|||
element :file_to_commit_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/editor/extensions/editor_lite_extension_base.js' do
|
||||
element :line_link
|
||||
end
|
||||
|
||||
def has_file?(file_name)
|
||||
within_element(:file_list) do
|
||||
has_element?(:file_name_content, file_name: file_name)
|
||||
|
@ -319,11 +315,15 @@ module QA
|
|||
end
|
||||
|
||||
def link_line(line_number)
|
||||
previous_url = page.current_url
|
||||
wait_for_animated_element(:editor_container)
|
||||
within_element(:editor_container) do
|
||||
find('.line-numbers', text: line_number).hover
|
||||
find_element(:line_link, number: "#L#{line_number}")['href'].to_s
|
||||
find('.line-numbers', text: line_number).hover.click
|
||||
end
|
||||
wait_until(max_duration: 5, reload: false) do
|
||||
page.current_url != previous_url
|
||||
end
|
||||
page.current_url.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module QA
|
||||
RSpec.describe 'Package' do
|
||||
describe 'Container Registry', only: { subdomain: %i[staging pre] } do
|
||||
describe 'Container Registry', :reliable, only: { subdomain: %i[staging pre] } do
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'project-with-registry'
|
||||
|
|
|
@ -9,12 +9,6 @@ RSpec.describe Admin::DevOpsReportController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'should_track_devops_score?' do
|
||||
it 'is always true' do
|
||||
expect(controller.should_track_devops_score?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
context 'as admin' do
|
||||
let(:user) { create(:admin) }
|
||||
|
@ -31,6 +25,8 @@ RSpec.describe Admin::DevOpsReportController do
|
|||
|
||||
it_behaves_like 'tracking unique visits', :show do
|
||||
let(:target_id) { 'i_analytics_dev_ops_score' }
|
||||
|
||||
let(:request_params) { { tab: 'devops-score' } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,6 +49,7 @@ RSpec.describe Boards::IssuesController do
|
|||
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
|
||||
create(:labeled_issue, project: project, labels: [development], assignees: [johndoe])
|
||||
issue.subscribe(johndoe, project)
|
||||
expect(Issue).to receive(:move_nulls_to_end)
|
||||
|
||||
list_issues user: user, board: board, list: list2
|
||||
|
||||
|
@ -119,6 +120,18 @@ RSpec.describe Boards::IssuesController do
|
|||
|
||||
expect(query_count).to eq(1)
|
||||
end
|
||||
|
||||
context 'when block_issue_repositioning feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(block_issue_repositioning: true)
|
||||
end
|
||||
|
||||
it 'does not reposition issues with null position' do
|
||||
expect(Issue).not_to receive(:move_nulls_to_end)
|
||||
|
||||
list_issues(user: user, board: group_board, list: list3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid list id' do
|
||||
|
|
|
@ -853,10 +853,7 @@ RSpec.describe Projects::PipelinesController do
|
|||
end
|
||||
|
||||
describe 'POST retry.json' do
|
||||
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
|
||||
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
|
||||
|
||||
before do
|
||||
subject(:post_retry) do
|
||||
post :retry, params: {
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
|
@ -865,15 +862,41 @@ RSpec.describe Projects::PipelinesController do
|
|||
format: :json
|
||||
end
|
||||
|
||||
it 'retries a pipeline without returning any content' do
|
||||
let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
|
||||
let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
|
||||
|
||||
let(:worker_spy) { class_spy(::Ci::RetryPipelineWorker) }
|
||||
|
||||
before do
|
||||
stub_const('::Ci::RetryPipelineWorker', worker_spy)
|
||||
end
|
||||
|
||||
it 'retries a pipeline in the background without returning any content' do
|
||||
post_retry
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(build.reload).to be_retried
|
||||
expect(::Ci::RetryPipelineWorker).to have_received(:perform_async).with(pipeline.id, user.id)
|
||||
end
|
||||
|
||||
context 'when feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(background_pipeline_retry_endpoint: false)
|
||||
end
|
||||
|
||||
it 'retries the pipeline without returning any content' do
|
||||
post_retry
|
||||
|
||||
expect(response).to have_gitlab_http_status(:no_content)
|
||||
expect(build.reload).to be_retried
|
||||
end
|
||||
end
|
||||
|
||||
context 'when builds are disabled' do
|
||||
let(:feature) { ProjectFeature::DISABLED }
|
||||
|
||||
it 'fails to retry pipeline' do
|
||||
post_retry
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,8 +20,6 @@ RSpec.describe 'Admin mode' do
|
|||
|
||||
context 'when not in admin mode' do
|
||||
it 'has no leave admin mode button' do
|
||||
pending_on_combined_menu_flag
|
||||
|
||||
visit new_admin_session_path
|
||||
|
||||
page.within('.navbar-sub-nav') do
|
||||
|
@ -180,8 +178,6 @@ RSpec.describe 'Admin mode' do
|
|||
end
|
||||
|
||||
it 'shows no admin mode buttons in navbar' do
|
||||
pending_on_combined_menu_flag
|
||||
|
||||
visit admin_root_path
|
||||
|
||||
page.within('.navbar-sub-nav') do
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { GlNavItemDropdown, GlTooltip } from '@gitlab/ui';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import TopNavApp from '~/nav/components/top_nav_app.vue';
|
||||
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
|
||||
import { TEST_NAV_DATA } from '../mock_data';
|
||||
|
||||
describe('~/nav/components/top_nav_app.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (mountFn = shallowMount) => {
|
||||
wrapper = mountFn(TopNavApp, {
|
||||
propsData: {
|
||||
navData: TEST_NAV_DATA,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown);
|
||||
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
|
||||
const findTooltip = () => wrapper.findComponent(GlTooltip);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders nav item dropdown', () => {
|
||||
expect(findNavItemDropdown().attributes('href')).toBeUndefined();
|
||||
expect(findNavItemDropdown().attributes()).toMatchObject({
|
||||
icon: 'dot-grid',
|
||||
text: TEST_NAV_DATA.activeTitle,
|
||||
'no-flip': '',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders top nav dropdown menu', () => {
|
||||
expect(findMenu().props()).toStrictEqual({
|
||||
primary: TEST_NAV_DATA.primary,
|
||||
secondary: TEST_NAV_DATA.secondary,
|
||||
views: TEST_NAV_DATA.views,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders tooltip', () => {
|
||||
expect(findTooltip().attributes()).toMatchObject({
|
||||
'boundary-padding': '0',
|
||||
placement: 'right',
|
||||
title: TopNavApp.TOOLTIP,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when full mounted', () => {
|
||||
beforeEach(() => {
|
||||
createComponent(mount);
|
||||
});
|
||||
|
||||
it('has dropdown toggle as tooltip target', () => {
|
||||
const targetFn = findTooltip().props('target');
|
||||
|
||||
expect(targetFn()).toBe(wrapper.find('.js-top-nav-dropdown-toggle').element);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import FrequentItemsApp from '~/frequent_items/components/app.vue';
|
||||
import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants';
|
||||
import eventHub from '~/frequent_items/event_hub';
|
||||
import TopNavContainerView from '~/nav/components/top_nav_container_view.vue';
|
||||
import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
|
||||
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
|
||||
import { TEST_NAV_DATA } from '../mock_data';
|
||||
|
||||
const DEFAULT_PROPS = {
|
||||
frequentItemsDropdownType: FREQUENT_ITEMS_PROJECTS.namespace,
|
||||
frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule,
|
||||
linksPrimary: TEST_NAV_DATA.primary,
|
||||
linksSecondary: TEST_NAV_DATA.secondary,
|
||||
};
|
||||
const TEST_OTHER_PROPS = {
|
||||
namespace: 'projects',
|
||||
currentUserName: '',
|
||||
currentItem: {},
|
||||
};
|
||||
|
||||
describe('~/nav/components/top_nav_container_view.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(TopNavContainerView, {
|
||||
propsData: {
|
||||
...DEFAULT_PROPS,
|
||||
...TEST_OTHER_PROPS,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findMenuItems = (parent = wrapper) => parent.findAll(TopNavMenuItem);
|
||||
const findMenuItemsModel = (parent = wrapper) =>
|
||||
findMenuItems(parent).wrappers.map((x) => x.props());
|
||||
const findMenuItemGroups = () => wrapper.findAll('[data-testid="menu-item-group"]');
|
||||
const findMenuItemGroupsModel = () => findMenuItemGroups().wrappers.map(findMenuItemsModel);
|
||||
const findFrequentItemsApp = () => {
|
||||
const parent = wrapper.findComponent(VuexModuleProvider);
|
||||
|
||||
return {
|
||||
vuexModule: parent.props('vuexModule'),
|
||||
props: parent.findComponent(FrequentItemsApp).props(),
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it.each(['projects', 'groups'])(
|
||||
'emits frequent items event to event hub (%s)',
|
||||
async (frequentItemsDropdownType) => {
|
||||
const listener = jest.fn();
|
||||
eventHub.$on(`${frequentItemsDropdownType}-dropdownOpen`, listener);
|
||||
createComponent({ frequentItemsDropdownType });
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(listener).toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders frequent items app', () => {
|
||||
expect(findFrequentItemsApp()).toEqual({
|
||||
vuexModule: DEFAULT_PROPS.frequentItemsVuexModule,
|
||||
props: TEST_OTHER_PROPS,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders menu item groups', () => {
|
||||
expect(findMenuItemGroupsModel()).toEqual([
|
||||
TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })),
|
||||
TEST_NAV_DATA.secondary.map((menuItem) => ({ menuItem })),
|
||||
]);
|
||||
});
|
||||
|
||||
it('only the first group does not have margin top', () => {
|
||||
expect(findMenuItemGroups().wrappers.map((x) => x.classes('gl-mt-3'))).toEqual([false, true]);
|
||||
});
|
||||
|
||||
it('only the first menu item does not have margin top', () => {
|
||||
const actual = findMenuItems(findMenuItemGroups().at(1)).wrappers.map((x) =>
|
||||
x.classes('gl-mt-1'),
|
||||
);
|
||||
|
||||
expect(actual).toEqual([false, ...TEST_NAV_DATA.secondary.slice(1).fill(true)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without secondary links', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
linksSecondary: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('renders one menu item group', () => {
|
||||
expect(findMenuItemGroupsModel()).toEqual([
|
||||
TEST_NAV_DATA.primary.map((menuItem) => ({ menuItem })),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
|
||||
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
|
||||
import { TEST_NAV_DATA } from '../mock_data';
|
||||
|
||||
const SECONDARY_GROUP_CLASSES = TopNavDropdownMenu.SECONDARY_GROUP_CLASS.split(' ');
|
||||
|
||||
describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(TopNavDropdownMenu, {
|
||||
propsData: {
|
||||
primary: TEST_NAV_DATA.primary,
|
||||
secondary: TEST_NAV_DATA.secondary,
|
||||
views: TEST_NAV_DATA.views,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findMenuItems = (parent = wrapper) => parent.findAll('[data-testid="menu-item"]');
|
||||
const findMenuItemsModel = (parent = wrapper) =>
|
||||
findMenuItems(parent).wrappers.map((x) => ({
|
||||
menuItem: x.props('menuItem'),
|
||||
isActive: x.classes('active'),
|
||||
}));
|
||||
const findMenuItemGroups = () => wrapper.findAll('[data-testid="menu-item-group"]');
|
||||
const findMenuItemGroupsModel = () =>
|
||||
findMenuItemGroups().wrappers.map((x) => ({
|
||||
classes: x.classes(),
|
||||
items: findMenuItemsModel(x),
|
||||
}));
|
||||
const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
|
||||
const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
|
||||
const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
|
||||
|
||||
const createItemsGroupModelExpectation = ({
|
||||
primary = TEST_NAV_DATA.primary,
|
||||
secondary = TEST_NAV_DATA.secondary,
|
||||
activeIndex = -1,
|
||||
} = {}) => [
|
||||
{
|
||||
classes: [],
|
||||
items: primary.map((menuItem, index) => ({ isActive: index === activeIndex, menuItem })),
|
||||
},
|
||||
{
|
||||
classes: SECONDARY_GROUP_CLASSES,
|
||||
items: secondary.map((menuItem) => ({ isActive: false, menuItem })),
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders menu item groups', () => {
|
||||
expect(findMenuItemGroupsModel()).toEqual(createItemsGroupModelExpectation());
|
||||
});
|
||||
|
||||
it('has full width menu sidebar', () => {
|
||||
expect(hasFullWidthMenuSidebar()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders hidden subview with no slot key', () => {
|
||||
const subview = findMenuSubview();
|
||||
|
||||
expect(subview.isVisible()).toBe(false);
|
||||
expect(subview.props()).toEqual({ slotKey: '' });
|
||||
});
|
||||
|
||||
it('the first menu item in a group does not render margin top', () => {
|
||||
const actual = findMenuItems(findMenuItemGroups().at(0)).wrappers.map((x) =>
|
||||
x.classes('gl-mt-1'),
|
||||
);
|
||||
|
||||
expect(actual).toEqual([false, ...TEST_NAV_DATA.primary.slice(1).fill(true)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with pre-initialized active view', () => {
|
||||
const primaryWithActive = [
|
||||
TEST_NAV_DATA.primary[0],
|
||||
{
|
||||
...TEST_NAV_DATA.primary[1],
|
||||
active: true,
|
||||
},
|
||||
...TEST_NAV_DATA.primary.slice(2),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
primary: primaryWithActive,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders menu item groups', () => {
|
||||
expect(findMenuItemGroupsModel()).toEqual(
|
||||
createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 1 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not have full width menu sidebar', () => {
|
||||
expect(hasFullWidthMenuSidebar()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders visible subview with slot key', () => {
|
||||
const subview = findMenuSubview();
|
||||
|
||||
expect(subview.isVisible()).toBe(true);
|
||||
expect(subview.props('slotKey')).toBe(primaryWithActive[1].view);
|
||||
});
|
||||
|
||||
it('does not change view if non-view menu item is clicked', async () => {
|
||||
const secondaryLink = findMenuItems().at(primaryWithActive.length);
|
||||
|
||||
// Ensure this doesn't have a view
|
||||
expect(secondaryLink.props('menuItem').view).toBeUndefined();
|
||||
|
||||
secondaryLink.vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[1].view);
|
||||
});
|
||||
|
||||
describe('when other view menu item is clicked', () => {
|
||||
let primaryLink;
|
||||
|
||||
beforeEach(async () => {
|
||||
primaryLink = findMenuItems().at(0);
|
||||
primaryLink.vm.$emit('click');
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
it('clicked on link with view', () => {
|
||||
expect(primaryLink.props('menuItem').view).toBeTruthy();
|
||||
});
|
||||
|
||||
it('changes active view', () => {
|
||||
expect(findMenuSubview().props('slotKey')).toBe(primaryWithActive[0].view);
|
||||
});
|
||||
|
||||
it('changes active status on menu item', () => {
|
||||
expect(findMenuItemGroupsModel()).toStrictEqual(
|
||||
createItemsGroupModelExpectation({ primary: primaryWithActive, activeIndex: 0 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
import { GlButton, GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
|
||||
|
||||
const TEST_MENU_ITEM = {
|
||||
title: 'Cheeseburger',
|
||||
icon: 'search',
|
||||
href: '/pretty/good/burger',
|
||||
view: 'burger-view',
|
||||
};
|
||||
|
||||
describe('~/nav/components/top_nav_menu_item.vue', () => {
|
||||
let listener;
|
||||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(TopNavMenuItem, {
|
||||
propsData: {
|
||||
menuItem: TEST_MENU_ITEM,
|
||||
...props,
|
||||
},
|
||||
listeners: {
|
||||
click: listener,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findButton = () => wrapper.find(GlButton);
|
||||
const findButtonIcons = () =>
|
||||
findButton()
|
||||
.findAllComponents(GlIcon)
|
||||
.wrappers.map((x) => x.props('name'));
|
||||
|
||||
beforeEach(() => {
|
||||
listener = jest.fn();
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders button href and text', () => {
|
||||
const button = findButton();
|
||||
|
||||
expect(button.attributes('href')).toBe(TEST_MENU_ITEM.href);
|
||||
expect(button.text()).toBe(TEST_MENU_ITEM.title);
|
||||
});
|
||||
|
||||
it('passes listeners to button', () => {
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
findButton().vm.$emit('click', 'TEST');
|
||||
|
||||
expect(listener).toHaveBeenCalledWith('TEST');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
desc | menuItem | expectedIcons
|
||||
${'default'} | ${TEST_MENU_ITEM} | ${[TEST_MENU_ITEM.icon, 'chevron-right']}
|
||||
${'with no icon'} | ${{ ...TEST_MENU_ITEM, icon: null }} | ${['chevron-right']}
|
||||
${'with no view'} | ${{ ...TEST_MENU_ITEM, view: null }} | ${[TEST_MENU_ITEM.icon]}
|
||||
${'with no icon or view'} | ${{ ...TEST_MENU_ITEM, view: null, icon: null }} | ${[]}
|
||||
`('$desc', ({ menuItem, expectedIcons }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ menuItem });
|
||||
});
|
||||
|
||||
it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => {
|
||||
expect(findButtonIcons()).toEqual(expectedIcons);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { range } from 'lodash';
|
||||
|
||||
export const TEST_NAV_DATA = {
|
||||
activeTitle: 'Test Active Title',
|
||||
primary: [
|
||||
...['projects', 'groups'].map((view) => ({
|
||||
id: view,
|
||||
href: null,
|
||||
title: view,
|
||||
view,
|
||||
})),
|
||||
...range(0, 2).map((idx) => ({
|
||||
id: `primary-link-${idx}`,
|
||||
href: `/path/to/primary/${idx}`,
|
||||
title: `Title ${idx}`,
|
||||
})),
|
||||
],
|
||||
secondary: range(0, 2).map((idx) => ({
|
||||
id: `secondary-link-${idx}`,
|
||||
href: `/path/to/secondary/${idx}`,
|
||||
title: `SecTitle ${idx}`,
|
||||
})),
|
||||
views: {
|
||||
projects: {
|
||||
namespace: 'projects',
|
||||
currentUserName: '',
|
||||
currentItem: {},
|
||||
},
|
||||
groups: {
|
||||
namespace: 'groups',
|
||||
currentUserName: '',
|
||||
currentItem: {},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -341,4 +341,65 @@ RSpec.describe IssuesHelper do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_manual_ordering_class' do
|
||||
context 'when sorting by relative position' do
|
||||
before do
|
||||
assign(:sort, 'relative_position')
|
||||
end
|
||||
|
||||
it 'returns manual ordering class' do
|
||||
expect(helper.issue_manual_ordering_class).to eq("manual-ordering")
|
||||
end
|
||||
|
||||
context 'when manual sorting disabled' do
|
||||
before do
|
||||
allow(helper).to receive(:issue_repositioning_disabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(helper.issue_manual_ordering_class).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#issue_repositioning_disabled?' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
subject { helper.issue_repositioning_disabled? }
|
||||
|
||||
context 'for project' do
|
||||
before do
|
||||
assign(:project, project)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
|
||||
context 'when block_issue_repositioning feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(block_issue_repositioning: group)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'for group' do
|
||||
before do
|
||||
assign(:group, group)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
|
||||
context 'when block_issue_repositioning feature flag is enabled' do
|
||||
before do
|
||||
stub_feature_flags(block_issue_repositioning: group)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
|
|||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :public) }
|
||||
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||
subject { described_class.new(Banzai::RenderContext.new(merge_request.target_project, user)) }
|
||||
subject(:parser) { described_class.new(Banzai::RenderContext.new(merge_request.target_project, user)) }
|
||||
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
|
@ -65,4 +65,49 @@ RSpec.describe Banzai::ReferenceParser::MergeRequestParser do
|
|||
|
||||
it_behaves_like 'no N+1 queries'
|
||||
end
|
||||
|
||||
describe '#can_read_reference?' do
|
||||
subject { parser.can_read_reference?(user, merge_request) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when merge request belongs to the private project' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
||||
it 'prevents user from reading merge request references' do
|
||||
is_expected.to be_falsey
|
||||
end
|
||||
|
||||
context 'when user has access to the project' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with memoization' do
|
||||
context 'when project is the same' do
|
||||
it 'calls #can? only once' do
|
||||
expect(parser).to receive(:can?).once
|
||||
|
||||
2.times { parser.can_read_reference?(user, merge_request) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge requests belong to different projects' do
|
||||
it 'calls #can? for each project' do
|
||||
expect(parser).to receive(:can?).twice
|
||||
|
||||
another_merge_request = create(:merge_request)
|
||||
|
||||
2.times do
|
||||
parser.can_read_reference?(user, merge_request)
|
||||
parser.can_read_reference?(user, another_merge_request)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue