diff options
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 |