Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3e68d38487
commit
16dbaf57bc
76 changed files with 997 additions and 399 deletions
|
@ -0,0 +1,98 @@
|
|||
<!---
|
||||
Please read this!
|
||||
|
||||
This template is for reporting a security vulnerability about GitLab or
|
||||
GitLab.com
|
||||
|
||||
Strongly consider reporting via https://hackerone.com/gitlab, as
|
||||
HackerOne is our preferred disclosure platform.
|
||||
|
||||
See also:
|
||||
|
||||
- https://about.gitlab.com/security/disclosure/
|
||||
- https://about.gitlab.com/handbook/engineering/security/#creating-new-security-issues
|
||||
- https://about.gitlab.com/handbook/engineering/security/#engaging-the-security-on-call
|
||||
|
||||
--->
|
||||
|
||||
### Summary
|
||||
|
||||
<!-- Summarize the bug encountered concisely. -->
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
<!-- Describe how one can reproduce the issue - this is very important. Please use an ordered list. -->
|
||||
|
||||
### Example Project
|
||||
|
||||
<!-- If possible, please create an example project here on GitLab.com that exhibits the problematic
|
||||
behavior, and link to it here in the bug report. If you are using an older version of GitLab, this
|
||||
will also determine whether the bug is fixed in a more recent version. -->
|
||||
|
||||
### What is the current *bug* behavior?
|
||||
|
||||
<!-- Describe what actually happens. -->
|
||||
|
||||
### What is the expected *correct* behavior?
|
||||
|
||||
<!-- Describe what you should see instead. -->
|
||||
|
||||
### Relevant logs and/or screenshots
|
||||
|
||||
<!-- Paste any relevant logs - please use code blocks (```) to format console output, logs, and code
|
||||
as it's tough to read otherwise. -->
|
||||
|
||||
### Output of checks
|
||||
|
||||
<!-- If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com -->
|
||||
|
||||
#### Results of GitLab environment info
|
||||
|
||||
<!-- Input any relevant GitLab environment information if needed. -->
|
||||
|
||||
<details>
|
||||
<summary>Expand for output related to GitLab environment info</summary>
|
||||
|
||||
<pre>
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
`sudo gitlab-rake gitlab:env:info`)
|
||||
|
||||
(For installations from source run and paste the output of:
|
||||
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
|
||||
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
#### Results of GitLab application Check
|
||||
|
||||
<!-- Input any relevant GitLab application check information if needed. -->
|
||||
|
||||
<details>
|
||||
<summary>Expand for output related to the GitLab application check</summary>
|
||||
<pre>
|
||||
|
||||
(For installations with omnibus-gitlab package run and paste the output of:
|
||||
`sudo gitlab-rake gitlab:check SANITIZE=true`)
|
||||
|
||||
(For installations from source run and paste the output of:
|
||||
`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
|
||||
|
||||
(we will only investigate if the tests are passing)
|
||||
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
### Possible fixes
|
||||
|
||||
<!-- If you can, link to the line of code that might be responsible for the problem. -->
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- Do not edit past here unless you are certain of the impact -->
|
||||
|
||||
cc @gitlab-com/gl-security/appsec
|
||||
|
||||
/label ~"type::bug" ~"bug::vulnerability"
|
||||
/confidential
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -2,6 +2,16 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 14.9.1 (2022-03-23)
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- [Fix backups not working when feature_flags table does not exist](gitlab-org/gitlab@4cc3cd6cf6eb256a9837ef92a6fdb4991cd1642c) ([merge request](gitlab-org/gitlab!83388))
|
||||
|
||||
### Changed (1 change)
|
||||
|
||||
- [Alias user_email_lookup_limit to search_rate_limit](gitlab-org/gitlab@424c277fc4c994df60ea68acb8988537526108e4) ([merge request](gitlab-org/gitlab!83388))
|
||||
|
||||
## 14.9.0 (2022-03-21)
|
||||
|
||||
### Added (119 changes)
|
||||
|
|
|
@ -33,10 +33,37 @@ export default class ShortcutsIssuable extends Shortcuts {
|
|||
Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), ShortcutsIssuable.replyWithSelectedText);
|
||||
Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue);
|
||||
Mousetrap.bind(keysFor(MR_COPY_SOURCE_BRANCH_NAME), ShortcutsIssuable.copyBranchName);
|
||||
|
||||
/**
|
||||
* We're attaching a global focus event listener on document for
|
||||
* every markdown input field.
|
||||
*/
|
||||
$(document).on(
|
||||
'focus',
|
||||
'.js-vue-markdown-field .js-gfm-input',
|
||||
ShortcutsIssuable.handleMarkdownFieldFocus,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event handler preserves last focused markdown input field.
|
||||
* @param {Object} event
|
||||
*/
|
||||
static handleMarkdownFieldFocus({ currentTarget }) {
|
||||
ShortcutsIssuable.$lastFocusedReplyField = $(currentTarget);
|
||||
}
|
||||
|
||||
static replyWithSelectedText() {
|
||||
const $replyField = $('.js-main-target-form .js-vue-comment-form');
|
||||
let $replyField = $('.js-main-target-form .js-vue-comment-form');
|
||||
|
||||
// Ensure that markdown input is still present in the DOM
|
||||
// otherwise fall back to main comment input field.
|
||||
if (
|
||||
ShortcutsIssuable.$lastFocusedReplyField &&
|
||||
isElementVisible(ShortcutsIssuable.$lastFocusedReplyField?.get(0))
|
||||
) {
|
||||
$replyField = ShortcutsIssuable.$lastFocusedReplyField;
|
||||
}
|
||||
|
||||
if (!$replyField.length || $replyField.is(':hidden') /* Other tab selected in MR */) {
|
||||
return false;
|
||||
|
|
|
@ -5,6 +5,8 @@ import AwardsList from '~/vue_shared/components/awards_list.vue';
|
|||
import createstore from './store';
|
||||
|
||||
export default (el) => {
|
||||
if (!el) return null;
|
||||
|
||||
const {
|
||||
dataset: { path },
|
||||
} = el;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import $ from 'jquery';
|
||||
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
|
||||
import loadAwardsHandler from '~/awards_handler';
|
||||
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
|
||||
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
|
||||
import GLForm from '~/gl_form';
|
||||
|
@ -22,6 +21,7 @@ import MilestoneSelect from '~/milestones/milestone_select';
|
|||
import initNotesApp from '~/notes';
|
||||
import { store } from '~/notes/stores';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import initAwardsApp from '~/emoji/awards_app';
|
||||
import FilteredSearchServiceDesk from './filtered_search_service_desk';
|
||||
|
||||
export function initFilteredSearchServiceDesk() {
|
||||
|
@ -72,15 +72,7 @@ export function initShow() {
|
|||
initRelatedMergeRequests();
|
||||
initSentryErrorStackTrace();
|
||||
|
||||
const awardEmojiEl = document.getElementById('js-vue-awards-block');
|
||||
|
||||
if (awardEmojiEl) {
|
||||
import('~/emoji/awards_app')
|
||||
.then((m) => m.default(awardEmojiEl))
|
||||
.catch(() => {});
|
||||
} else {
|
||||
loadAwardsHandler();
|
||||
}
|
||||
initAwardsApp(document.getElementById('js-vue-awards-block'));
|
||||
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
||||
.then((module) => module.default())
|
||||
|
|
|
@ -292,40 +292,18 @@ export default {
|
|||
class="line-resolve-btn note-action-button"
|
||||
@click="onResolve"
|
||||
/>
|
||||
<template v-if="canAwardEmoji">
|
||||
<emoji-picker
|
||||
v-if="glFeatures.improvedEmojiPicker"
|
||||
toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
|
||||
@click="setAwardEmoji"
|
||||
>
|
||||
<template #button-content>
|
||||
<gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
|
||||
<gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
|
||||
<gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
|
||||
</template>
|
||||
</emoji-picker>
|
||||
<gl-button
|
||||
v-else
|
||||
v-gl-tooltip
|
||||
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
|
||||
class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji"
|
||||
category="tertiary"
|
||||
variant="default"
|
||||
:title="$options.i18n.addReactionLabel"
|
||||
:aria-label="$options.i18n.addReactionLabel"
|
||||
data-position="right"
|
||||
>
|
||||
<span class="reaction-control-icon reaction-control-icon-neutral">
|
||||
<gl-icon name="slight-smile" />
|
||||
</span>
|
||||
<span class="reaction-control-icon reaction-control-icon-positive">
|
||||
<gl-icon name="smiley" />
|
||||
</span>
|
||||
<span class="reaction-control-icon reaction-control-icon-super-positive">
|
||||
<gl-icon name="smile" />
|
||||
</span>
|
||||
</gl-button>
|
||||
</template>
|
||||
<emoji-picker
|
||||
v-if="canAwardEmoji"
|
||||
toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
|
||||
data-testid="note-emoji-button"
|
||||
@click="setAwardEmoji"
|
||||
>
|
||||
<template #button-content>
|
||||
<gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
|
||||
<gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
|
||||
<gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
|
||||
</template>
|
||||
</emoji-picker>
|
||||
<reply-button
|
||||
v-if="showReply"
|
||||
ref="replyButton"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import loadAwardsHandler from '~/awards_handler';
|
||||
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
|
||||
import { initPipelineCountListener } from '~/commit/pipelines/utils';
|
||||
import { initIssuableSidebar } from '~/issuable';
|
||||
|
@ -8,23 +7,16 @@ import StatusBox from '~/issuable/components/status_box.vue';
|
|||
import createDefaultClient from '~/lib/graphql';
|
||||
import initSourcegraph from '~/sourcegraph';
|
||||
import ZenMode from '~/zen_mode';
|
||||
import initAwardsApp from '~/emoji/awards_app';
|
||||
import getStateQuery from './queries/get_state.query.graphql';
|
||||
|
||||
export default function initMergeRequestShow() {
|
||||
const awardEmojiEl = document.getElementById('js-vue-awards-block');
|
||||
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
initPipelineCountListener(document.querySelector('#commit-pipeline-table-view'));
|
||||
new ShortcutsIssuable(true); // eslint-disable-line no-new
|
||||
initSourcegraph();
|
||||
initIssuableSidebar();
|
||||
if (awardEmojiEl) {
|
||||
import('~/emoji/awards_app')
|
||||
.then((m) => m.default(awardEmojiEl))
|
||||
.catch(() => {});
|
||||
} else {
|
||||
loadAwardsHandler();
|
||||
}
|
||||
initAwardsApp(document.getElementById('js-vue-awards-block'));
|
||||
|
||||
const el = document.querySelector('.js-mr-status-box');
|
||||
const apolloProvider = new VueApollo({
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
import '~/snippet/snippet_show';
|
||||
import initAwardsApp from '~/emoji/awards_app';
|
||||
|
||||
const awardEmojiEl = document.getElementById('js-vue-awards-block');
|
||||
|
||||
if (awardEmojiEl) {
|
||||
import('~/emoji/awards_app')
|
||||
.then((m) => m.default(awardEmojiEl))
|
||||
.catch(() => {});
|
||||
}
|
||||
initAwardsApp(document.getElementById('js-vue-awards-block'));
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { GlTabs, GlTab } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
|
||||
import Dag from './dag/dag.vue';
|
||||
import JobsApp from './jobs/jobs_app.vue';
|
||||
import TestReports from './test_reports/test_reports.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
tabs: {
|
||||
failedJobsTitle: __('Failed Jobs'),
|
||||
jobsTitle: __('Jobs'),
|
||||
needsTitle: __('Needs'),
|
||||
pipelineTitle: __('Pipeline'),
|
||||
testsTitle: __('Tests'),
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Dag,
|
||||
GlTab,
|
||||
GlTabs,
|
||||
JobsApp,
|
||||
FailedJobsApp: JobsApp,
|
||||
PipelineGraphWrapper,
|
||||
TestReports,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-tabs>
|
||||
<gl-tab :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
|
||||
<pipeline-graph-wrapper />
|
||||
</gl-tab>
|
||||
<gl-tab :title="$options.i18n.tabs.needsTitle" data-testid="dag-tab">
|
||||
<dag />
|
||||
</gl-tab>
|
||||
<gl-tab :title="$options.i18n.tabs.jobsTitle" data-testid="jobs-tab">
|
||||
<jobs-app />
|
||||
</gl-tab>
|
||||
<gl-tab :title="$options.i18n.tabs.failedJobsTitle" data-testid="failed-jobs-tab">
|
||||
<failed-jobs-app />
|
||||
</gl-tab>
|
||||
<gl-tab :title="$options.i18n.tabs.testsTitle" data-testid="tests-tab">
|
||||
<test-reports />
|
||||
</gl-tab>
|
||||
<slot></slot>
|
||||
</gl-tabs>
|
||||
</template>
|
|
@ -13,6 +13,7 @@ const SELECTORS = {
|
|||
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
|
||||
PIPELINE_HEADER: '#js-pipeline-header-vue',
|
||||
PIPELINE_NOTIFICATION: '#js-pipeline-notification',
|
||||
PIPELINE_TABS: '#js-pipeline-tabs',
|
||||
PIPELINE_TESTS: '#js-pipeline-tests-detail',
|
||||
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
|
||||
};
|
||||
|
@ -28,22 +29,6 @@ export default async function initPipelineDetailsBundle() {
|
|||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the pipeline.'),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading a section of this page.'),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
|
||||
} catch {
|
||||
|
@ -52,27 +37,47 @@ export default async function initPipelineDetailsBundle() {
|
|||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createDagApp(apolloProvider);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the Needs tab.'),
|
||||
});
|
||||
}
|
||||
if (gon.features?.pipelineTabsVue) {
|
||||
const { createPipelineTabs } = await import('./pipeline_tabs');
|
||||
|
||||
try {
|
||||
createTestDetails(SELECTORS.PIPELINE_TESTS);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the Test Reports tab.'),
|
||||
});
|
||||
}
|
||||
try {
|
||||
createPipelineTabs(SELECTORS.PIPELINE_TABS, apolloProvider);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading a section of this page.'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the pipeline.'),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the Jobs tab.'),
|
||||
});
|
||||
try {
|
||||
createDagApp(apolloProvider);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the Needs tab.'),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createTestDetails(SELECTORS.PIPELINE_TESTS);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the Test Reports tab.'),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
|
||||
} catch {
|
||||
createFlash({
|
||||
message: __('An error occurred while loading the Jobs tab.'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
44
app/assets/javascripts/pipelines/pipeline_tabs.js
Normal file
44
app/assets/javascripts/pipelines/pipeline_tabs.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
|
||||
import { reportToSentry } from './utils';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const createPipelineTabs = (selector, apolloProvider) => {
|
||||
const el = document.querySelector(selector);
|
||||
|
||||
if (!el) return;
|
||||
|
||||
const { dataset } = document.querySelector(selector);
|
||||
const {
|
||||
canGenerateCodequalityReports,
|
||||
codequalityReportDownloadPath,
|
||||
downloadablePathForReportType,
|
||||
exposeSecurityDashboard,
|
||||
exposeLicenseScanningData,
|
||||
} = dataset;
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el: selector,
|
||||
components: {
|
||||
PipelineTabs,
|
||||
},
|
||||
apolloProvider,
|
||||
provide: {
|
||||
canGenerateCodequalityReports: JSON.parse(canGenerateCodequalityReports),
|
||||
codequalityReportDownloadPath,
|
||||
downloadablePathForReportType,
|
||||
exposeSecurityDashboard: JSON.parse(exposeSecurityDashboard),
|
||||
exposeLicenseScanningData: JSON.parse(exposeLicenseScanningData),
|
||||
},
|
||||
errorCaptured(err, _vm, info) {
|
||||
reportToSentry('pipeline_tabs', `error: ${err}, info: ${info}`);
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(PipelineTabs);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { createPipelineTabs };
|
|
@ -1,20 +0,0 @@
|
|||
import { AwardsHandler } from '~/awards_handler';
|
||||
|
||||
class EmojiMenuInModal extends AwardsHandler {
|
||||
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
|
||||
super(emoji);
|
||||
|
||||
this.selectEmojiCallback = selectEmojiCallback;
|
||||
this.toggleButtonSelector = toggleButtonSelector;
|
||||
this.menuClass = menuClass;
|
||||
this.targetContainerEl = targetContainerEl;
|
||||
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
postEmoji($emojiButton, awardUrl, selectedEmoji) {
|
||||
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojiMenuInModal;
|
|
@ -19,10 +19,8 @@ import { __, s__, sprintf } from '~/locale';
|
|||
import { updateUserStatus } from '~/rest_api';
|
||||
import { timeRanges } from '~/vue_shared/constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import EmojiMenuInModal from './emoji_menu_in_modal';
|
||||
import { isUserBusy } from './utils';
|
||||
|
||||
const emojiMenuClass = 'js-modal-status-emoji-menu';
|
||||
export const AVAILABILITY_STATUS = {
|
||||
BUSY: 'busy',
|
||||
NOT_SET: 'not_set',
|
||||
|
@ -83,7 +81,6 @@ export default {
|
|||
emoji: this.currentEmoji,
|
||||
emojiMenu: null,
|
||||
emojiTag: '',
|
||||
isEmojiMenuVisible: false,
|
||||
message: this.currentMessage,
|
||||
modalId: 'set-user-status-modal',
|
||||
noEmoji: true,
|
||||
|
@ -105,17 +102,11 @@ export default {
|
|||
mounted() {
|
||||
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.emojiMenu) {
|
||||
this.emojiMenu.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeModal() {
|
||||
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
|
||||
},
|
||||
setupEmojiListAndAutocomplete() {
|
||||
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
|
||||
const emojiAutocomplete = new GfmAutoComplete();
|
||||
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
|
||||
|
||||
|
@ -127,16 +118,6 @@ export default {
|
|||
this.noEmoji = this.emoji === '';
|
||||
this.defaultEmojiTag = Emoji.glEmojiTag(this.defaultEmoji);
|
||||
|
||||
if (!this.glFeatures.improvedEmojiPicker) {
|
||||
this.emojiMenu = new EmojiMenuInModal(
|
||||
Emoji,
|
||||
toggleEmojiMenuButtonSelector,
|
||||
emojiMenuClass,
|
||||
this.setEmoji,
|
||||
this.$refs.userStatusForm,
|
||||
);
|
||||
}
|
||||
|
||||
this.setDefaultEmoji();
|
||||
})
|
||||
.catch(() =>
|
||||
|
@ -145,19 +126,6 @@ export default {
|
|||
}),
|
||||
);
|
||||
},
|
||||
showEmojiMenu(e) {
|
||||
e.stopPropagation();
|
||||
this.isEmojiMenuVisible = true;
|
||||
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
|
||||
},
|
||||
hideEmojiMenu() {
|
||||
if (!this.isEmojiMenuVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEmojiMenuVisible = false;
|
||||
this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
|
||||
},
|
||||
setDefaultEmoji() {
|
||||
const { emojiTag } = this;
|
||||
const hasStatusMessage = Boolean(this.message.length);
|
||||
|
@ -173,16 +141,12 @@ export default {
|
|||
this.clearEmoji();
|
||||
}
|
||||
},
|
||||
setEmoji(emoji, emojiTag) {
|
||||
setEmoji(emoji) {
|
||||
this.emoji = emoji;
|
||||
this.noEmoji = false;
|
||||
this.clearEmoji();
|
||||
|
||||
if (this.glFeatures.improvedEmojiPicker) {
|
||||
this.emojiTag = Emoji.glEmojiTag(this.emoji);
|
||||
} else {
|
||||
this.emojiTag = emojiTag;
|
||||
}
|
||||
this.emojiTag = Emoji.glEmojiTag(this.emoji);
|
||||
},
|
||||
clearEmoji() {
|
||||
if (this.emojiTag) {
|
||||
|
@ -194,7 +158,6 @@ export default {
|
|||
this.message = '';
|
||||
this.noEmoji = true;
|
||||
this.clearEmoji();
|
||||
this.hideEmojiMenu();
|
||||
},
|
||||
removeStatus() {
|
||||
this.availability = false;
|
||||
|
@ -249,7 +212,6 @@ export default {
|
|||
:action-secondary="$options.actionSecondary"
|
||||
modal-class="set-user-status-modal"
|
||||
@shown="setupEmojiListAndAutocomplete"
|
||||
@hide="hideEmojiMenu"
|
||||
@primary="setStatus"
|
||||
@secondary="removeStatus"
|
||||
>
|
||||
|
@ -264,7 +226,6 @@ export default {
|
|||
<div class="input-group gl-mb-5">
|
||||
<span class="input-group-prepend">
|
||||
<emoji-picker
|
||||
v-if="glFeatures.improvedEmojiPicker"
|
||||
dropdown-class="gl-h-full"
|
||||
toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
|
||||
boundary="viewport"
|
||||
|
@ -283,27 +244,6 @@ export default {
|
|||
</span>
|
||||
</template>
|
||||
</emoji-picker>
|
||||
<button
|
||||
v-else
|
||||
ref="toggleEmojiMenuButton"
|
||||
v-gl-tooltip.bottom.hover
|
||||
:title="s__('SetStatusModal|Add status emoji')"
|
||||
:aria-label="s__('SetStatusModal|Add status emoji')"
|
||||
name="button"
|
||||
type="button"
|
||||
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
|
||||
@click="showEmojiMenu"
|
||||
>
|
||||
<span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span>
|
||||
<span
|
||||
v-show="noEmoji"
|
||||
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
|
||||
>
|
||||
<gl-icon name="slight-smile" class="award-control-icon-neutral" />
|
||||
<gl-icon name="smiley" class="award-control-icon-positive" />
|
||||
<gl-icon name="smile" class="award-control-icon-super-positive" />
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
ref="statusMessageField"
|
||||
|
@ -314,7 +254,6 @@ export default {
|
|||
name="user[status][message]"
|
||||
@keyup="setDefaultEmoji"
|
||||
@keyup.enter.prevent
|
||||
@click="hideEmojiMenu"
|
||||
/>
|
||||
<span v-show="isDirty" class="input-group-append">
|
||||
<button
|
||||
|
|
|
@ -198,10 +198,10 @@ export default {
|
|||
</gl-button>
|
||||
<div v-if="canAwardEmoji" class="award-menu-holder gl-my-2">
|
||||
<emoji-picker
|
||||
v-if="glFeatures.improvedEmojiPicker"
|
||||
v-gl-tooltip.viewport
|
||||
:title="__('Add reaction')"
|
||||
:toggle-class="['add-reaction-button btn-icon gl-relative!', { 'is-active': isMenuOpen }]"
|
||||
data-testid="emoji-picker"
|
||||
@click="handleAward"
|
||||
@shown="setIsMenuOpen(true)"
|
||||
@hidden="setIsMenuOpen(false)"
|
||||
|
@ -219,24 +219,6 @@ export default {
|
|||
</span>
|
||||
</template>
|
||||
</emoji-picker>
|
||||
<gl-button
|
||||
v-else
|
||||
v-gl-tooltip.viewport
|
||||
:class="addButtonClass"
|
||||
class="add-reaction-button js-add-award"
|
||||
title="Add reaction"
|
||||
:aria-label="__('Add reaction')"
|
||||
>
|
||||
<span class="reaction-control-icon reaction-control-icon-neutral">
|
||||
<gl-icon name="slight-smile" />
|
||||
</span>
|
||||
<span class="reaction-control-icon reaction-control-icon-positive">
|
||||
<gl-icon name="smiley" />
|
||||
</span>
|
||||
<span class="reaction-control-icon reaction-control-icon-super-positive">
|
||||
<gl-icon name="smile" />
|
||||
</span>
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -37,7 +37,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
before_action :authorize_download_code!, only: [:related_branches]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:vue_issues_list, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:contacts_autocomplete, project&.group, default_enabled: :yaml)
|
||||
|
@ -48,7 +47,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:issue_assignees_widget, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:paginated_issue_discussions, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:work_items, project&.group, default_enabled: :yaml)
|
||||
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
|
||||
end
|
||||
|
||||
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
|
||||
|
|
|
@ -37,7 +37,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
|
||||
push_frontend_feature_flag(:paginated_notes, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:confidential_notes, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)
|
||||
|
|
|
@ -17,6 +17,10 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
|
||||
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:pipeline_tabs_vue, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
|
||||
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
|
||||
|
||||
|
|
|
@ -14,10 +14,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
|
|||
before_action :authorize_read_snippet!, except: [:new, :index]
|
||||
before_action :authorize_update_snippet!, only: :edit
|
||||
|
||||
before_action only: [:show] do
|
||||
push_frontend_feature_flag(:improved_emoji_picker, @project, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
urgency :low, [:index]
|
||||
|
||||
def index
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
class Projects::WorkItemsController < Projects::ApplicationController
|
||||
before_action do
|
||||
push_frontend_feature_flag(:work_items, project, default_enabled: :yaml)
|
||||
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
|
||||
end
|
||||
|
||||
feature_category :not_owned
|
||||
|
||||
def index
|
||||
render_404 unless Feature.enabled?(:work_items, project, default_enabled: :yaml)
|
||||
render_404 unless project&.work_items_feature_flag_enabled?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
|
||||
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
||||
push_frontend_feature_flag(:work_items, @project, default_enabled: :yaml)
|
||||
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
|
||||
end
|
||||
|
||||
layout :determine_layout
|
||||
|
|
|
@ -33,7 +33,7 @@ module Mutations
|
|||
def resolve(project_path:, **attributes)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
unless Feature.enabled?(:work_items, project, default_enabled: :yaml)
|
||||
unless project.work_items_feature_flag_enabled?
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ module Mutations
|
|||
def resolve(id:, work_item_data:)
|
||||
work_item = authorized_find!(id: id)
|
||||
|
||||
unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
|
||||
unless work_item.project.work_items_feature_flag_enabled?
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ module Mutations
|
|||
def resolve(id:)
|
||||
work_item = authorized_find!(id: id)
|
||||
|
||||
unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
|
||||
unless work_item.project.work_items_feature_flag_enabled?
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ module Mutations
|
|||
def resolve(id:, **attributes)
|
||||
work_item = authorized_find!(id: id)
|
||||
|
||||
unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
|
||||
unless work_item.project.work_items_feature_flag_enabled?
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ module Resolvers
|
|||
|
||||
def resolve(id:)
|
||||
work_item = authorized_find!(id: id)
|
||||
return unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
|
||||
return unless work_item.project.work_items_feature_flag_enabled?
|
||||
|
||||
work_item
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module Resolvers
|
|||
' Argument is experimental and can be removed in the future without notice.'
|
||||
|
||||
def resolve(taskable: nil)
|
||||
return unless Feature.enabled?(:work_items, object, default_enabled: :yaml)
|
||||
return unless feature_flag_enabled_for_parent?(object)
|
||||
|
||||
# This will require a finder in the future when groups/projects get their work item types
|
||||
# All groups/projects use the default types for now
|
||||
|
@ -20,6 +20,14 @@ module Resolvers
|
|||
|
||||
base_scope.order_by_name_asc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def feature_flag_enabled_for_parent?(parent)
|
||||
return false unless parent.is_a?(::Project) || parent.is_a?(::Group)
|
||||
|
||||
parent.work_items_feature_flag_enabled?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -251,9 +251,7 @@ module IssuesHelper
|
|||
end
|
||||
|
||||
def award_emoji_issue_api_path(issue)
|
||||
if Feature.enabled?(:improved_emoji_picker, issue.project, default_enabled: :yaml)
|
||||
api_v4_projects_issues_award_emoji_path(id: issue.project.id, issue_iid: issue.iid)
|
||||
end
|
||||
api_v4_projects_issues_award_emoji_path(id: issue.project.id, issue_iid: issue.iid)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -203,9 +203,7 @@ module MergeRequestsHelper
|
|||
end
|
||||
|
||||
def award_emoji_merge_request_api_path(merge_request)
|
||||
if Feature.enabled?(:improved_emoji_picker, merge_request.project, default_enabled: :yaml)
|
||||
api_v4_projects_merge_requests_award_emoji_path(id: merge_request.project.id, merge_request_iid: merge_request.iid)
|
||||
end
|
||||
api_v4_projects_merge_requests_award_emoji_path(id: merge_request.project.id, merge_request_iid: merge_request.iid)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
16
app/helpers/projects/pipeline_helper.rb
Normal file
16
app/helpers/projects/pipeline_helper.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Projects
|
||||
module PipelineHelper
|
||||
def js_pipeline_tabs_data(project, pipeline)
|
||||
{
|
||||
can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
|
||||
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
|
||||
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
|
||||
pipeline_project_path: project.full_path
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Projects::PipelineHelper.prepend_mod
|
|
@ -77,8 +77,6 @@ module SnippetsHelper
|
|||
end
|
||||
|
||||
def project_snippets_award_api_path(snippet)
|
||||
if Feature.enabled?(:improved_emoji_picker, snippet.project, default_enabled: :yaml)
|
||||
api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id)
|
||||
end
|
||||
api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,10 +16,13 @@ class BulkImport < ApplicationRecord
|
|||
|
||||
enum source_type: { gitlab: 0 }
|
||||
|
||||
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
|
||||
|
||||
state_machine :status, initial: :created do
|
||||
state :created, value: 0
|
||||
state :started, value: 1
|
||||
state :finished, value: 2
|
||||
state :timeout, value: 3
|
||||
state :failed, value: -1
|
||||
|
||||
event :start do
|
||||
|
@ -30,6 +33,11 @@ class BulkImport < ApplicationRecord
|
|||
transition started: :finished
|
||||
end
|
||||
|
||||
event :cleanup_stale do
|
||||
transition created: :timeout
|
||||
transition started: :timeout
|
||||
end
|
||||
|
||||
event :fail_op do
|
||||
transition any => :failed
|
||||
end
|
||||
|
|
|
@ -51,11 +51,13 @@ class BulkImports::Entity < ApplicationRecord
|
|||
enum source_type: { group_entity: 0, project_entity: 1 }
|
||||
|
||||
scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) }
|
||||
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
|
||||
|
||||
state_machine :status, initial: :created do
|
||||
state :created, value: 0
|
||||
state :started, value: 1
|
||||
state :finished, value: 2
|
||||
state :timeout, value: 3
|
||||
state :failed, value: -1
|
||||
|
||||
event :start do
|
||||
|
@ -70,6 +72,11 @@ class BulkImports::Entity < ApplicationRecord
|
|||
event :fail_op do
|
||||
transition any => :failed
|
||||
end
|
||||
|
||||
event :cleanup_stale do
|
||||
transition created: :timeout
|
||||
transition started: :timeout
|
||||
end
|
||||
end
|
||||
|
||||
def self.all_human_statuses
|
||||
|
|
|
@ -14,6 +14,7 @@ class ContainerRepository < ApplicationRecord
|
|||
ACTIVE_MIGRATION_STATES = %w[pre_importing importing].freeze
|
||||
MIGRATION_STATES = (IDLE_MIGRATION_STATES + ACTIVE_MIGRATION_STATES).freeze
|
||||
ABORTABLE_MIGRATION_STATES = (ACTIVE_MIGRATION_STATES + %w[pre_import_done default]).freeze
|
||||
SKIPPABLE_MIGRATION_STATES = (ABORTABLE_MIGRATION_STATES + %w[import_aborted]).freeze
|
||||
|
||||
IRRECONCILABLE_MIGRATIONS_STATUSES = %w[import_in_progress pre_import_in_progress pre_import_canceled import_canceled].freeze
|
||||
|
||||
|
@ -35,7 +36,7 @@ class ContainerRepository < ApplicationRecord
|
|||
|
||||
enum status: { delete_scheduled: 0, delete_failed: 1 }
|
||||
enum expiration_policy_cleanup_status: { cleanup_unscheduled: 0, cleanup_scheduled: 1, cleanup_unfinished: 2, cleanup_ongoing: 3 }
|
||||
enum migration_skipped_reason: { not_in_plan: 0, too_many_retries: 1, too_many_tags: 2, root_namespace_in_deny_list: 3, migration_canceled: 4 }
|
||||
enum migration_skipped_reason: { not_in_plan: 0, too_many_retries: 1, too_many_tags: 2, root_namespace_in_deny_list: 3, migration_canceled: 4, not_found: 5 }
|
||||
|
||||
delegate :client, :gitlab_api_client, to: :registry
|
||||
|
||||
|
@ -137,7 +138,7 @@ class ContainerRepository < ApplicationRecord
|
|||
end
|
||||
|
||||
event :skip_import do
|
||||
transition ABORTABLE_MIGRATION_STATES.map(&:to_sym) => :import_skipped
|
||||
transition SKIPPABLE_MIGRATION_STATES.map(&:to_sym) => :import_skipped
|
||||
end
|
||||
|
||||
event :retry_pre_import do
|
||||
|
@ -184,6 +185,12 @@ class ContainerRepository < ApplicationRecord
|
|||
container_repository.migration_retries_count += 1
|
||||
end
|
||||
|
||||
after_transition any => :import_aborted do |container_repository|
|
||||
if container_repository.retried_too_many_times?
|
||||
container_repository.skip_import(reason: :too_many_retries)
|
||||
end
|
||||
end
|
||||
|
||||
before_transition import_aborted: any do |container_repository|
|
||||
container_repository.migration_aborted_at = nil
|
||||
container_repository.migration_aborted_in_state = nil
|
||||
|
@ -310,9 +317,16 @@ class ContainerRepository < ApplicationRecord
|
|||
try_count = 0
|
||||
begin
|
||||
try_count += 1
|
||||
return true if yield == :ok
|
||||
|
||||
abort_import
|
||||
case yield
|
||||
when :ok
|
||||
return true
|
||||
when :not_found
|
||||
skip_import(reason: :not_found)
|
||||
else
|
||||
abort_import
|
||||
end
|
||||
|
||||
false
|
||||
rescue TooManyImportsError
|
||||
if try_count <= ::ContainerRegistry::Migration.start_max_retries
|
||||
|
@ -325,6 +339,10 @@ class ContainerRepository < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def retried_too_many_times?
|
||||
migration_retries_count >= ContainerRegistry::Migration.max_retries
|
||||
end
|
||||
|
||||
def last_import_step_done_at
|
||||
[migration_pre_import_done_at, migration_import_done_at, migration_aborted_at].compact.max
|
||||
end
|
||||
|
|
|
@ -815,6 +815,15 @@ class Group < Namespace
|
|||
].compact.min
|
||||
end
|
||||
|
||||
def work_items_feature_flag_enabled?
|
||||
actors = [root_ancestor]
|
||||
actors << self if root_ancestor != self
|
||||
|
||||
actors.any? do |actor|
|
||||
Feature.enabled?(:work_items, actor, default_enabled: :yaml)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def max_member_access(user_ids)
|
||||
|
|
|
@ -2832,6 +2832,10 @@ class Project < ApplicationRecord
|
|||
pending_delete? || hidden?
|
||||
end
|
||||
|
||||
def work_items_feature_flag_enabled?
|
||||
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# overridden in EE
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
|
||||
|
||||
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
|
||||
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
|
||||
|
||||
- if Feature.enabled?(:pipeline_tabs_vue, @project, default_enabled: :yaml)
|
||||
#js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline) }
|
||||
- else
|
||||
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
|
||||
.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } }
|
||||
|
|
|
@ -192,6 +192,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:bulk_imports_stuck_import
|
||||
:worker_name: BulkImports::StuckImportWorker
|
||||
:feature_category: :importers
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: cronjob:ci_archive_traces_cron
|
||||
:worker_name: Ci::ArchiveTracesCronWorker
|
||||
:feature_category: :continuous_integration
|
||||
|
|
24
app/workers/bulk_imports/stuck_import_worker.rb
Normal file
24
app/workers/bulk_imports/stuck_import_worker.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module BulkImports
|
||||
class StuckImportWorker
|
||||
include ApplicationWorker
|
||||
|
||||
# This worker does not schedule other workers that require context.
|
||||
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
|
||||
|
||||
idempotent!
|
||||
data_consistency :always
|
||||
|
||||
feature_category :importers
|
||||
|
||||
def perform
|
||||
BulkImport.stale.find_each do |import|
|
||||
import.cleanup_stale
|
||||
end
|
||||
BulkImports::Entity.stale.find_each do |import|
|
||||
import.cleanup_stale
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: improved_emoji_picker
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54469
|
||||
rollout_issue_url:
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::code review
|
||||
default_enabled: true
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: ci_skip_legacy_extra_minutes_recalculation
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78476
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341730
|
||||
milestone: '14.8'
|
||||
name: pipeline_tabs_vue
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80401
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353118
|
||||
milestone: '14.10'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324351
|
|||
milestone: '13.11'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -500,6 +500,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW
|
|||
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
|
||||
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
|
||||
Settings.cron_jobs['bulk_imports_stuck_import_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['bulk_imports_stuck_import_worker']['cron'] ||= '0 */4 * * *'
|
||||
Settings.cron_jobs['bulk_imports_stuck_import_worker']['job_class'] = 'BulkImports::StuckImportWorker'
|
||||
Settings.cron_jobs['import_stuck_project_import_jobs'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['import_stuck_project_import_jobs']['cron'] ||= '15 * * * *'
|
||||
Settings.cron_jobs['import_stuck_project_import_jobs']['job_class'] = 'Gitlab::Import::StuckProjectImportJobsWorker'
|
||||
|
|
|
@ -14,12 +14,12 @@
|
|||
- Requests that have the `status` field set to `approved`.
|
||||
|
||||
Beginning in GitLab 15.0, status checks will only be updated to a passing state if the `status` field is both present
|
||||
and set to `pass`. Requests that:
|
||||
and set to `passed`. Requests that:
|
||||
|
||||
- Do not contain the `status` field will be rejected with a `422` error. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/338827).
|
||||
- Contain any value other than `pass` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
|
||||
- Contain any value other than `passed` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
|
||||
|
||||
To align with this change, API calls to list external status checks will also return the value of `pass` rather than
|
||||
To align with this change, API calls to list external status checks will also return the value of `passed` rather than
|
||||
`approved` for status checks that have passed.
|
||||
# The following items are not published on the docs page, but may be used in the future.
|
||||
stage: "Manage"
|
||||
|
|
|
@ -766,6 +766,8 @@ This example for Omnibus GitLab assumes you're using the following mailbox: `inc
|
|||
|
||||
##### Configure Microsoft Graph
|
||||
|
||||
> Alternative Azure deployments [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5978) in GitLab 14.9.
|
||||
|
||||
```ruby
|
||||
gitlab_rails['incoming_email_enabled'] = true
|
||||
|
||||
|
@ -788,4 +790,19 @@ gitlab_rails['incoming_email_inbox_options'] = {
|
|||
}
|
||||
```
|
||||
|
||||
For Microsoft Cloud for US Government or [other Azure deployments](https://docs.microsoft.com/en-us/graph/deployments), configure the `azure_ad_endpoint` and `graph_endpoint` settings.
|
||||
|
||||
- Example for Microsoft Cloud for US Government:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['incoming_email_inbox_options'] = {
|
||||
'azure_ad_endpoint': 'https://login.microsoftonline.us',
|
||||
'graph_endpoint': 'https://graph.microsoft.us',
|
||||
'tenant_id': '<YOUR-TENANT-ID>',
|
||||
'client_id': '<YOUR-CLIENT-ID>',
|
||||
'client_secret': '<YOUR-CLIENT-SECRET>',
|
||||
'poll_interval': 60 # Optional
|
||||
}
|
||||
```
|
||||
|
||||
The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details.
|
||||
|
|
|
@ -95,6 +95,12 @@ To remove a metric:
|
|||
used to test the [`UsageDataController#create`](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/3760ef28/spec/controllers/usage_data_controller_spec.rb#L75)
|
||||
endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload.
|
||||
|
||||
1. Remove data from Redis
|
||||
|
||||
For [Ordinary Redis](implement.md#ordinary-redis-counters) counters remove data stored in Redis.
|
||||
|
||||
- Add a migration to remove the data from Redis for the related Redis keys. For more details, see [this MR example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82604/diffs).
|
||||
|
||||
1. Create an issue in the
|
||||
[GitLab Data Team project](https://gitlab.com/gitlab-data/analytics/-/issues).
|
||||
Ask for confirmation that the metric is not referred to in any SiSense dashboards and
|
||||
|
|
|
@ -313,12 +313,12 @@ Specifically, the following are deprecated:
|
|||
- Requests that have the `status` field set to `approved`.
|
||||
|
||||
Beginning in GitLab 15.0, status checks will only be updated to a passing state if the `status` field is both present
|
||||
and set to `pass`. Requests that:
|
||||
and set to `passed`. Requests that:
|
||||
|
||||
- Do not contain the `status` field will be rejected with a `422` error. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/338827).
|
||||
- Contain any value other than `pass` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
|
||||
- Contain any value other than `passed` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
|
||||
|
||||
To align with this change, API calls to list external status checks will also return the value of `pass` rather than
|
||||
To align with this change, API calls to list external status checks will also return the value of `passed` rather than
|
||||
`approved` for status checks that have passed.
|
||||
|
||||
**Planned removal milestone: 15.0 (2022-05-22)**
|
||||
|
|
|
@ -138,9 +138,9 @@ migrated:
|
|||
In a [rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session),
|
||||
you can find the failure or error messages for the group import attempt using:
|
||||
|
||||
```shell
|
||||
```ruby
|
||||
# Get relevant import records
|
||||
import = BulkImports::Entity.where(namespace_id: Group.id).bulk_import
|
||||
import = BulkImports::Entity.where(namespace_id: Group.id).map(&:bulk_import)
|
||||
|
||||
# Alternative lookup by user
|
||||
import = BulkImport.where(user_id: User.find(...)).last
|
||||
|
@ -154,3 +154,18 @@ entities.map(&:failures).flatten
|
|||
# Alternative failure lookup by status
|
||||
entities.where(status: [-1]).pluck(:destination_name, :destination_namespace, :status)
|
||||
```
|
||||
|
||||
### Stale imports
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352985) in GitLab 14.10.
|
||||
|
||||
When troubleshooting group migration, an import may not complete because the import workers took
|
||||
longer than 8 hours to execute. In this case, the `status` of either a `BulkImport` or
|
||||
`BulkImport::Entity` is `3` (`timeout`):
|
||||
|
||||
```ruby
|
||||
# Get relevant import records
|
||||
import = BulkImports::Entity.where(namespace_id: Group.id).map(&:bulk_import)
|
||||
|
||||
import.status #=> 3 means that the import timed out.
|
||||
```
|
||||
|
|
|
@ -35,7 +35,7 @@ Projects include the following [features](https://about.gitlab.com/features/):
|
|||
- [Deploy tokens](deploy_tokens/index.md): Manage access to the repository and Container Registry.
|
||||
- [Web IDE](web_ide/index.md)
|
||||
- [CVE ID Requests](../application_security/cve_id_request.md): Request a CVE identifier to track a
|
||||
vulnerability in your project. **(FREE SAAS)**
|
||||
vulnerability in your project.
|
||||
|
||||
**Issues and merge requests:**
|
||||
|
||||
|
@ -83,7 +83,7 @@ Projects include the following [features](https://about.gitlab.com/features/):
|
|||
- [Kubernetes cluster integration](../infrastructure/clusters/index.md): Connect your GitLab project
|
||||
with a Kubernetes cluster.
|
||||
- [Feature Flags](../../operations/feature_flags.md): Ship different features
|
||||
by dynamically toggling functionality. **(PREMIUM)**
|
||||
by dynamically toggling functionality.
|
||||
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
|
||||
website.
|
||||
|
||||
|
@ -92,8 +92,8 @@ Projects include the following [features](https://about.gitlab.com/features/):
|
|||
- [Wiki](wiki/index.md): Document your GitLab project in an integrated Wiki.
|
||||
- [Snippets](../snippets.md): Store, share and collaborate on code snippets.
|
||||
- [Value Stream Analytics](../analytics/value_stream_analytics.md): Review your development lifecycle.
|
||||
- [Insights](insights/index.md): Configure the insights that matter for your projects. **(ULTIMATE)**
|
||||
- [Security Dashboard](../application_security/security_dashboard/index.md) **(ULTIMATE)**
|
||||
- [Insights](insights/index.md): Configure the insights that matter for your projects.
|
||||
- [Security Dashboard](../application_security/security_dashboard/index.md)
|
||||
- [Syntax highlighting](highlighting.md): Customize
|
||||
your code blocks, overriding the default language choice.
|
||||
- [Badges](badges.md): Add an image to the **Project information** page.
|
||||
|
@ -102,9 +102,9 @@ Projects include the following [features](https://about.gitlab.com/features/):
|
|||
associated with a released version of your code.
|
||||
- [Package Registry](../packages/package_registry/index.md): Publish and install packages.
|
||||
- [Code owners](code_owners.md): Specify code owners for specific files.
|
||||
- [License Compliance](../compliance/license_compliance/index.md): Approve and deny licenses for projects. **(ULTIMATE)**
|
||||
- [Dependency List](../application_security/dependency_list/index.md): View project dependencies. **(ULTIMATE)**
|
||||
- [Requirements](requirements/index.md): Create criteria to check your products against. **(ULTIMATE)**
|
||||
- [License Compliance](../compliance/license_compliance/index.md): Approve and deny licenses for projects.
|
||||
- [Dependency List](../application_security/dependency_list/index.md): View project dependencies.
|
||||
- [Requirements](requirements/index.md): Create criteria to check your products against.
|
||||
- [Code Intelligence](code_intelligence.md): Navigate code.
|
||||
|
||||
## Project integrations
|
||||
|
|
|
@ -241,7 +241,8 @@ The configuration options are the same as for configuring
|
|||
|
||||
##### Microsoft Graph
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11.
|
||||
> - Alternative Azure deployments [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5978) in GitLab 14.9.
|
||||
|
||||
Service Desk can be configured to read Microsoft Exchange Online mailboxes with the Microsoft
|
||||
Graph API instead of IMAP. Follow the [documentation in the incoming email section for setting up an OAuth2 application for Microsoft Graph](../../administration/incoming_email.md#microsoft-graph).
|
||||
|
@ -263,6 +264,22 @@ Graph API instead of IMAP. Follow the [documentation in the incoming email secti
|
|||
}
|
||||
```
|
||||
|
||||
For Microsoft Cloud for US Government or [other Azure deployments](https://docs.microsoft.com/en-us/graph/deployments), configure the `azure_ad_endpoint` and `graph_endpoint` settings.
|
||||
|
||||
- Example for Microsoft Cloud for US Government:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['service_desk_email_inbox_options'] = {
|
||||
'azure_ad_endpoint': 'https://login.microsoftonline.us',
|
||||
'graph_endpoint': 'https://graph.microsoft.us',
|
||||
'tenant_id': '<YOUR-TENANT-ID>',
|
||||
'client_id': '<YOUR-CLIENT-ID>',
|
||||
'client_secret': '<YOUR-CLIENT-SECRET>',
|
||||
'poll_interval': 60 # Optional
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details.
|
||||
|
||||
#### Configuring a custom email address suffix
|
||||
|
|
|
@ -76,13 +76,13 @@ To create a project in GitLab:
|
|||
- [custom template](#create-a-project-from-a-custom-template).
|
||||
- [HIPAA audit protocol template](#create-a-project-from-the-hipaa-audit-protocol-template).
|
||||
- [Import a project](../../user/project/import/index.md)
|
||||
from a different repository. Contact your GitLab administrator if this option is not available.
|
||||
from a different repository. Contact your GitLab administrator if this option is not available.
|
||||
- [Connect an external repository to GitLab CI/CD](../../ci/ci_cd_for_external_repos/index.md).
|
||||
|
||||
- For a list of words that you cannot use as project names, see
|
||||
[reserved project and group names](../../user/reserved_names.md).
|
||||
[reserved project and group names](../../user/reserved_names.md).
|
||||
- For a list of characters that you cannot use in project and group names, see
|
||||
[limitations on project and group names](../../user/reserved_names.md#limitations-on-project-and-group-names).
|
||||
[limitations on project and group names](../../user/reserved_names.md#limitations-on-project-and-group-names).
|
||||
|
||||
## Create a blank project
|
||||
|
||||
|
@ -100,11 +100,11 @@ To create a blank project:
|
|||
- In the **Project target (optional)** field, select your project's deployment target.
|
||||
This information helps GitLab better understand its users and their deployment requirements.
|
||||
- To modify the project's [viewing and access rights](../public_access.md) for
|
||||
users, change the **Visibility Level**.
|
||||
users, change the **Visibility Level**.
|
||||
- To create README file so that the Git repository is initialized, has a default branch, and
|
||||
can be cloned, select **Initialize repository with a README**.
|
||||
- To analyze the source code in the project for known security vulnerabilities,
|
||||
select **Enable Static Application Security Testing (SAST)**.
|
||||
select **Enable Static Application Security Testing (SAST)**.
|
||||
1. Select **Create project**.
|
||||
|
||||
## Create a project from a built-in template
|
||||
|
@ -133,12 +133,12 @@ To create a project from a built-in template:
|
|||
then change the slug.
|
||||
- In the **Project description (optional)** field, enter the description of your project's dashboard.
|
||||
- To modify the project's [viewing and access rights](../public_access.md) for users,
|
||||
change the **Visibility Level**.
|
||||
change the **Visibility Level**.
|
||||
1. Select **Create project**.
|
||||
|
||||
## Create a project from a custom template **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6860) in GitLab 11.2.
|
||||
|
||||
Custom project templates are available at:
|
||||
|
||||
|
@ -159,7 +159,7 @@ Custom project templates are available at:
|
|||
then change the slug.
|
||||
- The description of your project's dashboard in the **Project description (optional)** field.
|
||||
- To modify the project's [viewing and access rights](../public_access.md) for users,
|
||||
change the **Visibility Level**.
|
||||
change the **Visibility Level**.
|
||||
1. Select **Create project**.
|
||||
|
||||
## Create a project from the HIPAA Audit Protocol template **(ULTIMATE)**
|
||||
|
@ -185,7 +185,7 @@ To create a project from the HIPAA Audit Protocol template:
|
|||
then change the slug.
|
||||
- In the **Project description (optional)** field, enter the description of your project's dashboard.
|
||||
- To modify the project's [viewing and access rights](../public_access.md) for users,
|
||||
change the **Visibility Level**.
|
||||
change the **Visibility Level**.
|
||||
1. Select **Create project**.
|
||||
|
||||
## Create a new project with Git push
|
||||
|
@ -207,7 +207,7 @@ used or renamed project, use the [UI](#create-a-project) or the [Projects API](.
|
|||
Prerequisites:
|
||||
|
||||
- To push with SSH, you must have [an SSH key](../../ssh/index.md) that is
|
||||
[added to your GitLab account](../../ssh/index.md#add-an-ssh-key-to-your-gitlab-account).
|
||||
[added to your GitLab account](../../ssh/index.md#add-an-ssh-key-to-your-gitlab-account).
|
||||
- You must have permission to add new projects to a namespace. To check if you have permission:
|
||||
|
||||
1. On the top bar, select **Menu > Projects**.
|
||||
|
@ -409,9 +409,9 @@ To disable fetching:
|
|||
1. Disable checksum queries in `GONOSUMDB`.
|
||||
|
||||
- If the module name or its prefix is in `GOPRIVATE` or `GONOPROXY`, Go does not query module
|
||||
proxies.
|
||||
proxies.
|
||||
- If the module name or its prefix is in `GONOPRIVATE` or `GONOSUMDB`, Go does not query
|
||||
Checksum databases.
|
||||
Checksum databases.
|
||||
|
||||
### Fetch Go modules from Geo secondary sites
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
|
||||
get ':id/pipelines/:pipeline_id/bridges', feature_category: :pipeline_authoring do
|
||||
get ':id/pipelines/:pipeline_id/bridges', urgency: :low, feature_category: :pipeline_authoring do
|
||||
authorize!(:read_build, user_project)
|
||||
|
||||
pipeline = user_project.all_pipelines.find(params[:pipeline_id])
|
||||
|
|
|
@ -23,7 +23,7 @@ module API
|
|||
params do
|
||||
use :pagination
|
||||
end
|
||||
get ':id/variables' do
|
||||
get ':id/variables', urgency: :low do
|
||||
variables = user_project.variables
|
||||
present paginate(variables), with: Entities::Ci::Variable
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module API
|
|||
optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response'
|
||||
optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response'
|
||||
end
|
||||
post '/lint' do
|
||||
post '/lint', urgency: :low do
|
||||
unauthorized! unless can_lint_ci?
|
||||
|
||||
result = Gitlab::Ci::Lint.new(project: nil, current_user: current_user)
|
||||
|
|
|
@ -53,7 +53,6 @@ module Gitlab
|
|||
# made globally available to the frontend
|
||||
push_frontend_feature_flag(:usage_data_api, type: :ops, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
|
||||
push_frontend_feature_flag(:improved_emoji_picker, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml)
|
||||
|
@ -73,6 +72,15 @@ module Gitlab
|
|||
push_to_gon_attributes(:features, name, enabled)
|
||||
end
|
||||
|
||||
# Exposes the state of a feature flag to the frontend code.
|
||||
# Can be used for more complex feature flag checks.
|
||||
#
|
||||
# name - The name of the feature flag, e.g. `my_feature`.
|
||||
# enabled - Boolean to be pushed directly to the frontend. Should be fetched by checking a feature flag.
|
||||
def push_force_frontend_feature_flag(name, enabled)
|
||||
push_to_gon_attributes(:features, name, !!enabled)
|
||||
end
|
||||
|
||||
def push_to_gon_attributes(key, name, enabled)
|
||||
var_name = name.to_s.camelize(:lower)
|
||||
# Here the `true` argument signals gon that the value should be merged
|
||||
|
|
|
@ -34269,9 +34269,6 @@ msgstr ""
|
|||
msgid "Set what should be replicated by this secondary site."
|
||||
msgstr ""
|
||||
|
||||
msgid "SetStatusModal|Add status emoji"
|
||||
msgstr ""
|
||||
|
||||
msgid "SetStatusModal|An indicator appears next to your name and avatar"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ RSpec.describe 'Commits' do
|
|||
before do
|
||||
sign_in(user)
|
||||
stub_ci_pipeline_to_return_yaml_file
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
end
|
||||
|
||||
let(:creator) { create(:user, developer_projects: [project]) }
|
||||
|
@ -93,6 +94,7 @@ RSpec.describe 'Commits' do
|
|||
|
||||
context 'Download artifacts', :js do
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
||||
end
|
||||
|
||||
|
@ -122,6 +124,7 @@ RSpec.describe 'Commits' do
|
|||
|
||||
context "when logged as reporter", :js do
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
project.add_reporter(user)
|
||||
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
|
||||
visit builds_project_pipeline_path(project, pipeline)
|
||||
|
|
|
@ -15,12 +15,20 @@ RSpec.describe 'Blob shortcuts', :js do
|
|||
end
|
||||
|
||||
shared_examples "quotes the selected text" do
|
||||
it "quotes the selected text", :quarantine do
|
||||
select_element('.note-text')
|
||||
it 'quotes the selected text in main comment form', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/356388' do
|
||||
select_element('#notes-list .note:first-child .note-text')
|
||||
find('body').native.send_key('r')
|
||||
|
||||
expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
|
||||
end
|
||||
|
||||
it 'quotes the selected text in the discussion reply form', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/356388' do
|
||||
find('#notes-list .note:first-child .js-reply-button').click
|
||||
select_element('#notes-list .note:first-child .note-text')
|
||||
find('body').native.send_key('r')
|
||||
|
||||
expect(find('#notes-list .note:first-child .js-vue-markdown-field .js-gfm-input').value).to include(note_text)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'pressing "r"' do
|
||||
|
|
|
@ -8,8 +8,6 @@ RSpec.describe 'User edit profile' do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(improved_emoji_picker: false)
|
||||
|
||||
sign_in(user)
|
||||
visit(profile_path)
|
||||
end
|
||||
|
@ -169,10 +167,9 @@ RSpec.describe 'User edit profile' do
|
|||
|
||||
context 'user status', :js do
|
||||
def select_emoji(emoji_name, is_modal = false)
|
||||
emoji_menu_class = is_modal ? '.js-modal-status-emoji-menu' : '.js-status-emoji-menu'
|
||||
toggle_button = find('.js-toggle-emoji-menu')
|
||||
toggle_button = find('.emoji-menu-toggle-button')
|
||||
toggle_button.click
|
||||
emoji_button = find(%Q{#{emoji_menu_class} .js-emoji-btn gl-emoji[data-name="#{emoji_name}"]})
|
||||
emoji_button = find("gl-emoji[data-name=\"#{emoji_name}\"]")
|
||||
emoji_button.click
|
||||
end
|
||||
|
||||
|
@ -207,7 +204,7 @@ RSpec.describe 'User edit profile' do
|
|||
end
|
||||
|
||||
it 'adds message and emoji to user status' do
|
||||
emoji = 'tanabata_tree'
|
||||
emoji = '8ball'
|
||||
message = 'Playing outside'
|
||||
select_emoji(emoji)
|
||||
fill_in 'js-status-message-field', with: message
|
||||
|
@ -356,7 +353,7 @@ RSpec.describe 'User edit profile' do
|
|||
end
|
||||
|
||||
it 'adds emoji to user status' do
|
||||
emoji = 'biohazard'
|
||||
emoji = '8ball'
|
||||
open_user_status_modal
|
||||
select_emoji(emoji, true)
|
||||
set_user_status_in_modal
|
||||
|
@ -387,18 +384,18 @@ RSpec.describe 'User edit profile' do
|
|||
|
||||
it 'opens the emoji modal again after closing it' do
|
||||
open_user_status_modal
|
||||
select_emoji('biohazard', true)
|
||||
select_emoji('8ball', true)
|
||||
|
||||
find('.js-toggle-emoji-menu').click
|
||||
find('.emoji-menu-toggle-button').click
|
||||
|
||||
expect(page).to have_selector('.emoji-menu')
|
||||
expect(page).to have_selector('.emoji-picker-emoji')
|
||||
end
|
||||
|
||||
it 'does not update the awards panel emoji' do
|
||||
project.add_maintainer(user)
|
||||
visit(project_issue_path(project, issue))
|
||||
|
||||
emoji = 'biohazard'
|
||||
emoji = '8ball'
|
||||
open_user_status_modal
|
||||
select_emoji(emoji, true)
|
||||
|
||||
|
@ -420,7 +417,7 @@ RSpec.describe 'User edit profile' do
|
|||
end
|
||||
|
||||
it 'adds message and emoji to user status' do
|
||||
emoji = 'tanabata_tree'
|
||||
emoji = '8ball'
|
||||
message = 'Playing outside'
|
||||
open_user_status_modal
|
||||
select_emoji(emoji, true)
|
||||
|
@ -495,9 +492,7 @@ RSpec.describe 'User edit profile' do
|
|||
open_user_status_modal
|
||||
find('.js-status-message-field').native.send_keys(message)
|
||||
|
||||
within('.js-toggle-emoji-menu') do
|
||||
expect(page).to have_emoji('speech_balloon')
|
||||
end
|
||||
expect(page).to have_emoji('speech_balloon')
|
||||
end
|
||||
|
||||
context 'note header' do
|
||||
|
|
|
@ -15,6 +15,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
before do
|
||||
sign_in(user)
|
||||
project.add_role(user, role)
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
end
|
||||
|
||||
shared_context 'pipeline builds' do
|
||||
|
@ -356,6 +357,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
|
||||
context 'page tabs' do
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
visit_pipeline
|
||||
end
|
||||
|
||||
|
@ -388,6 +390,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
let(:pipeline) { create(:ci_pipeline, :with_test_reports, :with_report_results, project: project) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
visit_pipeline
|
||||
wait_for_requests
|
||||
end
|
||||
|
@ -924,6 +927,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
visit builds_project_pipeline_path(project, pipeline)
|
||||
end
|
||||
|
||||
|
@ -944,6 +948,10 @@ RSpec.describe 'Pipeline', :js do
|
|||
end
|
||||
|
||||
context 'page tabs' do
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
end
|
||||
|
||||
it 'shows Pipeline, Jobs and DAG tabs with link' do
|
||||
expect(page).to have_link('Pipeline')
|
||||
expect(page).to have_link('Jobs')
|
||||
|
@ -1014,6 +1022,10 @@ RSpec.describe 'Pipeline', :js do
|
|||
end
|
||||
|
||||
describe 'GET /:project/-/pipelines/:id/failures' do
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
end
|
||||
|
||||
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
|
||||
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
|
||||
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
|
||||
|
@ -1139,6 +1151,7 @@ RSpec.describe 'Pipeline', :js do
|
|||
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
visit dag_project_pipeline_path(project, pipeline)
|
||||
end
|
||||
|
||||
|
|
|
@ -623,6 +623,7 @@ RSpec.describe 'Pipelines', :js do
|
|||
|
||||
create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3, ref: 'master')
|
||||
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
visit project_pipeline_path(project, pipeline)
|
||||
wait_for_requests
|
||||
end
|
||||
|
|
|
@ -87,8 +87,7 @@ describe('noteActions', () => {
|
|||
});
|
||||
|
||||
it('should render emoji link', () => {
|
||||
expect(wrapper.find('.js-add-award').exists()).toBe(true);
|
||||
expect(wrapper.find('.js-add-award').attributes('data-position')).toBe('right');
|
||||
expect(wrapper.find('[data-testid="note-emoji-button"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('actions dropdown', () => {
|
||||
|
|
61
spec/frontend/pipelines/components/pipeline_tabs_spec.js
Normal file
61
spec/frontend/pipelines/components/pipeline_tabs_spec.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
|
||||
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
|
||||
import Dag from '~/pipelines/components/dag/dag.vue';
|
||||
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
|
||||
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
|
||||
|
||||
describe('The Pipeline Tabs', () => {
|
||||
let wrapper;
|
||||
|
||||
const findDagTab = () => wrapper.findByTestId('dag-tab');
|
||||
const findFailedJobsTab = () => wrapper.findByTestId('failed-jobs-tab');
|
||||
const findJobsTab = () => wrapper.findByTestId('jobs-tab');
|
||||
const findPipelineTab = () => wrapper.findByTestId('pipeline-tab');
|
||||
const findTestsTab = () => wrapper.findByTestId('tests-tab');
|
||||
|
||||
const findDagApp = () => wrapper.findComponent(Dag);
|
||||
const findFailedJobsApp = () => wrapper.findComponent(JobsApp);
|
||||
const findJobsApp = () => wrapper.findComponent(JobsApp);
|
||||
const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
|
||||
const findTestsApp = () => wrapper.findComponent(TestReports);
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(PipelineTabs, {
|
||||
propsData,
|
||||
stubs: {
|
||||
Dag: { template: '<div id="dag"/>' },
|
||||
JobsApp: { template: '<div class="jobs" />' },
|
||||
PipelineGraph: { template: '<div id="graph" />' },
|
||||
TestReports: { template: '<div id="tests" />' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
// The failed jobs MUST be removed from here and tested individually once
|
||||
// the logic for the tab is implemented.
|
||||
describe('Tabs', () => {
|
||||
it.each`
|
||||
tabName | tabComponent | appComponent
|
||||
${'Pipeline'} | ${findPipelineTab} | ${findPipelineApp}
|
||||
${'Dag'} | ${findDagTab} | ${findDagApp}
|
||||
${'Jobs'} | ${findJobsTab} | ${findJobsApp}
|
||||
${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp}
|
||||
${'Tests'} | ${findTestsTab} | ${findTestsApp}
|
||||
`('shows $tabName tab and its associated component', ({ appComponent, tabComponent }) => {
|
||||
expect(tabComponent().exists()).toBe(true);
|
||||
expect(appComponent().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -26,7 +26,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
defaultEmoji,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}, improvedEmojiPicker = false) => {
|
||||
const createComponent = (props = {}) => {
|
||||
return shallowMount(SetStatusModalWrapper, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
|
@ -35,19 +35,15 @@ describe('SetStatusModalWrapper', () => {
|
|||
mocks: {
|
||||
$toast,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: { improvedEmojiPicker },
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
const findFormField = (field) => wrapper.find(`[name="user[status][${field}]"]`);
|
||||
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
|
||||
const findNoEmojiPlaceholder = () => wrapper.find('.js-no-emoji-placeholder');
|
||||
const findToggleEmojiButton = () => wrapper.find('.js-toggle-emoji-menu');
|
||||
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
|
||||
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
|
||||
const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
|
||||
|
||||
const initModal = async ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
|
||||
const modal = findModal();
|
||||
|
@ -95,12 +91,6 @@ describe('SetStatusModalWrapper', () => {
|
|||
expect(findClearStatusButton().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking the toggle emoji button displays the emoji list', () => {
|
||||
expect(wrapper.vm.showEmojiMenu).not.toHaveBeenCalled();
|
||||
findToggleEmojiButton().trigger('click');
|
||||
expect(wrapper.vm.showEmojiMenu).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays the clear status at dropdown', () => {
|
||||
expect(wrapper.find('[data-testid="clear-status-at-dropdown"]').exists()).toBe(true);
|
||||
});
|
||||
|
@ -108,16 +98,6 @@ describe('SetStatusModalWrapper', () => {
|
|||
it('does not display the clear status at message', () => {
|
||||
expect(findClearStatusAtMessage().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('improvedEmojiPicker is true', () => {
|
||||
const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
|
||||
|
||||
beforeEach(async () => {
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({}, true);
|
||||
return initModal();
|
||||
});
|
||||
|
||||
it('renders emoji picker dropdown with custom positioning', () => {
|
||||
expect(getEmojiPicker().props()).toMatchObject({
|
||||
|
@ -147,10 +127,6 @@ describe('SetStatusModalWrapper', () => {
|
|||
it('hides the clear status button', () => {
|
||||
expect(findClearStatusButton().isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the placeholder emoji', () => {
|
||||
expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with no currentEmoji set', () => {
|
||||
|
@ -163,22 +139,6 @@ describe('SetStatusModalWrapper', () => {
|
|||
it('does not set the hidden status emoji field', () => {
|
||||
expect(findFormField('emoji').element.value).toBe('');
|
||||
});
|
||||
|
||||
it('hides the placeholder emoji', () => {
|
||||
expect(findNoEmojiPlaceholder().isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
describe('with no currentMessage set', () => {
|
||||
beforeEach(async () => {
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
|
||||
return initModal();
|
||||
});
|
||||
|
||||
it('shows the placeholder emoji', () => {
|
||||
expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with currentClearStatusAfter set', () => {
|
||||
|
|
|
@ -218,65 +218,88 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
|
|||
<div
|
||||
class="award-menu-holder gl-my-2"
|
||||
>
|
||||
<button
|
||||
aria-label="Add reaction"
|
||||
class="btn add-reaction-button js-add-award btn-default btn-md gl-button js-test-add-button-class"
|
||||
<div
|
||||
class="emoji-picker"
|
||||
data-testid="emoji-picker"
|
||||
title="Add reaction"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
<div
|
||||
boundary="scrollParent"
|
||||
class="dropdown b-dropdown gl-new-dropdown btn-group"
|
||||
id="__BVID__13"
|
||||
lazy=""
|
||||
menu-class="dropdown-extended-height"
|
||||
no-flip=""
|
||||
>
|
||||
<span
|
||||
class="reaction-control-icon reaction-control-icon-neutral"
|
||||
<!---->
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary"
|
||||
id="__BVID__13__BV_toggle_"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16"
|
||||
data-testid="slight-smile-icon"
|
||||
role="img"
|
||||
<span
|
||||
class="gl-sr-only"
|
||||
>
|
||||
<use
|
||||
href="#slight-smile"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="reaction-control-icon reaction-control-icon-positive"
|
||||
Add reaction
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="reaction-control-icon reaction-control-icon-neutral"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16"
|
||||
data-testid="slight-smile-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#slight-smile"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="reaction-control-icon reaction-control-icon-positive"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16"
|
||||
data-testid="smiley-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#smiley"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="reaction-control-icon reaction-control-icon-super-positive"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16"
|
||||
data-testid="smile-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#smile"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<ul
|
||||
aria-labelledby="__BVID__13__BV_toggle_"
|
||||
class="dropdown-menu dropdown-extended-height dropdown-menu-right"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16"
|
||||
data-testid="smiley-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#smiley"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="reaction-control-icon reaction-control-icon-super-positive"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s16"
|
||||
data-testid="smile-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#smile"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<!---->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -76,7 +76,7 @@ describe('vue_shared/components/awards_list', () => {
|
|||
count: Number(x.find('.js-counter').text()),
|
||||
};
|
||||
});
|
||||
const findAddAwardButton = () => wrapper.find('.js-add-award');
|
||||
const findAddAwardButton = () => wrapper.find('[data-testid="emoji-picker"]');
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -151,7 +151,6 @@ describe('vue_shared/components/awards_list', () => {
|
|||
const btn = findAddAwardButton();
|
||||
|
||||
expect(btn.exists()).toBe(true);
|
||||
expect(btn.classes(TEST_ADD_BUTTON_CLASS)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -53,5 +53,15 @@ RSpec.describe Resolvers::WorkItems::TypesResolver do
|
|||
|
||||
it_behaves_like 'a work item type resolver'
|
||||
end
|
||||
|
||||
context 'when parent is not a group or project' do
|
||||
let(:object) { 'not a project/group' }
|
||||
|
||||
it 'returns nil because of feature flag check' do
|
||||
result = resolve(described_class, obj: object, args: {})
|
||||
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
23
spec/helpers/projects/pipeline_helper_spec.rb
Normal file
23
spec/helpers/projects/pipeline_helper_spec.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Projects::PipelineHelper do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:raw_pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
|
||||
let_it_be(:pipeline) { Ci::PipelinePresenter.new(raw_pipeline, current_user: user)}
|
||||
|
||||
describe '#js_pipeline_tabs_data' do
|
||||
subject(:pipeline_tabs_data) { helper.js_pipeline_tabs_data(project, pipeline) }
|
||||
|
||||
it 'returns pipeline tabs data' do
|
||||
expect(pipeline_tabs_data).to include({
|
||||
can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
|
||||
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
|
||||
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
|
||||
pipeline_project_path: project.full_path
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -64,6 +64,34 @@ RSpec.describe Gitlab::GonHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#push_force_frontend_feature_flag' do
|
||||
let(:gon) { class_double('Gon') }
|
||||
|
||||
before do
|
||||
skip_feature_flags_yaml_validation
|
||||
|
||||
allow(helper)
|
||||
.to receive(:gon)
|
||||
.and_return(gon)
|
||||
end
|
||||
|
||||
it 'pushes a feature flag to the frontend with the provided value' do
|
||||
expect(gon)
|
||||
.to receive(:push)
|
||||
.with({ features: { 'myFeatureFlag' => true } }, true)
|
||||
|
||||
helper.push_force_frontend_feature_flag(:my_feature_flag, true)
|
||||
end
|
||||
|
||||
it 'pushes a disabled feature flag if provided value is nil' do
|
||||
expect(gon)
|
||||
.to receive(:push)
|
||||
.with({ features: { 'myFeatureFlag' => false } }, true)
|
||||
|
||||
helper.push_force_frontend_feature_flag(:my_feature_flag, nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#default_avatar_url' do
|
||||
it 'returns an absolute URL' do
|
||||
url = helper.default_avatar_url
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImport, type: :model do
|
||||
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
|
||||
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
|
||||
let_it_be(:finished_bulk_import) { create(:bulk_import, :finished) }
|
||||
let_it_be(:failed_bulk_import) { create(:bulk_import, :failed) }
|
||||
let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
|
||||
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:user).required }
|
||||
it { is_expected.to have_one(:configuration) }
|
||||
|
@ -16,9 +23,15 @@ RSpec.describe BulkImport, type: :model do
|
|||
it { is_expected.to define_enum_for(:source_type).with_values(%i[gitlab]) }
|
||||
end
|
||||
|
||||
describe '.stale scope' do
|
||||
subject { described_class.stale }
|
||||
|
||||
it { is_expected.to contain_exactly(stale_created_bulk_import, stale_started_bulk_import) }
|
||||
end
|
||||
|
||||
describe '.all_human_statuses' do
|
||||
it 'returns all human readable entity statuses' do
|
||||
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed')
|
||||
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed', 'timeout')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -151,7 +151,7 @@ RSpec.describe BulkImports::Entity, type: :model do
|
|||
|
||||
describe '.all_human_statuses' do
|
||||
it 'returns all human readable entity statuses' do
|
||||
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed')
|
||||
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed', 'timeout')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -302,6 +302,19 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
expect(repository.migration_aborted_in_state).to eq('importing')
|
||||
expect(repository).to be_import_aborted
|
||||
end
|
||||
|
||||
context 'above the max retry limit' do
|
||||
before do
|
||||
stub_application_setting(container_registry_import_max_retries: 1)
|
||||
end
|
||||
|
||||
it 'skips the migration' do
|
||||
expect { subject }.to change { repository.migration_skipped_at }
|
||||
|
||||
expect(repository.reload).to be_import_skipped
|
||||
expect(repository.migration_skipped_reason).to eq('too_many_retries')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#skip_import' do
|
||||
|
@ -309,7 +322,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
|
||||
subject { repository.skip_import(reason: :too_many_retries) }
|
||||
|
||||
it_behaves_like 'transitioning from allowed states', ContainerRepository::ABORTABLE_MIGRATION_STATES
|
||||
it_behaves_like 'transitioning from allowed states', ContainerRepository::SKIPPABLE_MIGRATION_STATES
|
||||
|
||||
it 'sets migration_skipped_at and migration_skipped_reason' do
|
||||
expect { subject }.to change { repository.reload.migration_skipped_at }
|
||||
|
@ -1119,6 +1132,17 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
context 'not found response' do
|
||||
let(:response) { :not_found }
|
||||
|
||||
it 'aborts the migration' do
|
||||
expect(subject).to eq(false)
|
||||
|
||||
expect(container_repository).to be_import_skipped
|
||||
expect(container_repository.reload.migration_skipped_reason).to eq('not_found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'other response' do
|
||||
let(:response) { :error }
|
||||
|
||||
|
@ -1136,6 +1160,30 @@ RSpec.describe ContainerRepository, :aggregate_failures do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#retried_too_many_times?' do
|
||||
subject { repository.retried_too_many_times? }
|
||||
|
||||
before do
|
||||
stub_application_setting(container_registry_import_max_retries: 3)
|
||||
end
|
||||
|
||||
context 'migration_retries_count is equal or greater than max_retries' do
|
||||
before do
|
||||
repository.update_column(:migration_retries_count, 3)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
end
|
||||
|
||||
context 'migration_retries_count is lower than max_retries' do
|
||||
before do
|
||||
repository.update_column(:migration_retries_count, 2)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with repositories' do
|
||||
let_it_be_with_reload(:repository) { create(:container_repository, :cleanup_unscheduled) }
|
||||
let_it_be(:other_repository) { create(:container_repository, :cleanup_unscheduled) }
|
||||
|
|
|
@ -3248,4 +3248,46 @@ RSpec.describe Group do
|
|||
it_behaves_like 'no effective expiration interval'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#work_items_feature_flag_enabled?' do
|
||||
let_it_be(:root_group) { create(:group) }
|
||||
let_it_be(:group) { create(:group, parent: root_group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
subject { group.work_items_feature_flag_enabled? }
|
||||
|
||||
context 'when work_items FF is enabled for the root group' do
|
||||
before do
|
||||
stub_feature_flags(work_items: root_group)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when work_items FF is enabled for the group' do
|
||||
before do
|
||||
stub_feature_flags(work_items: group)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'when root_group is the actor' do
|
||||
it 'is not enabled if the FF is enabled for a child' do
|
||||
expect(root_group).not_to be_work_items_feature_flag_enabled
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when work_items FF is disabled globally' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when work_items FF is enabled globally' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8011,6 +8011,62 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#work_items_feature_flag_enabled?' do
|
||||
shared_examples 'project checking work_items feature flag' do
|
||||
context 'when work_items FF is disabled globally' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'when work_items FF is enabled for the project' do
|
||||
before do
|
||||
stub_feature_flags(work_items: project)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when work_items FF is enabled globally' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
subject { project.work_items_feature_flag_enabled? }
|
||||
|
||||
context 'when a project does not belong to a group' do
|
||||
let_it_be(:project) { create(:project, namespace: namespace) }
|
||||
|
||||
it_behaves_like 'project checking work_items feature flag'
|
||||
end
|
||||
|
||||
context 'when project belongs to a group' do
|
||||
let_it_be(:root_group) { create(:group) }
|
||||
let_it_be(:group) { create(:group, parent: root_group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
it_behaves_like 'project checking work_items feature flag'
|
||||
|
||||
context 'when work_items FF is enabled for the root group' do
|
||||
before do
|
||||
stub_feature_flags(work_items: root_group)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when work_items FF is enabled for the group' do
|
||||
before do
|
||||
stub_feature_flags(work_items: group)
|
||||
end
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'serialization' do
|
||||
let(:object) { build(:project) }
|
||||
|
||||
|
|
38
spec/requests/projects/work_items_spec.rb
Normal file
38
spec/requests/projects/work_items_spec.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Work Items' do
|
||||
let_it_be(:work_item) { create(:work_item) }
|
||||
let_it_be(:developer) { create(:user) }
|
||||
|
||||
before_all do
|
||||
work_item.project.add_developer(developer)
|
||||
end
|
||||
|
||||
describe 'GET /:namespace/:project/work_items/:id' do
|
||||
before do
|
||||
sign_in(developer)
|
||||
end
|
||||
|
||||
context 'when the work_items feature flag is enabled' do
|
||||
it 'renders index' do
|
||||
get project_work_items_url(work_item.project, work_items_path: work_item.id)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the work_items feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it 'returns 404' do
|
||||
get project_work_items_url(work_item.project, work_items_path: work_item.id)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,6 +13,7 @@ RSpec.describe 'projects/pipelines/show' do
|
|||
before do
|
||||
assign(:project, project)
|
||||
assign(:pipeline, presented_pipeline)
|
||||
stub_feature_flags(pipeline_tabs_vue: false)
|
||||
end
|
||||
|
||||
context 'when pipeline has errors' do
|
||||
|
|
31
spec/workers/bulk_imports/stuck_import_worker_spec.rb
Normal file
31
spec/workers/bulk_imports/stuck_import_worker_spec.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe BulkImports::StuckImportWorker do
|
||||
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
|
||||
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
|
||||
let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
|
||||
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
|
||||
let_it_be(:stale_created_bulk_import_entity) { create(:bulk_import_entity, :created, created_at: 3.days.ago) }
|
||||
let_it_be(:stale_started_bulk_import_entity) { create(:bulk_import_entity, :started, created_at: 3.days.ago) }
|
||||
|
||||
subject { described_class.new.perform }
|
||||
|
||||
describe 'perform' do
|
||||
it 'updates the status of bulk imports to timeout' do
|
||||
expect { subject }.to change { stale_created_bulk_import.reload.status }.from(0).to(3)
|
||||
.and change { stale_started_bulk_import.reload.status }.from(1).to(3)
|
||||
end
|
||||
|
||||
it 'updates the status of bulk import entities to timeout' do
|
||||
expect { subject }.to change { stale_created_bulk_import_entity.reload.status }.from(0).to(3)
|
||||
.and change { stale_started_bulk_import_entity.reload.status }.from(1).to(3)
|
||||
end
|
||||
|
||||
it 'does not update the status of non-stale records' do
|
||||
expect { subject }.to not_change { created_bulk_import.reload.status }
|
||||
.and not_change { started_bulk_import.reload.status }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue