summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/releases/mount_edit.js2
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/services/incident_management/create_issue_service.rb2
-rw-r--r--changelogs/unreleased/bw-graphql-board-type.yml5
-rw-r--r--changelogs/unreleased/nfriend-fix-edit-release-page.yml5
-rw-r--r--changelogs/unreleased/pl-incident-issue-hline.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql105
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json277
-rw-r--r--doc/api/graphql/reference/index.md10
-rw-r--r--doc/api/groups.md8
-rw-r--r--doc/ci/pipelines/pipeline_architectures.md2
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb1
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb20
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb10
-rw-r--r--locale/gitlab.pot37
-rw-r--r--spec/graphql/types/board_type_spec.rb15
-rw-r--r--spec/graphql/types/group_type_spec.rb10
-rw-r--r--spec/graphql/types/project_type_spec.rb7
-rw-r--r--spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb26
-rw-r--r--spec/lib/banzai/filter/inline_metrics_filter_spec.rb79
-rw-r--r--spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb27
-rw-r--r--spec/requests/api/graphql/boards/boards_query_spec.rb27
-rw-r--r--spec/services/incident_management/create_issue_service_spec.rb13
-rw-r--r--spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb36
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb92
28 files changed, 771 insertions, 138 deletions
diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js
index 343de8c56d3..2bc2728312a 100644
--- a/app/assets/javascripts/releases/mount_edit.js
+++ b/app/assets/javascripts/releases/mount_edit.js
@@ -7,7 +7,7 @@ export default () => {
const el = document.getElementById('js-edit-release-page');
const store = createStore({ detail: detailModule });
- store.dispatch('setInitialState', el.dataset);
+ store.dispatch('detail/setInitialState', el.dataset);
return new Vue({
el,
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 718770ebfbc..9f3905000b2 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -46,6 +46,12 @@ module Types
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Find milestones',
resolver: Resolvers::MilestoneResolver
+
+ field :boards,
+ Types::BoardType.connection_type,
+ null: true,
+ description: 'Boards of the group',
+ resolver: Resolvers::BoardsResolver
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index b44baa50955..f89bd5575a3 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -179,6 +179,12 @@ module Types
null: true,
description: 'Paginated collection of Sentry errors on the project',
resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver
+
+ field :boards,
+ Types::BoardType.connection_type,
+ null: true,
+ description: 'Boards of the project',
+ resolver: Resolvers::BoardsResolver
end
end
diff --git a/app/services/incident_management/create_issue_service.rb b/app/services/incident_management/create_issue_service.rb
index 94b6f037924..43077e03e6d 100644
--- a/app/services/incident_management/create_issue_service.rb
+++ b/app/services/incident_management/create_issue_service.rb
@@ -58,7 +58,7 @@ module IncidentManagement
end
def issue_description
- horizontal_line = "\n---\n\n"
+ horizontal_line = "\n\n---\n\n"
[
alert_summary,
diff --git a/changelogs/unreleased/bw-graphql-board-type.yml b/changelogs/unreleased/bw-graphql-board-type.yml
new file mode 100644
index 00000000000..e4d97c4bfc1
--- /dev/null
+++ b/changelogs/unreleased/bw-graphql-board-type.yml
@@ -0,0 +1,5 @@
+---
+title: 'GraphQL: Add Board type'
+merge_request: 22497
+author: Alexander Koval
+type: added
diff --git a/changelogs/unreleased/nfriend-fix-edit-release-page.yml b/changelogs/unreleased/nfriend-fix-edit-release-page.yml
new file mode 100644
index 00000000000..5155499d4e0
--- /dev/null
+++ b/changelogs/unreleased/nfriend-fix-edit-release-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "Edit Release" page
+merge_request: 25469
+author:
+type: fixed
diff --git a/changelogs/unreleased/pl-incident-issue-hline.yml b/changelogs/unreleased/pl-incident-issue-hline.yml
new file mode 100644
index 00000000000..6b161aadb50
--- /dev/null
+++ b/changelogs/unreleased/pl-incident-issue-hline.yml
@@ -0,0 +1,5 @@
+---
+title: Fix markdown layout of incident issues
+merge_request: 25352
+author:
+type: fixed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 898ad11f450..76076a653a2 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -159,6 +159,61 @@ enum BlobViewersType {
simple
}
+"""
+Represents a project or group board
+"""
+type Board {
+ """
+ ID (global ID) of the board
+ """
+ id: ID!
+
+ """
+ Name of the board
+ """
+ name: String
+
+ """
+ Weight of the board
+ """
+ weight: Int
+}
+
+"""
+The connection type for Board.
+"""
+type BoardConnection {
+ """
+ A list of edges.
+ """
+ edges: [BoardEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Board]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type BoardEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Board
+}
+
type Commit {
"""
Author of the commit
@@ -2716,6 +2771,31 @@ type Group {
avatarUrl: String
"""
+ Boards of the group
+ """
+ boards(
+ """
+ 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
+ ): BoardConnection
+
+ """
Description of the namespace
"""
description: String
@@ -5175,6 +5255,31 @@ type Project {
avatarUrl: String
"""
+ Boards of the project
+ """
+ boards(
+ """
+ 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
+ ): BoardConnection
+
+ """
Indicates if the project stores Docker container images in a container registry
"""
containerRegistryEnabled: Boolean
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 41872ffa9c2..1f8e965e225 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -369,6 +369,59 @@
"deprecationReason": null
},
{
+ "name": "boards",
+ "description": "Boards of the project",
+ "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": "BoardConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "containerRegistryEnabled",
"description": "Indicates if the project stores Docker container images in a container registry",
"args": [
@@ -3123,6 +3176,59 @@
"deprecationReason": null
},
{
+ "name": "boards",
+ "description": "Boards of the group",
+ "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": "BoardConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "description",
"description": "Description of the namespace",
"args": [
@@ -4324,6 +4430,177 @@
},
{
"kind": "OBJECT",
+ "name": "BoardConnection",
+ "description": "The connection type for Board.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "BoardEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Board",
+ "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": "BoardEdge",
+ "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": "Board",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Board",
+ "description": "Represents a project or group board",
+ "fields": [
+ {
+ "name": "id",
+ "description": "ID (global ID) of the board",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the board",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "weight",
+ "description": "Weight of the board",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Epic",
"description": "Represents an epic.",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 19d145a664f..ab1efa6c5c1 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -49,6 +49,16 @@ An emoji awarded by a user.
| `type` | EntryType! | Type of tree entry |
| `webUrl` | String | Web URL of the blob |
+## Board
+
+Represents a project or group board
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `id` | ID! | ID (global ID) of the board |
+| `name` | String | Name of the board |
+| `weight` | Int | Weight of the board |
+
## Commit
| Name | Type | Description |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index de0b2543645..946626859f8 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -498,7 +498,7 @@ Parameters:
## Transfer project to group
-Transfer a project to the Group namespace. Available only for admin
+Transfer a project to the Group namespace. Available only to instance administrators. Transferring projects may fail when tagged packages exist in the project's repository.
```
POST /groups/:id/projects/:project_id
@@ -508,9 +508,13 @@ Parameters:
| Attribute | Type | Required | Description |
| ------------ | -------------- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the target group](README.md#namespaced-path-encoding) |
| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/projects/56
+```
+
## Update group
Updates the project group. Only available to group owners and administrators.
diff --git a/doc/ci/pipelines/pipeline_architectures.md b/doc/ci/pipelines/pipeline_architectures.md
index 0e6745a59eb..803d0130cf0 100644
--- a/doc/ci/pipelines/pipeline_architectures.md
+++ b/doc/ci/pipelines/pipeline_architectures.md
@@ -15,7 +15,7 @@ own advantages. These methods can be mixed and matched if needed:
- [Child/Parent Pipelines](#child--parent-pipelines): Good for monorepos and projects with lots of independently defined components.
For more details about
-any of the keywords used below, check out our [CI YAML reference](../yaml/) for details.
+any of the keywords used below, check out our [CI YAML reference](../yaml/README.md) for details.
## Basic Pipelines
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
index c1f4bf1f97f..e8145e93851 100644
--- a/lib/banzai/filter/inline_metrics_filter.rb
+++ b/lib/banzai/filter/inline_metrics_filter.rb
@@ -20,6 +20,7 @@ module Banzai
# the cost of doing a full regex match.
def xpath_search
"descendant-or-self::a[contains(@href,'metrics') and \
+ contains(@href,'environments') and \
starts-with(@href, '#{Gitlab.config.gitlab.url}')]"
end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
index ae830831a27..664a897970a 100644
--- a/lib/banzai/filter/inline_metrics_redactor_filter.rb
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -9,8 +9,8 @@ module Banzai
METRICS_CSS_CLASS = '.js-render-metrics'
EMBED_LIMIT = 100
- URL = Gitlab::Metrics::Dashboard::Url
+ Route = Struct.new(:regex, :permission)
Embed = Struct.new(:project_path, :permission)
# Finds all embeds based on the css class the FE
@@ -59,14 +59,28 @@ module Banzai
embed = Embed.new
url = node.attribute('data-dashboard-url').to_s
- set_path_and_permission(embed, url, URL.metrics_regex, :read_environment)
- set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission
+ permissions_by_route.each do |route|
+ set_path_and_permission(embed, url, route.regex, route.permission) unless embed.permission
+ end
embeds[node] = embed if embed.permission
end
end
end
+ def permissions_by_route
+ [
+ Route.new(
+ ::Gitlab::Metrics::Dashboard::Url.metrics_regex,
+ :read_environment
+ ),
+ Route.new(
+ ::Gitlab::Metrics::Dashboard::Url.grafana_regex,
+ :read_project
+ )
+ ]
+ end
+
# Attempts to determine the path and permission attributes
# of a url based on expected dashboard url formats and
# sets the attributes on an Embed object
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index dad0d95685e..b6238dfe7f0 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -29,8 +29,7 @@ module Banzai
Filter::AudioLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
- Filter::InlineMetricsFilter,
- Filter::InlineGrafanaMetricsFilter,
+ *metrics_filters,
Filter::TableOfContentsFilter,
Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
@@ -48,6 +47,13 @@ module Banzai
]
end
+ def self.metrics_filters
+ [
+ Filter::InlineMetricsFilter,
+ Filter::InlineGrafanaMetricsFilter
+ ]
+ end
+
def self.reference_filters
[
Filter::UserReferenceFilter,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b492921680b..a07c3f6d899 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11310,31 +11310,19 @@ msgstr ""
msgid "LicenseCompliance|Add licenses manually to approve or blacklist"
msgstr ""
-msgid "LicenseCompliance|Approve"
+msgid "LicenseCompliance|Allow"
msgstr ""
-msgid "LicenseCompliance|Approve license"
+msgid "LicenseCompliance|Allowed"
msgstr ""
-msgid "LicenseCompliance|Approve license?"
-msgstr ""
-
-msgid "LicenseCompliance|Approved"
-msgstr ""
-
-msgid "LicenseCompliance|Blacklist"
-msgstr ""
-
-msgid "LicenseCompliance|Blacklist license"
-msgstr ""
-
-msgid "LicenseCompliance|Blacklist license?"
+msgid "LicenseCompliance|Cancel"
msgstr ""
-msgid "LicenseCompliance|Blacklisted"
+msgid "LicenseCompliance|Denied"
msgstr ""
-msgid "LicenseCompliance|Cancel"
+msgid "LicenseCompliance|Deny"
msgstr ""
msgid "LicenseCompliance|Here you can approve or blacklist licenses for this project. Using %{ci} or %{license} will allow you to see if there are any unmanaged licenses and approve or blacklist them in merge request."
@@ -11378,6 +11366,9 @@ msgstr ""
msgid "LicenseCompliance|License name"
msgstr ""
+msgid "LicenseCompliance|License review"
+msgstr ""
+
msgid "LicenseCompliance|Packages"
msgstr ""
@@ -11423,6 +11414,9 @@ msgstr ""
msgid "Licenses|Components"
msgstr ""
+msgid "Licenses|Detected in Project"
+msgstr ""
+
msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest pipeline%{linkEnd} scan"
msgstr ""
@@ -11438,6 +11432,15 @@ msgstr ""
msgid "Licenses|Name"
msgstr ""
+msgid "Licenses|Policies"
+msgstr ""
+
+msgid "Licenses|Policy"
+msgstr ""
+
+msgid "Licenses|Specified policies in this project"
+msgstr ""
+
msgid "Licenses|The license list details information about the licenses used within your project."
msgstr ""
diff --git a/spec/graphql/types/board_type_spec.rb b/spec/graphql/types/board_type_spec.rb
new file mode 100644
index 00000000000..9d18065bbcd
--- /dev/null
+++ b/spec/graphql/types/board_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Board'] do
+ it { expect(described_class.graphql_name).to eq('Board') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_board) }
+
+ it 'has specific fields' do
+ expected_fields = %w[id name]
+
+ is_expected.to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 6a0028f6529..240dd9fa5e2 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -16,9 +16,17 @@ describe GitlabSchema.types['Group'] do
web_url avatar_url share_with_group_lock project_creation_level
subgroup_creation_level require_two_factor_authentication
two_factor_grace_period auto_devops_enabled emails_disabled
- mentions_disabled parent
+ mentions_disabled parent boards
]
is_expected.to include_graphql_fields(*expected_fields)
end
+
+ describe 'boards field' do
+ subject { described_class.fields['boards'] }
+
+ it 'returns boards' do
+ is_expected.to have_graphql_type(Types::BoardType.connection_type)
+ end
+ end
end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index ac2d2d6f7f0..9c6d1e3f35c 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -24,6 +24,7 @@ describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
+ boards
]
is_expected.to include_graphql_fields(*expected_fields)
@@ -77,4 +78,10 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::EnvironmentType.connection_type) }
it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) }
end
+
+ describe 'boards field' do
+ subject { described_class.fields['boards'] }
+
+ it { is_expected.to have_graphql_type(Types::BoardType.connection_type) }
+ end
end
diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
index fd6f8816b63..9ac06a90efd 100644
--- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
@@ -8,40 +8,26 @@ describe Banzai::Filter::InlineGrafanaMetricsFilter do
let_it_be(:project) { create(:project) }
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
- let(:input) { %(<a href="#{url}">example</a>) }
+ let(:input) { %(<a href="#{trigger_url}">example</a>) }
let(:doc) { filter(input) }
-
- let(:url) { grafana_integration.grafana_url + dashboard_path }
let(:dashboard_path) do
'/d/XDaNK6amz/gitlab-omnibus-redis' \
'?from=1570397739557&to=1570484139557' \
'&var-instance=All&panelId=14'
end
- it 'appends a metrics charts placeholder with dashboard url after metrics links' do
- node = doc.at_css('.js-render-metrics')
- expect(node).to be_present
-
- dashboard_url = urls.project_grafana_api_metrics_dashboard_url(
+ let(:trigger_url) { grafana_integration.grafana_url + dashboard_path }
+ let(:dashboard_url) do
+ urls.project_grafana_api_metrics_dashboard_url(
project,
embedded: true,
- grafana_url: url,
+ grafana_url: trigger_url,
start: "2019-10-06T21:35:39Z",
end: "2019-10-07T21:35:39Z"
)
-
- expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
end
- context 'when the dashboard link is part of a paragraph' do
- let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
- let(:input) { %(<p>#{paragraph}</p>) }
-
- it 'appends the charts placeholder after the enclosing paragraph' do
- expect(unescape(doc.at_css('p').to_s)).to include(paragraph)
- expect(doc.at_css('.js-render-metrics')).to be_present
- end
- end
+ it_behaves_like 'a metrics embed filter'
context 'when grafana is not configured' do
before do
diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
index 66bbcbf7292..1546a5e88ed 100644
--- a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
@@ -5,66 +5,31 @@ require 'spec_helper'
describe Banzai::Filter::InlineMetricsFilter do
include FilterSpecHelper
- let(:input) { %(<a href="#{url}">example</a>) }
- let(:doc) { filter(input) }
-
- context 'when the document has an external link' do
- let(:url) { 'https://foo.com' }
-
- it 'leaves regular non-metrics links unchanged' do
- expect(doc.to_s).to eq(input)
- end
- end
-
- context 'when the document has a metrics dashboard link' do
- let(:params) { ['foo', 'bar', 12] }
- let(:url) { urls.metrics_namespace_project_environment_url(*params) }
-
- it 'leaves the original link unchanged' do
- expect(doc.at_css('a').to_s).to eq(input)
- end
-
- it 'appends a metrics charts placeholder with dashboard url after metrics links' do
- node = doc.at_css('.js-render-metrics')
- expect(node).to be_present
-
- dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params, embedded: true)
- expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
+ let(:params) { ['foo', 'bar', 12] }
+ let(:query_params) { {} }
+
+ let(:trigger_url) { urls.metrics_namespace_project_environment_url(*params, query_params) }
+ let(:dashboard_url) { urls.metrics_dashboard_namespace_project_environment_url(*params, **query_params, embedded: true) }
+
+ it_behaves_like 'a metrics embed filter'
+
+ context 'with query params specified' do
+ let(:query_params) do
+ {
+ dashboard: 'config/prometheus/common_metrics.yml',
+ group: 'System metrics (Kubernetes)',
+ title: 'Core Usage (Pod Average)',
+ y_label: 'Cores per Pod'
+ }
end
- context 'when the metrics dashboard link is part of a paragraph' do
- let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
- let(:input) { %(<p>#{paragraph}</p>) }
-
- it 'appends the charts placeholder after the enclosing paragraph' do
- expect(doc.at_css('p').to_s).to include(paragraph)
- expect(doc.at_css('.js-render-metrics')).to be_present
- end
- end
-
- context 'with dashboard params specified' do
- let(:params) do
- [
- 'foo',
- 'bar',
- 12,
- {
- embedded: true,
- dashboard: 'config/prometheus/common_metrics.yml',
- group: 'System metrics (Kubernetes)',
- title: 'Core Usage (Pod Average)',
- y_label: 'Cores per Pod'
- }
- ]
- end
+ it_behaves_like 'a metrics embed filter'
+ end
- it 'appends a metrics charts placeholder with dashboard url after metrics links' do
- node = doc.at_css('.js-render-metrics')
- expect(node).to be_present
+ it 'leaves links to other dashboards unchanged' do
+ url = urls.namespace_project_grafana_api_metrics_dashboard_url('foo', 'bar')
+ input = %(<a href="#{url}">example</a>)
- dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params)
- expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
- end
- end
+ expect(filter(input).to_s).to eq(input)
end
end
diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
index e2615ea5069..f91927412cb 100644
--- a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
@@ -18,33 +18,6 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
end
context 'with a metrics charts placeholder' do
- shared_examples_for 'a supported metrics dashboard url' do
- context 'no user is logged in' do
- it 'redacts the placeholder' do
- expect(doc.to_s).to be_empty
- end
- end
-
- context 'the user does not have permission do see charts' do
- let(:doc) { filter(input, current_user: build(:user)) }
-
- it 'redacts the placeholder' do
- expect(doc.to_s).to be_empty
- end
- end
-
- context 'the user has requisite permissions' do
- let(:user) { create(:user) }
- let(:doc) { filter(input, current_user: user) }
-
- it 'leaves the placeholder' do
- project.add_maintainer(user)
-
- expect(doc.to_s).to eq input
- end
- end
- end
-
let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
it_behaves_like 'a supported metrics dashboard url'
diff --git a/spec/requests/api/graphql/boards/boards_query_spec.rb b/spec/requests/api/graphql/boards/boards_query_spec.rb
new file mode 100644
index 00000000000..d0a2d0fffaf
--- /dev/null
+++ b/spec/requests/api/graphql/boards/boards_query_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'get list of boards' do
+ include GraphqlHelpers
+
+ include_context 'group and project boards query context'
+
+ describe 'for a project' do
+ let(:board_parent) { create(:project, :repository, :private) }
+ let(:boards_data) { graphql_data['project']['boards']['edges'] }
+
+ it_behaves_like 'group and project boards query'
+ end
+
+ describe 'for a group' do
+ let(:board_parent) { create(:group, :private) }
+ let(:boards_data) { graphql_data['group']['boards']['edges'] }
+
+ before do
+ allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false)
+ end
+
+ it_behaves_like 'group and project boards query'
+ end
+end
diff --git a/spec/services/incident_management/create_issue_service_spec.rb b/spec/services/incident_management/create_issue_service_spec.rb
index e720aafb897..4c7fb682193 100644
--- a/spec/services/incident_management/create_issue_service_spec.rb
+++ b/spec/services/incident_management/create_issue_service_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe IncidentManagement::CreateIssueService do
let(:project) { create(:project, :repository, :private) }
- let(:user) { User.alert_bot }
+ let_it_be(:user) { User.alert_bot }
let(:service) { described_class.new(project, alert_payload) }
let(:alert_starts_at) { Time.now }
let(:alert_title) { 'TITLE' }
@@ -29,7 +29,6 @@ describe IncidentManagement::CreateIssueService do
context 'when create_issue enabled' do
let(:issue) { subject[:issue] }
- let(:summary_separator) { "\n---\n\n" }
before do
setting.update!(create_issue: true)
@@ -42,7 +41,7 @@ describe IncidentManagement::CreateIssueService do
expect(issue.author).to eq(user)
expect(issue.title).to eq(alert_title)
expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip)
- expect(separator_count(issue.description)).to eq 0
+ expect(separator_count(issue.description)).to eq(0)
end
end
@@ -74,7 +73,7 @@ describe IncidentManagement::CreateIssueService do
expect(subject).to include(status: :success)
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
- expect(separator_count(issue.description)).to eq 1
+ expect(separator_count(issue.description)).to eq(1)
expect(issue.description).to include(template_content)
end
end
@@ -134,7 +133,7 @@ describe IncidentManagement::CreateIssueService do
expect(issue.description).to include(alert_presenter.issue_summary_markdown)
expect(issue.description).to include(template_content)
expect(issue.description).to include(alt_template)
- expect(separator_count(issue.description)).to eq 2
+ expect(separator_count(issue.description)).to eq(2)
end
end
@@ -171,7 +170,7 @@ describe IncidentManagement::CreateIssueService do
expect(issue.title).to include(query_title)
expect(issue.title).to include('for 5 minutes')
expect(issue.description).to include(alert_presenter.issue_summary_markdown.strip)
- expect(separator_count(issue.description)).to eq 0
+ expect(separator_count(issue.description)).to eq(0)
end
end
@@ -306,6 +305,8 @@ describe IncidentManagement::CreateIssueService do
end
def separator_count(text)
+ summary_separator = "\n\n---\n\n"
+
text.scan(summary_separator).size
end
end
diff --git a/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb b/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb
new file mode 100644
index 00000000000..e744c3d0abb
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'group and project boards query context' do
+ let_it_be(:user) { create :user }
+ let(:current_user) { user }
+ let(:params) { '' }
+ let(:board_parent_type) { board_parent.class.to_s.downcase }
+ let(:start_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['startCursor'] }
+ let(:end_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['endCursor'] }
+
+ def query(board_params = params)
+ graphql_query_for(
+ board_parent_type,
+ { 'fullPath' => board_parent.full_path },
+ <<~BOARDS
+ boards(#{board_params}) {
+ pageInfo {
+ startCursor
+ endCursor
+ }
+ edges {
+ node {
+ #{all_graphql_fields_for('boards'.classify)}
+ }
+ }
+ }
+ BOARDS
+ )
+ end
+
+ def grab_names(data = boards_data)
+ data.map do |board|
+ board.dig('node', 'name')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
new file mode 100644
index 00000000000..599161abbfe
--- /dev/null
+++ b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+# Expects 2 attributes to be defined:
+# trigger_url - Url expected to trigger the insertion of a placeholder.
+# dashboard_url - Url expected to be present in the placeholder.
+RSpec.shared_examples 'a metrics embed filter' do
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ context 'when the document has an external link' do
+ let(:url) { 'https://foo.com' }
+
+ it 'leaves regular non-metrics links unchanged' do
+ expect(doc.to_s).to eq(input)
+ end
+ end
+
+ context 'when the document contains an embeddable link' do
+ let(:url) { trigger_url }
+
+ it 'leaves the original link unchanged' do
+ expect(unescape(doc.at_css('a').to_s)).to eq(input)
+ end
+
+ it 'appends a metrics charts placeholder' do
+ node = doc.at_css('.js-render-metrics')
+ expect(node).to be_present
+
+ expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
+ end
+
+ context 'in a paragraph' do
+ let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
+ let(:input) { %(<p>#{paragraph}</p>) }
+
+ it 'appends a metrics charts placeholder after the enclosing paragraph' do
+ expect(unescape(doc.at_css('p').to_s)).to include(paragraph)
+ expect(doc.at_css('.js-render-metrics')).to be_present
+ end
+ end
+ end
+
+ # Nokogiri escapes the URLs, but we don't care about that
+ # distinction for the purposes of these filters
+ def unescape(html)
+ CGI.unescapeHTML(html)
+ end
+end
diff --git a/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
new file mode 100644
index 00000000000..d283b3a3b27
--- /dev/null
+++ b/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a supported metrics dashboard url' do
+ context 'no user is logged in' do
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+
+ context 'the user does not have permission do see charts' do
+ let(:doc) { filter(input, current_user: build(:user)) }
+
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+
+ context 'the user has requisite permissions' do
+ let(:user) { create(:user) }
+ let(:doc) { filter(input, current_user: user) }
+
+ it 'leaves the placeholder' do
+ project.add_maintainer(user)
+
+ expect(doc.to_s).to eq(input)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
new file mode 100644
index 00000000000..6044fefd2f7
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'group and project boards query' do
+ include GraphqlHelpers
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ context 'when the user does not have access to the board parent' do
+ it 'returns nil' do
+ create(:board, resource_parent: board_parent, name: 'A')
+
+ post_graphql(query)
+
+ expect(graphql_data[board_parent_type]).to be_nil
+ end
+ end
+
+ context 'when no permission to read board' do
+ it 'does not return any boards' do
+ board_parent.add_guest(current_user)
+ board = create(:board, resource_parent: board_parent, name: 'A')
+
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_board, board).and_return(false)
+
+ post_graphql(query, current_user: current_user)
+
+ expect(boards_data).to be_empty
+ end
+ end
+
+ context 'when user can read the board parent' do
+ before do
+ board_parent.add_reporter(current_user)
+ end
+
+ it 'does not create a default board' do
+ post_graphql(query, current_user: current_user)
+
+ expect(boards_data).to be_empty
+ end
+
+ describe 'sorting and pagination' do
+ context 'when using default sorting' do
+ let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') }
+ let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') }
+ let!(:board_a) { create(:board, resource_parent: board_parent, name: 'a') }
+ let!(:board_A) { create(:board, resource_parent: board_parent, name: 'A') }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ context 'when ascending' do
+ let(:boards) { [board_a, board_A, board_B, board_C] }
+ let(:expected_boards) do
+ if board_parent.multiple_issue_boards_available?
+ boards
+ else
+ [boards.first]
+ end
+ end
+
+ it 'sorts boards' do
+ expect(grab_names).to eq expected_boards.map(&:name)
+ end
+
+ context 'when paginating' do
+ let(:params) { 'first: 2' }
+
+ it 'sorts boards' do
+ expect(grab_names).to eq expected_boards.first(2).map(&:name)
+
+ cursored_query = query("after: \"#{end_cursor}\"")
+ post_graphql(cursored_query, current_user: current_user)
+
+ response_data = JSON.parse(response.body)['data'][board_parent_type]['boards']['edges']
+
+ expect(grab_names(response_data)).to eq expected_boards.drop(2).first(2).map(&:name)
+ end
+ end
+ end
+ end
+ end
+ end
+end