@@ -183,10 +255,13 @@ export default {
>
diff --git a/app/assets/javascripts/performance_constants.js b/app/assets/javascripts/performance/constants.js
similarity index 100%
rename from app/assets/javascripts/performance_constants.js
rename to app/assets/javascripts/performance/constants.js
diff --git a/app/assets/javascripts/performance_utils.js b/app/assets/javascripts/performance/utils.js
similarity index 100%
rename from app/assets/javascripts/performance_utils.js
rename to app/assets/javascripts/performance/utils.js
diff --git a/app/assets/javascripts/performance_bar/performance_bar_log.js b/app/assets/javascripts/performance_bar/performance_bar_log.js
index 55b4d626e56..3ba7ff1c221 100644
--- a/app/assets/javascripts/performance_bar/performance_bar_log.js
+++ b/app/assets/javascripts/performance_bar/performance_bar_log.js
@@ -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 => {
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 9c9dc1b36ba..08683f25651 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -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';
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index 4a2f060ff7c..a3e5535c5fa 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -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';
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index c47559b82b8..5081c648e36 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -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_');
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 7a76888c916..6f7723955bf 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -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 {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 25478ad6f4f..96dce959828 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -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 {
+ { order(:name) }
+ scope :with_groups, -> { preload(:groups) }
+
private
def validate_segment_count
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 1272a216ee7..66e082501bc 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -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: ''.html_safe, strong_end: ''.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: ''.html_safe, strong_end: ''.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',
diff --git a/changelogs/unreleased/add_tooltips_to_design_buttons.yml b/changelogs/unreleased/add_tooltips_to_design_buttons.yml
new file mode 100644
index 00000000000..b5e5826eae8
--- /dev/null
+++ b/changelogs/unreleased/add_tooltips_to_design_buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Add tooltips to design buttons
+merge_request: 46922
+author: Lee Tickett
+type: added
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index a82329ad835..ab7289908ac 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -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
"""
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index d09396fb680..74d06140447 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -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",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index db0e92892a1..2a6b5f4f3ae 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/development/product_analytics/usage_ping.md b/doc/development/product_analytics/usage_ping.md
index 8362a87bd3b..44e3c58d165 100644
--- a/doc/development/product_analytics/usage_ping.md
+++ b/doc/development/product_analytics/usage_ping.md
@@ -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).
diff --git a/doc/user/packages/nuget_repository/img/visual_studio_nuget_source_added.png b/doc/user/packages/nuget_repository/img/visual_studio_nuget_source_added.png
index e4f6068f28c..bba2d680b25 100644
Binary files a/doc/user/packages/nuget_repository/img/visual_studio_nuget_source_added.png and b/doc/user/packages/nuget_repository/img/visual_studio_nuget_source_added.png differ
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index 22e1a95649d..c537d7c4197 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -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 project’s 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 "https://gitlab-instance.example.com/api/v4/projects//packages/nuget/index.json" -UserName -Password
+nuget source Add -Name -Source "https://gitlab.example.com/api/v4/projects//packages/nuget/index.json" -UserName -Password
```
-Where:
-
-- `` is your desired 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//packages/nuget/index.json`
- - Replace `` 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//packages/nuget/index.json`,
+ where `` 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.

1. Click **Save**.
- 
+The source is displayed in your list.
-In case of any warning, please make sure that the **Location**, **Username**, and **Password** are correct.
+
-### 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
-
-
+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
+
+
-
+
@@ -147,46 +146,51 @@ To add the GitLab NuGet Repository as a source for .NET, create a file named `nu
-
-```
+
+ ```
-## 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 -Source
```
-Where:
-
- `` is your package filename, ending in `.nupkg`.
-- `` is the [source name used during setup](#adding-the-gitlab-nuget-repository-as-a-source-to-nuget).
+- `` 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 --source
```
-Where:
-
- `` is your package filename, ending in `.nupkg`.
-- `` is the [source name used during setup](#adding-the-gitlab-nuget-repository-as-a-source-to-nuget).
+- `` 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 -OutputDirectory \
- -Version \
- -Source
-```
-
-Where:
-
-- `` is the package ID.
-- `` is the output directory, where the package is installed.
-- `` (Optional) is the package version.
-- `` (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 \
- -v
-```
-
-Where:
-
-- `` is the package ID.
-- `` (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 you’re 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 you’re 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 -OutputDirectory \
+ -Version \
+ -Source
+```
+
+- `` is the package ID.
+- `` is the output directory, where the package is installed.
+- `` The package version. Optional.
+- `` 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 \
+ -v
+```
+
+- `` is the package ID.
+- `` is the package version. Optional.
diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md
index 84a9a09d7cf..8a2fb1623d7 100644
--- a/doc/user/packages/package_registry/index.md
+++ b/doc/user/packages/package_registry/index.md
@@ -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:

-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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4acaff30542..2f9d3b64fc7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa.rb b/qa/qa.rb
index f281a4b6ef4..2861baf01ed 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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
##
diff --git a/qa/qa/flow/pipeline.rb b/qa/qa/flow/pipeline.rb
new file mode 100644
index 00000000000..ff23907c081
--- /dev/null
+++ b/qa/qa/flow/pipeline.rb
@@ -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
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index 8de739f1559..1e6cb4047f9 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -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,
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb
index 39d5fbaba6b..2f66ed697a3 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb
@@ -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',
diff --git a/qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb
index 9e37a425a34..2b06ba8646f 100644
--- a/qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb
@@ -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')
diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
index 6fcb0d1187e..e163fcbe574 100644
--- a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
@@ -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')
diff --git a/qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb
index 03a42bd1315..0b70adf9ff6 100644
--- a/qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb
@@ -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')
diff --git a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
index bf83e1a3ab9..35c41bbb2b0 100644
--- a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
@@ -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')
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index abac4f2b91d..8e61ec4d2d7 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -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|
diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
index ece45d093a7..ec26e338b28 100644
--- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
@@ -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',
diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
index 38cee0e62ca..d7f5a326b0e 100644
--- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
@@ -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',
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 6d31780f196..a619ccfad19 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -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')
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
index 723ac0491a7..e2ad4c68bea 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
@@ -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"
/>
`;
diff --git a/spec/frontend/issuable_list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/issuable_list/components/issuable_bulk_edit_sidebar_spec.js
new file mode 100644
index 00000000000..52a238eac7c
--- /dev/null
+++ b/spec/frontend/issuable_list/components/issuable_bulk_edit_sidebar_spec.js
@@ -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': `
+
+ `,
+ 'sidebar-items': `
+
+ `,
+ },
+ });
+
+describe('IssuableBulkEditSidebar', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ setFixtures('');
+ 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);
+ });
+ });
+});
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
index 565f5554cb6..27c3a746090 100644
--- a/spec/frontend/issuable_list/components/issuable_item_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -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: {
diff --git a/spec/frontend/issuable_list/components/issuable_list_root_spec.js b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
index 61825dc04a5..add5d9e8e2d 100644
--- a/spec/frontend/issuable_list/components/issuable_list_root_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_list_root_spec.js
@@ -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': `
@@ -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,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index c79880d4766..64bfff3dfa1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -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]);
diff --git a/spec/models/analytics/devops_adoption/segment_spec.rb b/spec/models/analytics/devops_adoption/segment_spec.rb
deleted file mode 100644
index d7f74d5bf88..00000000000
--- a/spec/models/analytics/devops_adoption/segment_spec.rb
+++ /dev/null
@@ -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