Commit 60f63d15 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 93d7441c
......@@ -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,
......
......@@ -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
......
......@@ -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
......
......@@ -58,7 +58,7 @@ module IncidentManagement
end
def issue_description
horizontal_line = "\n---\n\n"
horizontal_line = "\n\n---\n\n"
[
alert_summary,
......
---
title: 'GraphQL: Add Board type'
merge_request: 22497
author: Alexander Koval
type: added
---
title: Fix "Edit Release" page
merge_request: 25469
author:
type: fixed
---
title: Fix markdown layout of incident issues
merge_request: 25352
author:
type: fixed
......@@ -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
......@@ -2715,6 +2770,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
"""
......@@ -5174,6 +5254,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
"""
......
......@@ -368,6 +368,59 @@
"isDeprecated": false,
"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",
......@@ -3122,6 +3175,59 @@
"isDeprecated": false,
"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",
......@@ -4322,6 +4428,177 @@
],
"possibleTypes": null
},
{
"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",
......
......@@ -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 |
......
......@@ -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.
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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,
......
......@@ -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 ""
......
# 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
......@@ -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
......@@ -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
......@@ -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
......
......@@ -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
......@@ -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'
......
# 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
......@@ -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
# 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
# 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
# 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
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment