Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-05 21:08:51 +00:00
parent 3e1c760141
commit 1bc5af7661
47 changed files with 1213 additions and 242 deletions

View File

@ -1,7 +1,7 @@
<script>
import { debounce } from 'lodash';
import { initEditorLite } from '~/blob/utils';
import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance_constants';
import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
import eventHub from './eventhub';

View File

@ -1,7 +1,7 @@
<script>
/* global Mousetrap */
import 'mousetrap';
import { GlButton, GlButtonGroup } from '@gitlab/ui';
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import allDesignsMixin from '../../mixins/all_designs';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
@ -11,6 +11,9 @@ export default {
GlButton,
GlButtonGroup,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [allDesignsMixin],
props: {
id: {
@ -68,6 +71,7 @@ export default {
{{ paginationText }}
<gl-button-group class="gl-mx-5">
<gl-button
v-gl-tooltip.bottom
:disabled="!previousDesign"
:title="s__('DesignManagement|Go to previous design')"
icon="angle-left"
@ -75,6 +79,7 @@ export default {
@click="navigateToDesign(previousDesign)"
/>
<gl-button
v-gl-tooltip.bottom
:disabled="!nextDesign"
:title="s__('DesignManagement|Go to next design')"
icon="angle-right"

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@ -14,6 +14,9 @@ export default {
DesignNavigation,
DeleteButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
id: {
@ -112,14 +115,21 @@ export default {
</div>
</div>
<design-navigation :id="id" class="gl-ml-auto gl-flex-shrink-0" />
<gl-button :href="image" icon="download" />
<gl-button
v-gl-tooltip.bottom
:href="image"
icon="download"
:title="s__('DesignManagement|Download design')"
/>
<delete-button
v-if="isLatestVersion && canDeleteDesign"
v-gl-tooltip.bottom
class="gl-ml-3"
:is-deleting="isDeleting"
button-variant="warning"
button-icon="archive"
button-category="secondary"
:title="s__('DesignManagement|Archive design')"
@deleteSelectedDesigns="$emit('delete')"
/>
</header>

View File

@ -10,8 +10,8 @@ import {
WEBIDE_MEASURE_TREE_FROM_REQUEST,
WEBIDE_MEASURE_FILE_FROM_REQUEST,
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
} from '~/performance_constants';
import { performanceMarkAndMeasure } from '~/performance_utils';
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { modalTypes } from '../constants';
import eventHub from '../eventhub';
import FindFile from '~/vue_shared/components/file_finder/index.vue';

View File

@ -6,8 +6,8 @@ import {
WEBIDE_MARK_TREE_START,
WEBIDE_MEASURE_TREE_FROM_REQUEST,
WEBIDE_MARK_FILE_CLICKED,
} from '~/performance_constants';
import { performanceMarkAndMeasure } from '~/performance_utils';
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import eventHub from '../eventhub';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';

View File

@ -9,8 +9,8 @@ import {
WEBIDE_MARK_FILE_START,
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
WEBIDE_MEASURE_FILE_FROM_REQUEST,
} from '~/performance_constants';
import { performanceMarkAndMeasure } from '~/performance_utils';
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import eventHub from '../eventhub';
import {
leftSidebarViews,

View File

@ -1,7 +1,7 @@
import { languages } from 'monaco-editor';
import { flatten, isString } from 'lodash';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
import { performanceMarkAndMeasure } from '~/performance_utils';
import { performanceMarkAndMeasure } from '~/performance/utils';
const toLowerCase = x => x.toLowerCase();

View File

@ -0,0 +1,35 @@
<script>
export default {
props: {
expanded: {
type: Boolean,
required: true,
},
},
watch: {
expanded(value) {
const layoutPageEl = document.querySelector('.layout-page');
if (layoutPageEl) {
layoutPageEl.classList.toggle('right-sidebar-expanded', value);
layoutPageEl.classList.toggle('right-sidebar-collapsed', !value);
}
},
},
};
</script>
<template>
<aside
:class="{ 'right-sidebar-expanded': expanded, 'right-sidebar-collapsed': !expanded }"
class="issues-bulk-update right-sidebar"
aria-live="polite"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
<slot name="bulk-edit-actions"></slot>
</div>
<slot name="sidebar-items"></slot>
</aside>
</template>

View File

@ -1,5 +1,5 @@
<script>
import { GlLink, GlIcon, GlLabel, GlTooltipDirective } from '@gitlab/ui';
import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -14,6 +14,7 @@ export default {
GlLink,
GlIcon,
GlLabel,
GlFormCheckbox,
IssuableAssignees,
},
directives: {
@ -33,6 +34,15 @@ export default {
type: Boolean,
required: true,
},
showCheckbox: {
type: Boolean,
required: true,
},
checked: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
author() {
@ -109,8 +119,15 @@ export default {
</script>
<template>
<li class="issue px-3">
<li class="issue gl-px-5!">
<div class="issue-box">
<div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
class="gl-mr-0"
:checked="checked"
@input="$emit('checked-input', $event)"
/>
</div>
<div class="issuable-info-container">
<div class="issuable-main-info">
<div data-testid="issuable-title" class="issue-title title">

View File

@ -1,11 +1,13 @@
<script>
import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import IssuableTabs from './issuable_tabs.vue';
import IssuableItem from './issuable_item.vue';
import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
import { DEFAULT_SKELETON_COUNT } from '../constants';
@ -15,6 +17,7 @@ export default {
IssuableTabs,
FilteredSearchBar,
IssuableItem,
IssuableBulkEditSidebar,
GlPagination,
},
props: {
@ -85,6 +88,11 @@ export default {
required: false,
default: false,
},
showBulkEditSidebar: {
type: Boolean,
required: false,
default: false,
},
defaultPageSize: {
type: Number,
required: false,
@ -116,6 +124,11 @@ export default {
default: true,
},
},
data() {
return {
checkedIssuables: {},
};
},
computed: {
skeletonItemCount() {
const { totalItems, defaultPageSize, currentPage } = this;
@ -128,8 +141,40 @@ export default {
}
return DEFAULT_SKELETON_COUNT;
},
allIssuablesChecked() {
return this.bulkEditIssuables.length === this.issuables.length;
},
/**
* Returns all the checked issuables from `checkedIssuables` map.
*/
bulkEditIssuables() {
return Object.keys(this.checkedIssuables).reduce((acc, issuableId) => {
if (this.checkedIssuables[issuableId].checked) {
acc.push(this.checkedIssuables[issuableId].issuable);
}
return acc;
}, []);
},
},
watch: {
issuables(list) {
this.checkedIssuables = list.reduce((acc, issuable) => {
const id = this.issuableId(issuable);
acc[id] = {
// By default, an issuable is not checked,
// But if `checkedIssuables` is already
// populated, use existing value.
checked:
typeof this.checkedIssuables[id] !== 'boolean'
? false
: this.checkedIssuables[id].checked,
// We're caching issuable reference here
// for ease of populating in `bulkEditIssuables`.
issuable,
};
return acc;
}, {});
},
urlParams: {
deep: true,
immediate: true,
@ -144,6 +189,22 @@ export default {
},
},
},
methods: {
issuableId(issuable) {
return issuable.id || issuable.iid || uniqueId();
},
issuableChecked(issuable) {
return this.checkedIssuables[this.issuableId(issuable)]?.checked;
},
handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value;
},
handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach(issuableId => {
this.checkedIssuables[issuableId].checked = value;
});
},
},
};
</script>
@ -167,10 +228,21 @@ export default {
:sort-options="sortOptions"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
:show-checkbox="showBulkEditSidebar"
:checkbox-checked="allIssuablesChecked"
class="gl-flex-grow-1 row-content-block"
@checked-input="handleAllIssuablesCheckedInput"
@onFilter="$emit('filter', $event)"
@onSort="$emit('sort', $event)"
/>
<issuable-bulk-edit-sidebar :expanded="showBulkEditSidebar">
<template #bulk-edit-actions>
<slot name="bulk-edit-actions" :checked-issuables="bulkEditIssuables"></slot>
</template>
<template #sidebar-items>
<slot name="sidebar-items" :checked-issuables="bulkEditIssuables"></slot>
</template>
</issuable-bulk-edit-sidebar>
<div class="issuables-holder">
<ul v-if="issuablesLoading" class="content-list">
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
@ -183,10 +255,13 @@ export default {
>
<issuable-item
v-for="issuable in issuables"
:key="issuable.id"
:key="issuableId(issuable)"
:issuable-symbol="issuableSymbol"
:issuable="issuable"
:enable-label-permalinks="enableLabelPermalinks"
:show-checkbox="showBulkEditSidebar"
:checked="issuableChecked(issuable)"
@checked-input="handleIssuableCheckedInput(issuable, $event)"
>
<template #reference>
<slot name="reference" :issuable="issuable"></slot>

View File

@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { getCLS, getFID, getLCP } from 'web-vitals';
import { PERFORMANCE_TYPE_MARK, PERFORMANCE_TYPE_MEASURE } from '~/performance_constants';
import { PERFORMANCE_TYPE_MARK, PERFORMANCE_TYPE_MEASURE } from '~/performance/constants';
const initVitalsLog = () => {
const reportVital = data => {

View File

@ -9,9 +9,9 @@ import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.
import {
SNIPPET_MARK_EDIT_APP_START,
SNIPPET_MEASURE_BLOBS_CONTENT,
} from '~/performance_constants';
} from '~/performance/constants';
import eventHub from '~/blob/components/eventhub';
import { performanceMarkAndMeasure } from '~/performance_utils';
import { performanceMarkAndMeasure } from '~/performance/utils';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';

View File

@ -9,8 +9,8 @@ import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
import {
SNIPPET_MARK_VIEW_APP_START,
SNIPPET_MEASURE_BLOBS_CONTENT,
} from '~/performance_constants';
import { performanceMarkAndMeasure } from '~/performance_utils';
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import eventHub from '~/blob/components/eventhub';
import { getSnippetMixin } from '../mixins/snippets';

View File

@ -7,8 +7,8 @@ import {
SNIPPET_LEVELS_MAP,
SNIPPET_VISIBILITY,
} from '../constants';
import { performanceMarkAndMeasure } from '~/performance_utils';
import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance_constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { SNIPPET_MARK_BLOBS_CONTENT, SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
const createLocalId = () => uniqueId('blob_local_');

View File

@ -1,4 +1,4 @@
import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance_constants';
import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
import eventHub from '~/blob/components/eventhub';
export default {

View File

@ -5,6 +5,7 @@ import {
GlButton,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
@ -25,6 +26,7 @@ export default {
GlButton,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -59,6 +61,16 @@ export default {
default: '',
validator: value => value === '' || /(_desc)|(_asc)/g.test(value),
},
showCheckbox: {
type: Boolean,
required: false,
default: false,
},
checkboxChecked: {
type: Boolean,
required: false,
default: false,
},
searchInputPlaceholder: {
type: String,
required: true,
@ -291,6 +303,12 @@ export default {
<template>
<div class="vue-filtered-search-bar-container d-md-flex">
<gl-form-checkbox
v-if="showCheckbox"
class="gl-align-self-center"
:checked="checkboxChecked"
@input="$emit('checked-input', $event)"
/>
<gl-filtered-search
ref="filteredSearchInput"
v-model="filterValue"

View File

@ -4,10 +4,16 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
ALLOWED_SEGMENT_COUNT = 20
has_many :segment_selections
has_many :groups, through: :segment_selections
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validate :validate_segment_count
accepts_nested_attributes_for :segment_selections, allow_destroy: true
scope :ordered_by_name, -> { order(:name) }
scope :with_groups, -> { preload(:groups) }
private
def validate_segment_count

View File

@ -70,6 +70,8 @@
= render 'shared/members/sort_dropdown'
- if vue_members_list_enabled
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member',
@ -86,6 +88,8 @@
= html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled
.js-group-linked-list{ data: linked_groups_list_data_attributes(@group) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list{ data: { qa_selector: 'groups_list' } }
- @group.shared_with_group_links.each do |group_link|
@ -100,6 +104,8 @@
= render 'shared/members/search_field', name: 'search_invited'
- if vue_members_list_enabled
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list
= render partial: 'shared/members/member',
@ -116,6 +122,8 @@
= html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- if vue_members_list_enabled
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
.loading
.spinner.spinner-md
- else
%ul.content-list.members-list
= render partial: 'shared/members/member',

View File

@ -0,0 +1,5 @@
---
title: Add tooltips to design buttons
merge_request: 46922
author: Lee Tickett
type: added

View File

@ -6025,6 +6025,81 @@ type DetailedStatus {
tooltip: String
}
"""
Segment
"""
type DevopsAdoptionSegment {
"""
Assigned groups
"""
groups(
"""
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
"""
Returns the last _n_ elements from the list.
"""
last: Int
): GroupConnection
"""
ID of the segment
"""
id: ID!
"""
Name of the segment
"""
name: String!
}
"""
The connection type for DevopsAdoptionSegment.
"""
type DevopsAdoptionSegmentConnection {
"""
A list of edges.
"""
edges: [DevopsAdoptionSegmentEdge]
"""
A list of nodes.
"""
nodes: [DevopsAdoptionSegment]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type DevopsAdoptionSegmentEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: DevopsAdoptionSegment
}
input DiffImagePositionInput {
"""
Merge base of the branch the comment was made on
@ -9165,6 +9240,41 @@ type Group {
webUrl: String!
}
"""
The connection type for Group.
"""
type GroupConnection {
"""
A list of edges.
"""
edges: [GroupEdge]
"""
A list of nodes.
"""
nodes: [Group]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type GroupEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Group
}
"""
Identifier of Group
"""
@ -16540,6 +16650,31 @@ type Query {
"""
designManagement: DesignManagement!
"""
Get configured DevOps adoption segments on the instance
"""
devopsAdoptionSegments(
"""
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
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DevopsAdoptionSegmentConnection
"""
Text to echo back
"""

View File

@ -16527,6 +16527,220 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DevopsAdoptionSegment",
"description": "Segment",
"fields": [
{
"name": "groups",
"description": "Assigned groups",
"args": [
{
"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": "GroupConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the segment",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the segment",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DevopsAdoptionSegmentConnection",
"description": "The connection type for DevopsAdoptionSegment.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DevopsAdoptionSegmentEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "DevopsAdoptionSegment",
"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": "DevopsAdoptionSegmentEdge",
"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": "DevopsAdoptionSegment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DiffImagePositionInput",
@ -24901,6 +25115,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupConnection",
"description": "The connection type for Group.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GroupEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Group",
"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": "GroupEdge",
"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": "Group",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "GroupID",
@ -48034,6 +48360,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "devopsAdoptionSegments",
"description": "Get configured DevOps adoption segments on the instance",
"args": [
{
"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": "DevopsAdoptionSegmentConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "echo",
"description": "Text to echo back",

View File

@ -1000,6 +1000,16 @@ Autogenerated return type of DestroySnippet.
| `text` | String | Text of the status |
| `tooltip` | String | Tooltip associated with the status |
### DevopsAdoptionSegment
Segment.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `groups` | GroupConnection | Assigned groups |
| `id` | ID! | ID of the segment |
| `name` | String! | Name of the segment |
### DiffPosition
| Field | Type | Description |

View File

@ -945,3 +945,7 @@ bin/rake gitlab:usage_data:dump_sql_in_json
# You may pipe the output into a file
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
```
## Generating and troubleshooting usage ping
To get a usage ping, or to troubleshoot caching issues on your GitLab instance, please follow [instructions to generate usage ping](../../administration/troubleshooting/gitlab_rails_cheat_sheet.md#generate-usage-ping).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -4,34 +4,38 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab NuGet Repository
# NuGet packages in the Package Registry
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/20050) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Core in 13.3.
With the GitLab NuGet Repository, every project can have its own space to store NuGet packages.
Publish NuGet packages in your projects Package Registry. Then, install the
packages whenever you need to use them as a dependency.
The GitLab NuGet Repository works with:
The Package Registry works with:
- [NuGet CLI](https://docs.microsoft.com/en-us/nuget/reference/nuget-exe-cli-reference)
- [.NET Core CLI](https://docs.microsoft.com/en-us/dotnet/core/tools/)
- [Visual Studio](https://visualstudio.microsoft.com/vs/)
## Setting up your development environment
## Install NuGet
[NuGet CLI 5.1 or later](https://www.nuget.org/downloads) is required. Earlier versions have not been tested
against the GitLab NuGet Repository and might not work. If you have [Visual Studio](https://visualstudio.microsoft.com/vs/),
NuGet CLI is probably already installed.
The required minimum versions are:
Alternatively, you can use [.NET SDK 3.0 or later](https://dotnet.microsoft.com/download/dotnet-core/3.0), which installs NuGet CLI.
- [NuGet CLI 5.1 or later](https://www.nuget.org/downloads). If you have
[Visual Studio](https://visualstudio.microsoft.com/vs/), the NuGet CLI is
probably already installed.
- Alternatively, you can use [.NET SDK 3.0 or later](https://dotnet.microsoft.com/download/dotnet-core/3.0),
which installs the NuGet CLI.
- NuGet protocol version 3 or later.
You can confirm that [NuGet CLI](https://www.nuget.org/) is properly installed with:
Verify that the [NuGet CLI](https://www.nuget.org/) is installed by running:
```shell
nuget help
```
You should see something similar to:
The output should be similar to:
```plaintext
NuGet Version: 5.1.0.6013
@ -43,103 +47,98 @@ Available commands:
[output truncated]
```
NOTE: **Note:**
GitLab currently only supports NuGet's protocol version 3. Earlier versions are not supported.
### Install NuGet on macOS
### macOS support
For macOS, you can use [Mono](https://www.mono-project.com/) to run the
NuGet CLI.
For macOS, you can also use [Mono](https://www.mono-project.com/) to run
the NuGet CLI. For Homebrew users, run `brew install mono` to install
Mono. Then you should be able to download the Windows C# binary
`nuget.exe` from the [NuGet CLI page](https://www.nuget.org/downloads)
and run:
1. If you use Homebrew, to install Mono, run `brew install mono`.
1. Download the Windows C# binary `nuget.exe` from the [NuGet CLI page](https://www.nuget.org/downloads).
1. Run this command:
```shell
mono nuget.exe
```
```shell
mono nuget.exe
```
## Enabling the NuGet Repository
## Add the Package Registry as a source for NuGet packages
NOTE: **Note:**
This option is available only if your GitLab administrator has
[enabled support for the Package Registry](../../../administration/packages/index.md).
To publish and install packages to the Package Registry, you must add the
Package Registry as a source for your packages.
When the NuGet Repository is enabled, it is available for all new projects
by default. To enable it for existing projects, or if you want to disable it:
1. Navigate to your project's **Settings > General > Visibility, project features, permissions**.
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
You should then be able to see the **Packages & Registries** section on the left sidebar.
## Adding the GitLab NuGet Repository as a source to NuGet
You need the following:
Prerequisites:
- Your GitLab username.
- A personal access token or deploy token. For repository authentication:
- You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api`.
- You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
- A suitable name for your source.
- Your project ID which can be found on the home page of your project.
- You can generate a [personal access token](../../../user/profile/personal_access_tokens.md)
with the scope set to `api`.
- You can generate a [deploy token](./../../project/deploy_tokens/index.md)
with the scope set to `read_package_registry`, `write_package_registry`, or
both.
- A name for your source.
- Your project ID, which is found on your project's home page.
You can now add a new source to NuGet with:
- [NuGet CLI](#add-nuget-repository-source-with-nuget-cli)
- [Visual Studio](#add-nuget-repository-source-with-visual-studio).
- [.NET CLI](#add-nuget-repository-source-with-net-cli)
- [NuGet CLI](#add-a-source-with-the-nuget-cli)
- [Visual Studio](#add-a-source-with-visual-studio)
- [.NET CLI](#add-a-source-with-the-net-cli)
### Add NuGet Repository source with NuGet CLI
### Add a source with the NuGet CLI
To add the GitLab NuGet Repository as a source with `nuget`:
To add the Package Registry as a source with `nuget`:
```shell
nuget source Add -Name <source_name> -Source "https://gitlab-instance.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
```
Where:
- `<source_name>` is your desired source name.
- `<source_name>` is the desired source name.
For example:
```shell
nuget source Add -Name "GitLab" -Source "https://gitlab.example/api/v4/projects/10/packages/nuget/index.json" -UserName carol -Password 12345678asdf
nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/projects/10/packages/nuget/index.json" -UserName carol -Password 12345678asdf
```
### Add NuGet Repository source with Visual Studio
### Add a source with Visual Studio
To add the Package Registry as a source with Visual Studio:
1. Open [Visual Studio](https://visualstudio.microsoft.com/vs/).
1. Open the **FILE > OPTIONS** (Windows) or **Visual Studio > Preferences** (Mac OS).
1. In the **NuGet** section, open **Sources** to see a list of all your NuGet sources.
1. Click **Add**.
1. Fill the fields with:
- **Name**: Desired name for the source
- **Location**: `https://gitlab.com/api/v4/projects/<your_project_id>/packages/nuget/index.json`
- Replace `<your_project_id>` with your project ID.
- If you have a self-managed GitLab installation, replace `gitlab.com` with your domain name.
- **Username**: Your GitLab username or deploy token username
- **Password**: Your personal access token or deploy token
1. In Windows, select **File > Options**. On macOS, select **Visual Studio > Preferences**.
1. In the **NuGet** section, select **Sources** to view a list of all your NuGet sources.
1. Select **Add**.
1. Complete the following fields:
- **Name**: Name for the source.
- **Location**: `https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json`,
where `<your_project_id>` is your project ID, and `gitlab.example.com` is
your domain name.
- **Username**: Your GitLab username or deploy token username.
- **Password**: Your personal access token or deploy token.
![Visual Studio Adding a NuGet source](img/visual_studio_adding_nuget_source.png)
1. Click **Save**.
![Visual Studio NuGet source added](img/visual_studio_nuget_source_added.png)
The source is displayed in your list.
In case of any warning, please make sure that the **Location**, **Username**, and **Password** are correct.
![Visual Studio NuGet source added](img/visual_studio_nuget_source_added.png)
### Add NuGet Repository source with .NET CLI
If you get a warning, ensure that the **Location**, **Username**, and
**Password** are correct.
To add the GitLab NuGet Repository as a source for .NET, create a file named `nuget.config` in the root of your project with the following content:
### Add a source with the .NET CLI
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
To add the Package Registry as a source for .NET:
1. In the root of your project, create a file named `nuget.config`.
1. Add this content:
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="gitlab" value="https://gitlab-instance.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" />
<add key="gitlab" value="https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<gitlab>
@ -147,46 +146,51 @@ To add the GitLab NuGet Repository as a source for .NET, create a file named `nu
<add key="ClearTextPassword" value="<gitlab_personal_access_token or deploy_token>" />
</gitlab>
</packageSourceCredentials>
</configuration>
```
</configuration>
```
## Uploading packages
## Publish a NuGet package
When uploading packages, note that:
When publishing packages:
- The Package Registry on GitLab.com can store up to 500 MB of content. This limit is [configurable for self-managed GitLab instances](../../../administration/instance_limits.md#package-registry-limits).
- If you upload the same package with the same version multiple times, each consecutive upload
is saved as a separate file. When installing a package, GitLab serves the most recent file.
- When uploading packages to GitLab, they are not displayed in the packages UI of your project
immediately. It can take up to 10 minutes to process a package.
- The Package Registry on GitLab.com can store up to 500 MB of content.
This limit is [configurable for self-managed GitLab instances](../../../administration/instance_limits.md#package-registry-limits).
- If you publish the same package with the same version multiple times, each
consecutive upload is saved as a separate file. When installing a package,
GitLab serves the most recent file.
- When publishing packages to GitLab, they aren't displayed in the packages user
interface of your project immediately. It can take up to 10 minutes to process
a package.
### Upload packages with NuGet CLI
### Publish a package with the NuGet CLI
This section assumes that your project is properly built and you already [created a NuGet package with NuGet CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package).
Upload your package using the following command:
Prerequisite:
- [A NuGet package created with NuGet CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package).
Publish a package by running this command:
```shell
nuget push <package_file> -Source <source_name>
```
Where:
- `<package_file>` is your package filename, ending in `.nupkg`.
- `<source_name>` is the [source name used during setup](#adding-the-gitlab-nuget-repository-as-a-source-to-nuget).
- `<source_name>` is the [source name used during setup](#add-a-source-with-the-nuget-cli).
### Upload packages with .NET CLI
### Publish a package with the .NET CLI
This section assumes that your project is properly built and you already [created a NuGet package with .NET CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package-dotnet-cli).
Upload your package using the following command:
Prerequisite:
[A NuGet package created with .NET CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package-dotnet-cli).
Publish a package by running this command:
```shell
dotnet nuget push <package_file> --source <source_name>
```
Where:
- `<package_file>` is your package filename, ending in `.nupkg`.
- `<source_name>` is the [source name used during setup](#adding-the-gitlab-nuget-repository-as-a-source-to-nuget).
- `<source_name>` is the [source name used during setup](#add-a-source-with-the-net-cli).
For example:
@ -194,58 +198,16 @@ For example:
dotnet nuget push MyPackage.1.0.0.nupkg --source gitlab
```
## Install packages
### Install a package with NuGet CLI
CAUTION: **Warning:**
By default, `nuget` checks the official source at `nuget.org` first. If you have a package in the
GitLab NuGet Repository with the same name as a package at `nuget.org`, you must specify the source
name to install the correct package.
Install the latest version of a package using the following command:
```shell
nuget install <package_id> -OutputDirectory <output_directory> \
-Version <package_version> \
-Source <source_name>
```
Where:
- `<package_id>` is the package ID.
- `<output_directory>` is the output directory, where the package is installed.
- `<package_version>` (Optional) is the package version.
- `<source_name>` (Optional) is the source name.
### Install a package with .NET CLI
CAUTION: **Warning:**
If you have a package in the GitLab NuGet Repository with the same name as a package at a different source,
you should verify the order in which `dotnet` checks sources during install. This is defined in the
`nuget.config` file.
Install the latest version of a package using the following command:
```shell
dotnet add package <package_id> \
-v <package_version>
```
Where:
- `<package_id>` is the package ID.
- `<package_version>` (Optional) is the package version.
## Publishing a NuGet package with CI/CD
### Publish a NuGet package by using CI/CD
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36424) in GitLab 13.3.
If youre using NuGet with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token.
The token inherits the permissions of the user that generates the pipeline.
If youre using NuGet with GitLab CI/CD, a CI job token can be used instead of a
personal access token or deploy token. The token inherits the permissions of the
user that generates the pipeline.
This example shows how to create a new package each time the `master` branch
is updated:
This example shows how to create a new package each time the `master` branch is
updated:
1. Add a `deploy` job to your `.gitlab-ci.yml` file:
@ -267,4 +229,43 @@ is updated:
- master
```
1. Commit the changes and push it to your GitLab repository to trigger a new CI build.
1. Commit the changes and push it to your GitLab repository to trigger a new CI/CD build.
## Install packages
### Install a package with the NuGet CLI
CAUTION: **Warning:**
By default, `nuget` checks the official source at `nuget.org` first. If you have
a NuGet package in the Package Registry with the same name as a package at
`nuget.org`, you must specify the source name to install the correct package.
Install the latest version of a package by running this command:
```shell
nuget install <package_id> -OutputDirectory <output_directory> \
-Version <package_version> \
-Source <source_name>
```
- `<package_id>` is the package ID.
- `<output_directory>` is the output directory, where the package is installed.
- `<package_version>` The package version. Optional.
- `<source_name>` The source name. Optional.
### Install a package with the .NET CLI
CAUTION: **Warning:**
If you have a package in the Package Registry with the same name as a package at
a different source, verify the order in which `dotnet` checks sources during
install. This is defined in the `nuget.config` file.
Install the latest version of a package by running this command:
```shell
dotnet add package <package_id> \
-v <package_version>
```
- `<package_id>` is the package ID.
- `<package_version>` is the package version. Optional.

View File

@ -31,23 +31,31 @@ authenticate with GitLab by using the `CI_JOB_TOKEN`.
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#create-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publish-an-npm-package-by-using-cicd), [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd), [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages), and [generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd).
Learn more about using CI/CD to build:
If you use CI/CD to build a package, extended activity
information is displayed when you view the package details:
- [Maven packages](../maven_repository/index.md#create-maven-packages-with-gitlab-cicd)
- [NPM packages](../npm_registry/index.md#publish-an-npm-package-by-using-cicd)
- [Composer packages](../composer_repository/index.md#publish-a-composer-package-by-using-cicd)
- [NuGet packages](../nuget_repository/index.md#publish-a-nuget-package-by-using-cicd)
- [Conan packages](../conan_repository/index.md#publish-a-conan-package-by-using-cicd)
- [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages)
- [Generic packages](../generic_packages/index.md#publish-a-generic-package-by-using-cicd)
If you use CI/CD to build a package, extended activity information is displayed
when you view the package details:
![Package CI/CD activity](img/package_activity_v12_10.png)
When using Maven and NPM, you can view which pipeline published the package, as well as the commit and
user who triggered it.
When using Maven and NPM, you can view which pipeline published the package, and
the commit and user who triggered it.
## Download a package
To download a package:
1. Go to **Packages & Registries > Package Registry**.
1. Click the name of the package you want to download.
1. In the **Activity** section, click the name of the package you want to download.
1. Select the name of the package you want to download.
1. In the **Activity** section, select the name of the package you want to download.
## Delete a package

View File

@ -9219,6 +9219,9 @@ msgstr ""
msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version."
msgstr ""
msgid "DesignManagement|Archive design"
msgstr ""
msgid "DesignManagement|Archive designs"
msgstr ""
@ -9276,6 +9279,9 @@ msgstr ""
msgid "DesignManagement|Discard comment"
msgstr ""
msgid "DesignManagement|Download design"
msgstr ""
msgid "DesignManagement|Error uploading a new design. Please try again."
msgstr ""

View File

@ -19,6 +19,7 @@ module QA
autoload :Saml, 'qa/flow/saml'
autoload :User, 'qa/flow/user'
autoload :MergeRequest, 'qa/flow/merge_request'
autoload :Pipeline, 'qa/flow/pipeline'
end
##

17
qa/qa/flow/pipeline.rb Normal file
View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module QA
module Flow
module Pipeline
module_function
# In some cases we don't need to wait for anything, blocked, running or pending is acceptable
# Some cases only need pipeline to finish with different condition (completion, success or replication)
def visit_latest_pipeline(pipeline_condition: nil)
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:"wait_for_latest_pipeline_#{pipeline_condition}") if pipeline_condition
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
end
end
end
end

View File

@ -65,8 +65,7 @@ module QA
)
end.project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
{
'test-success': :passed,

View File

@ -29,7 +29,7 @@ module QA
Flow::Login.sign_in
add_ci_files
project.visit!
view_the_last_pipeline
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'success')
end
after do
@ -64,12 +64,6 @@ module QA
end
end
def view_the_last_pipeline
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
end
def parent_ci_file
{
file_path: '.gitlab-ci.yml',

View File

@ -57,8 +57,7 @@ module QA
end
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('create_package')

View File

@ -94,8 +94,7 @@ module QA
end
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('deploy')

View File

@ -61,8 +61,7 @@ module QA
end
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('deploy')

View File

@ -74,8 +74,7 @@ module QA
end
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('run')

View File

@ -77,8 +77,7 @@ module QA
sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform(&:click_on_first_job)
Page::Project::Job::Show.perform do |job|

View File

@ -27,7 +27,7 @@ module QA
it 'parent pipelines passes if child passes', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/751' do
add_ci_files(success_child_ci_file)
view_pipelines
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'completion')
Page::Project::Pipeline::Show.perform do |parent_pipeline|
expect(parent_pipeline).to have_child_pipeline
@ -37,7 +37,7 @@ module QA
it 'parent pipeline fails if child fails', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/752' do
add_ci_files(fail_child_ci_file)
view_pipelines
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'completion')
Page::Project::Pipeline::Show.perform do |parent_pipeline|
expect(parent_pipeline).to have_child_pipeline
@ -47,12 +47,6 @@ module QA
private
def view_pipelines
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_completion)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
end
def success_child_ci_file
{
file_path: '.child-ci.yml',

View File

@ -27,7 +27,7 @@ module QA
it 'parent pipelines passes if child passes', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/754' do
add_ci_files(success_child_ci_file)
view_pipelines
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'completion')
Page::Project::Pipeline::Show.perform do |parent_pipeline|
expect(parent_pipeline).to have_child_pipeline
@ -37,7 +37,7 @@ module QA
it 'parent pipeline passes even if child fails', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/753' do
add_ci_files(fail_child_ci_file)
view_pipelines
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'completion')
Page::Project::Pipeline::Show.perform do |parent_pipeline|
expect(parent_pipeline).to have_child_pipeline
@ -47,12 +47,6 @@ module QA
private
def view_pipelines
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_completion)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
end
def success_child_ci_file
{
file_path: '.child-ci.yml',

View File

@ -54,8 +54,7 @@ module QA
push.commit_message = 'Create Auto DevOps compatible rack application'
end
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('build')
@ -119,8 +118,7 @@ module QA
end
it 'runs an AutoDevOps pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/444' do
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).to have_tag('Auto DevOps')

View File

@ -46,6 +46,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d"
icon="download"
size="medium"
title="Download design"
variant="default"
/>
@ -57,6 +58,7 @@ exports[`Design management toolbar component renders design and updated data 1`]
buttonvariant="warning"
class="gl-ml-3"
hasselecteddesigns="true"
title="Archive design"
/>
</header>
`;

View File

@ -0,0 +1,97 @@
import { shallowMount } from '@vue/test-utils';
import IssuableBulkEditSidebar from '~/issuable_list/components/issuable_bulk_edit_sidebar.vue';
const createComponent = ({ expanded = true } = {}) =>
shallowMount(IssuableBulkEditSidebar, {
propsData: {
expanded,
},
slots: {
'bulk-edit-actions': `
<button class="js-edit-issuables">Edit issuables</button>
`,
'sidebar-items': `
<button class="js-sidebar-dropdown">Labels</button>
`,
},
});
describe('IssuableBulkEditSidebar', () => {
let wrapper;
beforeEach(() => {
setFixtures('<div class="layout-page right-sidebar-collapsed"></div>');
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('watch', () => {
describe('expanded', () => {
it.each`
expanded | layoutPageClass
${true} | ${'right-sidebar-expanded'}
${false} | ${'right-sidebar-collapsed'}
`(
'sets class "$layoutPageClass" on element `.layout-page` when expanded prop is $expanded',
async ({ expanded, layoutPageClass }) => {
const wrappeCustom = createComponent({
expanded: !expanded,
});
// We need to manually flip the value of `expanded` for
// watcher to trigger.
wrappeCustom.setProps({
expanded,
});
await wrappeCustom.vm.$nextTick();
expect(document.querySelector('.layout-page').classList.contains(layoutPageClass)).toBe(
true,
);
wrappeCustom.destroy();
},
);
});
});
describe('template', () => {
it.each`
expanded | layoutPageClass
${true} | ${'right-sidebar-expanded'}
${false} | ${'right-sidebar-collapsed'}
`(
'renders component container with class "$layoutPageClass" when expanded prop is $expanded',
async ({ expanded, layoutPageClass }) => {
const wrappeCustom = createComponent({
expanded: !expanded,
});
// We need to manually flip the value of `expanded` for
// watcher to trigger.
wrappeCustom.setProps({
expanded,
});
await wrappeCustom.vm.$nextTick();
expect(wrappeCustom.classes()).toContain(layoutPageClass);
wrappeCustom.destroy();
},
);
it('renders contents for slot `bulk-edit-actions`', () => {
expect(wrapper.find('button.js-edit-issuables').exists()).toBe(true);
});
it('renders contents for slot `sidebar-items`', () => {
expect(wrapper.find('button.js-sidebar-dropdown').exists()).toBe(true);
});
});
});

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlLabel } from '@gitlab/ui';
import { GlLink, GlLabel, GlFormCheckbox } from '@gitlab/ui';
import IssuableItem from '~/issuable_list/components/issuable_item.vue';
import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
@ -12,6 +12,7 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots
issuableSymbol,
issuable,
enableLabelPermalinks: true,
showCheckbox: false,
},
slots,
});
@ -196,6 +197,25 @@ describe('IssuableItem', () => {
expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title);
});
it('renders checkbox when `showCheckbox` prop is true', async () => {
wrapper.setProps({
showCheckbox: true,
});
await wrapper.vm.$nextTick();
expect(wrapper.find(GlFormCheckbox).exists()).toBe(true);
expect(wrapper.find(GlFormCheckbox).attributes('checked')).not.toBeDefined();
wrapper.setProps({
checked: true,
});
await wrapper.vm.$nextTick();
expect(wrapper.find(GlFormCheckbox).attributes('checked')).toBe('true');
});
it('renders issuable title with `target` set as "_blank" when issuable.webUrl is external', async () => {
wrapper.setProps({
issuable: {

View File

@ -8,11 +8,14 @@ import IssuableTabs from '~/issuable_list/components/issuable_tabs.vue';
import IssuableItem from '~/issuable_list/components/issuable_item.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { mockIssuableListProps } from '../mock_data';
import { mockIssuableListProps, mockIssuables } from '../mock_data';
const createComponent = (propsData = mockIssuableListProps) =>
const createComponent = ({ props = mockIssuableListProps, data = {} } = {}) =>
mount(IssuableListRoot, {
propsData,
propsData: props,
data() {
return data;
},
slots: {
'nav-actions': `
<button class="js-new-issuable">New issuable</button>
@ -35,6 +38,14 @@ describe('IssuableListRoot', () => {
});
describe('computed', () => {
const mockCheckedIssuables = {
[mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
[mockIssuables[1].iid]: { checked: true, issuable: mockIssuables[1] },
[mockIssuables[2].iid]: { checked: true, issuable: mockIssuables[2] },
};
const mIssuables = [mockIssuables[0], mockIssuables[1], mockIssuables[2]];
describe('skeletonItemCount', () => {
it.each`
totalItems | defaultPageSize | currentPage | returnValue
@ -57,9 +68,62 @@ describe('IssuableListRoot', () => {
},
);
});
describe('allIssuablesChecked', () => {
it.each`
checkedIssuables | issuables | specTitle | returnValue
${mockCheckedIssuables} | ${mIssuables} | ${'same as'} | ${true}
${{}} | ${mIssuables} | ${'not same as'} | ${false}
`(
'returns $returnValue when bulkEditIssuables count is $specTitle issuables count',
async ({ checkedIssuables, issuables, returnValue }) => {
wrapper.setProps({
issuables,
});
await wrapper.vm.$nextTick();
wrapper.setData({
checkedIssuables,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.allIssuablesChecked).toBe(returnValue);
},
);
});
describe('bulkEditIssuables', () => {
it('returns array of issuables which have `checked` set to true within checkedIssuables map', async () => {
wrapper.setData({
checkedIssuables: mockCheckedIssuables,
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.bulkEditIssuables).toHaveLength(mIssuables.length);
});
});
});
describe('watch', () => {
describe('issuables', () => {
it('populates `checkedIssuables` prop with all issuables', async () => {
wrapper.setProps({
issuables: [mockIssuables[0]],
});
await wrapper.vm.$nextTick();
expect(Object.keys(wrapper.vm.checkedIssuables)).toHaveLength(1);
expect(wrapper.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({
checked: false,
issuable: mockIssuables[0],
});
});
});
describe('urlParams', () => {
it('updates window URL reflecting props within `urlParams`', async () => {
const urlParams = {
@ -82,6 +146,30 @@ describe('IssuableListRoot', () => {
});
});
describe('methods', () => {
describe('issuableId', () => {
it('returns id value from provided issuable object', () => {
expect(wrapper.vm.issuableId({ id: 1 })).toBe(1);
expect(wrapper.vm.issuableId({ iid: 1 })).toBe(1);
expect(wrapper.vm.issuableId({})).toBeDefined();
});
});
describe('issuableChecked', () => {
it('returns boolean value representing checked status of issuable item', async () => {
wrapper.setData({
checkedIssuables: {
[mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
},
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.issuableChecked(mockIssuables[0])).toBe(true);
});
});
});
describe('template', () => {
it('renders component container element with class "issuable-list-container"', () => {
expect(wrapper.classes()).toContain('issuable-list-container');
@ -183,12 +271,44 @@ describe('IssuableListRoot', () => {
});
describe('events', () => {
let wrapperChecked;
beforeEach(() => {
wrapperChecked = createComponent({
data: {
checkedIssuables: {
[mockIssuables[0].iid]: { checked: true, issuable: mockIssuables[0] },
},
},
});
});
afterEach(() => {
wrapperChecked.destroy();
});
it('issuable-tabs component emits `click-tab` event on `click-tab` event', () => {
wrapper.find(IssuableTabs).vm.$emit('click');
expect(wrapper.emitted('click-tab')).toBeTruthy();
});
it('sets all issuables as checked when filtered-search-bar component emits `checked-input` event', async () => {
const searchEl = wrapperChecked.find(FilteredSearchBar);
searchEl.vm.$emit('checked-input', true);
await wrapperChecked.vm.$nextTick();
expect(searchEl.emitted('checked-input')).toBeTruthy();
expect(searchEl.emitted('checked-input').length).toBe(1);
expect(wrapperChecked.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({
checked: true,
issuable: mockIssuables[0],
});
});
it('filtered-search-bar component emits `filter` event on `onFilter` & `sort` event on `onSort` events', () => {
const searchEl = wrapper.find(FilteredSearchBar);
@ -198,6 +318,22 @@ describe('IssuableListRoot', () => {
expect(wrapper.emitted('sort')).toBeTruthy();
});
it('sets an issuable as checked when issuable-item component emits `checked-input` event', async () => {
const issuableItem = wrapperChecked.findAll(IssuableItem).at(0);
issuableItem.vm.$emit('checked-input', true);
await wrapperChecked.vm.$nextTick();
expect(issuableItem.emitted('checked-input')).toBeTruthy();
expect(issuableItem.emitted('checked-input').length).toBe(1);
expect(wrapperChecked.vm.checkedIssuables[mockIssuables[0].iid]).toEqual({
checked: true,
issuable: mockIssuables[0],
});
});
it('gl-pagination component emits `page-change` event on `input` event', async () => {
wrapper.setProps({
showPaginationControls: true,

View File

@ -1,5 +1,12 @@
import { shallowMount, mount } from '@vue/test-utils';
import { GlFilteredSearch, GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import {
GlFilteredSearch,
GlButtonGroup,
GlButton,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
} from '@gitlab/ui';
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { uniqueTokens } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@ -30,6 +37,8 @@ const createComponent = ({
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
sortOptions,
showCheckbox = false,
checkboxChecked = false,
searchInputPlaceholder = 'Filter requirements',
} = {}) => {
const mountMethod = shallow ? shallowMount : mount;
@ -40,6 +49,8 @@ const createComponent = ({
recentSearchesStorageKey,
tokens,
sortOptions,
showCheckbox,
checkboxChecked,
searchInputPlaceholder,
},
});
@ -364,6 +375,26 @@ describe('FilteredSearchBarRoot', () => {
expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
});
it('renders checkbox when `showCheckbox` prop is true', async () => {
let wrapperWithCheckbox = createComponent({
showCheckbox: true,
});
expect(wrapperWithCheckbox.find(GlFormCheckbox).exists()).toBe(true);
expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).not.toBeDefined();
wrapperWithCheckbox.destroy();
wrapperWithCheckbox = createComponent({
showCheckbox: true,
checkboxChecked: true,
});
expect(wrapperWithCheckbox.find(GlFormCheckbox).attributes('checked')).toBe('true');
wrapperWithCheckbox.destroy();
});
it('renders search history items dropdown with formatting done using token symbols', async () => {
const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false });
wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]);

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Analytics::DevopsAdoption::Segment, type: :model do
subject { build(:devops_adoption_segment) }
describe 'validation' do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
context 'limit the number of segments' do
subject { build(:devops_adoption_segment) }
before do
create_list(:devops_adoption_segment, 2)
stub_const("#{described_class}::ALLOWED_SEGMENT_COUNT", 2)
end
it 'shows validation error' do
subject.validate
expect(subject.errors[:name]).to eq([s_('DevopsAdoptionSegment|The maximum number of segments has been reached')])
end
end
end
end