Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
34b3acb5a3
commit
dce5758796
|
|
@ -1 +1 @@
|
|||
9096b269b6abfa8cbe8cf6c48a03e3bec93d47ae
|
||||
69c5d9bad86e9402d5e2b0d7c09e88fd5603b572
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
8.47.0
|
||||
8.48.0
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ export class AwardsHandler {
|
|||
}
|
||||
|
||||
findMatchingEmojiElements(query) {
|
||||
const emojiMatches = this.emoji.queryEmojiNames(query);
|
||||
const emojiMatches = this.emoji.searchEmoji(query).map(({ name }) => name);
|
||||
const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]');
|
||||
const $matchingElements = $emojiElements.filter(
|
||||
(i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,143 @@
|
|||
<script>
|
||||
import { GlTable, GlButton, GlBadge, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlTable,
|
||||
GlButton,
|
||||
GlBadge,
|
||||
ClipboardButton,
|
||||
TooltipOnTruncate,
|
||||
UserAvatarLink,
|
||||
TimeAgoTooltip,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
triggers: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
key: 'token',
|
||||
label: s__('Pipelines|Token'),
|
||||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: s__('Pipelines|Description'),
|
||||
},
|
||||
{
|
||||
key: 'owner',
|
||||
label: s__('Pipelines|Owner'),
|
||||
},
|
||||
{
|
||||
key: 'lastUsed',
|
||||
label: s__('Pipelines|Last Used'),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '',
|
||||
tdClass: 'gl-text-right gl-white-space-nowrap',
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-table
|
||||
v-if="triggers.length"
|
||||
:fields="$options.fields"
|
||||
:items="triggers"
|
||||
class="triggers-list"
|
||||
responsive
|
||||
>
|
||||
<template #cell(token)="{item}">
|
||||
{{ item.token }}
|
||||
<clipboard-button
|
||||
v-if="item.hasTokenExposed"
|
||||
:text="item.token"
|
||||
data-testid="clipboard-btn"
|
||||
data-qa-selector="clipboard_button"
|
||||
:title="s__('Pipelines|Copy trigger token')"
|
||||
css-class="gl-border-none gl-py-0 gl-px-2"
|
||||
/>
|
||||
<div class="label-container">
|
||||
<gl-badge v-if="!item.canAccessProject" variant="danger">
|
||||
<span
|
||||
v-gl-tooltip.viewport
|
||||
boundary="viewport"
|
||||
:title="s__('Pipelines|Trigger user has insufficient permissions to project')"
|
||||
>{{ s__('Pipelines|invalid') }}</span
|
||||
>
|
||||
</gl-badge>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(description)="{item}">
|
||||
<tooltip-on-truncate
|
||||
:title="item.description"
|
||||
truncate-target="child"
|
||||
placement="top"
|
||||
class="trigger-description gl-display-flex"
|
||||
>
|
||||
<div class="gl-flex-fill-1 gl-text-truncate">{{ item.description }}</div>
|
||||
</tooltip-on-truncate>
|
||||
</template>
|
||||
<template #cell(owner)="{item}">
|
||||
<span class="trigger-owner sr-only">{{ item.owner.name }}</span>
|
||||
<user-avatar-link
|
||||
v-if="item.owner"
|
||||
:link-href="item.owner.path"
|
||||
:img-src="item.owner.avatarUrl"
|
||||
:tooltip-text="item.owner.name"
|
||||
:img-alt="item.owner.name"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(lastUsed)="{item}">
|
||||
<time-ago-tooltip v-if="item.lastUsed" :time="item.lastUsed" />
|
||||
<span v-else>{{ __('Never') }}</span>
|
||||
</template>
|
||||
<template #cell(actions)="{item}">
|
||||
<gl-button
|
||||
:title="s__('Pipelines|Edit')"
|
||||
icon="pencil"
|
||||
data-testid="edit-btn"
|
||||
:href="item.editProjectTriggerPath"
|
||||
/>
|
||||
<gl-button
|
||||
:title="s__('Pipelines|Revoke')"
|
||||
icon="remove"
|
||||
variant="warning"
|
||||
:data-confirm="
|
||||
s__(
|
||||
'Pipelines|By revoking a trigger you will break any processes making use of it. Are you sure?',
|
||||
)
|
||||
"
|
||||
data-method="delete"
|
||||
rel="nofollow"
|
||||
class="gl-ml-3"
|
||||
data-testid="trigger_revoke_button"
|
||||
data-qa-selector="trigger_revoke_button"
|
||||
:href="item.projectTriggerPath"
|
||||
/>
|
||||
</template>
|
||||
</gl-table>
|
||||
<div
|
||||
v-else
|
||||
data-testid="no_triggers_content"
|
||||
data-qa-selector="no_triggers_content"
|
||||
class="settings-message gl-text-center gl-mb-3"
|
||||
>
|
||||
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import Vue from 'vue';
|
||||
import TriggersList from './components/triggers_list.vue';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
|
||||
const parseJsonArray = triggers => {
|
||||
try {
|
||||
return convertObjectPropsToCamelCase(JSON.parse(triggers), { deep: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export default (containerId = 'js-ci-pipeline-triggers-list') => {
|
||||
const containerEl = document.getElementById(containerId);
|
||||
|
||||
// Note: Remove this check when FF `ci_pipeline_triggers_settings_vue_ui` is removed.
|
||||
if (!containerEl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const triggers = parseJsonArray(containerEl.dataset.triggers);
|
||||
|
||||
return new Vue({
|
||||
el: containerEl,
|
||||
components: {
|
||||
TriggersList,
|
||||
},
|
||||
render(h) {
|
||||
return h(TriggersList, {
|
||||
props: {
|
||||
triggers,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -63,7 +63,7 @@ export default {
|
|||
title: s__('DesignManagement|Are you sure you want to archive the selected designs?'),
|
||||
actionPrimary: {
|
||||
text: s__('DesignManagement|Archive designs'),
|
||||
attributes: { variant: 'warning' },
|
||||
attributes: { variant: 'warning', 'data-qa-selector': 'confirm_archiving_button' },
|
||||
},
|
||||
actionCancel: {
|
||||
text: __('Cancel'),
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export default {
|
|||
:alt="filename"
|
||||
class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
|
||||
data-qa-selector="design_image"
|
||||
:data-qa-filename="filename"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ export default {
|
|||
button-category="secondary"
|
||||
button-class="gl-mr-3"
|
||||
button-size="small"
|
||||
data-qa-selector="archive_button"
|
||||
:loading="loading"
|
||||
:has-selected-designs="hasSelectedDesigns"
|
||||
@deleteSelectedDesigns="mutate()"
|
||||
|
|
@ -417,6 +418,8 @@ export default {
|
|||
:checked="isDesignSelected(design.filename)"
|
||||
type="checkbox"
|
||||
class="design-checkbox"
|
||||
data-qa-selector="design_checkbox"
|
||||
:data-qa-design="design.filename"
|
||||
@change="changeSelectedDesigns(design.filename)"
|
||||
/>
|
||||
</li>
|
||||
|
|
@ -426,6 +429,7 @@ export default {
|
|||
:is-dragging-design="isDraggingDesign"
|
||||
:class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
|
||||
:has-designs="hasDesigns"
|
||||
data-qa-selector="design_dropzone_content"
|
||||
@change="onUploadDesign"
|
||||
/>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -2,53 +2,57 @@ import { uniq } from 'lodash';
|
|||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
|
||||
import AccessorUtilities from '../lib/utils/accessor';
|
||||
|
||||
let emojiMap = null;
|
||||
let emojiPromise = null;
|
||||
let validEmojiNames = null;
|
||||
|
||||
export const EMOJI_VERSION = '1';
|
||||
|
||||
const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
|
||||
|
||||
export function initEmojiMap() {
|
||||
emojiPromise =
|
||||
emojiPromise ||
|
||||
new Promise((resolve, reject) => {
|
||||
if (emojiMap) {
|
||||
resolve(emojiMap);
|
||||
} else if (
|
||||
isLocalStorageAvailable &&
|
||||
window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION &&
|
||||
window.localStorage.getItem('gl-emoji-map')
|
||||
) {
|
||||
emojiMap = JSON.parse(window.localStorage.getItem('gl-emoji-map'));
|
||||
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
|
||||
resolve(emojiMap);
|
||||
} else {
|
||||
// We load the JSON file direct from the server
|
||||
// because it can't be loaded from a CDN due to
|
||||
// cross domain problems with JSON
|
||||
axios
|
||||
.get(`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`)
|
||||
.then(({ data }) => {
|
||||
emojiMap = data;
|
||||
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
|
||||
resolve(emojiMap);
|
||||
if (isLocalStorageAvailable) {
|
||||
window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION);
|
||||
window.localStorage.setItem('gl-emoji-map', JSON.stringify(emojiMap));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
async function loadEmoji() {
|
||||
if (
|
||||
isLocalStorageAvailable &&
|
||||
window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION &&
|
||||
window.localStorage.getItem('gl-emoji-map')
|
||||
) {
|
||||
return JSON.parse(window.localStorage.getItem('gl-emoji-map'));
|
||||
}
|
||||
|
||||
return emojiPromise;
|
||||
// We load the JSON file direct from the server
|
||||
// because it can't be loaded from a CDN due to
|
||||
// cross domain problems with JSON
|
||||
const { data } = await axios.get(
|
||||
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
|
||||
);
|
||||
window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION);
|
||||
window.localStorage.setItem('gl-emoji-map', JSON.stringify(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
async function prepareEmojiMap() {
|
||||
emojiMap = await loadEmoji();
|
||||
|
||||
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
|
||||
|
||||
Object.keys(emojiMap).forEach(name => {
|
||||
emojiMap[name].aliases = [];
|
||||
emojiMap[name].name = name;
|
||||
});
|
||||
Object.entries(emojiAliases).forEach(([alias, name]) => {
|
||||
// This check, `if (name in emojiMap)` is necessary during testing. In
|
||||
// production, it shouldn't be necessary, because at no point should there
|
||||
// be an entry in aliases.json with no corresponding entry in emojis.json.
|
||||
// However, during testing, the endpoint for emojis.json is mocked with a
|
||||
// small dataset, whereas aliases.json is always `import`ed directly.
|
||||
if (name in emojiMap) emojiMap[name].aliases.push(alias);
|
||||
});
|
||||
}
|
||||
|
||||
export function initEmojiMap() {
|
||||
initEmojiMap.promise = initEmojiMap.promise || prepareEmojiMap();
|
||||
return initEmojiMap.promise;
|
||||
}
|
||||
|
||||
export function normalizeEmojiName(name) {
|
||||
|
|
@ -77,6 +81,37 @@ export function queryEmojiNames(filter) {
|
|||
return uniq(matches.map(name => normalizeEmojiName(name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches emoji by name, alias, description, and unicode value and returns an
|
||||
* array of matches.
|
||||
*
|
||||
* Note: `initEmojiMap` must have been called and completed before this method
|
||||
* can safely be called.
|
||||
*
|
||||
* @param {String} query The search query
|
||||
* @returns {Object[]} A list of emoji that match the query
|
||||
*/
|
||||
export function searchEmoji(query) {
|
||||
if (!emojiMap)
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
throw new Error('The emoji map is uninitialized or initialization has not completed');
|
||||
|
||||
const matches = s => fuzzaldrinPlus.score(s, query) > 0;
|
||||
|
||||
// Search emoji
|
||||
return Object.values(emojiMap).filter(
|
||||
emoji =>
|
||||
// by name
|
||||
matches(emoji.name) ||
|
||||
// by alias
|
||||
emoji.aliases.some(matches) ||
|
||||
// by description
|
||||
matches(emoji.d) ||
|
||||
// by unicode value
|
||||
query === emoji.e,
|
||||
);
|
||||
}
|
||||
|
||||
let emojiCategoryMap;
|
||||
export function getEmojiCategoryMap() {
|
||||
if (!emojiCategoryMap) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlPopover } from '@gitlab/ui';
|
||||
import { __, sprintf } from '../../../locale';
|
||||
import popover from '../../../vue_shared/directives/popover';
|
||||
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
|
||||
|
|
@ -10,6 +10,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
GlPopover,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
|
|
@ -58,7 +59,7 @@ export default {
|
|||
},
|
||||
},
|
||||
popoverOptions: {
|
||||
trigger: 'hover',
|
||||
triggers: 'hover',
|
||||
placement: 'top',
|
||||
content: sprintf(
|
||||
__(`
|
||||
|
|
@ -83,9 +84,16 @@ export default {
|
|||
<ul class="nav-links">
|
||||
<li>
|
||||
{{ __('Commit Message') }}
|
||||
<span v-popover="$options.popoverOptions" class="form-text text-muted gl-ml-3">
|
||||
<gl-icon name="question" />
|
||||
</span>
|
||||
<div id="ide-commit-message-popover-container">
|
||||
<span id="ide-commit-message-question" class="form-text text-muted gl-ml-3">
|
||||
<gl-icon name="question" />
|
||||
</span>
|
||||
<gl-popover
|
||||
target="ide-commit-message-question"
|
||||
container="ide-commit-message-popover-container"
|
||||
v-bind="$options.popoverOptions"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ function initDeferred() {
|
|||
|
||||
const whatsNewTriggerEl = document.querySelector('.js-whats-new-trigger');
|
||||
if (whatsNewTriggerEl) {
|
||||
const storageKey = whatsNewTriggerEl.getAttribute('data-storage-key');
|
||||
|
||||
$('.header-help').on('show.bs.dropdown', () => {
|
||||
const displayNotification = JSON.parse(localStorage.getItem(storageKey));
|
||||
if (displayNotification === false) {
|
||||
$('.js-whats-new-notification-count').remove();
|
||||
}
|
||||
});
|
||||
|
||||
whatsNewTriggerEl.addEventListener('click', () => {
|
||||
import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
|
||||
.then(({ default: initWhatsNew }) => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
|
|||
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
|
||||
import initVariableList from '~/ci_variable_list';
|
||||
import initDeployFreeze from '~/deploy_freeze';
|
||||
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize expandable settings panels
|
||||
|
|
@ -42,4 +43,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
registrySettingsApp();
|
||||
initDeployFreeze();
|
||||
|
||||
initSettingsPipelinesTriggers();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -295,9 +295,13 @@ export default {
|
|||
<div
|
||||
class="gl-border-t-solid gl-border-gray-100 gl-border-t-1 gl-p-5 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
<gl-button type="submit" category="primary" variant="success">{{
|
||||
s__('Pipeline|Run Pipeline')
|
||||
}}</gl-button>
|
||||
<gl-button
|
||||
type="submit"
|
||||
category="primary"
|
||||
variant="success"
|
||||
data-qa-selector="run_pipeline_button"
|
||||
>{{ s__('Pipeline|Run Pipeline') }}</gl-button
|
||||
>
|
||||
<gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
|
||||
</div>
|
||||
</gl-form>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ export default {
|
|||
category="primary"
|
||||
class="js-run-pipeline"
|
||||
data-testid="run-pipeline-button"
|
||||
data-qa-selector="run_pipeline_button"
|
||||
>
|
||||
{{ s__('Pipelines|Run Pipeline') }}
|
||||
</gl-button>
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export default {
|
|||
:labels-select-in-progress="labelsSelectInProgress"
|
||||
:selected-labels="selectedLabels"
|
||||
:variant="$options.sidebar"
|
||||
data-qa-selector="labels_block"
|
||||
@onDropdownClose="handleDropdownClose"
|
||||
@updateSelectedLabels="handleUpdateSelectedLabels"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
|
||||
import eventHub from '../../event_hub';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
|
||||
|
|
@ -8,6 +8,7 @@ export default {
|
|||
components: {
|
||||
statusIcon,
|
||||
GlLoadingIcon,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
mr: {
|
||||
|
|
@ -33,20 +34,21 @@ export default {
|
|||
<template>
|
||||
<div class="mr-widget-body media">
|
||||
<status-icon status="warning" />
|
||||
<div class="media-body space-children">
|
||||
<div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center">
|
||||
<span class="bold">
|
||||
<template v-if="mr.mergeError">{{ mr.mergeError }}</template>
|
||||
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
|
||||
</span>
|
||||
<button
|
||||
<gl-button
|
||||
:disabled="isRefreshing"
|
||||
type="button"
|
||||
class="btn btn-sm btn-default"
|
||||
category="secondary"
|
||||
variant="default"
|
||||
size="small"
|
||||
@click="refreshWidget"
|
||||
>
|
||||
<gl-loading-icon v-if="isRefreshing" :inline="true" />
|
||||
{{ s__('mrWidget|Refresh') }}
|
||||
</button>
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
class="labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute"
|
||||
data-qa-selector="labels_dropdown_content"
|
||||
:style="directionStyle"
|
||||
>
|
||||
<component :is="dropdownContentsView" />
|
||||
|
|
|
|||
|
|
@ -156,7 +156,11 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
<div class="dropdown-input" @click.stop="() => {}">
|
||||
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
|
||||
<gl-search-box-by-type
|
||||
v-model="searchKey"
|
||||
:autofocus="true"
|
||||
data-qa-selector="dropdown_input_field"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="showListContainer"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export default {
|
|||
<template v-for="label in selectedLabels" v-else>
|
||||
<gl-label
|
||||
:key="label.id"
|
||||
data-qa-selector="selected_label_content"
|
||||
:data-qa-label-name="label.title"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:background-color="label.color"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
storageKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['open']),
|
||||
|
|
@ -31,7 +36,7 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.openDrawer();
|
||||
this.openDrawer(this.storageKey);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['openDrawer', 'closeDrawer']),
|
||||
|
|
@ -41,7 +46,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<gl-drawer class="mt-6" :open="open" @close="closeDrawer">
|
||||
<gl-drawer class="whats-new-drawer" :open="open" @close="closeDrawer">
|
||||
<template #header>
|
||||
<h4 class="page-title my-2">{{ __("What's new at GitLab") }}</h4>
|
||||
</template>
|
||||
|
|
@ -69,5 +74,6 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</gl-drawer>
|
||||
<div v-if="open" class="whats-new-modal-backdrop modal-backdrop"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export default () => {
|
|||
return createElement('app', {
|
||||
props: {
|
||||
features: whatsNewElm.getAttribute('data-features'),
|
||||
storageKey: whatsNewElm.getAttribute('data-storage-key'),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ export default {
|
|||
closeDrawer({ commit }) {
|
||||
commit(types.CLOSE_DRAWER);
|
||||
},
|
||||
openDrawer({ commit }) {
|
||||
openDrawer({ commit }, storageKey) {
|
||||
commit(types.OPEN_DRAWER);
|
||||
|
||||
if (storageKey) {
|
||||
localStorage.setItem(storageKey, JSON.stringify(false));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,32 @@
|
|||
.whats-new-drawer {
|
||||
margin-top: $header-height;
|
||||
@include gl-shadow-none;
|
||||
}
|
||||
|
||||
.with-performance-bar .whats-new-drawer {
|
||||
margin-top: calc(#{$performance-bar-height} + #{$header-height});
|
||||
}
|
||||
|
||||
.gl-badge.whats-new-item-badge {
|
||||
background-color: $purple-light;
|
||||
color: $purple;
|
||||
font-weight: bold;
|
||||
@include gl-font-weight-bold;
|
||||
}
|
||||
|
||||
.whats-new-item-image {
|
||||
border-color: $gray-50;
|
||||
}
|
||||
|
||||
.whats-new-modal-backdrop {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.whats-new-notification-count {
|
||||
@include gl-bg-gray-900;
|
||||
@include gl-font-sm;
|
||||
@include gl-line-height-normal;
|
||||
@include gl-text-white;
|
||||
@include gl-vertical-align-top;
|
||||
border-radius: 20px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.trigger-description {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.trigger-actions {
|
||||
white-space: nowrap;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ module Projects
|
|||
end
|
||||
|
||||
def show
|
||||
if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
|
||||
@triggers_json = ::Ci::TriggerSerializer.new.represent(
|
||||
@project.triggers, current_user: current_user, project: @project
|
||||
).to_json
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
|
|
@ -116,6 +121,7 @@ module Projects
|
|||
def define_triggers_variables
|
||||
@triggers = @project.triggers
|
||||
.present(current_user: current_user)
|
||||
|
||||
@trigger = ::Ci::Trigger.new
|
||||
.present(current_user: current_user)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ module IssueResolverArguments
|
|||
argument :assignee_username, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'Username of a user assigned to the issue'
|
||||
argument :assignee_usernames, [GraphQL::STRING_TYPE],
|
||||
required: false,
|
||||
description: 'Usernames of users assigned to the issue'
|
||||
argument :assignee_id, GraphQL::STRING_TYPE,
|
||||
required: false,
|
||||
description: 'ID of a user assigned to the issues, "none" and "any" values supported'
|
||||
|
|
|
|||
|
|
@ -3,6 +3,24 @@
|
|||
module WhatsNewHelper
|
||||
EMPTY_JSON = ''.to_json
|
||||
|
||||
def whats_new_most_recent_release_items_count
|
||||
items = parsed_most_recent_release_items
|
||||
|
||||
return unless items.is_a?(Array)
|
||||
|
||||
items.count
|
||||
end
|
||||
|
||||
def whats_new_storage_key
|
||||
items = parsed_most_recent_release_items
|
||||
|
||||
return unless items.is_a?(Array)
|
||||
|
||||
release = items.first.try(:[], 'release')
|
||||
|
||||
['display-whats-new-notification', release].compact.join('-')
|
||||
end
|
||||
|
||||
def whats_new_most_recent_release_items
|
||||
YAML.load_file(most_recent_release_file_path).to_json
|
||||
|
||||
|
|
@ -14,6 +32,10 @@ module WhatsNewHelper
|
|||
|
||||
private
|
||||
|
||||
def parsed_most_recent_release_items
|
||||
Gitlab::Json.parse(whats_new_most_recent_release_items)
|
||||
end
|
||||
|
||||
def most_recent_release_file_path
|
||||
Dir.glob(files_path).max
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class TriggerEntity < Grape::Entity
|
||||
include Gitlab::Routing
|
||||
include Gitlab::Allowable
|
||||
|
||||
expose :description
|
||||
expose :owner, using: UserEntity
|
||||
expose :last_used
|
||||
|
||||
expose :token do |trigger|
|
||||
can_admin_trigger?(trigger) ? trigger.token : trigger.short_token
|
||||
end
|
||||
|
||||
expose :has_token_exposed do |trigger|
|
||||
can_admin_trigger?(trigger)
|
||||
end
|
||||
|
||||
expose :can_access_project do |trigger|
|
||||
trigger.can_access_project?
|
||||
end
|
||||
|
||||
expose :project_trigger_path, if: -> (trigger) { can_manage_trigger?(trigger) } do |trigger|
|
||||
project_trigger_path(options[:project], trigger)
|
||||
end
|
||||
|
||||
expose :edit_project_trigger_path, if: -> (trigger) { can_admin_trigger?(trigger) } do |trigger|
|
||||
edit_project_trigger_path(options[:project], trigger)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_manage_trigger?(trigger)
|
||||
can?(options[:current_user], :manage_trigger, trigger)
|
||||
end
|
||||
|
||||
def can_admin_trigger?(trigger)
|
||||
can?(options[:current_user], :admin_trigger, trigger)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class TriggerSerializer < BaseSerializer
|
||||
entity ::Ci::TriggerEntity
|
||||
end
|
||||
end
|
||||
|
|
@ -17,12 +17,16 @@ module QuickActions
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def issue(type_id)
|
||||
return project.issues.build if type_id.nil?
|
||||
|
||||
IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def merge_request(type_id)
|
||||
return project.merge_requests.build if type_id.nil?
|
||||
|
||||
MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@
|
|||
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
|
||||
|
||||
- if ::Feature.enabled?(:whats_new_drawer)
|
||||
#whats-new-app{ data: { features: whats_new_most_recent_release_items } }
|
||||
#whats-new-app{ data: { features: whats_new_most_recent_release_items, storage_key: whats_new_storage_key } }
|
||||
|
||||
- if can?(current_user, :update_user_status, current_user)
|
||||
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
- if domain_presenter.errors.any?
|
||||
.alert.alert-danger
|
||||
.gl-alert.gl-alert-danger
|
||||
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
|
||||
- domain_presenter.errors.full_messages.each do |msg|
|
||||
= msg
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@
|
|||
%td.responsive-table-cell.build-failure{ data: { column: _('Failure')} }
|
||||
= build.present.callout_failure_message
|
||||
%td.responsive-table-cell.build-actions
|
||||
- if can?(current_user, :update_build, job)
|
||||
- if can?(current_user, :update_build, job) && job.retryable?
|
||||
= link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build gl-button btn-icon btn-default' do
|
||||
= sprite_icon('repeat', css_class: 'gl-icon')
|
||||
- if can?(current_user, :read_build, job)
|
||||
|
|
|
|||
|
|
@ -6,23 +6,26 @@
|
|||
.card-body
|
||||
= render "projects/triggers/form", btn_text: "Add trigger"
|
||||
%hr
|
||||
- if @triggers.any?
|
||||
.table-responsive.triggers-list
|
||||
%table.table
|
||||
%thead
|
||||
%th
|
||||
%strong Token
|
||||
%th
|
||||
%strong Description
|
||||
%th
|
||||
%strong Owner
|
||||
%th
|
||||
%strong Last used
|
||||
%th
|
||||
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
|
||||
- if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
|
||||
#js-ci-pipeline-triggers-list.triggers-list{ data: { triggers: @triggers_json } }
|
||||
- else
|
||||
%p.settings-message.text-center.gl-mb-3
|
||||
No triggers have been created yet. Add one using the form above.
|
||||
- if @triggers.any?
|
||||
.table-responsive.triggers-list
|
||||
%table.table
|
||||
%thead
|
||||
%th
|
||||
%strong Token
|
||||
%th
|
||||
%strong Description
|
||||
%th
|
||||
%strong Owner
|
||||
%th
|
||||
%strong Last used
|
||||
%th
|
||||
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
|
||||
- else
|
||||
%p.settings-message.text-center.gl-mb-3{ data: { testid: 'no_triggers_content' } }
|
||||
No triggers have been created yet. Add one using the form above.
|
||||
|
||||
.card-footer
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
%td
|
||||
- if trigger.has_token_exposed?
|
||||
%span= trigger.token
|
||||
= clipboard_button(text: trigger.token, title: _("Copy trigger token"))
|
||||
= clipboard_button(text: trigger.token, title: _("Copy trigger token"), testid: 'clipboard-btn')
|
||||
- else
|
||||
%span= trigger.short_token
|
||||
|
||||
|
|
@ -33,5 +33,5 @@
|
|||
= link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do
|
||||
= sprite_icon('pencil')
|
||||
- if can?(current_user, :manage_trigger, trigger)
|
||||
= link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
|
||||
= link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation, testid: 'trigger_revoke_button' }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do
|
||||
= sprite_icon('remove')
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
selected_labels: issuable_sidebar[:labels].to_json } }
|
||||
- else
|
||||
- selected_labels = issuable_sidebar[:labels]
|
||||
.block.labels
|
||||
.block.labels{ data: { qa_selector: 'labels_block' } }
|
||||
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } }
|
||||
= sprite_icon('labels')
|
||||
%span
|
||||
|
|
@ -125,11 +125,11 @@
|
|||
= _('Labels')
|
||||
= loading_icon(css_class: 'gl-vertical-align-text-bottom hidden block-loading')
|
||||
- if can_edit_issuable
|
||||
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "edit_labels_link", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
|
||||
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?), data: { qa_selector: 'labels_block' } }
|
||||
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: "labels_edit_button", track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
|
||||
.value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
|
||||
- if selected_labels.any?
|
||||
- selected_labels.each do |label_hash|
|
||||
= render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'label', qa_label_name: label_hash[:title] })
|
||||
= render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]), dataset: { qa_selector: 'selected_label_content', qa_label_name: label_hash[:title] })
|
||||
- else
|
||||
%span.no-value
|
||||
= _('None')
|
||||
|
|
@ -141,7 +141,7 @@
|
|||
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
|
||||
= multi_label_name(selected_labels, "Labels")
|
||||
= icon('chevron-down', 'aria-hidden': 'true')
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
|
||||
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height{ data: { qa_selector: "labels_dropdown_content"} }
|
||||
= render partial: "shared/issuable/label_page_default"
|
||||
- if issuable_sidebar.dig(:current_user, :can_admin_label)
|
||||
= render partial: "shared/issuable/label_page_create"
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
end
|
||||
|
||||
def cleanup_orphan_lfs_file_references(project)
|
||||
return unless Feature.enabled?(:cleanup_lfs_during_gc, project)
|
||||
return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
|
||||
|
||||
::Gitlab::Cleanup::OrphanLfsFileReferences.new(project, dry_run: false, logger: logger).run!
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove bootstrap from pages/form
|
||||
merge_request: 43442
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Clean up unused LFS objects during repository housekeeping
|
||||
merge_request: 40979
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add assignee usernames to issue resolver
|
||||
merge_request: 43294
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Fix copy_indexes migration helper skipping the opclass for indexes
|
||||
with operator classes defined for them
|
||||
merge_request: 43471
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update popover to gl-popover on WebIDE commit message
|
||||
merge_request: 43499
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update GitLab Workhorse to v8.48.0
|
||||
merge_request: 43586
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove retry icon on failed job if merge pipeline
|
||||
merge_request: 42495
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add approval rules with approvers to usage ping
|
||||
merge_request: 36737
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve issuable reaction search
|
||||
merge_request: 42321
|
||||
author: Ethan Reesor (@firelizzard)
|
||||
type: added
|
||||
|
|
@ -239,16 +239,6 @@ module Gitlab
|
|||
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/64091#note_194512508
|
||||
config.assets.paths << "#{config.root}/node_modules"
|
||||
|
||||
if Gitlab.ee?
|
||||
# Compile non-JS/CSS assets in the ee/app/assets folder by default
|
||||
# Mimic sprockets-rails default: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87
|
||||
LOOSE_EE_APP_ASSETS = lambda do |logical_path, filename|
|
||||
filename.start_with?(config.root.join("ee/app/assets").to_s) &&
|
||||
!['.js', '.css', ''].include?(File.extname(logical_path))
|
||||
end
|
||||
config.assets.precompile << LOOSE_EE_APP_ASSETS
|
||||
end
|
||||
|
||||
# Version of your assets, change this if you want to expire all your assets
|
||||
config.assets.version = '1.0'
|
||||
|
||||
|
|
@ -313,6 +303,34 @@ module Gitlab
|
|||
g.factory_bot false
|
||||
end
|
||||
|
||||
# sprocket-rails adds some precompile assets we actually do not need.
|
||||
#
|
||||
# It copies all _non_ js and CSS files from the app/assets/ older.
|
||||
#
|
||||
# In our case this copies for example: Vue, Markdown and Graphql, which we do not need
|
||||
# for production.
|
||||
#
|
||||
# We remove this default behavior and then reimplement it in order to consider ee/ as well
|
||||
# and remove those other files we do not need.
|
||||
#
|
||||
# For reference: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87
|
||||
initializer :correct_precompile_targets, after: :set_default_precompile do |app|
|
||||
app.config.assets.precompile.reject! { |entry| entry == Sprockets::Railtie::LOOSE_APP_ASSETS }
|
||||
|
||||
asset_roots = [config.root.join("app/assets").to_s]
|
||||
|
||||
if Gitlab.ee?
|
||||
asset_roots << config.root.join("ee/app/assets").to_s
|
||||
end
|
||||
|
||||
LOOSE_APP_ASSETS = lambda do |logical_path, filename|
|
||||
filename.start_with?(*asset_roots) &&
|
||||
!['.js', '.css', '.md', '.vue', '.graphql', ''].include?(File.extname(logical_path))
|
||||
end
|
||||
|
||||
app.config.assets.precompile << LOOSE_APP_ASSETS
|
||||
end
|
||||
|
||||
# This empty initializer forces the :let_zeitwerk_take_over initializer to run before we load
|
||||
# initializers in config/initializers. This is done because autoloading before Zeitwerk takes
|
||||
# over is deprecated but our initializers do a lot of autoloading.
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: api_kaminari_count_with_limit
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_key_autocomplete
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29124
|
||||
rollout_issue_url:
|
||||
group: group::progressive delivery
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: cleanup_lfs_during_gc
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38813
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238605
|
||||
group: group::source code
|
||||
name: ci_pipeline_triggers_settings_vue_ui
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41864
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247486
|
||||
group: group::continuous integration
|
||||
type: development
|
||||
default_enabled: false
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: dag_pipeline_tab
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30310
|
||||
rollout_issue_url:
|
||||
group: group::pipeline authoring
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: deploy_from_footer
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25427
|
||||
rollout_issue_url:
|
||||
group: group::progressive delivery
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: metrics_dashboard
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29634
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/257902
|
||||
group: group::health
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: release_evidence_collection
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group:
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group: group::release management
|
||||
type: development
|
||||
default_enabled: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: api_kaminari_count_with_limit
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23931
|
||||
rollout_issue_url:
|
||||
group: group::ecosystem
|
||||
type: ops
|
||||
default_enabled: false
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
packages: [Ultimate, Gold]
|
||||
url: https://docs.gitlab.com/ee/user/project/requirements/index.html
|
||||
image_url:
|
||||
published_at: 2020-04-22
|
||||
release: 12.10
|
||||
published_at: 2020-04-22
|
||||
release: 12.10
|
||||
- title: Retrieve CI/CD secrets from HashiCorp Vault
|
||||
body: In this release, GitLab adds support for lightweight JSON Web Token (JWT) authentication to integrate with your existing HashiCorp Vault. Now, you can seamlessly provide secrets to CI/CD jobs by taking advantage of HashiCorp's JWT authentication method rather than manually having to provide secrets as a variable in GitLab.
|
||||
stage: Release
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class AddIndicesToApprovalProjectRules < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
RULE_TYPE_INDEX_NAME = 'index_approval_project_rules_on_id_with_regular_type'
|
||||
RULE_ID_INDEX_NAME = 'index_approval_project_rules_users_on_approval_project_rule_id'
|
||||
|
||||
def up
|
||||
add_concurrent_index :approval_project_rules, :id, where: 'rule_type = 0', name: RULE_TYPE_INDEX_NAME
|
||||
add_concurrent_index :approval_project_rules_users, :approval_project_rule_id, name: RULE_ID_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :approval_project_rules, :id, where: 'rule_type = 0', name: RULE_TYPE_INDEX_NAME
|
||||
remove_concurrent_index :approval_project_rules_users, :approval_project_rule_id, name: RULE_ID_INDEX_NAME
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
dd630c76819641ad64a5f6ae40ad4f49e7fbe1c783398d97886dc7e9852a245e
|
||||
|
|
@ -19466,6 +19466,8 @@ CREATE UNIQUE INDEX index_approval_project_rules_groups_1 ON approval_project_ru
|
|||
|
||||
CREATE INDEX index_approval_project_rules_groups_2 ON approval_project_rules_groups USING btree (group_id);
|
||||
|
||||
CREATE INDEX index_approval_project_rules_on_id_with_regular_type ON approval_project_rules USING btree (id) WHERE (rule_type = 0);
|
||||
|
||||
CREATE INDEX index_approval_project_rules_on_project_id ON approval_project_rules USING btree (project_id);
|
||||
|
||||
CREATE INDEX index_approval_project_rules_on_rule_type ON approval_project_rules USING btree (rule_type);
|
||||
|
|
@ -19478,6 +19480,8 @@ CREATE UNIQUE INDEX index_approval_project_rules_users_1 ON approval_project_rul
|
|||
|
||||
CREATE INDEX index_approval_project_rules_users_2 ON approval_project_rules_users USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_approval_project_rules_users_on_approval_project_rule_id ON approval_project_rules_users USING btree (approval_project_rule_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id, name) WHERE ((rule_type = 2) AND (section IS NULL));
|
||||
|
||||
CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ verification methods:
|
|||
| Blobs | Container registry _(object storage)_ | Geo with API/Managed/Docker API (*2*) | _Not implemented_ |
|
||||
| Blobs | Package registry _(filesystem)_ | Geo with API | _Not implemented_ |
|
||||
| Blobs | Package registry _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
| Blobs | Versioned Terraform State _(filesystem)_ | Geo with API | _Not implemented_ |
|
||||
| Blobs | Versioned Terraform State _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
|
||||
- (*1*): Redis replication can be used as part of HA with Redis sentinel. It's not used between Geo nodes.
|
||||
- (*2*): Object storage replication can be performed by Geo or by your object storage provider/appliance
|
||||
|
|
@ -185,7 +187,7 @@ successfully, you must replicate their data using some other means.
|
|||
| [PyPi Repository](../../../user/packages/pypi_repository/index.md) | **Yes** (13.2) | No | Behind feature flag `geo_package_file_replication`, enabled by default |
|
||||
| [Composer Repository](../../../user/packages/composer_repository/index.md) | **Yes** (13.2) | No | Behind feature flag `geo_package_file_replication`, enabled by default |
|
||||
| [External merge request diffs](../../merge_request_diffs.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/33817) | No | |
|
||||
| [Terraform State](../../terraform_state.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/3112)(*3*) | No | |
|
||||
| [Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | No | |
|
||||
| [Vulnerability Export](../../../user/application_security/security_dashboard/#export-vulnerabilities) | [No](https://gitlab.com/groups/gitlab-org/-/epics/3111)(*3*) | No | |
|
||||
| Content in object storage | **Yes** (12.4) | No | |
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ the `pushes_since_gc` value is 200 a `git gc` will be run.
|
|||
`git add`.
|
||||
- `git repack` ([man page](https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-repack.html)) re-organize existing packs into a single, more efficient pack.
|
||||
|
||||
Housekeeping will also [remove unreferenced LFS files](../raketasks/cleanup.md#remove-unreferenced-lfs-files)
|
||||
from your project on the same schedule as the `git gc` operation, freeing up storage space for your project.
|
||||
|
||||
You can find this option under your project's **Settings > General > Advanced**.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -6879,6 +6879,37 @@ type GeoNode {
|
|||
"""
|
||||
internalUrl: String
|
||||
|
||||
"""
|
||||
Find merge request diff registries on this Geo node. Available only when
|
||||
feature flag `geo_merge_request_diff_replication` is enabled
|
||||
"""
|
||||
mergeRequestDiffRegistries(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
Filters registries by their ID
|
||||
"""
|
||||
ids: [ID!]
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
): MergeRequestDiffRegistryConnection
|
||||
|
||||
"""
|
||||
The interval (in days) in which the repository verification is valid. Once expired, it will be reverified
|
||||
"""
|
||||
|
|
@ -7001,8 +7032,7 @@ type GeoNode {
|
|||
): TerraformStateRegistryConnection
|
||||
|
||||
"""
|
||||
Find terraform state version registries on this Geo node. Available only when
|
||||
feature flag `geo_terraform_state_version_replication` is enabled
|
||||
Find terraform state version registries on this Geo node
|
||||
"""
|
||||
terraformStateVersionRegistries(
|
||||
"""
|
||||
|
|
@ -7363,6 +7393,11 @@ type Group {
|
|||
"""
|
||||
assigneeUsername: String
|
||||
|
||||
"""
|
||||
Usernames of users assigned to the issue
|
||||
"""
|
||||
assigneeUsernames: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
|
|
@ -10671,6 +10706,86 @@ type MergeRequestCreatePayload {
|
|||
mergeRequest: MergeRequest
|
||||
}
|
||||
|
||||
"""
|
||||
Represents the Geo sync and verification state of a Merge Request diff
|
||||
"""
|
||||
type MergeRequestDiffRegistry {
|
||||
"""
|
||||
Timestamp when the MergeRequestDiffRegistry was created
|
||||
"""
|
||||
createdAt: Time
|
||||
|
||||
"""
|
||||
ID of the MergeRequestDiffRegistry
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Error message during sync of the MergeRequestDiffRegistry
|
||||
"""
|
||||
lastSyncFailure: String
|
||||
|
||||
"""
|
||||
Timestamp of the most recent successful sync of the MergeRequestDiffRegistry
|
||||
"""
|
||||
lastSyncedAt: Time
|
||||
|
||||
"""
|
||||
ID of the Merge Request diff
|
||||
"""
|
||||
mergeRequestDiffId: ID!
|
||||
|
||||
"""
|
||||
Timestamp after which the MergeRequestDiffRegistry should be resynced
|
||||
"""
|
||||
retryAt: Time
|
||||
|
||||
"""
|
||||
Number of consecutive failed sync attempts of the MergeRequestDiffRegistry
|
||||
"""
|
||||
retryCount: Int
|
||||
|
||||
"""
|
||||
Sync state of the MergeRequestDiffRegistry
|
||||
"""
|
||||
state: RegistryState
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for MergeRequestDiffRegistry.
|
||||
"""
|
||||
type MergeRequestDiffRegistryConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [MergeRequestDiffRegistryEdge]
|
||||
|
||||
"""
|
||||
A list of nodes.
|
||||
"""
|
||||
nodes: [MergeRequestDiffRegistry]
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type MergeRequestDiffRegistryEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
cursor: String!
|
||||
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: MergeRequestDiffRegistry
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
|
|
@ -12086,7 +12201,7 @@ type PackageEdge {
|
|||
}
|
||||
|
||||
"""
|
||||
Represents the sync and verification state of a package file
|
||||
Represents the Geo sync and verification state of a package file
|
||||
"""
|
||||
type PackageFileRegistry {
|
||||
"""
|
||||
|
|
@ -12930,6 +13045,11 @@ type Project {
|
|||
"""
|
||||
assigneeUsername: String
|
||||
|
||||
"""
|
||||
Usernames of users assigned to the issue
|
||||
"""
|
||||
assigneeUsernames: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
|
|
@ -13025,6 +13145,11 @@ type Project {
|
|||
"""
|
||||
assigneeUsername: String
|
||||
|
||||
"""
|
||||
Usernames of users assigned to the issue
|
||||
"""
|
||||
assigneeUsernames: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
|
|
@ -13110,6 +13235,11 @@ type Project {
|
|||
"""
|
||||
assigneeUsername: String
|
||||
|
||||
"""
|
||||
Usernames of users assigned to the issue
|
||||
"""
|
||||
assigneeUsernames: [String!]
|
||||
|
||||
"""
|
||||
Username of the author of the issue
|
||||
"""
|
||||
|
|
@ -17483,7 +17613,7 @@ type TaskCompletionStatus {
|
|||
}
|
||||
|
||||
"""
|
||||
Represents the sync and verification state of a terraform state
|
||||
Represents the Geo sync and verification state of a terraform state
|
||||
"""
|
||||
type TerraformStateRegistry {
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -19119,6 +19119,77 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "mergeRequestDiffRegistries",
|
||||
"description": "Find merge request diff registries on this Geo node. Available only when feature flag `geo_merge_request_diff_replication` is enabled",
|
||||
"args": [
|
||||
{
|
||||
"name": "ids",
|
||||
"description": "Filters registries by their ID",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistryConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "minimumReverificationInterval",
|
||||
"description": "The interval (in days) in which the repository verification is valid. Once expired, it will be reverified",
|
||||
|
|
@ -19422,7 +19493,7 @@
|
|||
},
|
||||
{
|
||||
"name": "terraformStateVersionRegistries",
|
||||
"description": "Find terraform state version registries on this Geo node. Available only when feature flag `geo_terraform_state_version_replication` is enabled",
|
||||
"description": "Find terraform state version registries on this Geo node",
|
||||
"args": [
|
||||
{
|
||||
"name": "ids",
|
||||
|
|
@ -20370,6 +20441,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsernames",
|
||||
"description": "Usernames of users assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
|
@ -29424,6 +29513,251 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistry",
|
||||
"description": "Represents the Geo sync and verification state of a Merge Request diff",
|
||||
"fields": [
|
||||
{
|
||||
"name": "createdAt",
|
||||
"description": "Timestamp when the MergeRequestDiffRegistry was created",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID of the MergeRequestDiffRegistry",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSyncFailure",
|
||||
"description": "Error message during sync of the MergeRequestDiffRegistry",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSyncedAt",
|
||||
"description": "Timestamp of the most recent successful sync of the MergeRequestDiffRegistry",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "mergeRequestDiffId",
|
||||
"description": "ID of the Merge Request diff",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "retryAt",
|
||||
"description": "Timestamp after which the MergeRequestDiffRegistry should be resynced",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "retryCount",
|
||||
"description": "Number of consecutive failed sync attempts of the MergeRequestDiffRegistry",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"description": "Sync state of the MergeRequestDiffRegistry",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "RegistryState",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistryConnection",
|
||||
"description": "The connection type for MergeRequestDiffRegistry.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistryEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistry",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistryEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestDiffRegistry",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "MergeRequestEdge",
|
||||
|
|
@ -35766,7 +36100,7 @@
|
|||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "PackageFileRegistry",
|
||||
"description": "Represents the sync and verification state of a package file",
|
||||
"description": "Represents the Geo sync and verification state of a package file",
|
||||
"fields": [
|
||||
{
|
||||
"name": "createdAt",
|
||||
|
|
@ -38127,6 +38461,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsernames",
|
||||
"description": "Usernames of users assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
|
@ -38348,6 +38700,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsernames",
|
||||
"description": "Usernames of users assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
|
@ -38535,6 +38905,24 @@
|
|||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeUsernames",
|
||||
"description": "Usernames of users assigned to the issue",
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "assigneeId",
|
||||
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
|
||||
|
|
@ -50932,7 +51320,7 @@
|
|||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "TerraformStateRegistry",
|
||||
"description": "Represents the sync and verification state of a terraform state",
|
||||
"description": "Represents the Geo sync and verification state of a terraform state",
|
||||
"fields": [
|
||||
{
|
||||
"name": "createdAt",
|
||||
|
|
|
|||
|
|
@ -1548,6 +1548,21 @@ Autogenerated return type of MergeRequestCreate.
|
|||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `mergeRequest` | MergeRequest | The merge request after mutation |
|
||||
|
||||
### MergeRequestDiffRegistry
|
||||
|
||||
Represents the Geo sync and verification state of a Merge Request diff.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `createdAt` | Time | Timestamp when the MergeRequestDiffRegistry was created |
|
||||
| `id` | ID! | ID of the MergeRequestDiffRegistry |
|
||||
| `lastSyncFailure` | String | Error message during sync of the MergeRequestDiffRegistry |
|
||||
| `lastSyncedAt` | Time | Timestamp of the most recent successful sync of the MergeRequestDiffRegistry |
|
||||
| `mergeRequestDiffId` | ID! | ID of the Merge Request diff |
|
||||
| `retryAt` | Time | Timestamp after which the MergeRequestDiffRegistry should be resynced |
|
||||
| `retryCount` | Int | Number of consecutive failed sync attempts of the MergeRequestDiffRegistry |
|
||||
| `state` | RegistryState | Sync state of the MergeRequestDiffRegistry |
|
||||
|
||||
### MergeRequestPermissions
|
||||
|
||||
Check permissions for the current user on a merge request.
|
||||
|
|
@ -1764,7 +1779,7 @@ Represents a package.
|
|||
|
||||
### PackageFileRegistry
|
||||
|
||||
Represents the sync and verification state of a package file.
|
||||
Represents the Geo sync and verification state of a package file.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
|
|
@ -2457,7 +2472,7 @@ Completion status of tasks.
|
|||
|
||||
### TerraformStateRegistry
|
||||
|
||||
Represents the sync and verification state of a terraform state.
|
||||
Represents the Geo sync and verification state of a terraform state.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
|
|
|
|||
|
|
@ -122,15 +122,15 @@ Proposal:
|
|||
|
||||
| Role | Who
|
||||
|------------------------------|-------------------------|
|
||||
| Author | Kamil Trzciński |
|
||||
| Author | Kamil Trzciński |
|
||||
| Architecture Evolution Coach | Gerardo Lopez-Fernandez |
|
||||
| Engineering Leader | Kamil Trzciński |
|
||||
| Domain Expert | Shinya Maeda |
|
||||
| Engineering Leader | Kamil Trzciński |
|
||||
| Domain Expert | Shinya Maeda |
|
||||
|
||||
DRIs:
|
||||
|
||||
| Role | Who
|
||||
|------------------------------|------------------------|
|
||||
| Product | ? |
|
||||
| Leadership | Craig Gomes |
|
||||
| Engineering | Kamil Trzciński |
|
||||
| Product | Kenny Johnston |
|
||||
| Leadership | Craig Gomes |
|
||||
| Engineering | Kamil Trzciński |
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ For feature flags disabled by default, if they can be used by end users:
|
|||
do not say anything about it.
|
||||
- Say whether it's recommended for production use.
|
||||
- Document how to enable and disable it.
|
||||
- Add a warning to the user saying that the feature is disabled.
|
||||
- Add a warning to the user saying that the feature might be disabled.
|
||||
|
||||
For example, for a feature disabled by default, disabled on GitLab.com, cannot
|
||||
be enabled for a single project, and is not ready for production use:
|
||||
|
|
@ -250,7 +250,7 @@ be enabled by project, and is ready for production use:
|
|||
> - [Introduced](link-to-issue) in GitLab 12.0.
|
||||
> - It's [deployed behind a feature flag](<replace with path to>/user/feature_flags.md), enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It can be enabled or disable for a single project.
|
||||
> - It can be enabled or disabled for a single project.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#anchor-to-section). **(CORE ONLY)**
|
||||
|
||||
|
|
|
|||
|
|
@ -633,7 +633,7 @@ the Admin Area UI, and Prometheus!
|
|||
include ::Types::Geo::RegistryType
|
||||
|
||||
graphql_name 'WidgetRegistry'
|
||||
description 'Represents the sync and verification state of a widget'
|
||||
description 'Represents the Geo sync and verification state of a widget'
|
||||
|
||||
field :widget_id, GraphQL::ID_TYPE, null: false, description: 'ID of the Widget'
|
||||
end
|
||||
|
|
@ -672,6 +672,12 @@ the Admin Area UI, and Prometheus!
|
|||
}
|
||||
```
|
||||
|
||||
1. Update the GraphQL reference documentation:
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:graphql:compile_docs
|
||||
```
|
||||
|
||||
Individual widget synchronization and verification data should now be available
|
||||
via the GraphQL API!
|
||||
|
||||
|
|
|
|||
|
|
@ -374,12 +374,16 @@ which is shared by the analyzers that GitLab maintains. You can [contribute](htt
|
|||
new generic identifiers to if needed. Analyzers may also produce vendor-specific or product-specific
|
||||
identifiers, which don't belong in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common).
|
||||
|
||||
The first item of the `identifiers` array is called the primary identifier.
|
||||
The first item of the `identifiers` array is called the [primary
|
||||
identifier](../../user/application_security/terminology/#primary-identifier).
|
||||
The primary identifier is particularly important, because it is used to
|
||||
[track vulnerabilities](#tracking-and-merging-vulnerabilities) as new commits are pushed to the repository.
|
||||
Identifiers are also used to [merge duplicate vulnerabilities](#tracking-and-merging-vulnerabilities)
|
||||
reported for the same commit, except for `CWE` and `WASC`.
|
||||
|
||||
Not all vulnerabilities have CVEs, and a CVE can be identified multiple times. As a result, a CVE
|
||||
isn't a stable identifier and you shouldn't assume it as such when tracking vulnerabilities.
|
||||
|
||||
### Location
|
||||
|
||||
The `location` indicates where the vulnerability has been detected.
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ More useful links:
|
|||
- The usage data is primarily composed of row counts for different tables in the instance’s database. By comparing these counts month over month (or week over week), we can get a rough sense for how an instance is using the different features within the product. In addition to counts, other facts
|
||||
that help us classify and understand GitLab installations are collected.
|
||||
- Usage ping is important to GitLab as we use it to calculate our Stage Monthly Active Users (SMAU) which helps us measure the success of our stages and features.
|
||||
- Once usage ping is enabled, GitLab will gather data from the other instances and will be able to show usage statistics of your instance to your users.
|
||||
- While usage ping is enabled, GitLab will gather data from the other instances and will be able to show usage statistics of your instance to your users.
|
||||
|
||||
### Why should we enable Usage Ping?
|
||||
|
||||
|
|
@ -431,15 +431,6 @@ Recommendations:
|
|||
All events added in [`known_events.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/known_events.yml) are automatically added to usage data generation under the `redis_hll_counters` key. This column is stored in [version-app as a JSON](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/db/schema.rb#L209).
|
||||
For each event we add metrics for the weekly and monthly time frames, and totals for each where applicable:
|
||||
|
||||
- `#{event_name}_weekly` data for 7 days for daily [aggregation](#adding-new-events) events and data for last complete week for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{event_name}_monthly` data for 28 days for daily [aggregation](#adding-new-events) events and data for last 4 complete weeks for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{category}_total_unique_counts_weekly` total unique counts for events in same category for last 7 days or last complete week, if events are in the same Redis slot and if we have more than one metric.
|
||||
- `#{event_name}_weekly` - Data for 7 days for daily [aggregation](#adding-new-events) events and data for the last complete week for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{event_name}_monthly` - Data for 28 days for daily [aggregation](#adding-new-events) events and data for the last 4 complete weeks for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{category}_total_unique_counts_weekly` - Total unique counts for events in the same category for the last 7 days or the last complete week, if events are in the same Redis slot and we have more than one metric.
|
||||
- `#{event_name}_weekly`: Data for 7 days for daily [aggregation](#adding-new-events) events and data for last complete week for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{event_name}_monthly`: Data for 28 days for daily [aggregation](#adding-new-events) events and data for last 4 complete weeks for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{category}_total_unique_counts_weekly` total unique counts for events in same category for last 7 days or last complete week, if events are in the same Redis slot and if we have more than one metric.
|
||||
- `#{event_name}_weekly`: Data for 7 days for daily [aggregation](#adding-new-events) events and data for the last complete week for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{event_name}_monthly`: Data for 28 days for daily [aggregation](#adding-new-events) events and data for the last 4 complete weeks for weekly [aggregation](#adding-new-events) events.
|
||||
- `#{category}_total_unique_counts_weekly`: Total unique counts for events in the same category for the last 7 days or the last complete week, if events are in the same Redis slot and we have more than one metric.
|
||||
|
|
|
|||
|
|
@ -12,26 +12,26 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Value Stream Analytics measures the time spent to go from an
|
||||
[idea to production](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab)
|
||||
(also known as cycle time) for each of your projects. Value Stream Analytics displays the median time
|
||||
(also known as cycle time) for each of your projects or groups. Value Stream Analytics displays the median time
|
||||
spent in each stage defined in the process.
|
||||
|
||||
For information on how to contribute to the development of Value Stream Analytics, see our [contributor documentation](../../development/value_stream_analytics.md).
|
||||
|
||||
Value Stream Analytics is useful in order to quickly determine the velocity of a given
|
||||
project. It points to bottlenecks in the development process, enabling management
|
||||
to uncover, triage, and identify the root cause of slowdowns in the software development life cycle.
|
||||
|
||||
Value Stream Analytics is tightly coupled with the [GitLab flow](../../topics/gitlab_flow.md) and
|
||||
calculates a separate median for each stage.
|
||||
For information on how to contribute to the development of Value Stream Analytics, see our [contributor documentation](../../development/value_stream_analytics.md).
|
||||
|
||||
## Overview
|
||||
## Project Level Value Stream Analytics **CORE**
|
||||
|
||||
Value Stream Analytics is available:
|
||||
Project Level Value Stream Analytics is available via **Project > Analytics > Value Stream**.
|
||||
|
||||
- From GitLab 12.9, at the group level via **Group > Analytics > Value Stream**. **(PREMIUM)**
|
||||
- At the project level via **Project > Analytics > Value Stream**.
|
||||
## Group Level Value Stream Analytics **PREMIUM**
|
||||
|
||||
There are seven stages that are tracked as part of the Value Stream Analytics calculations.
|
||||
From GitLab 12.9, group level Value Stream Analytics is available via **Group > Analytics > Value Stream**.
|
||||
|
||||
## Default stages
|
||||
|
||||
The stages tracked by Value Stream Analytics by default represent the [GitLab flow](../../topics/gitlab_flow.md). These stages can be customized in Group Level Value Stream Analytics.
|
||||
|
||||
- **Issue** (Tracker)
|
||||
- Time to schedule an issue (by milestone or by adding it to an issue board)
|
||||
|
|
|
|||
|
|
@ -483,3 +483,11 @@ This error occurs when the Docker version that runs the Dependency Scanning job
|
|||
Consider updating to Docker `19.03.1` or greater. Older versions are not
|
||||
affected. Read more in
|
||||
[this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/13830#note_211354992 "Current SAST container fails").
|
||||
|
||||
### Limitation when using rules:exists
|
||||
|
||||
The [Dependency Scanning CI template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml)
|
||||
uses the [`rules:exists`](../../../ci/yaml/README.md#rulesexists)
|
||||
syntax. This directive is limited to 10000 checks and always returns `true` after reaching this
|
||||
number. Because of this, and depending on the number of files in your repository, a Dependency
|
||||
Scanning job might be triggered even if the scanner doesn't support your project.
|
||||
|
|
|
|||
|
|
@ -260,15 +260,9 @@ It is important that this SCIM `id` and SCIM `externalId` are configured to the
|
|||
|
||||
Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page.
|
||||
|
||||
Alternatively, the [SCIM API](../../../api/scim.md#get-a-list-of-saml-users) can be used to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`.
|
||||
A possible alternative is to use the [SCIM API](../../../api/scim.md#get-a-list-of-saml-users) to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`.
|
||||
|
||||
For example:
|
||||
|
||||
```shell
|
||||
curl 'https://gitlab.example.com/api/scim/v2/groups/GROUP_NAME/Users?startIndex=1"' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
|
||||
```
|
||||
|
||||
To see how this compares to the value returned as the SAML NameId, you can have the user use a [SAML Tracer](index.md#saml-debugging-tools).
|
||||
To see how the `external_uid` compares to the value returned as the SAML NameId, you can have the user use a [SAML Tracer](index.md#saml-debugging-tools).
|
||||
|
||||
#### Update or fix mismatched SCIM externalId and SAML NameId
|
||||
|
||||
|
|
@ -285,15 +279,9 @@ you can address the problem in the following ways:
|
|||
|
||||
- You can have users unlink and relink themselves, based on the ["SAML authentication failed: User has already been taken"](./index.md#message-saml-authentication-failed-user-has-already-been-taken) section.
|
||||
- You can unlink all users simultaneously, by removing all users from the SAML app while provisioning is turned on.
|
||||
- You can use the [SCIM API](../../../api/scim.md#update-a-single-saml-user) to manually correct the `externalId` stored for users to match the SAML `NameId`.
|
||||
- It may be possible to use the [SCIM API](../../../api/scim.md#update-a-single-saml-user) to manually correct the `externalId` stored for users to match the SAML `NameId`.
|
||||
To look up a user, you'll need to know the desired value that matches the `NameId` as well as the current `externalId`.
|
||||
|
||||
It is then possible to issue a manual SCIM#update request, for example:
|
||||
|
||||
```shell
|
||||
curl --verbose --request PATCH 'https://gitlab.com/api/scim/v2/groups/YOUR_GROUP/Users/OLD_EXTERNAL_UID' --data '{ "Operations": [{"op":"Replace","path":"externalId","value":"NEW_EXTERNAL_UID"}] }' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
|
||||
```
|
||||
|
||||
It is important not to update these to incorrect values, since this will cause users to be unable to sign in. It is also important not to assign a value to the wrong user, as this would cause users to get signed into the wrong account.
|
||||
|
||||
#### I need to change my SCIM app
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ The following table depicts the various user permission levels in a project.
|
|||
|
||||
\* Owner permission is only available at the group or personal namespace level (and for instance admins) and is inherited by its projects.
|
||||
|
||||
1. Guest users are able to perform this action on public and internal projects, but not private projects.
|
||||
1. Guest users are able to perform this action on public and internal projects, but not private projects. This doesn't apply to [external users](#external-users) where explicit access must be given even if the project is internal.
|
||||
1. Guest users can only view the confidential issues they created themselves.
|
||||
1. If **Public pipelines** is enabled in **Project Settings > CI/CD**.
|
||||
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [Protected Branches](./project/protected_branches.md).
|
||||
|
|
@ -313,7 +313,7 @@ External users:
|
|||
Access can be granted by adding the user as member to the project or group.
|
||||
Like usual users, they receive a role in the project or group with all
|
||||
the abilities that are mentioned in the [permissions table above](#project-members-permissions).
|
||||
For example, if an external user is added as Guest, and your project is
|
||||
For example, if an external user is added as Guest, and your project is internal or
|
||||
private, they do not have access to the code; you need to grant the external
|
||||
user access at the Reporter level or above if you want them to have access to the code. You should
|
||||
always take into account the
|
||||
|
|
|
|||
|
|
@ -37,12 +37,7 @@ Import your projects from Bitbucket Server to GitLab with minimal effort.
|
|||
empty changes.
|
||||
1. Attachments in Markdown are currently not imported.
|
||||
1. Task lists are not imported.
|
||||
1. Emoji reactions are not imported.
|
||||
1. [LFS objects](../../../topics/git/lfs/index.md) are not imported.
|
||||
|
||||
NOTE: **Note:**
|
||||
To import a repository including LFS objects from a Bitbucket server repository, use the [Repo by URL](../import/repo_by_url.md) importer.
|
||||
|
||||
1. Emoji reactions are not imported
|
||||
1. Project filtering does not support fuzzy search (only `starts with` or `full
|
||||
match strings` are currently supported)
|
||||
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ This will:
|
|||
- Run `git gc` against the repository to remove unreferenced objects. Repacking your repository will temporarily
|
||||
cause the size of your repository to increase significantly, because the old pack files are not removed until the
|
||||
new pack files have been created.
|
||||
- Unlink any unused LFS objects currently attached to your project, freeing up storage space.
|
||||
- Recalculate the size of your repository on disk.
|
||||
|
||||
You will receive an email notification with the recalculated repository size after the cleanup has completed.
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
|
|
@ -19,7 +19,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def increment_trace_bytes(size)
|
||||
self.class.trace_bytes.increment(by: size.to_i)
|
||||
self.class.trace_bytes.increment({}, size.to_i)
|
||||
end
|
||||
|
||||
def self.trace_operations
|
||||
|
|
|
|||
|
|
@ -133,11 +133,11 @@ module Gitlab
|
|||
end
|
||||
|
||||
def actual_start(start)
|
||||
start || @relation.unscope(:group).minimum(@column) || 0
|
||||
start || @relation.unscope(:group, :having).minimum(@column) || 0
|
||||
end
|
||||
|
||||
def actual_finish(finish)
|
||||
finish || @relation.unscope(:group).maximum(@column) || 0
|
||||
finish || @relation.unscope(:group, :having).maximum(@column) || 0
|
||||
end
|
||||
|
||||
def check_mode!(mode)
|
||||
|
|
|
|||
|
|
@ -882,7 +882,7 @@ module Gitlab
|
|||
# column.
|
||||
opclasses[new] = opclasses.delete(old) if opclasses[old]
|
||||
|
||||
options[:opclasses] = opclasses
|
||||
options[:opclass] = opclasses
|
||||
end
|
||||
|
||||
add_concurrent_index(table, new_columns, options)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation)
|
||||
return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit)
|
||||
return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit, type: :ops)
|
||||
|
||||
limited_total_count = pagination_data.total_count_with_limit
|
||||
if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ module Gitlab
|
|||
|
||||
FALLBACK = -1
|
||||
|
||||
def count(relation, column = nil, batch: true, start: nil, finish: nil)
|
||||
def count(relation, column = nil, batch: true, batch_size: nil, start: nil, finish: nil)
|
||||
if batch
|
||||
Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
|
||||
Gitlab::Database::BatchCount.batch_count(relation, column, batch_size: batch_size, start: start, finish: finish)
|
||||
else
|
||||
relation.count
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18665,6 +18665,9 @@ msgstr ""
|
|||
msgid "Pipelines|Build with confidence"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|By revoking a trigger you will break any processes making use of it. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|CI Lint"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18677,6 +18680,15 @@ msgstr ""
|
|||
msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Copy trigger token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Get started with Pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18692,15 +18704,27 @@ msgstr ""
|
|||
msgid "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Last Used"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Loading Pipelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|More Information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Owner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Project cache successfully reset."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Revoke"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Run Pipeline"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -18725,6 +18749,15 @@ msgstr ""
|
|||
msgid "Pipelines|This project is not currently set up to run pipelines."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Trigger user has insufficient permissions to project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|invalid"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|parent"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.168.0",
|
||||
"@gitlab/ui": "21.8.2",
|
||||
"@gitlab/ui": "21.8.3",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@rails/ujs": "^6.0.3-2",
|
||||
|
|
|
|||
2
qa/qa.rb
2
qa/qa.rb
|
|
@ -91,6 +91,7 @@ module QA
|
|||
autoload :UserGPG, 'qa/resource/user_gpg'
|
||||
autoload :Visibility, 'qa/resource/visibility'
|
||||
autoload :ProjectSnippet, 'qa/resource/project_snippet'
|
||||
autoload :Design, 'qa/resource/design'
|
||||
|
||||
module KubernetesCluster
|
||||
autoload :Base, 'qa/resource/kubernetes_cluster/base'
|
||||
|
|
@ -260,6 +261,7 @@ module QA
|
|||
module Pipeline
|
||||
autoload :Index, 'qa/page/project/pipeline/index'
|
||||
autoload :Show, 'qa/page/project/pipeline/show'
|
||||
autoload :New, 'qa/page/project/pipeline/new'
|
||||
end
|
||||
|
||||
module Tag
|
||||
|
|
|
|||
|
|
@ -31,6 +31,16 @@ module QA
|
|||
element :design_file_name
|
||||
element :design_image
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/design_management/pages/index.vue' do
|
||||
element :archive_button
|
||||
element :design_checkbox
|
||||
element :design_dropzone_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/design_management/components/delete_button.vue' do
|
||||
element :confirm_archiving_button
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -52,12 +62,14 @@ module QA
|
|||
# It accepts a `class:` option, but that only works for class attributes
|
||||
# It doesn't work as a CSS selector.
|
||||
# So instead we use the name attribute as a locator
|
||||
page.attach_file("design_file", design_file_path, make_visible: { display: 'block' })
|
||||
within_element(:design_dropzone_content) do
|
||||
page.attach_file("design_file", design_file_path, make_visible: { display: 'block' })
|
||||
end
|
||||
|
||||
filename = ::File.basename(design_file_path)
|
||||
|
||||
found = wait_until(reload: false, sleep_interval: 1) do
|
||||
image = find_element(:design_image)
|
||||
image = find_element(:design_image, filename: filename)
|
||||
|
||||
has_element?(:design_file_name, text: filename) &&
|
||||
image["complete"] &&
|
||||
|
|
@ -71,11 +83,24 @@ module QA
|
|||
click_element(:design_file_name, text: filename)
|
||||
end
|
||||
|
||||
def select_design(filename)
|
||||
click_element(:design_checkbox, design: filename)
|
||||
end
|
||||
|
||||
def archive_selected_designs
|
||||
click_element(:archive_button)
|
||||
click_element(:confirm_archiving_button)
|
||||
end
|
||||
|
||||
def has_annotation?(note)
|
||||
within_element_by_index(:design_discussion_content, 0) do
|
||||
has_element?(:note_content, text: note)
|
||||
end
|
||||
end
|
||||
|
||||
def has_design?(filename)
|
||||
has_element?(:design_file_name, text: filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -18,16 +18,29 @@ module QA
|
|||
element :more_assignees_link
|
||||
end
|
||||
|
||||
base.view 'app/helpers/dropdowns_helper.rb' do
|
||||
base.view 'app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue' do
|
||||
element :labels_block
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue' do
|
||||
element :selected_label_content
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do
|
||||
element :labels_dropdown_content
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue' do
|
||||
element :labels_edit_button
|
||||
end
|
||||
|
||||
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue' do
|
||||
element :dropdown_input_field
|
||||
end
|
||||
|
||||
base.view 'app/views/shared/issuable/_sidebar.html.haml' do
|
||||
element :assignee_block
|
||||
element :dropdown_menu_labels
|
||||
element :edit_labels_link
|
||||
element :edit_milestone_link
|
||||
element :labels_block
|
||||
element :milestone_block
|
||||
element :milestone_link
|
||||
end
|
||||
|
|
@ -64,7 +77,7 @@ module QA
|
|||
|
||||
def has_label?(label)
|
||||
within_element(:labels_block) do
|
||||
!!has_element?(:label, label_name: label)
|
||||
!!has_element?(:selected_label_content, label_name: label)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -80,23 +93,25 @@ module QA
|
|||
|
||||
def select_labels_and_refresh(labels)
|
||||
Support::Retrier.retry_until do
|
||||
click_element(:edit_labels_link)
|
||||
has_element?(:dropdown_menu_labels, text: labels.first)
|
||||
click_element(:labels_edit_button)
|
||||
has_element?(:labels_dropdown_content, text: labels.first)
|
||||
end
|
||||
|
||||
labels.each do |label|
|
||||
within_element(:dropdown_menu_labels, text: label) do
|
||||
within_element(:labels_dropdown_content) do
|
||||
send_keys_to_element(:dropdown_input_field, [label, :enter])
|
||||
end
|
||||
end
|
||||
|
||||
click_element(:edit_labels_link)
|
||||
click_element(:labels_edit_button)
|
||||
|
||||
labels.each do |label|
|
||||
has_element?(:labels_block, text: label, wait: 0)
|
||||
end
|
||||
|
||||
refresh
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def toggle_more_assignees_link
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ module QA
|
|||
element :pipeline_retry_button
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue' do
|
||||
element :run_pipeline_button
|
||||
end
|
||||
|
||||
def click_on_latest_pipeline
|
||||
all_elements(:pipeline_url_link, minimum: 1, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).first.click
|
||||
end
|
||||
|
|
@ -40,6 +44,14 @@ module QA
|
|||
wait_for_latest_pipeline_success
|
||||
end
|
||||
end
|
||||
|
||||
def has_pipeline?
|
||||
has_element? :pipeline_url_link
|
||||
end
|
||||
|
||||
def click_run_pipeline_button
|
||||
click_element :run_pipeline_button, Page::Project::Pipeline::New
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Project
|
||||
module Pipeline
|
||||
class New < QA::Page::Base
|
||||
view 'app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue' do
|
||||
element :run_pipeline_button, required: true
|
||||
end
|
||||
|
||||
def click_run_pipeline_button
|
||||
click_element :run_pipeline_button
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Resource
|
||||
class Design < Base
|
||||
attribute :issue do
|
||||
Issue.fabricate_via_api!
|
||||
end
|
||||
|
||||
attribute :filepath do
|
||||
::File.absolute_path(::File.join('spec', 'fixtures', @filename))
|
||||
end
|
||||
|
||||
attribute :id
|
||||
attribute :filename
|
||||
|
||||
def initialize
|
||||
@filename = 'banana_sample.gif'
|
||||
end
|
||||
|
||||
# TODO This will be replaced as soon as file uploads over GraphQL are implemented
|
||||
def fabricate!
|
||||
issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
issue.add_design(filepath)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
context 'Design Management' do
|
||||
let(:first_design) { Resource::Design.fabricate! }
|
||||
|
||||
let(:second_design) do
|
||||
Resource::Design.fabricate! do |design|
|
||||
design.issue = first_design.issue
|
||||
design.filename = 'values.png'
|
||||
end
|
||||
end
|
||||
|
||||
let(:third_design) do
|
||||
Resource::Design.fabricate! do |design|
|
||||
design.issue = second_design.issue
|
||||
design.filename = 'tanuki.jpg'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'user archives a design', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/274' do
|
||||
third_design.issue.visit!
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
issue.select_design(third_design.filename)
|
||||
|
||||
issue.archive_selected_designs
|
||||
|
||||
expect(issue).not_to have_design(third_design.filename)
|
||||
expect(issue).to have_design(first_design.filename)
|
||||
expect(issue).to have_design(second_design.filename)
|
||||
end
|
||||
|
||||
Page::Project::Issue::Show.perform do |issue|
|
||||
issue.select_design(second_design.filename)
|
||||
issue.select_design(first_design.filename)
|
||||
|
||||
issue.archive_selected_designs
|
||||
|
||||
expect(issue).not_to have_design(first_design.filename)
|
||||
expect(issue).not_to have_design(second_design.filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -29,7 +29,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'creates a merge request with a milestone and label', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/514', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/254988', type: :stale } do
|
||||
it 'creates a merge request with a milestone and label', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/514' do
|
||||
gitlab_account_username = "@#{Runtime::User.username}"
|
||||
|
||||
milestone = Resource::ProjectMilestone.fabricate_via_api! do |milestone|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Verify' do
|
||||
describe 'Run pipeline', :requires_admin, :skip_live_env do
|
||||
# [TODO]: Developer to remove :requires_admin and :skip_live_env once FF is removed in https://gitlab.com/gitlab-org/gitlab/-/issues/229632
|
||||
|
||||
context 'with web only rule' do
|
||||
let(:feature_flag) { 'new_pipeline_form' }
|
||||
let(:job_name) { 'test_job' }
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.name = 'web-only-pipeline'
|
||||
end
|
||||
end
|
||||
|
||||
let!(:ci_file) do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
commit.add_files(
|
||||
[
|
||||
{
|
||||
file_path: '.gitlab-ci.yml',
|
||||
content: <<~YAML
|
||||
#{job_name}:
|
||||
tags:
|
||||
- #{project.name}
|
||||
script: echo 'OK'
|
||||
only:
|
||||
- web
|
||||
YAML
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Runtime::Feature.enable_and_verify(feature_flag) # [TODO]: Developer to remove when feature flag is removed
|
||||
Flow::Login.sign_in
|
||||
project.visit!
|
||||
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
|
||||
end
|
||||
|
||||
after do
|
||||
Runtime::Feature.disable_and_verify(feature_flag) # [TODO]: Developer to remove when feature flag is removed
|
||||
end
|
||||
|
||||
it 'can trigger pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/946' do
|
||||
Page::Project::Pipeline::Index.perform do |index|
|
||||
expect(index).not_to have_pipeline # should not auto trigger pipeline
|
||||
index.click_run_pipeline_button
|
||||
end
|
||||
|
||||
Page::Project::Pipeline::New.perform(&:click_run_pipeline_button)
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
expect(pipeline).to have_job(job_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
|
|
@ -23,7 +23,7 @@ FactoryBot.define do
|
|||
factory :ci_pipeline do
|
||||
transient { ci_ref_presence { true } }
|
||||
|
||||
after(:build) do |pipeline, evaluator|
|
||||
before(:create) do |pipeline, evaluator|
|
||||
pipeline.ensure_ci_ref! if evaluator.ci_ref_presence && pipeline.ci_ref_id.nil?
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,7 @@
|
|||
|
||||
FactoryBot.define do
|
||||
factory :merge_request_diff do
|
||||
merge_request do
|
||||
build(:merge_request) do |merge_request|
|
||||
# MergeRequest should not create a MergeRequestDiff in the callback
|
||||
allow(merge_request).to receive(:ensure_merge_request_diff)
|
||||
end
|
||||
end
|
||||
|
||||
association :merge_request, factory: :merge_request_without_merge_request_diff
|
||||
state { :collected }
|
||||
commits_count { 1 }
|
||||
|
||||
|
|
|
|||
|
|
@ -286,5 +286,13 @@ FactoryBot.define do
|
|||
merge_request.update!(labels: evaluator.labels)
|
||||
end
|
||||
end
|
||||
|
||||
factory :merge_request_without_merge_request_diff, class: 'MergeRequestWithoutMergeRequestDiff'
|
||||
end
|
||||
end
|
||||
|
||||
class MergeRequestWithoutMergeRequestDiff < ::MergeRequest
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
def ensure_merge_request_diff; end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ FactoryBot.define do
|
|||
project_key { nil }
|
||||
end
|
||||
|
||||
after(:build) do |service, evaluator|
|
||||
before(:create) do |service, evaluator|
|
||||
if evaluator.create_data
|
||||
create(:jira_tracker_data, service: service,
|
||||
url: evaluator.url, api_url: evaluator.api_url, jira_issue_transition_id: evaluator.jira_issue_transition_id,
|
||||
|
|
@ -130,7 +130,7 @@ FactoryBot.define do
|
|||
new_issue_url { 'http://new-issue.example.com' }
|
||||
end
|
||||
|
||||
after(:build) do |service, evaluator|
|
||||
before(:create) do |service, evaluator|
|
||||
if evaluator.create_data
|
||||
create(:issue_tracker_data, service: service,
|
||||
project_url: evaluator.project_url, issues_url: evaluator.issues_url, new_issue_url: evaluator.new_issue_url
|
||||
|
|
@ -151,7 +151,7 @@ FactoryBot.define do
|
|||
project_identifier_code { 'PRJ-1' }
|
||||
end
|
||||
|
||||
after(:build) do |service, evaluator|
|
||||
before(:create) do |service, evaluator|
|
||||
create(:open_project_tracker_data, service: service,
|
||||
url: evaluator.url, api_url: evaluator.api_url, token: evaluator.token,
|
||||
closed_status_id: evaluator.closed_status_id, project_identifier_code: evaluator.project_identifier_code
|
||||
|
|
|
|||
|
|
@ -19,114 +19,132 @@ RSpec.describe 'Triggers', :js do
|
|||
visit project_settings_ci_cd_path(@project)
|
||||
end
|
||||
|
||||
describe 'create trigger workflow' do
|
||||
it 'prevents adding new trigger with no description' do
|
||||
fill_in 'trigger_description', with: ''
|
||||
click_button 'Add trigger'
|
||||
shared_examples 'triggers page' do
|
||||
describe 'create trigger workflow' do
|
||||
it 'prevents adding new trigger with no description' do
|
||||
fill_in 'trigger_description', with: ''
|
||||
click_button 'Add trigger'
|
||||
|
||||
# See if input has error due to empty value
|
||||
expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible
|
||||
end
|
||||
|
||||
it 'adds new trigger with description' do
|
||||
fill_in 'trigger_description', with: 'trigger desc'
|
||||
click_button 'Add trigger'
|
||||
|
||||
# See if "trigger creation successful" message displayed and description and owner are correct
|
||||
expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
|
||||
expect(page.find('.triggers-list')).to have_content 'trigger desc'
|
||||
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
|
||||
end
|
||||
end
|
||||
|
||||
describe 'edit trigger workflow' do
|
||||
let(:new_trigger_title) { 'new trigger' }
|
||||
|
||||
it 'click on edit trigger opens edit trigger page' do
|
||||
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
# See if edit page has correct descrption
|
||||
find('a[title="Edit"]').send_keys(:return)
|
||||
expect(page.find('#trigger_description').value).to have_content 'trigger desc'
|
||||
end
|
||||
|
||||
it 'edit trigger and save' do
|
||||
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
# See if edit page opens, then fill in new description and save
|
||||
find('a[title="Edit"]').send_keys(:return)
|
||||
fill_in 'trigger_description', with: new_trigger_title
|
||||
click_button 'Save trigger'
|
||||
|
||||
# See if "trigger updated successfully" message displayed and description and owner are correct
|
||||
expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
|
||||
expect(page.find('.triggers-list')).to have_content new_trigger_title
|
||||
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
|
||||
end
|
||||
end
|
||||
|
||||
describe 'trigger "Revoke" workflow' do
|
||||
before do
|
||||
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
end
|
||||
|
||||
it 'button "Revoke" has correct alert' do
|
||||
expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?'
|
||||
expect(page.find('a.btn-trigger-revoke')['data-confirm']).to eq expected_alert
|
||||
end
|
||||
|
||||
it 'revoke trigger' do
|
||||
# See if "Revoke" on trigger works post trigger creation
|
||||
page.accept_confirm do
|
||||
find('a.btn-trigger-revoke').send_keys(:return)
|
||||
# See if input has error due to empty value
|
||||
expect(page.find('form.gl-show-field-errors .gl-field-error')).to be_visible
|
||||
end
|
||||
|
||||
expect(page.find('.flash-notice')).to have_content 'Trigger removed'
|
||||
expect(page).to have_selector('p.settings-message.text-center.gl-mb-3')
|
||||
it 'adds new trigger with description' do
|
||||
fill_in 'trigger_description', with: 'trigger desc'
|
||||
click_button 'Add trigger'
|
||||
|
||||
aggregate_failures 'display creation notice and trigger is created' do
|
||||
expect(page.find('.flash-notice')).to have_content 'Trigger was created successfully.'
|
||||
expect(page.find('.triggers-list')).to have_content 'trigger desc'
|
||||
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'edit trigger workflow' do
|
||||
let(:new_trigger_title) { 'new trigger' }
|
||||
|
||||
it 'click on edit trigger opens edit trigger page' do
|
||||
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
# See if edit page has correct descrption
|
||||
find('a[title="Edit"]').send_keys(:return)
|
||||
expect(page.find('#trigger_description').value).to have_content 'trigger desc'
|
||||
end
|
||||
|
||||
it 'edit trigger and save' do
|
||||
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
# See if edit page opens, then fill in new description and save
|
||||
find('a[title="Edit"]').send_keys(:return)
|
||||
fill_in 'trigger_description', with: new_trigger_title
|
||||
click_button 'Save trigger'
|
||||
|
||||
aggregate_failures 'display update notice and trigger is updated' do
|
||||
expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
|
||||
expect(page.find('.triggers-list')).to have_content new_trigger_title
|
||||
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'trigger "Revoke" workflow' do
|
||||
before do
|
||||
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
end
|
||||
|
||||
it 'button "Revoke" has correct alert' do
|
||||
expected_alert = 'By revoking a trigger you will break any processes making use of it. Are you sure?'
|
||||
expect(page.find('[data-testid="trigger_revoke_button"]')['data-confirm']).to eq expected_alert
|
||||
end
|
||||
|
||||
it 'revoke trigger' do
|
||||
# See if "Revoke" on trigger works post trigger creation
|
||||
page.accept_confirm do
|
||||
find('[data-testid="trigger_revoke_button"]').send_keys(:return)
|
||||
end
|
||||
|
||||
aggregate_failures 'trigger is removed' do
|
||||
expect(page.find('.flash-notice')).to have_content 'Trigger removed'
|
||||
expect(page).to have_css('[data-testid="no_triggers_content"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'show triggers workflow' do
|
||||
it 'contains trigger description placeholder' do
|
||||
expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
|
||||
end
|
||||
|
||||
it 'show "invalid" badge for trigger with owner having insufficient permissions' do
|
||||
create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
aggregate_failures 'has invalid badge and no edit link' do
|
||||
expect(page.find('.triggers-list')).to have_content 'invalid'
|
||||
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'do not show "Edit" or full token for not owned trigger' do
|
||||
# Create trigger with user different from current_user
|
||||
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
aggregate_failures 'shows truncated token, no clipboard button and no edit link' do
|
||||
expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
|
||||
expect(page.find('.triggers-list')).not_to have_selector('[data-testid="clipboard-btn"]')
|
||||
expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name
|
||||
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
|
||||
end
|
||||
end
|
||||
|
||||
it 'show "Edit" and full token for owned trigger' do
|
||||
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
aggregate_failures 'shows full token, clipboard button and edit link' do
|
||||
expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
|
||||
expect(page.find('.triggers-list')).to have_selector('[data-testid="clipboard-btn"]')
|
||||
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
|
||||
expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'show triggers workflow' do
|
||||
it 'contains trigger description placeholder' do
|
||||
expect(page.find('#trigger_description')['placeholder']).to eq 'Trigger description'
|
||||
context 'when ci_pipeline_triggers_settings_vue_ui is enabled' do
|
||||
it_behaves_like 'triggers page'
|
||||
end
|
||||
|
||||
context 'when ci_pipeline_triggers_settings_vue_ui is disabled' do
|
||||
before do
|
||||
stub_feature_flags(ci_pipeline_triggers_settings_vue_ui: false)
|
||||
end
|
||||
|
||||
it 'show "invalid" badge for trigger with owner having insufficient permissions' do
|
||||
create(:ci_trigger, owner: guest_user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
expect(page.find('.triggers-list')).to have_content 'invalid'
|
||||
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
|
||||
end
|
||||
|
||||
it 'do not show "Edit" or full token for not owned trigger' do
|
||||
# Create trigger with user different from current_user
|
||||
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
# See if trigger not owned by current_user shows only first few token chars and doesn't have copy-to-clipboard button
|
||||
expect(page.find('.triggers-list')).to have_content(@project.triggers.first.token[0..3])
|
||||
expect(page.find('.triggers-list')).not_to have_selector('button.btn-clipboard')
|
||||
|
||||
# See if trigger owner name doesn't match with current_user and trigger is non-editable
|
||||
expect(page.find('.triggers-list .trigger-owner')).not_to have_content user.name
|
||||
expect(page.find('.triggers-list')).not_to have_selector('a[title="Edit"]')
|
||||
end
|
||||
|
||||
it 'show "Edit" and full token for owned trigger' do
|
||||
create(:ci_trigger, owner: user, project: @project, description: trigger_title)
|
||||
visit project_settings_ci_cd_path(@project)
|
||||
|
||||
# See if trigger shows full token and has copy-to-clipboard button
|
||||
expect(page.find('.triggers-list')).to have_content @project.triggers.first.token
|
||||
expect(page.find('.triggers-list')).to have_selector('button.btn-clipboard')
|
||||
|
||||
# See if trigger owner name matches with current_user and is editable
|
||||
expect(page.find('.triggers-list .trigger-owner')).to have_content user.name
|
||||
expect(page.find('.triggers-list')).to have_selector('a[title="Edit"]')
|
||||
end
|
||||
it_behaves_like 'triggers page'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"description",
|
||||
"owner",
|
||||
"last_used",
|
||||
"has_token_exposed",
|
||||
"token",
|
||||
"can_access_project"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"owner": {
|
||||
"type": "object",
|
||||
"$ref": "user.json"
|
||||
},
|
||||
"last_used": {
|
||||
"type": ["datetime", "null"]
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"has_token_exposed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_access_project": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"edit_project_trigger_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"project_trigger_path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue