Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-28 18:09:40 +00:00
parent 34b3acb5a3
commit dce5758796
118 changed files with 2165 additions and 389 deletions

View File

@ -1 +1 @@
9096b269b6abfa8cbe8cf6c48a03e3bec93d47ae
69c5d9bad86e9402d5e2b0d7c09e88fd5603b572

View File

@ -1 +1 @@
8.47.0
8.48.0

View File

@ -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,

View File

@ -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>

View File

@ -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,
},
});
},
});
};

View File

@ -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'),

View File

@ -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"
/>

View File

@ -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>

View File

@ -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) {

View File

@ -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>

View File

@ -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 }) => {

View File

@ -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();
});

View File

@ -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>

View File

@ -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>

View File

@ -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"
>

View File

@ -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>

View File

@ -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" />

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -20,6 +20,7 @@ export default () => {
return createElement('app', {
props: {
features: whatsNewElm.getAttribute('data-features'),
storageKey: whatsNewElm.getAttribute('data-storage-key'),
},
});
},

View File

@ -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));
}
},
};

View File

@ -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;
}

View File

@ -5,6 +5,10 @@
}
}
.trigger-description {
max-width: 100px;
}
.trigger-actions {
white-space: nowrap;

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Ci
class TriggerSerializer < BaseSerializer
entity ::Ci::TriggerEntity
end
end

View File

@ -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

View File

@ -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 : '' } }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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"

View File

@ -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!

View File

@ -0,0 +1,5 @@
---
title: Remove bootstrap from pages/form
merge_request: 43442
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Clean up unused LFS objects during repository housekeeping
merge_request: 40979
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add assignee usernames to issue resolver
merge_request: 43294
author:
type: changed

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Update popover to gl-popover on WebIDE commit message
merge_request: 43499
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Update GitLab Workhorse to v8.48.0
merge_request: 43586
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Remove retry icon on failed job if merge pipeline
merge_request: 42495
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add approval rules with approvers to usage ping
merge_request: 36737
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Improve issuable reaction search
merge_request: 42321
author: Ethan Reesor (@firelizzard)
type: added

View File

@ -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.

View File

@ -1,7 +0,0 @@
---
name: api_kaminari_count_with_limit
introduced_by_url:
rollout_issue_url:
group:
type: development
default_enabled: false

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
dd630c76819641ad64a5f6ae40ad4f49e7fbe1c783398d97886dc7e9852a245e

View File

@ -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);

View File

@ -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 | |

View File

@ -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**.
![Housekeeping settings](img/housekeeping_settings.png)

View File

@ -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 {
"""

View File

@ -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",

View File

@ -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 |
| ----- | ---- | ----------- |

View File

@ -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 |

View File

@ -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)**

View File

@ -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!

View File

@ -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.

View File

@ -31,7 +31,7 @@ More useful links:
- The usage data is primarily composed of row counts for different tables in the instances 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.

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

BIN
geo_architecture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

31
qa/qa/resource/design.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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|

View File

@ -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

BIN
qa/spec/fixtures/tanuki.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
qa/spec/fixtures/values.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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