Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/merge_request_templates/Documentation.md8
-rw-r--r--.prettierignore1
-rw-r--r--.rubocop_todo.yml2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock3
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue2
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql10
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql2
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql6
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/details.query.graphql12
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql56
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql23
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql2
-rw-r--r--app/assets/javascripts/boards/queries/board.fragment.graphql2
-rw-r--r--app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql20
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql2
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql2
-rw-r--r--app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql2
-rw-r--r--app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql2
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue5
-rw-r--r--app/assets/javascripts/error_tracking/queries/details.query.graphql50
-rw-r--r--app/assets/javascripts/gl_form.js4
-rw-r--r--app/assets/javascripts/ide/queries/getUserPermissions.query.graphql4
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue6
-rw-r--r--app/assets/javascripts/notes.js2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue3
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_item.vue6
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql6
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql6
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql15
-rw-r--r--app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql4
-rw-r--r--app/assets/javascripts/snippets/queries/projectPermissions.query.graphql2
-rw-r--r--app/assets/javascripts/snippets/queries/userPermissions.query.graphql2
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql2
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql2
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue105
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js9
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss4
-rw-r--r--app/assets/stylesheets/components/popover.scss10
-rw-r--r--app/assets/stylesheets/framework/awards.scss2
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss4
-rw-r--r--app/assets/stylesheets/framework/memory_graph.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/stacked_progress_bar.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss12
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss12
-rw-r--r--app/assets/stylesheets/pages/alert_management/details.scss2
-rw-r--r--app/assets/stylesheets/pages/branches.scss4
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss2
-rw-r--r--app/assets/stylesheets/performance_bar.scss2
-rw-r--r--app/controllers/admin/clusters_controller.rb9
-rw-r--r--app/controllers/clusters/clusters_controller.rb1
-rw-r--r--app/controllers/groups/clusters_controller.rb10
-rw-r--r--app/controllers/projects/clusters_controller.rb11
-rw-r--r--app/finders/snippets_finder.rb13
-rw-r--r--app/helpers/projects/alert_management_helper.rb2
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/concerns/has_repository.rb6
-rw-r--r--app/models/epic.rb2
-rw-r--r--app/models/personal_access_token.rb6
-rw-r--r--app/models/webauthn_registration.rb11
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/clusters/cluster_presenter.rb19
-rw-r--r--app/presenters/group_clusterable_presenter.rb4
-rw-r--r--app/presenters/instance_clusterable_presenter.rb4
-rw-r--r--app/presenters/project_clusterable_presenter.rb4
-rw-r--r--app/views/admin/application_settings/_initial_branch_name.html.haml12
-rw-r--r--app/views/admin/application_settings/repository.html.haml12
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml3
-rw-r--r--app/views/projects/services/alerts/_top.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_top.html.haml2
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml3
-rw-r--r--changelogs/unreleased/221301-update-gray-200-value-and-usages.yml5
-rw-r--r--changelogs/unreleased/22506-webauthn-step-1-migrations.yml5
-rw-r--r--changelogs/unreleased/227190-add-pagerduty-related-columns-to-project_incident_management_setti.yml5
-rw-r--r--changelogs/unreleased/25429-fix-disabled-quick-actions-in-notes.yml4
-rw-r--r--changelogs/unreleased/36250-custom-renderer-html.yml5
-rw-r--r--changelogs/unreleased/alert-settings-link-follow-through.yml5
-rw-r--r--changelogs/unreleased/astoicescu-allowOotbDashboardsToBeCloned.yml5
-rw-r--r--changelogs/unreleased/backfill-routes-for-users-migration.yml5
-rw-r--r--changelogs/unreleased/change-pipeline-widget-when-no-pipeline-ran-for-commit.yml5
-rw-r--r--changelogs/unreleased/fix_blocked_issue_warning.yml5
-rw-r--r--changelogs/unreleased/fj-227311-optimize-snippets-finder-query.yml5
-rw-r--r--changelogs/unreleased/mwaw-208224-move-cluster-metrics-dashboard-endpoint-into-gitlab-core-BE.yml5
-rw-r--r--changelogs/unreleased/rails-save-bang-1.yml5
-rw-r--r--changelogs/unreleased/sh-handle-distant-pat-expiry.yml5
-rw-r--r--changelogs/unreleased/swap-to-oj.yml5
-rw-r--r--changelogs/unreleased/tr-prettify-graphql-files.yml5
-rw-r--r--config/initializers/multi_json.rb5
-rw-r--r--config/initializers/oj.rb4
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20191112212815_create_web_authn_table.rb26
-rw-r--r--db/migrate/20200510181937_add_web_authn_xid_to_user_details.rb12
-rw-r--r--db/migrate/20200510182218_add_text_limit_to_user_details_webauthn_xid.rb16
-rw-r--r--db/migrate/20200510182556_add_text_limit_to_webauthn_registrations_name.rb16
-rw-r--r--db/migrate/20200510182824_add_text_limit_to_webauthn_registrations_credential_xid.rb16
-rw-r--r--db/migrate/20200510183128_add_foreign_key_from_webauthn_registrations_to_users.rb21
-rw-r--r--db/migrate/20200624142107_create_analytics_cycle_analytics_group_value_streams.rb33
-rw-r--r--db/migrate/20200624142207_add_group_value_stream_to_cycle_analytics_group_stages.rb19
-rw-r--r--db/migrate/20200701064756_add_not_valid_foreign_key_to_cycle_analytics_group_stages.rb22
-rw-r--r--db/migrate/20200708080631_add_pager_duty_integration_columns_to_project_incident_management_settings.rb13
-rw-r--r--db/migrate/20200710130234_add_limit_constraints_to_project_incident_management_settings_token.rb18
-rw-r--r--db/post_migrate/20200703064117_generate_missing_routes_for_bots.rb92
-rw-r--r--db/structure.sql93
-rw-r--r--doc/administration/postgresql/replication_and_failover.md29
-rw-r--r--doc/ci/troubleshooting.md38
-rw-r--r--doc/development/fe_guide/tooling.md12
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/operations/metrics/dashboards/yaml.md2
-rw-r--r--lib/banzai/filter/inline_cluster_metrics_filter.rb40
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb4
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb3
-rw-r--r--lib/gitlab/json.rb174
-rw-r--r--lib/gitlab/json_logger.rb2
-rw-r--r--locale/gitlab.pot18
-rw-r--r--scripts/frontend/prettier.js2
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb10
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb11
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb12
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb45
-rw-r--r--spec/features/clusters/cluster_health_dashboard_spec.rb93
-rw-r--r--spec/features/markdown/metrics_spec.rb38
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb8
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb19
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb1
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb17
-rw-r--r--spec/finders/snippets_finder_spec.rb6
-rw-r--r--spec/fixtures/api/graphql/introspection.graphql16
-rw-r--r--spec/frontend/fixtures/static/mini_dropdown_graph.html24
-rw-r--r--spec/frontend/gl_form_spec.js16
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js65
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js39
-rw-r--r--spec/frontend/monitoring/mock_data.js40
-rw-r--r--spec/frontend/notes/old_notes_spec.js44
-rw-r--r--spec/frontend/registry/explorer/components/list_item_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js300
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js34
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb2
-rw-r--r--spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb25
-rw-r--r--spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb4
-rw-r--r--spec/lib/gitlab/json_spec.rb377
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/response_spec.rb2
-rw-r--r--spec/migrations/generate_missing_routes_for_bots_spec.rb80
-rw-r--r--spec/models/personal_access_token_spec.rb1
-rw-r--r--spec/models/project_spec.rb30
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb40
-rw-r--r--spec/presenters/group_clusterable_presenter_spec.rb6
-rw-r--r--spec/presenters/instance_clusterable_presenter_spec.rb6
-rw-r--r--spec/presenters/project_clusterable_presenter_spec.rb6
-rw-r--r--spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb27
-rw-r--r--spec/views/admin/application_settings/repository.html.haml_spec.rb46
178 files changed, 2328 insertions, 688 deletions
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index b2abbce8ca7..fb828b995b1 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -45,11 +45,11 @@ All reviewers can help ensure accuracy, clarity, completeness, and adherence to
**2. Technical Writer**
-- [ ] Optional: Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages).
- - [ ] Add ~"Technical Writing" and `docs::` workflow label.
+- [ ] Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/product-categories/#devops-stages).
+ - [ ] Ensure ~"Technical Writing", ~"documentation", and a `docs::` scoped label are added.
- [ ] Add ~docs-only when the only files changed are under `doc/*`.
- - [ ] Add ~tw::doing when starting work on the MR.
- - [ ] Add ~tw::finished after approving and/or merging the MR.
+ - [ ] Add ~"tw::doing" when starting work on the MR.
+ - [ ] Add ~"tw::finished" if Technical Writing team work on the MR is complete but it remains open.
**3. Maintainer**
diff --git a/.prettierignore b/.prettierignore
index c9b945ac96d..ff8188bbda4 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -3,6 +3,7 @@
/public/
/vendor/
/tmp/
+doc/api/graphql/reference/gitlab_schema.graphql
# ignore stylesheets for now as this clashes with our linter
*.css
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index edb3921609d..f15d4c7f286 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -738,8 +738,6 @@ Style/SymbolProc:
# Configuration parameters: AllowImplicitReturn, AllowedReceivers.
Rails/SaveBang:
Exclude:
- - 'ee/spec/controllers/boards/issues_controller_spec.rb'
- - 'ee/spec/controllers/boards/lists_controller_spec.rb'
- 'ee/spec/controllers/ee/sent_notifications_controller_spec.rb'
- 'ee/spec/controllers/groups/epic_issues_controller_spec.rb'
- 'ee/spec/controllers/groups/epic_links_controller_spec.rb'
diff --git a/Gemfile b/Gemfile
index c1f93c78932..20d59af8db5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -502,3 +502,5 @@ gem 'valid_email', '~> 0.1'
# JSON
gem 'json', '~> 2.3.0'
gem 'json-schema', '~> 2.8.0'
+gem 'oj', '~> 3.10.6'
+gem 'multi_json', '~> 1.14.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 03c213333df..601ac0db0ee 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -687,6 +687,7 @@ GEM
octokit (4.15.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
+ oj (3.10.6)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3)
@@ -1312,6 +1313,7 @@ DEPENDENCIES
mimemagic (~> 0.3.2)
mini_magick
minitest (~> 5.11.0)
+ multi_json (~> 1.14.1)
nakayoshi_fork (~> 0.0.4)
net-ldap
net-ntp
@@ -1319,6 +1321,7 @@ DEPENDENCIES
nokogiri (~> 1.10.9)
oauth2 (~> 1.4)
octokit (~> 4.15)
+ oj (~> 3.10.6)
omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.3)
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 8749f102442..be9a5a7ca61 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -204,7 +204,7 @@ export default {
:class="{ 'pr-sm-8': sidebarStatus }"
>
<div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid flex-column flex-sm-row"
>
<div
data-testid="alert-header"
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
index d9e4d7505e7..18fab429164 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
+++ b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
@@ -16,5 +16,4 @@ fragment AlertDetailItem on AlertManagementAlert {
...AlertNote
}
}
-
}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql
index 18c9652b262..bc4d91a51d1 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql
@@ -1,8 +1,8 @@
mutation createAlertIssue($projectPath: ID!, $iid: String!) {
- createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
- errors
- issue {
- iid
- }
+ createAlertIssue(input: { iid: $iid, projectPath: $projectPath }) {
+ errors
+ issue {
+ iid
}
+ }
}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
index d9c4813f8e8..f666fcd6782 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/toggle_sidebar_status.mutation.graphql
@@ -1,3 +1,3 @@
mutation toggleSidebarStatus {
- toggleSidebarStatus @client
+ toggleSidebarStatus @client
}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql
index f3ee80e9e82..ba1e607bc10 100644
--- a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql
+++ b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.mutation.graphql
@@ -4,9 +4,9 @@ mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {
- iid,
- status,
- endedAt,
+ iid
+ status
+ endedAt
notes {
nodes {
...AlertNote
diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
index c02b8accdd1..8881f49b689 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
@@ -1,11 +1,11 @@
#import "../fragments/detail_item.fragment.graphql"
query alertDetails($fullPath: ID!, $alertId: String) {
- project(fullPath: $fullPath) {
- alertManagementAlerts(iid: $alertId) {
- nodes {
- ...AlertDetailItem
- }
- }
+ project(fullPath: $fullPath) {
+ alertManagementAlerts(iid: $alertId) {
+ nodes {
+ ...AlertDetailItem
+ }
}
+ }
}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
index 7545a5e8c4d..8ac00bbc6b5 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
@@ -1,34 +1,34 @@
#import "../fragments/list_item.fragment.graphql"
query getAlerts(
- $searchTerm: String,
- $projectPath: ID!,
- $statuses: [AlertManagementStatus!],
- $sort: AlertManagementAlertSort,
- $firstPageSize: Int,
- $lastPageSize: Int,
- $prevPageCursor: String = ""
- $nextPageCursor: String = ""
+ $searchTerm: String
+ $projectPath: ID!
+ $statuses: [AlertManagementStatus!]
+ $sort: AlertManagementAlertSort
+ $firstPageSize: Int
+ $lastPageSize: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
) {
- project(fullPath: $projectPath, ) {
- alertManagementAlerts(
- search: $searchTerm,
- statuses: $statuses,
- sort: $sort,
- first: $firstPageSize
- last: $lastPageSize,
- after: $nextPageCursor,
- before: $prevPageCursor
- ) {
- nodes {
- ...AlertListItem
- },
- pageInfo {
- hasNextPage
- endCursor
- hasPreviousPage
- startCursor
- }
- }
+ project(fullPath: $projectPath) {
+ alertManagementAlerts(
+ search: $searchTerm
+ statuses: $statuses
+ sort: $sort
+ first: $firstPageSize
+ last: $lastPageSize
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
+ nodes {
+ ...AlertListItem
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ hasPreviousPage
+ startCursor
+ }
}
+ }
}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
index 326e8cec272..5a6faea5cd8 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
@@ -1,16 +1,11 @@
-query getAlertsCount(
- $searchTerm: String,
- $projectPath: ID!
-) {
- project(fullPath: $projectPath) {
- alertManagementAlertStatusCounts(
- search: $searchTerm
- ) {
- all
- open
- acknowledged
- resolved
- triggered
- }
+query getAlertsCount($searchTerm: String, $projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ alertManagementAlertStatusCounts(search: $searchTerm) {
+ all
+ open
+ acknowledged
+ resolved
+ triggered
}
+ }
}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
index 0836f702189..61c570c5cd0 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/sidebar_status.query.graphql
@@ -1,3 +1,3 @@
query sidebarStatus {
- sidebarStatus @client
+ sidebarStatus @client
}
diff --git a/app/assets/javascripts/boards/queries/board.fragment.graphql b/app/assets/javascripts/boards/queries/board.fragment.graphql
index 48f55e899bf..872a4c4afbc 100644
--- a/app/assets/javascripts/boards/queries/board.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/board.fragment.graphql
@@ -1,4 +1,4 @@
fragment BoardFragment on Board {
- id,
+ id
name
}
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
index 6ba6c05d6d9..5b532906f6a 100644
--- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
@@ -1,15 +1,15 @@
fragment BoardListShared on BoardList {
- id,
- title,
- position,
- listType,
- collapsed,
+ id
+ title
+ position
+ listType
+ collapsed
label {
- id,
- title,
- color,
- textColor,
- description,
+ id
+ title
+ color
+ textColor
+ description
descriptionHtml
}
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql
index 343de4e3025..a24b6737159 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql
@@ -1,3 +1,3 @@
mutation updateActiveDiscussion($id: String, $source: String) {
- updateActiveDiscussion (id: $id, source: $source ) @client
+ updateActiveDiscussion(id: $id, source: $source) @client
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
index 904acef599b..d694e6558a0 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
@@ -11,7 +11,7 @@ mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
sha
}
}
- },
+ }
}
skippedDesigns {
filename
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql
index 343de4e3025..a24b6737159 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql
+++ b/app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql
@@ -1,3 +1,3 @@
mutation updateActiveDiscussion($id: String, $source: String) {
- updateActiveDiscussion (id: $id, source: $source ) @client
+ updateActiveDiscussion(id: $id, source: $source) @client
}
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql
index 904acef599b..d694e6558a0 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql
@@ -11,7 +11,7 @@ mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
sha
}
}
- },
+ }
}
skippedDesigns {
filename
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index a737d956599..9558bfb04c6 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -148,7 +148,10 @@ export default {
<template>
<div class="content discussion-form discussion-form-container discussion-notes">
- <div v-if="glFeatures.multilineComments" class="gl-mb-3 gl-text-gray-700">
+ <div
+ v-if="glFeatures.multilineComments"
+ class="gl-mb-3 gl-text-gray-700 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-3"
+ >
<multiline-comment-form
v-model="commentLineStart"
:line="line"
diff --git a/app/assets/javascripts/error_tracking/queries/details.query.graphql b/app/assets/javascripts/error_tracking/queries/details.query.graphql
index b49c5b6626d..593cbf2ae52 100644
--- a/app/assets/javascripts/error_tracking/queries/details.query.graphql
+++ b/app/assets/javascripts/error_tracking/queries/details.query.graphql
@@ -1,29 +1,29 @@
query errorDetails($fullPath: ID!, $errorId: ID!) {
- project(fullPath: $fullPath) {
- sentryErrors {
- detailedError(id: $errorId) {
- id
- sentryId
- title
- userCount
- count
- status
- firstSeen
- lastSeen
- message
- culprit
- tags {
- level
- logger
- }
- externalUrl
- externalBaseUrl
- firstReleaseVersion
- lastReleaseVersion
- gitlabCommit
- gitlabCommitPath
- gitlabIssuePath
- }
+ project(fullPath: $fullPath) {
+ sentryErrors {
+ detailedError(id: $errorId) {
+ id
+ sentryId
+ title
+ userCount
+ count
+ status
+ firstSeen
+ lastSeen
+ message
+ culprit
+ tags {
+ level
+ logger
}
+ externalUrl
+ externalBaseUrl
+ firstReleaseVersion
+ lastReleaseVersion
+ gitlabCommit
+ gitlabCommitPath
+ gitlabIssuePath
+ }
}
+ }
}
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 28f1e3afd3d..4793a5c6250 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -106,4 +106,8 @@ export default class GLForm {
.removeClass('is-focused');
});
}
+
+ get supportsQuickActions() {
+ return Boolean(this.textarea.data('supports-quick-actions'));
+ }
}
diff --git a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
index 2c9013ffa9c..f0b50793226 100644
--- a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
+++ b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql
@@ -1,8 +1,8 @@
query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
- createMergeRequestIn,
- readMergeRequest,
+ createMergeRequestIn
+ readMergeRequest
pushCode
}
}
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index a122e0f70f6..bcf5dc2aaaf 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -420,7 +420,7 @@ export default {
<transition name="issuable-header-slide">
<div
v-if="shouldShowStickyHeader"
- class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-200 gl-py-3"
+ class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
data-testid="issue-sticky-header"
>
<div
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 960c79f65f6..1d929b3b558 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -129,8 +129,8 @@ export default {
'operationsSettingsPath',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
- isSystemDashboard() {
- return this.selectedDashboard?.system_dashboard;
+ isOutOfTheBoxDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard;
},
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
@@ -417,7 +417,7 @@ export default {
:project-path="projectPath"
/>
- <template v-if="isSystemDashboard">
+ <template v-if="isOutOfTheBoxDashboard">
<gl-new-dropdown-divider />
<gl-new-dropdown-item
ref="duplicateDashboardItem"
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 0e96d57b8a1..574f48a72fe 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -44,8 +44,8 @@ export default {
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
...mapGetters('monitoringDashboard', ['selectedDashboard']),
- isSystemDashboard() {
- return this.selectedDashboard?.system_dashboard;
+ isOutOfTheBoxDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard;
},
selectedDashboardText() {
return this.selectedDashboard?.display_name;
@@ -139,7 +139,7 @@ export default {
This Duplicate Dashboard item will be removed from the dashboards dropdown
in https://gitlab.com/gitlab-org/gitlab/-/issues/223223
-->
- <template v-if="isSystemDashboard">
+ <template v-if="isOutOfTheBoxDashboard">
<gl-dropdown-divider />
<gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem">
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 6e695de447d..f4982507adb 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1617,7 +1617,7 @@ export default class Notes {
}
tempFormContent = formContent;
- if (this.hasQuickActions(formContent)) {
+ if (this.glForm.supportsQuickActions && this.hasQuickActions(formContent)) {
tempFormContent = this.stripQuickActions(formContent);
hasQuickActions = true;
}
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 611bd19e628..45cf7888aff 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -340,7 +340,7 @@ export default {
:line="line"
:comment-line-options="commentLineOptions"
:line-range="note.position.line_range"
- class="gl-mb-3 gl-text-gray-700"
+ class="gl-mb-3 gl-text-gray-700 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-3"
/>
<div
v-else
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
index c63d4f10e0a..f25994a7506 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue
@@ -309,7 +309,8 @@ export default {
<div
v-for="(stage, index) in pipeline.details.stages"
:key="index"
- class="stage-container dropdown js-mini-pipeline-graph"
+ class="stage-container dropdown"
+ data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage
:type="$options.pipelinesTable"
diff --git a/app/assets/javascripts/registry/explorer/components/list_item.vue b/app/assets/javascripts/registry/explorer/components/list_item.vue
index 97175b1c2ea..7b5afe8fd9d 100644
--- a/app/assets/javascripts/registry/explorer/components/list_item.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_item.vue
@@ -40,7 +40,7 @@ export default {
'gl-border-b-1': !this.last,
'gl-border-b-2': this.last,
'disabled-content': this.disabled,
- 'gl-border-gray-200': !this.selected,
+ 'gl-border-gray-100': !this.selected,
'gl-bg-blue-50 gl-border-blue-200': this.selected,
};
},
@@ -109,14 +109,14 @@ export default {
<div class="gl-w-7"></div>
<div
v-if="isDetailsShown"
- class="gl-display-flex gl-flex-direction-column gl-flex-fill-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-200 gl-mb-3"
+ class="gl-display-flex gl-flex-direction-column gl-flex-fill-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100 gl-mb-3"
>
<div
v-for="(row, detailIndex) in detailsSlots"
:key="detailIndex"
class="gl-px-5 gl-py-2"
:class="{
- 'gl-border-gray-200 gl-border-t-solid gl-border-t-1': detailIndex !== 0,
+ 'gl-border-gray-100 gl-border-t-solid gl-border-t-1': detailIndex !== 0,
}"
>
<slot :name="row"></slot>
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
index 8cc68f6ea9a..2aff7da4605 100644
--- a/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/sidebarDetails.query.graphql
@@ -1,6 +1,6 @@
-query ($fullPath: ID!, $iid: String!) {
- project (fullPath: $fullPath) {
- issue (iid: $iid) {
+query($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ issue(iid: $iid) {
iid
}
}
diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
index 8cc68f6ea9a..2aff7da4605 100644
--- a/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql
@@ -1,6 +1,6 @@
-query ($fullPath: ID!, $iid: String!) {
- project (fullPath: $fullPath) {
- issue (iid: $iid) {
+query($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ issue(iid: $iid) {
iid
}
}
diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
index 2cca71708ca..a5425c7e87b 100644
--- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
+++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
@@ -26,6 +26,21 @@ fragment SnippetBase on Snippet {
...BlobViewer
}
}
+ blob {
+ binary
+ name
+ path
+ rawPath
+ size
+ externalStorage
+ renderedAsText
+ simpleViewer {
+ ...BlobViewer
+ }
+ richViewer {
+ ...BlobViewer
+ }
+ }
userPermissions {
adminSnippet
updateSnippet
diff --git a/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql b/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
index 0c829cbdee6..f43d53661f4 100644
--- a/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
+++ b/app/assets/javascripts/snippets/mutations/deleteSnippet.mutation.graphql
@@ -1,5 +1,5 @@
mutation DeleteSnippet($id: ID!) {
- destroySnippet(input: {id: $id}) {
+ destroySnippet(input: { id: $id }) {
errors
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql b/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
index 288bd0889bf..03c81460fb5 100644
--- a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
+++ b/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
@@ -4,4 +4,4 @@ query CanCreateProjectSnippet($fullPath: ID!) {
createSnippet
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql b/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
index f5b97b3d0f0..c3e5519e266 100644
--- a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
+++ b/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
@@ -4,4 +4,4 @@ query CanCreatePersonalSnippet {
createSnippet
}
}
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql b/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
index 2840d419966..cd130aa7dbb 100644
--- a/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
@@ -1,5 +1,5 @@
mutation submitContentChanges($input: SubmitContentChangesInput) {
- submitContentChanges(input: $input) @client {
+ submitContentChanges(input: $input) @client {
branch
commit
mergeRequest
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
index fdbf4459aee..946d80efff0 100644
--- a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
@@ -3,7 +3,7 @@ query appData {
isSupportedContent
project
sourcePath
- username,
+ username
returnUrl
}
}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
index e36d244ae57..cfe30c601ed 100644
--- a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
@@ -1,6 +1,6 @@
query sourceContent($project: ID!, $sourcePath: String!) {
project(fullPath: $project) {
- fullPath,
+ fullPath
file(path: $sourcePath) @client {
title
content
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 3c468adc176..a096eb1a1fe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,21 +1,22 @@
<script>
/* eslint-disable vue/require-default-prop */
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
-import { sprintf, s__ } from '~/locale';
+import { s__ } from '~/locale';
import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
name: 'MRWidgetPipeline',
components: {
- PipelineStage,
CiIcon,
- Icon,
- TooltipOnTruncate,
GlLink,
+ GlLoadingIcon,
+ GlIcon,
+ GlSprintf,
+ PipelineStage,
+ TooltipOnTruncate,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
@@ -54,7 +55,11 @@ export default {
type: String,
required: false,
},
- troubleshootingDocsPath: {
+ mrTroubleshootingDocsPath: {
+ type: String,
+ required: true,
+ },
+ ciTroubleshootingDocsPath: {
type: String,
required: true,
},
@@ -64,10 +69,7 @@ export default {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
hasCIError() {
- return (this.hasCi && !this.ciStatus) || this.hasPipelineMustSucceedConflict;
- },
- hasPipelineMustSucceedConflict() {
- return !this.hasCi && this.pipelineMustSucceed;
+ return this.hasPipeline && !this.ciStatus;
},
status() {
return this.pipeline.details && this.pipeline.details.status
@@ -82,22 +84,6 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
- errorText() {
- if (this.hasPipelineMustSucceedConflict) {
- return s__('Pipeline|No pipeline has been run for this commit.');
- }
-
- return sprintf(
- s__(
- 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
- ),
- {
- linkStart: `<a href="${this.troubleshootingDocsPath}">`,
- linkEnd: '</a>',
- },
- false,
- );
- },
isTriggeredByMergeRequest() {
return Boolean(this.pipeline.merge_request);
},
@@ -118,15 +104,51 @@ export default {
return '';
},
},
+ errorText: s__(
+ 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
+ ),
+ monitoringPipelineText: s__('Pipeline|Checking pipeline status.'),
};
</script>
<template>
- <div class="ci-widget media js-ci-widget">
- <template v-if="!hasPipeline || hasCIError">
- <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error">
- <icon :size="24" name="status_failed_borderless" />
+ <div class="ci-widget media">
+ <template v-if="hasCIError">
+ <gl-icon name="status_failed" class="gl-text-red-500" :size="24" />
+ <div
+ class="gl-flex-fill-1 gl-ml-5"
+ tabindex="0"
+ role="text"
+ :aria-label="$options.errorText"
+ data-testid="ci-error-message"
+ >
+ <gl-sprintf :message="$options.errorText">
+ <template #link="{content}">
+ <gl-link :href="mrTroubleshootingDocsPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <template v-else-if="!hasPipeline">
+ <gl-loading-icon size="md" />
+ <div class="gl-flex-fill-1 gl-display-flex gl-ml-5" data-testid="monitoring-pipeline-message">
+ <span tabindex="0" role="text" :aria-label="$options.monitoringPipelineText">
+ <gl-sprintf :message="$options.monitoringPipelineText" />
+ </span>
+ <gl-link
+ :href="ciTroubleshootingDocsPath"
+ target="_blank"
+ class="gl-display-flex gl-align-items-center gl-ml-2"
+ tabindex="0"
+ >
+ <gl-icon
+ name="question"
+ :small="12"
+ tabindex="0"
+ role="text"
+ :aria-label="__('Link to go to GitLab pipeline documentation')"
+ />
+ </gl-link>
</div>
- <div class="media-body gl-ml-3" v-html="errorText"></div>
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="align-self-start gl-mr-3">
@@ -136,13 +158,15 @@ export default {
<div class="ci-widget-content">
<div class="media-body">
<div
- class="font-weight-bold js-pipeline-info-container"
+ class="gl-font-weight-bold"
+ data-testid="pipeline-info-container"
data-qa-selector="merge_request_pipeline_info_content"
>
{{ pipeline.details.name }}
<gl-link
:href="pipeline.path"
- class="pipeline-id font-weight-normal pipeline-number"
+ class="pipeline-id gl-font-weight-normal pipeline-number"
+ data-testid="pipeline-id"
data-qa-selector="pipeline_link"
>#{{ pipeline.id }}</gl-link
>
@@ -151,7 +175,8 @@ export default {
{{ s__('Pipeline|for') }}
<gl-link
:href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link font-weight-normal"
+ class="commit-sha gl-font-weight-normal"
+ data-testid="commit-link"
>{{ pipeline.commit.short_id }}</gl-link
>
</template>
@@ -160,18 +185,18 @@ export default {
<tooltip-on-truncate
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate font-weight-normal"
+ class="label-branch label-truncate gl-font-weight-normal"
v-html="sourceBranchLink"
/>
</template>
</div>
- <div v-if="pipeline.coverage" class="coverage">
+ <div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
<span
v-if="pipelineCoverageDelta"
- class="js-pipeline-coverage-delta"
:class="coverageDeltaClass"
+ data-testid="pipeline-coverage-delta"
>
({{ pipelineCoverageDelta }}%)
</span>
@@ -189,13 +214,13 @@ export default {
:class="{
'has-downstream': hasDownstream(i),
}"
- class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ class="stage-container dropdown mr-widget-pipeline-stages"
+ data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage :stage="stage" />
</div>
</template>
</span>
-
<linked-pipelines-mini-list v-if="triggered.length" :triggered="triggered" />
</span>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 8fba0e2981f..5c307b5ff0c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -82,7 +82,8 @@ export default {
:pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds"
:source-branch="branch"
:source-branch-link="branchLink"
- :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ :mr-troubleshooting-docs-path="mr.mrTroubleshootingDocsPath"
+ :ci-troubleshooting-docs-path="mr.ciTroubleshootingDocsPath"
/>
<template #footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 6f6d145815e..1002bb728a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -1,3 +1,4 @@
+export const SUCCESS = 'success';
export const WARNING = 'warning';
export const DANGER = 'danger';
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 8bba40a593d..0386448dbd2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -163,7 +163,8 @@ export default class MergeRequestStore {
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
- this.troubleshootingDocsPath = data.troubleshooting_docs_path;
+ this.mrTroubleshootingDocsPath = data.mr_troubleshooting_docs_path;
+ this.ciTroubleshootingDocsPath = data.ci_troubleshooting_docs_path;
this.pipelineMustSucceedDocsPath = data.pipeline_must_succeed_docs_path;
this.mergeRequestBasicPath = data.merge_request_basic_path;
this.mergeRequestWidgetPath = data.merge_request_widget_path;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index 28cd33454ea..f0f807c403f 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,8 +1,10 @@
+import renderHtml from './renderers/render_html';
import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
+const htmlRenderers = [renderHtml];
const listRenderers = [renderKramdownList];
const paragraphRenderers = [renderIdentifierParagraph];
const textRenderers = [renderKramdownText, renderEmbeddedRubyText];
@@ -23,8 +25,15 @@ const buildCustomRendererFunctions = (customRenderers, defaults) => {
return Object.fromEntries(customEntries);
};
-const buildCustomHTMLRenderer = (customRenderers = { list: [], paragraph: [], text: [] }) => {
+const buildCustomHTMLRenderer = (
+ customRenderers = { htmlBlock: [], list: [], paragraph: [], text: [] },
+) => {
const defaults = {
+ htmlBlock(node, context) {
+ const allHtmlRenderers = [...customRenderers.list, ...htmlRenderers];
+
+ return executeRenderer(allHtmlRenderers, node, context);
+ },
list(node, context) {
const allListRenderers = [...customRenderers.list, ...listRenderers];
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js
new file mode 100644
index 00000000000..a3b467851dc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_html.js
@@ -0,0 +1,9 @@
+import { buildUneditableTokens } from './build_uneditable_token';
+
+const canRender = ({ type }) => {
+ return type === 'htmlBlock';
+};
+
+const render = (_, { origin }) => buildUneditableTokens(origin());
+
+export default { canRender, render };
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 77354456fe5..60b71137b25 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -98,7 +98,7 @@
&::before {
content: '';
- border-left: 1px solid $gray-200;
+ border-left: 1px solid $gray-100;
position: absolute;
left: 28px;
top: -18px;
@@ -146,7 +146,7 @@
}
.design-dropzone-border {
- border: 2px dashed $gray-200;
+ border: 2px dashed $gray-100;
}
.design-dropzone-card {
diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss
index 445cd971d66..f870948cc4f 100644
--- a/app/assets/stylesheets/components/popover.scss
+++ b/app/assets/stylesheets/components/popover.scss
@@ -1,6 +1,6 @@
.popover {
max-width: $popover-max-width;
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
box-shadow: $popover-box-shadow;
font-size: $gl-font-size-small;
@@ -50,7 +50,7 @@
* due to the box-shadow include in our custom styles.
*/
> .arrow::before {
- border-top-color: $gray-200;
+ border-top-color: $gray-100;
bottom: 1px;
}
@@ -61,7 +61,7 @@
.bs-popover-bottom {
> .arrow::before {
- border-bottom-color: $gray-200;
+ border-bottom-color: $gray-100;
}
> .popover-header::before {
@@ -70,11 +70,11 @@
}
.bs-popover-right > .arrow::before {
- border-right-color: $gray-200;
+ border-right-color: $gray-100;
}
.bs-popover-left > .arrow::before {
- border-left-color: $gray-200;
+ border-left-color: $gray-100;
}
.popover-header {
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 0eab86ff7ea..86e701604b5 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -179,7 +179,7 @@
&.user-authored {
cursor: default;
background-color: $gray-light;
- border-color: $gray-200;
+ border-color: $gray-100;
color: $gl-text-color-disabled;
gl-emoji {
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index 534ada08b85..f7836213e5c 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -28,7 +28,7 @@
max-width: 300px;
width: auto;
background: $white;
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
box-shadow: 0 1px 2px 0 rgba($black, 0.1);
border-radius: $border-radius-default;
z-index: 999;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 1ed4484a934..fd5b3f74c4a 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -502,7 +502,7 @@
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light;
- border-color: $gray-200;
+ border-color: $gray-100;
color: $gl-text-color-disabled;
opacity: 1;
text-decoration: none;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 622dbc20ac1..9da0b0da598 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1087,7 +1087,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.color-input-container {
.dropdown-label-color-preview {
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
border-right: 0;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 72299691ae5..8f209d2d99a 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -158,7 +158,7 @@
.filtered-search-token .selected,
.filtered-search-term .selected {
.name {
- background-color: $gray-200;
+ background-color: $gray-100;
}
.operator {
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 2c85f8ed7cc..288849ba438 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -342,7 +342,7 @@ body {
.navbar-toggler,
.navbar-toggler:hover {
color: $gray-700;
- border-left: 1px solid $gray-200;
+ border-left: 1px solid $gray-100;
}
}
}
@@ -360,7 +360,7 @@ body {
.search-input-wrap {
.search-icon {
- fill: $gray-200;
+ fill: $gray-100;
}
.search-input {
diff --git a/app/assets/stylesheets/framework/memory_graph.scss b/app/assets/stylesheets/framework/memory_graph.scss
index b0bfc4f47ff..510969e149a 100644
--- a/app/assets/stylesheets/framework/memory_graph.scss
+++ b/app/assets/stylesheets/framework/memory_graph.scss
@@ -1,4 +1,4 @@
.memory-graph-container {
background: $white;
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 1131248dd3f..9b33ed1b630 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -214,7 +214,7 @@
.health-status {
.dropdown-body {
.health-divider {
- border-top-color: $gray-200;
+ border-top-color: $gray-100;
}
.dropdown-item:not(.health-dropdown-item) {
diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss
index 0a57a74eafc..2d16fdf4ee7 100644
--- a/app/assets/stylesheets/framework/stacked_progress_bar.scss
+++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss
@@ -36,7 +36,7 @@
}
.status-neutral {
- background-color: $gray-200;
+ background-color: $gray-100;
color: $gl-gray-dark;
&:hover {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 6e07a2b5de1..b5b86b807a6 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -89,7 +89,7 @@
background-color: $gray-10;
border-width: 1px;
border-style: solid;
- border-color: $gray-200 $gray-200 $gray-400;
+ border-color: $gray-100 $gray-100 $gray-400;
border-image: none;
border-radius: 3px;
box-shadow: 0 -1px 0 $gray-400 inset;
@@ -181,7 +181,7 @@
background-color: $white;
td {
- border-color: $gray-200;
+ border-color: $gray-100;
}
}
@@ -611,7 +611,7 @@ pre {
word-wrap: break-word;
color: $gl-text-color;
background-color: $gray-light;
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
border-radius: $border-radius-small;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index fc64dae14eb..79f9cf5941f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -164,7 +164,7 @@ $red-950: #4d0a00 !default;
$gray-10: #fafafa !default;
$gray-50: #f0f0f0 !default;
$gray-100: #dbdbdb !default;
-$gray-200: #dfdfdf !default;
+$gray-200: #bfbfbf !default;
$gray-300: #ccc !default;
$gray-400: #bababa !default;
$gray-500: #a7a7a7 !default;
@@ -333,7 +333,7 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
/*
* UI elements
*/
-$border-color: $gray-200;
+$border-color: $gray-100;
$shadow-color: $t-gray-a-08;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
@@ -867,7 +867,7 @@ $priority-label-empty-state-width: 114px;
Popovers
*/
$popover-max-width: 384px;
-$popover-box-shadow: 0 2px 3px 1px $gray-200;
+$popover-box-shadow: 0 2px 3px 1px $gray-100;
/*
Issues Analytics
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index fc5a30a5d7b..acfda718e77 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -5,14 +5,14 @@
$secondary: $gray-light;
$input-disabled-bg: $gray-light;
-$input-border-color: $gray-200;
+$input-border-color: $gray-100;
$input-color: $gl-text-color;
$input-font-size: $gl-font-size;
$font-family-sans-serif: $regular-font;
$font-family-monospace: $monospace-font;
$btn-line-height: 20px;
$table-accent-bg: $gray-light;
-$table-border-color: $gray-200;
+$table-border-color: $gray-100;
$card-border-color: $border-color;
$card-cap-bg: $gray-light !default;
$success: $green-500;
@@ -21,7 +21,7 @@ $warning: $orange-500;
$danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
-$dropdown-divider-bg: $gray-200;
+$dropdown-divider-bg: $gray-100;
$dropdown-item-padding-y: 8px;
$dropdown-item-padding-x: 12px;
$popover-max-width: 300px;
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index 2b82b2226c6..a8d10ea1a29 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -146,7 +146,7 @@
}
pre {
- border-color: var(--ide-border-color-alt, $gray-200);
+ border-color: var(--ide-border-color-alt, $gray-100);
code {
background-color: var(--ide-border-color, inherit);
@@ -216,7 +216,7 @@
color: var(--ide-text-color, $gl-text-color);
&:hover {
- background-color: var(--ide-input-border, $gray-200);
+ background-color: var(--ide-input-border, $gray-100);
}
}
@@ -300,8 +300,8 @@
}
.divider {
- background-color: var(--ide-dropdown-hover-background, $gray-200);
- border-color: var(--ide-dropdown-hover-background, $gray-200);
+ background-color: var(--ide-dropdown-hover-background, $gray-100);
+ border-color: var(--ide-dropdown-hover-background, $gray-100);
}
li > a:not(.disable-hover):hover,
@@ -316,7 +316,7 @@
.dropdown-title,
.dropdown-input {
- border-color: var(--ide-dropdown-hover-background, $gray-200) !important;
+ border-color: var(--ide-dropdown-hover-background, $gray-100) !important;
}
.btn-primary,
@@ -356,7 +356,7 @@
.btn[disabled] {
background-color: var(--ide-btn-default-background, $gray-light) !important;
- border: 1px solid var(--ide-btn-disabled-border, $gray-200) !important;
+ border: 1px solid var(--ide-btn-disabled-border, $gray-100) !important;
color: var(--ide-btn-disabled-color, $gl-text-color-disabled) !important;
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 83623cbbc65..a07755724dd 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -145,7 +145,7 @@ $ide-commit-header-height: 48px;
}
&:not([disabled]):hover {
- background-color: var(--ide-input-border, $gray-200);
+ background-color: var(--ide-input-border, $gray-100);
}
&:not([disabled]):focus {
@@ -396,7 +396,7 @@ $ide-commit-header-height: 48px;
}
&:active {
- background: var(--ide-background, $gray-200);
+ background: var(--ide-background, $gray-100);
}
&.is-active {
@@ -567,7 +567,7 @@ $ide-commit-header-height: 48px;
&:focus {
color: var(--ide-text-color, $gl-text-color);
- background-color: var(--ide-background-hover, $gray-200);
+ background-color: var(--ide-background-hover, $gray-100);
}
&.active {
@@ -1046,7 +1046,7 @@ $ide-commit-header-height: 48px;
background-color: var(--ide-background, $gray-50);
&:hover {
- background-color: var(--ide-file-row-btn-hover-background, $gray-200);
+ background-color: var(--ide-file-row-btn-hover-background, $gray-100);
}
&:active,
@@ -1097,7 +1097,7 @@ $ide-commit-header-height: 48px;
&:focus {
outline: 0;
box-shadow: none;
- border-color: var(--ide-border-color, $gray-200);
+ border-color: var(--ide-border-color, $gray-100);
}
}
@@ -1140,7 +1140,7 @@ $ide-commit-header-height: 48px;
}
.file-row:active {
- background: var(--ide-background, $gray-200);
+ background: var(--ide-background, $gray-100);
}
.file-row.is-active {
diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss
index 6c1370dbb5c..73a4af00c5a 100644
--- a/app/assets/stylesheets/pages/alert_management/details.scss
+++ b/app/assets/stylesheets/pages/alert_management/details.scss
@@ -61,7 +61,7 @@
&.is-active {
&:last-child {
- border-bottom: 1px solid $gray-200;
+ border-bottom: 1px solid $gray-100;
}
}
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
index e1715b8e1bf..3c49cc54ac4 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -23,7 +23,7 @@
.bar {
height: 4px;
- background-color: $gl-gray-200;
+ background-color: $gl-gray-100;
}
.count {
@@ -34,7 +34,7 @@
.graph-separator {
width: $graph-separator-width;
height: 18px;
- background-color: $gl-gray-200;
+ background-color: $gl-gray-100;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index f50d4bc736e..02c42d5b779 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -236,7 +236,7 @@
.trigger-variables-table-cell {
font-size: $gl-font-size-small;
line-height: $gl-line-height;
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
padding: $gl-padding-4 6px;
width: 50%;
vertical-align: top;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6fe94aa49cf..a7d0d4259ea 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -382,7 +382,7 @@
overflow: hidden;
&:hover {
- background-color: $gray-200;
+ background-color: $gray-100;
}
&.issuable-sidebar-header {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 1ed7b8580bf..61232b00275 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -64,7 +64,7 @@ $mr-widget-min-height: 69px;
background-color: $gray-light;
&.clickable:hover {
- background-color: $gl-gray-200;
+ background-color: $gl-gray-100;
cursor: pointer;
}
}
@@ -75,7 +75,7 @@ $mr-widget-min-height: 69px;
&::before {
content: '';
- border-left: 1px solid $gray-200;
+ border-left: 1px solid $gray-100;
position: absolute;
left: 28px;
top: -17px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 5873ced59da..40f0104a2bf 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -186,8 +186,8 @@ $note-form-margin-left: 72px;
padding: $gl-padding;
.dummy-avatar {
- background-color: $gl-gray-200;
- border: 1px solid darken($gl-gray-200, 25%);
+ background-color: $gl-gray-100;
+ border: 1px solid darken($gl-gray-100, 25%);
}
.note-headline-light,
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 43d766db9e0..d76cddfb46e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -816,7 +816,7 @@
&.ci-status-icon-created,
&.ci-status-icon-skipped {
- @include mini-pipeline-graph-color($white, $gray-200, $gray-300, $gray-500, $gray-600, $gray-700);
+ @include mini-pipeline-graph-color($white, $gray-100, $gray-300, $gray-500, $gray-600, $gray-700);
}
}
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index 26db1fb9f58..6461d09bb47 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -34,7 +34,7 @@
.draggable {
&.draggable-enabled {
.draggable-panel {
- border: $gray-200 1px solid;
+ border: $gray-100 1px solid;
border-radius: $border-radius-default;
margin: -1px;
cursor: grab;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index d26c07ce51b..f1df9099d82 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -347,7 +347,7 @@
.btn-clipboard {
background-color: $white;
- border: 1px solid $gray-200;
+ border: 1px solid $gray-100;
}
.deploy-token-help-block {
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 4eef4d361a1..daeab80d373 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -61,7 +61,7 @@
padding: 4px 6px;
font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1;
- color: $gl-gray-200;
+ color: $gl-gray-100;
border-radius: 3px;
box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
diff --git a/app/controllers/admin/clusters_controller.rb b/app/controllers/admin/clusters_controller.rb
index 5b1902fad51..9a642e53d86 100644
--- a/app/controllers/admin/clusters_controller.rb
+++ b/app/controllers/admin/clusters_controller.rb
@@ -10,6 +10,11 @@ class Admin::ClustersController < Clusters::ClustersController
def clusterable
@clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user)
end
-end
-Admin::ClustersController.prepend_if_ee('EE::Admin::ClustersController')
+ def metrics_dashboard_params
+ {
+ cluster: cluster,
+ cluster_type: :admin
+ }
+ end
+end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index ceda37737b6..2e8b3d764ca 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -3,6 +3,7 @@
class Clusters::ClustersController < Clusters::BaseController
include RoutableActions
include Metrics::Dashboard::PrometheusApiProxy
+ include MetricsDashboard
before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache]
before_action :generate_gcp_authorize_url, only: [:new]
diff --git a/app/controllers/groups/clusters_controller.rb b/app/controllers/groups/clusters_controller.rb
index 2165dee45fb..33bfc24885f 100644
--- a/app/controllers/groups/clusters_controller.rb
+++ b/app/controllers/groups/clusters_controller.rb
@@ -17,6 +17,12 @@ class Groups::ClustersController < Clusters::ClustersController
def group
@group ||= find_routable!(Group, params[:group_id] || params[:id])
end
-end
-Groups::ClustersController.prepend_if_ee('EE::Groups::ClustersController')
+ def metrics_dashboard_params
+ {
+ cluster: cluster,
+ cluster_type: :group,
+ group: group
+ }
+ end
+end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 079d30127d6..8acf5235c1a 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -23,6 +23,13 @@ class Projects::ClustersController < Clusters::ClustersController
def repository
@repository ||= project.repository
end
-end
-Projects::ClustersController.prepend_if_ee('EE::Projects::ClustersController')
+ def metrics_dashboard_params
+ params.permit(:embedded, :group, :title, :y_label).merge(
+ {
+ cluster: cluster,
+ cluster_type: :project
+ }
+ )
+ end
+end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index ecde2c9f475..941abb70400 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -117,7 +117,7 @@ class SnippetsFinder < UnionFinder
queries << snippets_of_authorized_projects if current_user
end
- find_union(queries, Snippet)
+ prepared_union(queries)
end
def snippets_for_a_single_project
@@ -208,6 +208,17 @@ class SnippetsFinder < UnionFinder
def sort_param
sort.presence || 'id_desc'
end
+
+ def prepared_union(queries)
+ return Snippet.none if queries.empty?
+ return queries.first if queries.length == 1
+
+ # The queries are going to be part of a global `where`
+ # therefore we only need to retrieve the `id` column
+ # which will speed the query
+ queries.map! { |rel| rel.select(:id) }
+ Snippet.id_in(find_union(queries, Snippet))
+ end
end
SnippetsFinder.prepend_if_ee('EE::SnippetsFinder')
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index df3adcc48e5..cbae25d4aba 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -4,7 +4,7 @@ module Projects::AlertManagementHelper
def alert_management_data(current_user, project)
{
'project-path' => project.full_path,
- 'enable-alert-management-path' => edit_project_service_path(project, AlertsService),
+ 'enable-alert-management-path' => project_settings_operations_path(project, anchor: 'js-alert-management-settings'),
'populating-alerts-help-url' => help_page_url('user/project/operations/alert_management.html', anchor: 'enable-alert-management'),
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 02b67706061..8172e55d75c 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -344,6 +344,10 @@ module Clusters
Feature.enabled?(:managed_apps_local_tiller, clusterable, default_enabled: true)
end
+ def prometheus_adapter
+ application_prometheus
+ end
+
private
def unique_management_project_environment_scope
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index f7f8aac861e..d909b67d7ba 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -76,7 +76,11 @@ module HasRepository
end
def default_branch
- @default_branch ||= repository.root_ref
+ @default_branch ||= repository.root_ref || default_branch_from_preferences
+ end
+
+ def default_branch_from_preferences
+ empty_repo? ? Gitlab::CurrentSettings.default_branch_name : nil
end
def reload_default_branch
diff --git a/app/models/epic.rb b/app/models/epic.rb
index e09dc1080e6..93f286f97d3 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -5,8 +5,6 @@
class Epic < ApplicationRecord
include IgnorableColumns
- ignore_column :health_status, remove_with: '13.0', remove_after: '2019-05-22'
-
def self.link_reference_pattern
nil
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 1061e2bd0c6..488ebd531a8 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -17,9 +17,9 @@ class PersonalAccessToken < ApplicationRecord
before_save :ensure_token
- scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
- scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= NOW() AND expires_at <= ?", date]) }
- scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
+ scope :active, -> { where("revoked = false AND (expires_at >= CURRENT_DATE OR expires_at IS NULL)") }
+ scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) }
+ scope :inactive, -> { where("revoked = true OR expires_at < CURRENT_DATE") }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
scope :revoked, -> { where(revoked: true) }
diff --git a/app/models/webauthn_registration.rb b/app/models/webauthn_registration.rb
new file mode 100644
index 00000000000..76f8faa11c7
--- /dev/null
+++ b/app/models/webauthn_registration.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# Registration information for WebAuthn credentials
+
+class WebauthnRegistration < ApplicationRecord
+ belongs_to :user
+
+ validates :credential_xid, :public_key, :name, :counter, presence: true
+ validates :counter,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
+end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 6d21ae8a4f8..efb3cf7f348 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -64,6 +64,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
raise NotImplementedError
end
+ def metrics_dashboard_path(cluster)
+ raise NotImplementedError
+ end
+
# Will be overridden in EE
def environments_cluster_path(cluster)
nil
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index c4e3393cac9..85a62fefd8f 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -64,8 +64,27 @@ module Clusters
!cluster.provided_by_user?
end
+ def health_data(clusterable)
+ {
+ 'clusters-path': clusterable.index_path,
+ 'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
+ 'documentation-path': help_page_path('user/project/clusters/index', anchor: 'monitoring-your-kubernetes-cluster-ultimate'),
+ 'empty-getting-started-svg-path': image_path('illustrations/monitoring/getting_started.svg'),
+ 'empty-loading-svg-path': image_path('illustrations/monitoring/loading.svg'),
+ 'empty-no-data-svg-path': image_path('illustrations/monitoring/no_data.svg'),
+ 'empty-unable-to-connect-svg-path': image_path('illustrations/monitoring/unable_to_connect.svg'),
+ 'settings-path': '',
+ 'project-path': '',
+ 'tags-path': ''
+ }
+ end
+
private
+ def image_path(path)
+ ActionController::Base.helpers.image_path(path)
+ end
+
def clusterable
if cluster.group_type?
cluster.group
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
index 21db2f6f96b..dfe8e315f94 100644
--- a/app/presenters/group_clusterable_presenter.rb
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -43,6 +43,10 @@ class GroupClusterablePresenter < ClusterablePresenter
def learn_more_link
link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/group/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
+
+ def metrics_dashboard_path(cluster)
+ metrics_dashboard_group_cluster_path(clusterable, cluster)
+ end
end
GroupClusterablePresenter.prepend_if_ee('EE::GroupClusterablePresenter')
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 41071bc7bc7..7704e6b59c1 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -81,6 +81,10 @@ class InstanceClusterablePresenter < ClusterablePresenter
def learn_more_link
link_to(s_('ClusterIntegration|Learn more about instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
+
+ def metrics_dashboard_path(cluster)
+ metrics_dashboard_admin_cluster_path(cluster)
+ end
end
InstanceClusterablePresenter.prepend_if_ee('EE::InstanceClusterablePresenter')
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 5c56d42ed27..718f653eab1 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -38,6 +38,10 @@ class ProjectClusterablePresenter < ClusterablePresenter
def learn_more_link
link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
+
+ def metrics_dashboard_path(cluster)
+ metrics_dashboard_project_cluster_path(clusterable, cluster)
+ end
end
ProjectClusterablePresenter.prepend_if_ee('EE::ProjectClusterablePresenter')
diff --git a/app/views/admin/application_settings/_initial_branch_name.html.haml b/app/views/admin/application_settings/_initial_branch_name.html.haml
new file mode 100644
index 00000000000..e76374e88a8
--- /dev/null
+++ b/app/views/admin/application_settings/_initial_branch_name.html.haml
@@ -0,0 +1,12 @@
+= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ - fallback_branch_name = '<code>master</code>'
+
+ %fieldset
+ .form-group
+ = f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
+ = f.text_field :default_branch_name, placeholder: 'master', class: 'form-control'
+ %span.form-text.text-muted
+ = (_("Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name } ).html_safe
+ = f.submit _('Save changes'), class: 'gl-button btn-success'
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index b0934a9d9fb..11de79cf4a2 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -2,6 +2,18 @@
- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
+- if Feature.enabled?(:global_default_branch_name)
+ %section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Default initial branch name')
+ %button.gl-button.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Set the default name of the initial branch when creating new repositories through the user interface.')
+ .settings-content
+ = render 'initial_branch_name'
+
%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 9c129fa9ecc..bcc74e8d1d9 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -7,7 +7,7 @@
%section.issuable-discussion.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue).to_json,
- noteable_data: serialize_issuable(@issue, with_blocking_issues: Feature.enabled?(:prevent_closing_blocked_issues, @issue.project)),
+ noteable_data: serialize_issuable(@issue, with_blocking_issues: true),
noteable_type: 'Issue',
target_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json } }
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index bf2c5593cee..ee1dd756610 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -7,7 +7,8 @@
window.gl.mrWidgetData = #{serialize_issuable(@merge_request, serializer: 'widget', issues_links: true)}
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
- window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
+ window.gl.mrWidgetData.ci_troubleshooting_docs_path = '#{help_page_path('ci/troubleshooting.md')}';
+ window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
diff --git a/app/views/projects/services/alerts/_top.html.haml b/app/views/projects/services/alerts/_top.html.haml
index ad8f843cf7a..ebc93978832 100644
--- a/app/views/projects/services/alerts/_top.html.haml
+++ b/app/views/projects/services/alerts/_top.html.haml
@@ -5,4 +5,4 @@
.gl-alert-body
= _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.')
.gl-alert-actions
- = link_to _('Visit settings page'), project_settings_operations_path(@project), class: 'btn gl-alert-action btn-info new-gl-button'
+ = link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'btn gl-alert-action btn-info new-gl-button'
diff --git a/app/views/projects/services/prometheus/_top.html.haml b/app/views/projects/services/prometheus/_top.html.haml
index 08b09739341..62225958c04 100644
--- a/app/views/projects/services/prometheus/_top.html.haml
+++ b/app/views/projects/services/prometheus/_top.html.haml
@@ -7,4 +7,4 @@
.gl-alert-body
= s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
.gl-alert-actions
- = link_to _('Visit settings page'), project_settings_operations_path(@project), class: 'btn gl-alert-action btn-info gl-button'
+ = link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'btn gl-alert-action btn-info gl-button'
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index a77e38831b5..f8f3ecb6273 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -1,6 +1,7 @@
- return unless can?(current_user, :admin_operations, @project)
+- expanded = expanded_by_default?
-%section.settings.no-animate.js-alert-management-settings
+%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded) }
.settings-header
%h3{ :class => "h4" }
= _('Alerts')
diff --git a/changelogs/unreleased/221301-update-gray-200-value-and-usages.yml b/changelogs/unreleased/221301-update-gray-200-value-and-usages.yml
new file mode 100644
index 00000000000..dbf92732837
--- /dev/null
+++ b/changelogs/unreleased/221301-update-gray-200-value-and-usages.yml
@@ -0,0 +1,5 @@
+---
+title: Updating $gray-200 hex value and remapping current instances to $gray-100
+merge_request: 36128
+author:
+type: other
diff --git a/changelogs/unreleased/22506-webauthn-step-1-migrations.yml b/changelogs/unreleased/22506-webauthn-step-1-migrations.yml
new file mode 100644
index 00000000000..d9bf27590ef
--- /dev/null
+++ b/changelogs/unreleased/22506-webauthn-step-1-migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Prepare database for WebAuthn
+merge_request: 35797
+author: Jan Beckmann
+type: other
diff --git a/changelogs/unreleased/227190-add-pagerduty-related-columns-to-project_incident_management_setti.yml b/changelogs/unreleased/227190-add-pagerduty-related-columns-to-project_incident_management_setti.yml
new file mode 100644
index 00000000000..5d48397e2e7
--- /dev/null
+++ b/changelogs/unreleased/227190-add-pagerduty-related-columns-to-project_incident_management_setti.yml
@@ -0,0 +1,5 @@
+---
+title: Add PagerDuty integration columns to `project_incident_management_settings` table.
+merge_request: 36277
+author:
+type: added
diff --git a/changelogs/unreleased/25429-fix-disabled-quick-actions-in-notes.yml b/changelogs/unreleased/25429-fix-disabled-quick-actions-in-notes.yml
new file mode 100644
index 00000000000..150d377b465
--- /dev/null
+++ b/changelogs/unreleased/25429-fix-disabled-quick-actions-in-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Snippet comments where any line begins with a slash following an alphabetic character can't be published
+merge_request: 36563
+type: fixed
diff --git a/changelogs/unreleased/36250-custom-renderer-html.yml b/changelogs/unreleased/36250-custom-renderer-html.yml
new file mode 100644
index 00000000000..8c4c521b1ca
--- /dev/null
+++ b/changelogs/unreleased/36250-custom-renderer-html.yml
@@ -0,0 +1,5 @@
+---
+title: Add initial custom HTML renderer to the Static Site Editor to prevent editing in WYSIWYG mode
+merge_request: 36250
+author:
+type: added
diff --git a/changelogs/unreleased/alert-settings-link-follow-through.yml b/changelogs/unreleased/alert-settings-link-follow-through.yml
new file mode 100644
index 00000000000..2864793837f
--- /dev/null
+++ b/changelogs/unreleased/alert-settings-link-follow-through.yml
@@ -0,0 +1,5 @@
+---
+title: Expand Operations > Alerts section by default via link follow through
+merge_request: 36649
+author:
+type: other
diff --git a/changelogs/unreleased/astoicescu-allowOotbDashboardsToBeCloned.yml b/changelogs/unreleased/astoicescu-allowOotbDashboardsToBeCloned.yml
new file mode 100644
index 00000000000..0423640571b
--- /dev/null
+++ b/changelogs/unreleased/astoicescu-allowOotbDashboardsToBeCloned.yml
@@ -0,0 +1,5 @@
+---
+title: Allow self monitoring dashboard to be duplicated
+merge_request: 36433
+author:
+type: fixed
diff --git a/changelogs/unreleased/backfill-routes-for-users-migration.yml b/changelogs/unreleased/backfill-routes-for-users-migration.yml
new file mode 100644
index 00000000000..c883d95ca0d
--- /dev/null
+++ b/changelogs/unreleased/backfill-routes-for-users-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Backfill missing routes for Bot users
+merge_request: 35960
+author:
+type: fixed
diff --git a/changelogs/unreleased/change-pipeline-widget-when-no-pipeline-ran-for-commit.yml b/changelogs/unreleased/change-pipeline-widget-when-no-pipeline-ran-for-commit.yml
new file mode 100644
index 00000000000..6fb36f72b98
--- /dev/null
+++ b/changelogs/unreleased/change-pipeline-widget-when-no-pipeline-ran-for-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Add generic message when no pipeline in MR
+merge_request: 35980
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_blocked_issue_warning.yml b/changelogs/unreleased/fix_blocked_issue_warning.yml
new file mode 100644
index 00000000000..3f587601e54
--- /dev/null
+++ b/changelogs/unreleased/fix_blocked_issue_warning.yml
@@ -0,0 +1,5 @@
+---
+title: Remove `:prevent_closing_blocked_issues` feature flag
+merge_request: 32630
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/fj-227311-optimize-snippets-finder-query.yml b/changelogs/unreleased/fj-227311-optimize-snippets-finder-query.yml
new file mode 100644
index 00000000000..8b230b60ae8
--- /dev/null
+++ b/changelogs/unreleased/fj-227311-optimize-snippets-finder-query.yml
@@ -0,0 +1,5 @@
+---
+title: Improve snippet finders queries
+merge_request: 36292
+author:
+type: performance
diff --git a/changelogs/unreleased/mwaw-208224-move-cluster-metrics-dashboard-endpoint-into-gitlab-core-BE.yml b/changelogs/unreleased/mwaw-208224-move-cluster-metrics-dashboard-endpoint-into-gitlab-core-BE.yml
new file mode 100644
index 00000000000..c0bcc88cc92
--- /dev/null
+++ b/changelogs/unreleased/mwaw-208224-move-cluster-metrics-dashboard-endpoint-into-gitlab-core-BE.yml
@@ -0,0 +1,5 @@
+---
+title: Open source cluster health dashboard and make it available to all users
+merge_request: 35721
+author:
+type: changed
diff --git a/changelogs/unreleased/rails-save-bang-1.yml b/changelogs/unreleased/rails-save-bang-1.yml
new file mode 100644
index 00000000000..c7d20372b9a
--- /dev/null
+++ b/changelogs/unreleased/rails-save-bang-1.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor issues controller spec to fix SaveBang Cop
+merge_request: 36582
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-distant-pat-expiry.yml b/changelogs/unreleased/sh-handle-distant-pat-expiry.yml
new file mode 100644
index 00000000000..fc89ae67492
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-distant-pat-expiry.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid 500 errors with long expiration dates in tokens
+merge_request: 36657
+author:
+type: fixed
diff --git a/changelogs/unreleased/swap-to-oj.yml b/changelogs/unreleased/swap-to-oj.yml
new file mode 100644
index 00000000000..000516df915
--- /dev/null
+++ b/changelogs/unreleased/swap-to-oj.yml
@@ -0,0 +1,5 @@
+---
+title: Add oj gem for faster JSON
+merge_request: 36555
+author:
+type: performance
diff --git a/changelogs/unreleased/tr-prettify-graphql-files.yml b/changelogs/unreleased/tr-prettify-graphql-files.yml
new file mode 100644
index 00000000000..136595d2d32
--- /dev/null
+++ b/changelogs/unreleased/tr-prettify-graphql-files.yml
@@ -0,0 +1,5 @@
+---
+title: Format graphql files with prettier
+merge_request: 36244
+author:
+type: other
diff --git a/config/initializers/multi_json.rb b/config/initializers/multi_json.rb
new file mode 100644
index 00000000000..93a81d8320d
--- /dev/null
+++ b/config/initializers/multi_json.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+# Explicitly set the JSON adapter used by MultiJson
+# Currently we want this to default to the existing json gem
+MultiJson.use(:json_gem)
diff --git a/config/initializers/oj.rb b/config/initializers/oj.rb
new file mode 100644
index 00000000000..3fa26259fc6
--- /dev/null
+++ b/config/initializers/oj.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+# Ensure Oj runs in json-gem compatibility mode by default
+Oj.default_options = { mode: :rails }
diff --git a/config/routes.rb b/config/routes.rb
index c823be6084d..36e995bc0af 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -190,7 +190,6 @@ Rails.application.routes.draw do
member do
Gitlab.ee do
get :metrics, format: :json
- get :metrics_dashboard
get :environments, format: :json
end
@@ -200,6 +199,7 @@ Rails.application.routes.draw do
delete '/:application', to: 'clusters/applications#destroy', as: :uninstall_applications
end
+ get :metrics_dashboard
get :'/prometheus/api/v1/*proxy_path', to: 'clusters#prometheus_proxy', as: :prometheus_api
get :cluster_status, format: :json
delete :clear_cache
diff --git a/db/migrate/20191112212815_create_web_authn_table.rb b/db/migrate/20191112212815_create_web_authn_table.rb
new file mode 100644
index 00000000000..72895f955df
--- /dev/null
+++ b/db/migrate/20191112212815_create_web_authn_table.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class CreateWebAuthnTable < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # disable_ddl_transaction!
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limits are added in subsequent migration
+ def change
+ create_table :webauthn_registrations do |t|
+ t.bigint :user_id, null: false, index: true
+
+ t.bigint :counter, default: 0, null: false
+ t.timestamps_with_timezone
+ t.text :credential_xid, null: false, index: { unique: true }
+ t.text :name, null: false
+ # The length of the public key is determined by the device
+ # and not specified. Thus we can't set a limit
+ t.text :public_key, null: false # rubocop:disable Migration/AddLimitToTextColumns
+ end
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20200510181937_add_web_authn_xid_to_user_details.rb b/db/migrate/20200510181937_add_web_authn_xid_to_user_details.rb
new file mode 100644
index 00000000000..d41e6611c45
--- /dev/null
+++ b/db/migrate/20200510181937_add_web_authn_xid_to_user_details.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddWebAuthnXidToUserDetails < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # limit is added in subsequent migration
+ def change
+ add_column :user_details, :webauthn_xid, :text
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+end
diff --git a/db/migrate/20200510182218_add_text_limit_to_user_details_webauthn_xid.rb b/db/migrate/20200510182218_add_text_limit_to_user_details_webauthn_xid.rb
new file mode 100644
index 00000000000..715cfd771f5
--- /dev/null
+++ b/db/migrate/20200510182218_add_text_limit_to_user_details_webauthn_xid.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTextLimitToUserDetailsWebauthnXid < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :user_details, :webauthn_xid, 100
+ end
+
+ def down
+ remove_text_limit :user_details, :webauthn_xid
+ end
+end
diff --git a/db/migrate/20200510182556_add_text_limit_to_webauthn_registrations_name.rb b/db/migrate/20200510182556_add_text_limit_to_webauthn_registrations_name.rb
new file mode 100644
index 00000000000..28805505ba6
--- /dev/null
+++ b/db/migrate/20200510182556_add_text_limit_to_webauthn_registrations_name.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTextLimitToWebauthnRegistrationsName < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :webauthn_registrations, :name, 255
+ end
+
+ def down
+ remove_text_limit :webauthn_registrations, :name
+ end
+end
diff --git a/db/migrate/20200510182824_add_text_limit_to_webauthn_registrations_credential_xid.rb b/db/migrate/20200510182824_add_text_limit_to_webauthn_registrations_credential_xid.rb
new file mode 100644
index 00000000000..51155f370ef
--- /dev/null
+++ b/db/migrate/20200510182824_add_text_limit_to_webauthn_registrations_credential_xid.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTextLimitToWebauthnRegistrationsCredentialXid < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :webauthn_registrations, :credential_xid, 255
+ end
+
+ def down
+ remove_text_limit :webauthn_registrations, :credential_xid
+ end
+end
diff --git a/db/migrate/20200510183128_add_foreign_key_from_webauthn_registrations_to_users.rb b/db/migrate/20200510183128_add_foreign_key_from_webauthn_registrations_to_users.rb
new file mode 100644
index 00000000000..f71a5276dee
--- /dev/null
+++ b/db/migrate/20200510183128_add_foreign_key_from_webauthn_registrations_to_users.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddForeignKeyFromWebauthnRegistrationsToUsers < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ add_foreign_key :webauthn_registrations, :users, column: :user_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :webauthn_registrations, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20200624142107_create_analytics_cycle_analytics_group_value_streams.rb b/db/migrate/20200624142107_create_analytics_cycle_analytics_group_value_streams.rb
new file mode 100644
index 00000000000..24afe463684
--- /dev/null
+++ b/db/migrate/20200624142107_create_analytics_cycle_analytics_group_value_streams.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class CreateAnalyticsCycleAnalyticsGroupValueStreams < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_analytics_ca_group_value_streams_on_group_id_and_name'
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:analytics_cycle_analytics_group_value_streams)
+ with_lock_retries do
+ create_table :analytics_cycle_analytics_group_value_streams do |t|
+ t.timestamps_with_timezone
+ t.references(:group, {
+ null: false,
+ index: false,
+ foreign_key: { to_table: :namespaces, on_delete: :cascade }
+ })
+ t.text :name, null: false
+ t.index [:group_id, :name], unique: true, name: INDEX_NAME
+ end
+ end
+ end
+
+ add_text_limit :analytics_cycle_analytics_group_value_streams, :name, 100
+ end
+
+ def down
+ drop_table :analytics_cycle_analytics_group_value_streams
+ end
+end
diff --git a/db/migrate/20200624142207_add_group_value_stream_to_cycle_analytics_group_stages.rb b/db/migrate/20200624142207_add_group_value_stream_to_cycle_analytics_group_stages.rb
new file mode 100644
index 00000000000..3ce912eb440
--- /dev/null
+++ b/db/migrate/20200624142207_add_group_value_stream_to_cycle_analytics_group_stages.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddGroupValueStreamToCycleAnalyticsGroupStages < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_column :analytics_cycle_analytics_group_stages, :group_value_stream_id, :bigint
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :analytics_cycle_analytics_group_stages, :group_value_stream_id
+ end
+ end
+end
diff --git a/db/migrate/20200701064756_add_not_valid_foreign_key_to_cycle_analytics_group_stages.rb b/db/migrate/20200701064756_add_not_valid_foreign_key_to_cycle_analytics_group_stages.rb
new file mode 100644
index 00000000000..e54cecc5af8
--- /dev/null
+++ b/db/migrate/20200701064756_add_not_valid_foreign_key_to_cycle_analytics_group_stages.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddNotValidForeignKeyToCycleAnalyticsGroupStages < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ CONSTRAINT_NAME = 'fk_analytics_cycle_analytics_group_stages_group_value_stream_id'
+ INDEX_NAME = 'index_analytics_ca_group_stages_on_value_stream_id'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :analytics_cycle_analytics_group_stages, :group_value_stream_id, name: INDEX_NAME
+ add_foreign_key :analytics_cycle_analytics_group_stages, :analytics_cycle_analytics_group_value_streams,
+ column: :group_value_stream_id, name: CONSTRAINT_NAME, on_delete: :cascade, validate: false
+ end
+
+ def down
+ remove_foreign_key_if_exists :analytics_cycle_analytics_group_stages, column: :group_value_stream_id, name: CONSTRAINT_NAME
+ remove_concurrent_index :analytics_cycle_analytics_group_stages, :group_value_stream_id
+ end
+end
diff --git a/db/migrate/20200708080631_add_pager_duty_integration_columns_to_project_incident_management_settings.rb b/db/migrate/20200708080631_add_pager_duty_integration_columns_to_project_incident_management_settings.rb
new file mode 100644
index 00000000000..ab56a863f51
--- /dev/null
+++ b/db/migrate/20200708080631_add_pager_duty_integration_columns_to_project_incident_management_settings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddPagerDutyIntegrationColumnsToProjectIncidentManagementSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ # limit constraints added in a separate migration:
+ # 20200710130234_add_limit_constraints_to_project_incident_management_settings_token.rb
+ def change
+ add_column :project_incident_management_settings, :pagerduty_active, :boolean, null: false, default: false
+ add_column :project_incident_management_settings, :encrypted_pagerduty_token, :binary, null: true
+ add_column :project_incident_management_settings, :encrypted_pagerduty_token_iv, :binary, null: true
+ end
+end
diff --git a/db/migrate/20200710130234_add_limit_constraints_to_project_incident_management_settings_token.rb b/db/migrate/20200710130234_add_limit_constraints_to_project_incident_management_settings_token.rb
new file mode 100644
index 00000000000..8af927d0959
--- /dev/null
+++ b/db/migrate/20200710130234_add_limit_constraints_to_project_incident_management_settings_token.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddLimitConstraintsToProjectIncidentManagementSettingsToken < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_check_constraint :project_incident_management_settings, 'octet_length(encrypted_pagerduty_token) <= 255', 'pagerduty_token_length_constraint'
+ add_check_constraint :project_incident_management_settings, 'octet_length(encrypted_pagerduty_token_iv) <= 12', 'pagerduty_token_iv_length_constraint'
+ end
+
+ def down
+ remove_check_constraint :project_incident_management_settings, 'pagerduty_token_length_constraint'
+ remove_check_constraint :project_incident_management_settings, 'pagerduty_token_iv_length_constraint'
+ end
+end
diff --git a/db/post_migrate/20200703064117_generate_missing_routes_for_bots.rb b/db/post_migrate/20200703064117_generate_missing_routes_for_bots.rb
new file mode 100644
index 00000000000..85d62cbb6dd
--- /dev/null
+++ b/db/post_migrate/20200703064117_generate_missing_routes_for_bots.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+class GenerateMissingRoutesForBots < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ USER_TYPES = {
+ human: nil,
+ support_bot: 1,
+ alert_bot: 2,
+ visual_review_bot: 3,
+ service_user: 4,
+ ghost: 5,
+ project_bot: 6,
+ migration_bot: 7
+ }.with_indifferent_access.freeze
+
+ BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot].freeze
+
+ scope :bots, -> { where(user_type: USER_TYPES.values_at(*BOT_USER_TYPES)) }
+ end
+
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+
+ validates :path,
+ uniqueness: { case_sensitive: false }
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+
+ belongs_to :owner, class_name: 'GenerateMissingRoutesForBots::User'
+
+ scope :for_user, -> { where('type IS NULL') }
+ scope :for_bots, -> { for_user.joins(:owner).merge(GenerateMissingRoutesForBots::User.bots) }
+
+ scope :without_routes, -> do
+ where(
+ 'NOT EXISTS (
+ SELECT 1
+ FROM routes
+ WHERE source_type = ?
+ AND source_id = namespaces.id
+ )',
+ self.source_type_for_route
+ )
+ end
+
+ def self.source_type_for_route
+ 'Namespace'
+ end
+
+ def attributes_for_insert
+ {
+ source_type: self.class.source_type_for_route,
+ source_id: id,
+ name: name,
+ path: path
+ }
+ end
+ end
+
+ def up
+ # Reset the column information of all the models that update the database
+ # to ensure the Active Record's knowledge of the table structure is current
+ Route.reset_column_information
+
+ logger = Gitlab::BackgroundMigration::Logger.build
+ attributes_to_be_logged = %w(id path name)
+
+ GenerateMissingRoutesForBots::Namespace.for_bots.without_routes.each do |namespace|
+ route = GenerateMissingRoutesForBots::Route.create(namespace.attributes_for_insert)
+ namespace_details = namespace.as_json.slice(*attributes_to_be_logged)
+
+ if route.persisted?
+ logger.info namespace_details.merge(message: 'a new route was created for the namespace')
+ else
+ errors = route.errors.full_messages.join(',')
+ logger.info namespace_details.merge(message: 'route creation failed for the namespace', errors: errors)
+ end
+ end
+ end
+
+ def down
+ # no op
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 5519f7150ca..49c023d6ed4 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -8800,7 +8800,8 @@ CREATE TABLE public.analytics_cycle_analytics_group_stages (
end_event_label_id bigint,
hidden boolean DEFAULT false NOT NULL,
custom boolean DEFAULT true NOT NULL,
- name character varying(255) NOT NULL
+ name character varying(255) NOT NULL,
+ group_value_stream_id bigint
);
CREATE SEQUENCE public.analytics_cycle_analytics_group_stages_id_seq
@@ -8812,6 +8813,24 @@ CREATE SEQUENCE public.analytics_cycle_analytics_group_stages_id_seq
ALTER SEQUENCE public.analytics_cycle_analytics_group_stages_id_seq OWNED BY public.analytics_cycle_analytics_group_stages.id;
+CREATE TABLE public.analytics_cycle_analytics_group_value_streams (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ group_id bigint NOT NULL,
+ name text NOT NULL,
+ CONSTRAINT check_bc1ed5f1f7 CHECK ((char_length(name) <= 100))
+);
+
+CREATE SEQUENCE public.analytics_cycle_analytics_group_value_streams_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.analytics_cycle_analytics_group_value_streams_id_seq OWNED BY public.analytics_cycle_analytics_group_value_streams.id;
+
CREATE TABLE public.analytics_cycle_analytics_project_stages (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -14111,7 +14130,12 @@ CREATE TABLE public.project_incident_management_settings (
project_id integer NOT NULL,
create_issue boolean DEFAULT false NOT NULL,
send_email boolean DEFAULT false NOT NULL,
- issue_template_key text
+ issue_template_key text,
+ pagerduty_active boolean DEFAULT false NOT NULL,
+ encrypted_pagerduty_token bytea,
+ encrypted_pagerduty_token_iv bytea,
+ CONSTRAINT pagerduty_token_iv_length_constraint CHECK ((octet_length(encrypted_pagerduty_token_iv) <= 12)),
+ CONSTRAINT pagerduty_token_length_constraint CHECK ((octet_length(encrypted_pagerduty_token) <= 255))
);
CREATE SEQUENCE public.project_incident_management_settings_project_id_seq
@@ -15642,7 +15666,9 @@ CREATE TABLE public.user_details (
job_title character varying(200) DEFAULT ''::character varying NOT NULL,
bio character varying(255) DEFAULT ''::character varying NOT NULL,
bio_html text,
- cached_markdown_version integer
+ cached_markdown_version integer,
+ webauthn_xid text,
+ CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100))
);
CREATE SEQUENCE public.user_details_user_id_seq
@@ -16209,6 +16235,28 @@ CREATE SEQUENCE public.web_hooks_id_seq
ALTER SEQUENCE public.web_hooks_id_seq OWNED BY public.web_hooks.id;
+CREATE TABLE public.webauthn_registrations (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ counter bigint DEFAULT 0 NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ credential_xid text NOT NULL,
+ name text NOT NULL,
+ public_key text NOT NULL,
+ CONSTRAINT check_242f0cc65c CHECK ((char_length(credential_xid) <= 255)),
+ CONSTRAINT check_2f02e74321 CHECK ((char_length(name) <= 255))
+);
+
+CREATE SEQUENCE public.webauthn_registrations_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.webauthn_registrations_id_seq OWNED BY public.webauthn_registrations.id;
+
CREATE TABLE public.wiki_page_meta (
id integer NOT NULL,
project_id bigint NOT NULL,
@@ -16335,6 +16383,8 @@ ALTER TABLE ONLY public.allowed_email_domains ALTER COLUMN id SET DEFAULT nextva
ALTER TABLE ONLY public.analytics_cycle_analytics_group_stages ALTER COLUMN id SET DEFAULT nextval('public.analytics_cycle_analytics_group_stages_id_seq'::regclass);
+ALTER TABLE ONLY public.analytics_cycle_analytics_group_value_streams ALTER COLUMN id SET DEFAULT nextval('public.analytics_cycle_analytics_group_value_streams_id_seq'::regclass);
+
ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('public.analytics_cycle_analytics_project_stages_id_seq'::regclass);
ALTER TABLE ONLY public.appearances ALTER COLUMN id SET DEFAULT nextval('public.appearances_id_seq'::regclass);
@@ -16969,6 +17019,8 @@ ALTER TABLE ONLY public.web_hook_logs ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.web_hooks ALTER COLUMN id SET DEFAULT nextval('public.web_hooks_id_seq'::regclass);
+ALTER TABLE ONLY public.webauthn_registrations ALTER COLUMN id SET DEFAULT nextval('public.webauthn_registrations_id_seq'::regclass);
+
ALTER TABLE ONLY public.wiki_page_meta ALTER COLUMN id SET DEFAULT nextval('public.wiki_page_meta_id_seq'::regclass);
ALTER TABLE ONLY public.wiki_page_slugs ALTER COLUMN id SET DEFAULT nextval('public.wiki_page_slugs_id_seq'::regclass);
@@ -17197,6 +17249,9 @@ ALTER TABLE ONLY public.allowed_email_domains
ALTER TABLE ONLY public.analytics_cycle_analytics_group_stages
ADD CONSTRAINT analytics_cycle_analytics_group_stages_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.analytics_cycle_analytics_group_value_streams
+ ADD CONSTRAINT analytics_cycle_analytics_group_value_streams_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages
ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id);
@@ -18229,6 +18284,9 @@ ALTER TABLE ONLY public.web_hook_logs
ALTER TABLE ONLY public.web_hooks
ADD CONSTRAINT web_hooks_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.webauthn_registrations
+ ADD CONSTRAINT webauthn_registrations_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.wiki_page_meta
ADD CONSTRAINT wiki_page_meta_pkey PRIMARY KEY (id);
@@ -18529,6 +18587,10 @@ CREATE INDEX index_analytics_ca_group_stages_on_relative_position ON public.anal
CREATE INDEX index_analytics_ca_group_stages_on_start_event_label_id ON public.analytics_cycle_analytics_group_stages USING btree (start_event_label_id);
+CREATE INDEX index_analytics_ca_group_stages_on_value_stream_id ON public.analytics_cycle_analytics_group_stages USING btree (group_value_stream_id);
+
+CREATE UNIQUE INDEX index_analytics_ca_group_value_streams_on_group_id_and_name ON public.analytics_cycle_analytics_group_value_streams USING btree (group_id, name);
+
CREATE INDEX index_analytics_ca_project_stages_on_end_event_label_id ON public.analytics_cycle_analytics_project_stages USING btree (end_event_label_id);
CREATE INDEX index_analytics_ca_project_stages_on_project_id ON public.analytics_cycle_analytics_project_stages USING btree (project_id);
@@ -20453,6 +20515,10 @@ CREATE INDEX index_web_hooks_on_project_id ON public.web_hooks USING btree (proj
CREATE INDEX index_web_hooks_on_type ON public.web_hooks USING btree (type);
+CREATE UNIQUE INDEX index_webauthn_registrations_on_credential_xid ON public.webauthn_registrations USING btree (credential_xid);
+
+CREATE INDEX index_webauthn_registrations_on_user_id ON public.webauthn_registrations USING btree (user_id);
+
CREATE INDEX index_wiki_page_meta_on_project_id ON public.wiki_page_meta USING btree (project_id);
CREATE UNIQUE INDEX index_wiki_page_slugs_on_slug_and_wiki_page_meta_id ON public.wiki_page_slugs USING btree (slug, wiki_page_meta_id);
@@ -21192,6 +21258,9 @@ ALTER TABLE ONLY public.ci_variables
ALTER TABLE ONLY public.merge_request_metrics
ADD CONSTRAINT fk_ae440388cc FOREIGN KEY (latest_closed_by_id) REFERENCES public.users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.analytics_cycle_analytics_group_stages
+ ADD CONSTRAINT fk_analytics_cycle_analytics_group_stages_group_value_stream_id FOREIGN KEY (group_value_stream_id) REFERENCES public.analytics_cycle_analytics_group_value_streams(id) ON DELETE CASCADE NOT VALID;
+
ALTER TABLE ONLY public.fork_network_members
ADD CONSTRAINT fk_b01280dae4 FOREIGN KEY (forked_from_project_id) REFERENCES public.projects(id) ON DELETE SET NULL;
@@ -21771,6 +21840,9 @@ ALTER TABLE ONLY public.project_repository_storage_moves
ALTER TABLE ONLY public.x509_commit_signatures
ADD CONSTRAINT fk_rails_53fe41188f FOREIGN KEY (x509_certificate_id) REFERENCES public.x509_certificates(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.analytics_cycle_analytics_group_value_streams
+ ADD CONSTRAINT fk_rails_540627381a FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.geo_node_namespace_links
ADD CONSTRAINT fk_rails_546bf08d3e FOREIGN KEY (geo_node_id) REFERENCES public.geo_nodes(id) ON DELETE CASCADE;
@@ -22194,6 +22266,9 @@ ALTER TABLE ONLY public.vulnerability_statistics
ALTER TABLE ONLY public.resource_label_events
ADD CONSTRAINT fk_rails_b126799f57 FOREIGN KEY (label_id) REFERENCES public.labels(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.webauthn_registrations
+ ADD CONSTRAINT fk_rails_b15c016782 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.packages_build_infos
ADD CONSTRAINT fk_rails_b18868292d FOREIGN KEY (package_id) REFERENCES public.packages_packages(id) ON DELETE CASCADE;
@@ -22948,6 +23023,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20191112105448
20191112115247
20191112115317
+20191112212815
20191112214305
20191112221821
20191112232338
@@ -23481,6 +23557,11 @@ COPY "schema_migrations" (version) FROM STDIN;
20200508140959
20200508203901
20200509203901
+20200510181937
+20200510182218
+20200510182556
+20200510182824
+20200510183128
20200511080113
20200511083541
20200511092246
@@ -23621,6 +23702,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200623170000
20200623185440
20200624075411
+20200624142107
+20200624142207
20200624222443
20200625045442
20200625082258
@@ -23631,9 +23714,11 @@ COPY "schema_migrations" (version) FROM STDIN;
20200629192638
20200630091656
20200630110826
+20200701064756
20200701093859
20200701205710
20200702123805
+20200703064117
20200703121557
20200703154822
20200704143633
@@ -23645,6 +23730,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200707071941
20200707094341
20200707095849
+20200708080631
20200710102846
+20200710130234
\.
diff --git a/doc/administration/postgresql/replication_and_failover.md b/doc/administration/postgresql/replication_and_failover.md
index 3a682a49fd0..29311aaf539 100644
--- a/doc/administration/postgresql/replication_and_failover.md
+++ b/doc/administration/postgresql/replication_and_failover.md
@@ -1250,24 +1250,27 @@ with:
sudo gitlab-ctl stop patroni
```
-### Failover procedure for Patroni
+### Manual failover procedure for Patroni
-With Patroni, you have two slightly different options: failover and switchover. Essentially, failover allows you to
-perform a manual failover when there are no healthy nodes, while switchover only works when the cluster is healthy and
-allows you to schedule a switchover (it can happen immediately). For further details, see
-[Patroni documentation on this subject](https://patroni.readthedocs.io/en/latest/rest_api.html#switchover-and-failover-endpoints).
+While Patroni supports automatic failover, you also have the ability to perform
+a manual one, where you have two slightly different options:
-To schedule a switchover:
+- **Failover**: allows you to perform a manual failover when there are no healthy nodes.
+ You can perform this action in any PostgreSQL node:
-```shell
-sudo gitlab-ctl patroni switchover
-```
+ ```shell
+ sudo gitlab-ctl patroni failover
+ ```
-For manual failover:
+- **Switchover**: only works when the cluster is healthy and allows you to schedule a switchover (it can happen immediately).
+ You can perform this action in any PostgreSQL node:
-```shell
-sudo gitlab-ctl patroni failover
-```
+ ```shell
+ sudo gitlab-ctl patroni switchover
+ ```
+
+For further details on this subject, see the
+[Patroni documentation](https://patroni.readthedocs.io/en/latest/rest_api.html#switchover-and-failover-endpoints).
### Recovering the Patroni cluster
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
new file mode 100644
index 00000000000..a019f8232a9
--- /dev/null
+++ b/doc/ci/troubleshooting.md
@@ -0,0 +1,38 @@
+---
+stage: Verify
+group: Continuous Integration
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+type: reference
+---
+
+# Troubleshooting CI/CD
+
+## Merge request pipeline widget
+
+The merge request pipeline widget shows information about the pipeline status in a Merge Request. It's displayed above the [merge request ability to merge widget](#merge-request-ability-to-merge-widget).
+
+There are several messages that can be displayed depending on the status of the pipeline.
+
+### "Checking pipeline status"
+
+This message is shown when the merge request has no pipeline associated with the latest commit yet and [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds) is turned on. This might be because:
+
+- GitLab hasn't finished creating the pipeline yet.
+- You are using an external CI service and GitLab hasn't heard back from the service yet.
+- You are not using CI/CD pipelines in your project.
+
+After the pipeline is created, the message will update with the pipeline status.
+
+Note: Currently if you delete the latest pipeline of a Merge Request, this message will be shown instead of a meaningful error message. This is a known issue and should be resolved soon.
+
+## Merge request ability to merge widget
+
+The merge request status widget shows the **Merge** button and whether or not a merge request is ready to merge. If the merge request can't be merged, the reason for this is displayed.
+
+If the pipeline is still running, the **Merge** button is replaced with the **Merge when pipeline succeeds** button.
+
+If [**Merge Trains**](merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md) are enabled, the button is either **Add to merge train** or **Add to merge train when pipeline succeeds**. **(PREMIUM)**
+
+### "A CI/CD pipeline must run and be successful before merge"
+
+This message is shown if the [Pipelines must succeed](../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds) setting is enabled in the project and a pipeline has not yet run successfully. This also applies if the pipeline has not been created yet, or if you are waiting for an external CI service. If you don't use pipelines for your project, then you should disable **Pipelines must succeed** so you can accept merge requests.
diff --git a/doc/development/fe_guide/tooling.md b/doc/development/fe_guide/tooling.md
index c693df36e6e..28deb7d95f9 100644
--- a/doc/development/fe_guide/tooling.md
+++ b/doc/development/fe_guide/tooling.md
@@ -94,13 +94,15 @@ When declaring multiple globals, always use one `/* global [name] */` line per v
## Formatting with Prettier
-Our code is automatically formatted with [Prettier](https://prettier.io) to follow our style guides. Prettier is taking care of formatting `.js`, `.vue`, and `.scss` files based on the standard prettier rules. You can find all settings for Prettier in `.prettierrc`.
+> Support for `.graphql` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227280) in GitLab 13.2.
+
+Our code is automatically formatted with [Prettier](https://prettier.io) to follow our style guides. Prettier is taking care of formatting `.js`, `.vue`, `.graphql`, and `.scss` files based on the standard prettier rules. You can find all settings for Prettier in `.prettierrc`.
### Editor
The easiest way to include prettier in your workflow is by setting up your preferred editor (all major editors are supported) accordingly. We suggest setting up prettier to run automatically when each file is saved. Find [here](https://prettier.io/docs/en/editors.html) the best way to set it up in your preferred editor.
-Please take care that you only let Prettier format the same file types as the global Yarn script does (`.js`, `.vue`, and `.scss`). In VSCode by example you can easily exclude file formats in your settings file:
+Please take care that you only let Prettier format the same file types as the global Yarn script does (`.js`, `.vue`, `.graphql`, and `.scss`). In VSCode by example you can easily exclude file formats in your settings file:
```json
"prettier.disableLanguages": [
@@ -169,6 +171,9 @@ To select Prettier as a formatter, add the following properties to your User or
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[graphql]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
```
@@ -188,5 +193,8 @@ To automatically format your files with Prettier, add the following properties t
"[vue]": {
"editor.formatOnSave": true
},
+ "[graphql]": {
+ "editor.formatOnSave": true
+ },
}
```
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 68971c510d5..ccce4672cbf 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -12,7 +12,7 @@ When you create an OAuth 2 app in GitHub, you'll need the following information:
- The authorization callback URL; in this case, `https://gitlab.example.com/users/auth`. Include the port number if your GitLab instance uses a non-default port.
NOTE: **Note:**
-To prevent an [OAuth2 covert redirect](http://tetraph.com/covert_redirect/) vulnerability, append `/users/auth` to the end of the GitHub authorization callback URL.
+To prevent an [OAuth2 covert redirect](https://oauth.net/advisories/2014-1-covert-redirect/) vulnerability, append `/users/auth` to the end of the GitHub authorization callback URL.
See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
diff --git a/doc/operations/metrics/dashboards/yaml.md b/doc/operations/metrics/dashboards/yaml.md
index 59641570c94..45820a3cf49 100644
--- a/doc/operations/metrics/dashboards/yaml.md
+++ b/doc/operations/metrics/dashboards/yaml.md
@@ -55,7 +55,7 @@ Panels in a panel group are laid out in rows consisting of two panels per row. A
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------- |
-| `type` | enum | no, defaults to `area-chart` | Specifies the chart type to use, can be: `area-chart`, `line-chart` or `anomaly-chart`. |
+| `type` | enum | no, defaults to `area-chart` | Specifies the panel type to use, for example `area-chart`, `line-chart` or `anomaly-chart`. [View documentation on all panel types.](panel_types.md) |
| `title` | string | yes | Heading for the panel. |
| `y_label` | string | no, but highly encouraged | Y-Axis label for the panel. |
| `y_axis` | string | no | Y-Axis configuration for the panel. |
diff --git a/lib/banzai/filter/inline_cluster_metrics_filter.rb b/lib/banzai/filter/inline_cluster_metrics_filter.rb
new file mode 100644
index 00000000000..5ef68388ea9
--- /dev/null
+++ b/lib/banzai/filter/inline_cluster_metrics_filter.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ class InlineClusterMetricsFilter < ::Banzai::Filter::InlineEmbedsFilter
+ def embed_params(node)
+ url = node['href']
+ @query_params = query_params(url)
+ return unless [:group, :title, :y_label].all? do |param|
+ @query_params.include?(param)
+ end
+
+ link_pattern.match(url) { |m| m.named_captures }.symbolize_keys
+ end
+
+ def xpath_search
+ "descendant-or-self::a[contains(@href,'clusters') and \
+ starts-with(@href, '#{::Gitlab.config.gitlab.url}')]"
+ end
+
+ def link_pattern
+ ::Gitlab::Metrics::Dashboard::Url.clusters_regex
+ end
+
+ def metrics_dashboard_url(params)
+ ::Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_cluster_url(
+ params[:namespace],
+ params[:project],
+ params[:cluster_id],
+ # Only Project clusters are supported for now
+ # admin and group cluster types may be supported in the future
+ cluster_type: :project,
+ embedded: true,
+ format: :json,
+ **@query_params
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
index 75bd3325bd4..7f98a52d421 100644
--- a/lib/banzai/filter/inline_metrics_redactor_filter.rb
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -77,6 +77,10 @@ module Banzai
Route.new(
::Gitlab::Metrics::Dashboard::Url.grafana_regex,
:read_project
+ ),
+ Route.new(
+ ::Gitlab::Metrics::Dashboard::Url.clusters_regex,
+ :read_cluster
)
]
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 2ea5fd3388a..10ac813ea15 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -48,7 +48,8 @@ module Banzai
def self.metrics_filters
[
Filter::InlineMetricsFilter,
- Filter::InlineGrafanaMetricsFilter
+ Filter::InlineGrafanaMetricsFilter,
+ Filter::InlineClusterMetricsFilter
]
end
diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb
index 5b6689dbefe..6c254e171dc 100644
--- a/lib/gitlab/json.rb
+++ b/lib/gitlab/json.rb
@@ -1,59 +1,199 @@
# frozen_string_literal: true
+# This is a GitLab-specific JSON interface. You should use this instead
+# of using `JSON` directly. This allows us to swap the adapter and handle
+# legacy issues.
+
module Gitlab
module Json
INVALID_LEGACY_TYPES = [String, TrueClass, FalseClass].freeze
class << self
- def parse(string, *args, **named_args)
- legacy_mode = legacy_mode_enabled?(named_args.delete(:legacy_mode))
- data = adapter.parse(string, *args, **named_args)
+ # Parse a string and convert it to a Ruby object
+ #
+ # @param string [String] the JSON string to convert to Ruby objects
+ # @param opts [Hash] an options hash in the standard JSON gem format
+ # @return [Boolean, String, Array, Hash]
+ # @raise [JSON::ParserError] raised if parsing fails
+ def parse(string, opts = {})
+ # First we should ensure this really is a string, not some other
+ # type which purports to be a string. This handles some legacy
+ # usage of the JSON class.
+ string = string.to_s unless string.is_a?(String)
+
+ legacy_mode = legacy_mode_enabled?(opts.delete(:legacy_mode))
+ data = adapter_load(string, opts)
handle_legacy_mode!(data) if legacy_mode
data
end
- def parse!(string, *args, **named_args)
- legacy_mode = legacy_mode_enabled?(named_args.delete(:legacy_mode))
- data = adapter.parse!(string, *args, **named_args)
+ alias_method :parse!, :parse
+
+ # Restricted method for converting a Ruby object to JSON. If you
+ # need to pass options to this, you should use `.generate` instead,
+ # as the underlying implementation of this varies wildly based on
+ # the adapter in use.
+ #
+ # @param object [Object] the object to convert to JSON
+ # @return [String]
+ def dump(object)
+ adapter_dump(object)
+ end
- handle_legacy_mode!(data) if legacy_mode
+ # Generates JSON for an object. In Oj this takes fewer options than .dump,
+ # in the JSON gem this is the only method which takes an options argument.
+ #
+ # @param object [Hash, Array, Object] must be hash, array, or an object that responds to .to_h or .to_json
+ # @param opts [Hash] an options hash with fewer supported settings than .dump
+ # @return [String]
+ def generate(object, opts = {})
+ adapter_generate(object, opts)
+ end
- data
+ # Generates JSON for an object and makes it look purdy
+ #
+ # The Oj variant in this looks seriously weird but these are the settings
+ # needed to emulate the style generated by the JSON gem.
+ #
+ # NOTE: This currently ignores Oj, because Oj doesn't generate identical
+ # formatting, issue: https://github.com/ohler55/oj/issues/608
+ #
+ # @param object [Hash, Array, Object] must be hash, array, or an object that responds to .to_h or .to_json
+ # @param opts [Hash] an options hash with fewer supported settings than .dump
+ # @return [String]
+ def pretty_generate(object, opts = {})
+ ::JSON.pretty_generate(object, opts)
end
- def dump(*args)
- adapter.dump(*args)
+ private
+
+ # Convert JSON string into Ruby through toggleable adapters.
+ #
+ # Must rescue adapter-specific errors and return `parser_error`, and
+ # must also standardize the options hash to support each adapter as
+ # they all take different options.
+ #
+ # @param string [String] the JSON string to convert to Ruby objects
+ # @param opts [Hash] an options hash in the standard JSON gem format
+ # @return [Boolean, String, Array, Hash]
+ # @raise [JSON::ParserError]
+ def adapter_load(string, *args, **opts)
+ opts = standardize_opts(opts)
+
+ if enable_oj?
+ Oj.load(string, opts)
+ else
+ ::JSON.parse(string, opts)
+ end
+ rescue Oj::ParseError, Encoding::UndefinedConversionError => ex
+ raise parser_error.new(ex)
end
- def generate(*args)
- adapter.generate(*args)
+ # Take a Ruby object and convert it to a string. This method varies
+ # based on the underlying JSON interpreter. Oj treats this like JSON
+ # treats `.generate`. JSON.dump takes no options.
+ #
+ # This supports these options to ensure this difference is recorded here,
+ # as it's very surprising. The public interface is more restrictive to
+ # prevent adapter-specific options being passed.
+ #
+ # @overload adapter_dump(object, opts)
+ # @param object [Object] the object to convert to JSON
+ # @param opts [Hash] options as named arguments, only supported by Oj
+ #
+ # @overload adapter_dump(object, anIO, limit)
+ # @param object [Object] the object, will have JSON.generate called on it
+ # @param anIO [Object] an IO-like object that responds to .write, default nil
+ # @param limit [Fixnum] the nested array/object limit, default nil
+ # @raise [ArgumentError] when depth limit exceeded
+ #
+ # @return [String]
+ def adapter_dump(object, *args, **opts)
+ if enable_oj?
+ Oj.dump(object, opts)
+ else
+ ::JSON.dump(object, *args)
+ end
end
- def pretty_generate(*args)
- adapter.pretty_generate(*args)
+ # Generates JSON for an object but with fewer options, using toggleable adapters.
+ #
+ # @param object [Hash, Array, Object] must be hash, array, or an object that responds to .to_h or .to_json
+ # @param opts [Hash] an options hash with fewer supported settings than .dump
+ # @return [String]
+ def adapter_generate(object, opts = {})
+ opts = standardize_opts(opts)
+
+ if enable_oj?
+ Oj.generate(object, opts)
+ else
+ ::JSON.generate(object, opts)
+ end
end
- private
+ # Take a JSON standard options hash and standardize it to work across adapters
+ # An example of this is Oj taking :symbol_keys instead of :symbolize_names
+ #
+ # @param opts [Hash, Nil]
+ # @return [Hash]
+ def standardize_opts(opts)
+ opts ||= {}
+
+ if enable_oj?
+ opts[:mode] = :rails
+ opts[:symbol_keys] = opts[:symbolize_keys] || opts[:symbolize_names]
+ end
- def adapter
- ::JSON
+ opts
end
+ # The standard parser error we should be returning. Defined in a method
+ # so we can potentially override it later.
+ #
+ # @return [JSON::ParserError]
def parser_error
::JSON::ParserError
end
+ # @param [Nil, Boolean] an extracted :legacy_mode key from the opts hash
+ # @return [Boolean]
def legacy_mode_enabled?(arg_value)
arg_value.nil? ? false : arg_value
end
+ # If legacy mode is enabled, we need to raise an error depending on the values
+ # provided in the string. This will be deprecated.
+ #
+ # @param data [Boolean, String, Array, Hash, Object]
+ # @return [Boolean, String, Array, Hash, Object]
+ # @raise [JSON::ParserError]
def handle_legacy_mode!(data)
+ return data unless feature_table_exists?
return data unless Feature.enabled?(:json_wrapper_legacy_mode, default_enabled: true)
raise parser_error if INVALID_LEGACY_TYPES.any? { |type| data.is_a?(type) }
end
+
+ # @return [Boolean]
+ def enable_oj?
+ return false unless feature_table_exists?
+
+ Feature.enabled?(:oj_json, default_enabled: true)
+ end
+
+ # There are a variety of database errors possible when checking the feature
+ # flags at the wrong time during boot, e.g. during migrations. We don't care
+ # about these errors, we just need to ensure that we skip feature detection
+ # if they will fail.
+ #
+ # @return [Boolean]
+ def feature_table_exists?
+ Feature::FlipperFeature.table_exists?
+ rescue
+ false
+ end
end
end
end
diff --git a/lib/gitlab/json_logger.rb b/lib/gitlab/json_logger.rb
index ab34fb03158..3a74df8dc8f 100644
--- a/lib/gitlab/json_logger.rb
+++ b/lib/gitlab/json_logger.rb
@@ -19,7 +19,7 @@ module Gitlab
data.merge!(message)
end
- data.to_json + "\n"
+ Gitlab::Json.dump(data) + "\n"
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1610bf040fe..2b37e01d269 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4266,6 +4266,9 @@ msgstr ""
msgid "Changes"
msgstr ""
+msgid "Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used."
+msgstr ""
+
msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
msgstr ""
@@ -7423,6 +7426,9 @@ msgstr ""
msgid "Default first day of the week in calendars and date pickers."
msgstr ""
+msgid "Default initial branch name"
+msgstr ""
+
msgid "Default issue template"
msgstr ""
@@ -13795,6 +13801,9 @@ msgstr ""
msgid "Link title is required"
msgstr ""
+msgid "Link to go to GitLab pipeline documentation"
+msgstr ""
+
msgid "Linked emails (%{email_count})"
msgstr ""
@@ -16878,6 +16887,9 @@ msgstr ""
msgid "Pipeline|Canceled"
msgstr ""
+msgid "Pipeline|Checking pipeline status."
+msgstr ""
+
msgid "Pipeline|Commit"
msgstr ""
@@ -16917,9 +16929,6 @@ msgstr ""
msgid "Pipeline|Merged result pipeline"
msgstr ""
-msgid "Pipeline|No pipeline has been run for this commit."
-msgstr ""
-
msgid "Pipeline|Passed"
msgstr ""
@@ -21107,6 +21116,9 @@ msgstr ""
msgid "Set the default expiration time for each job's artifacts. 0 for unlimited. The default unit is in seconds, but you can define an alternative. For example: <code>4 mins 2 sec</code>, <code>2h42min</code>."
msgstr ""
+msgid "Set the default name of the initial branch when creating new repositories through the user interface."
+msgstr ""
+
msgid "Set the due date to %{due_date}."
msgstr ""
diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js
index 45cdef2ba86..7772f80c233 100644
--- a/scripts/frontend/prettier.js
+++ b/scripts/frontend/prettier.js
@@ -3,7 +3,7 @@ const prettier = require('prettier');
const fs = require('fs');
const { getStagedFiles } = require('./frontend_script_utils');
-const matchExtensions = ['js', 'vue'];
+const matchExtensions = ['js', 'vue', 'graphql'];
// This will improve glob performance by excluding certain directories.
// The .prettierignore file will also be respected, but after the glob has executed.
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index 8257e8d33b2..2e0ee671d3f 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -171,6 +171,16 @@ RSpec.describe Admin::ClustersController do
end
end
+ it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do
+ let(:cluster) { create(:cluster, :instance, :provided_by_gcp) }
+
+ let(:metrics_dashboard_req_params) do
+ {
+ id: cluster.id
+ }
+ end
+ end
+
describe 'GET #prometheus_proxy' do
let(:user) { admin }
let(:proxyable) do
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index bd6c976b003..1593e1290c4 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -192,6 +192,17 @@ RSpec.describe Groups::ClustersController do
end
end
+ it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) }
+
+ let(:metrics_dashboard_req_params) do
+ {
+ id: cluster.id,
+ group_id: group.name
+ }
+ end
+ end
+
describe 'GET #prometheus_proxy' do
let(:proxyable) do
create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group])
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index fc4657dafd9..da4faad2a39 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -230,6 +230,18 @@ RSpec.describe Projects::ClustersController do
end
end
+ it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ let(:metrics_dashboard_req_params) do
+ {
+ id: cluster.id,
+ namespace_id: project.namespace.full_path,
+ project_id: project.name
+ }
+ end
+ end
+
describe 'POST create for new cluster' do
let(:legacy_abac_param) { 'true' }
let(:params) do
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index cca4b597f4c..85ec1f7396d 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -546,28 +546,20 @@ RSpec.describe Projects::EnvironmentsController do
end
describe 'GET #metrics_dashboard' do
- shared_examples_for 'correctly formatted response' do |status_code|
- it 'returns a json object with the correct keys' do
- get :metrics_dashboard, params: environment_params(dashboard_params)
-
- # Exlcude `all_dashboards` to handle separately.
- found_keys = json_response.keys - ['all_dashboards']
-
- expect(response).to have_gitlab_http_status(status_code)
- expect(found_keys).to contain_exactly(*expected_keys)
- end
- end
+ let(:metrics_dashboard_req_params) { environment_params(dashboard_params) }
shared_examples_for '200 response' do
- let(:expected_keys) { %w(dashboard status metrics_data) }
-
- it_behaves_like 'correctly formatted response', :ok
+ it_behaves_like 'GET #metrics_dashboard correctly formatted response' do
+ let(:expected_keys) { %w(dashboard status metrics_data) }
+ let(:status_code) { :ok }
+ end
end
shared_examples_for 'error response' do |status_code|
- let(:expected_keys) { %w(message status) }
-
- it_behaves_like 'correctly formatted response', status_code
+ it_behaves_like 'GET #metrics_dashboard correctly formatted response' do
+ let(:expected_keys) { %w(message status) }
+ let(:status_code) { status_code }
+ end
end
shared_examples_for 'includes all dashboards' do
@@ -581,29 +573,14 @@ RSpec.describe Projects::EnvironmentsController do
end
shared_examples_for 'the default dashboard' do
- it_behaves_like '200 response'
it_behaves_like 'includes all dashboards'
-
- it 'is the default dashboard' do
- get :metrics_dashboard, params: environment_params(dashboard_params)
-
- expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
- end
+ it_behaves_like 'GET #metrics_dashboard for dashboard', 'Environment metrics'
end
shared_examples_for 'the specified dashboard' do |expected_dashboard|
- it_behaves_like '200 response'
it_behaves_like 'includes all dashboards'
- it 'has the correct name' do
- get :metrics_dashboard, params: environment_params(dashboard_params)
-
- dashboard_name = json_response['dashboard']['dashboard']
-
- # 'Environment metrics' is the default dashboard.
- expect(dashboard_name).not_to eq('Environment metrics')
- expect(dashboard_name).to eq(expected_dashboard)
- end
+ it_behaves_like 'GET #metrics_dashboard for dashboard', expected_dashboard
context 'when the dashboard cannot not be processed' do
before do
diff --git a/spec/features/clusters/cluster_health_dashboard_spec.rb b/spec/features/clusters/cluster_health_dashboard_spec.rb
new file mode 100644
index 00000000000..e9e3b48e9c0
--- /dev/null
+++ b/spec/features/clusters/cluster_health_dashboard_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+ include KubernetesHelpers
+ include PrometheusHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:clusterable) { create(:project) }
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
+ let_it_be(:cluster_path) { project_cluster_path(clusterable, cluster) }
+
+ before do
+ clusterable.add_maintainer(current_user)
+
+ sign_in(current_user)
+ end
+
+ it 'shows cluster board section within the page' do
+ visit cluster_path
+
+ expect(page).to have_text('Health')
+
+ click_link 'Health'
+
+ expect(page).to have_css('.cluster-health-graphs')
+ end
+
+ context 'no prometheus installed' do
+ it 'shows install prometheus message' do
+ visit cluster_path
+
+ click_link 'Health'
+
+ expect(page).to have_text('you must first install Prometheus in the Applications tab')
+ end
+ end
+
+ context 'when there is cluster with installed prometheus' do
+ before do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ stub_kubeclient_discover(cluster.platform.api_url)
+ end
+
+ context 'waiting for data' do
+ before do
+ stub_empty_response
+ end
+
+ it 'shows container and waiting for data message' do
+ visit cluster_path
+
+ click_link 'Health'
+
+ wait_for_requests
+
+ expect(page).to have_css('.prometheus-graphs')
+ expect(page).to have_text('Waiting for performance data')
+ end
+ end
+
+ context 'connected, prometheus returns data' do
+ before do
+ stub_connected
+ end
+
+ it 'renders charts' do
+ visit cluster_path
+
+ click_link 'Health'
+
+ wait_for_requests
+
+ expect(page).to have_css('.prometheus-graphs')
+ expect(page).to have_css('.prometheus-graph')
+ expect(page).to have_css('.prometheus-graph-title')
+ expect(page).to have_css('[_echarts_instance_]')
+ expect(page).to have_content('Avg')
+ end
+ end
+
+ def stub_empty_response
+ stub_prometheus_request(/prometheus-prometheus-server/, status: 204, body: {})
+ stub_prometheus_request(/prometheus\/api\/v1/, status: 204, body: {})
+ end
+
+ def stub_connected
+ stub_prometheus_request(/prometheus-prometheus-server/, body: prometheus_values_body)
+ stub_prometheus_request(/prometheus\/api\/v1/, body: prometheus_values_body)
+ end
+ end
+end
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index 092408c2be0..3e63ae67f19 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidekiq_inline do
+RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline do
include PrometheusHelpers
+ include KubernetesHelpers
include GrafanaApiHelpers
include MetricsDashboardUrlHelpers
@@ -166,6 +167,41 @@ RSpec.describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching,
end
end
+ context 'for GitLab embedded cluster health metrics' do
+ before do
+ project.add_maintainer(user)
+ import_common_metrics
+ stub_any_prometheus_request_with_response
+
+ allow(Prometheus::ProxyService).to receive(:new).and_call_original
+
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ stub_kubeclient_discover(cluster.platform.api_url)
+ stub_prometheus_request(/prometheus-prometheus-server/, body: prometheus_values_body)
+ stub_prometheus_request(/prometheus\/api\/v1/, body: prometheus_values_body)
+ end
+
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [project], user: user) }
+ let(:params) { [project.namespace.path, project.path, cluster.id] }
+ let(:query_params) { { group: 'Cluster Health', title: 'CPU Usage', y_label: 'CPU (cores)' } }
+ let(:metrics_url) { urls.namespace_project_cluster_url(*params, **query_params) }
+ let(:description) { "# Summary \n[](#{metrics_url})" }
+
+ it 'shows embedded metrics' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_css('div.prometheus-graph')
+ expect(page).to have_text(query_params[:title])
+ expect(page).to have_text(query_params[:y_label])
+ expect(page).not_to have_text(metrics_url)
+
+ expect(Prometheus::ProxyService)
+ .to have_received(:new)
+ .with(cluster, 'GET', 'query_range', hash_including('start', 'end', 'step'))
+ .at_least(:once)
+ end
+ end
+
def import_common_metrics
::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index bd140a0643d..ce49e9f4141 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -268,7 +268,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
end
end
- context 'view merge request where project has CI set up but no CI status' do
+ context 'view merge request where there is no pipeline yet' do
before do
pipeline = create(:ci_pipeline, project: project,
sha: merge_request.diff_head_sha,
@@ -278,11 +278,11 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'has pipeline error text' do
+ it 'has pipeline loading state' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- expect(page).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
+ expect(page).to have_text("Checking pipeline status")
end
end
@@ -889,9 +889,9 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'renders a CI pipeline error' do
+ it 'renders a CI pipeline loading state' do
within '.ci-widget' do
- expect(page).to have_content('Could not retrieve the pipeline status.')
+ expect(page).to have_content('Checking pipeline status')
end
end
end
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index 2d125087cb6..b6f375b8815 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -38,14 +38,6 @@ RSpec.describe 'Merge request > User sees pipelines', :js do
expect(page).to have_selector('.stage-cell')
end
- it 'pipeline sha does not equal last commit sha' do
- pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434')
- visit project_merge_request_path(project, merge_request)
- wait_for_requests
-
- expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.")
- end
-
context 'with a detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 78024b2e93c..253c47cf89b 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -111,7 +111,7 @@ RSpec.describe 'Pipeline', :js do
end
context 'when there is one related merge request' do
- before do
+ let!(:merge_request) do
create(:merge_request,
source_project: project,
source_branch: pipeline.ref)
@@ -123,7 +123,7 @@ RSpec.describe 'Pipeline', :js do
within '.related-merge-requests' do
expect(page).to have_content('1 related merge request: ')
expect(page).to have_selector('.js-truncated-mr-list')
- expect(page).to have_link('!1 My title 1')
+ expect(page).to have_link("#{merge_request.to_reference} #{merge_request.title}")
expect(page).not_to have_selector('.js-full-mr-list')
expect(page).not_to have_selector('.text-expander')
@@ -132,9 +132,16 @@ RSpec.describe 'Pipeline', :js do
end
context 'when there are two related merge requests' do
- before do
- create(:merge_request, source_project: project, source_branch: pipeline.ref)
- create(:merge_request, source_project: project, source_branch: pipeline.ref, target_branch: 'fix')
+ let!(:merge_request1) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: pipeline.ref)
+ end
+ let!(:merge_request2) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: pipeline.ref,
+ target_branch: 'fix')
end
it 'links to the most recent related merge request' do
@@ -142,7 +149,7 @@ RSpec.describe 'Pipeline', :js do
within '.related-merge-requests' do
expect(page).to have_content('2 related merge requests: ')
- expect(page).to have_link('!2 My title 3')
+ expect(page).to have_link("#{merge_request2.to_reference} #{merge_request2.title}")
expect(page).to have_selector('.text-expander')
expect(page).to have_selector('.js-full-mr-list', visible: false)
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index d08faab3e64..0eb92f3e679 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -528,7 +528,7 @@ RSpec.describe 'Pipelines', :js do
end
it 'renders a mini pipeline graph' do
- expect(page).to have_selector('.js-mini-pipeline-graph')
+ expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
expect(page).to have_selector('.js-builds-dropdown-button')
end
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index f7f1d7e81d6..3b77fd7eebf 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -67,6 +67,7 @@ RSpec.describe 'Projects > Show > User sees Git instructions' do
before do
expect(Gitlab::CurrentSettings)
.to receive(:default_branch_name)
+ .at_least(:once)
.and_return('example_branch')
sign_in(project.owner)
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index 0f10b0a4010..afa9de5ce86 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -63,6 +63,23 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
expect(page).to have_link('Add LICENSE', href: presenter.add_license_path)
end
end
+
+ context 'Gitlab::CurrentSettings.default_branch_name is available' do
+ before do
+ expect(Gitlab::CurrentSettings)
+ .to receive(:default_branch_name)
+ .at_least(:once)
+ .and_return('example_branch')
+
+ visit project_path(project)
+ end
+
+ it '"New file" button linked to new file page' do
+ page.within('.project-buttons') do
+ expect(page).to have_link('New file', href: project_new_blob_path(project, 'example_branch'))
+ end
+ end
+ end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index cb49d5fd135..6fc1cbcee0a 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -283,6 +283,12 @@ RSpec.describe SnippetsFinder do
it 'returns only personal snippets when the user cannot read cross project' do
expect(described_class.new(user).execute).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
end
+
+ context 'when only project snippets are required' do
+ it 'returns no records' do
+ expect(described_class.new(user, only_project: true).execute).to be_empty
+ end
+ end
end
context 'when project snippets are disabled' do
diff --git a/spec/fixtures/api/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql
index 7b712068fcd..6b6de2efbaf 100644
--- a/spec/fixtures/api/graphql/introspection.graphql
+++ b/spec/fixtures/api/graphql/introspection.graphql
@@ -1,9 +1,15 @@
# pulled from GraphiQL query
query IntrospectionQuery {
__schema {
- queryType { name }
- mutationType { name }
- subscriptionType { name }
+ queryType {
+ name
+ }
+ mutationType {
+ name
+ }
+ subscriptionType {
+ name
+ }
types {
...FullType
}
@@ -54,7 +60,9 @@ fragment FullType on __Type {
fragment InputValue on __InputValue {
name
description
- type { ...TypeRef }
+ type {
+ ...TypeRef
+ }
defaultValue
}
diff --git a/spec/frontend/fixtures/static/mini_dropdown_graph.html b/spec/frontend/fixtures/static/mini_dropdown_graph.html
index cd0b8dec3fc..cb55698b709 100644
--- a/spec/frontend/fixtures/static/mini_dropdown_graph.html
+++ b/spec/frontend/fixtures/static/mini_dropdown_graph.html
@@ -1,13 +1,13 @@
-<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph">
-<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar">
-Dropdown
-</button>
-<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
-<li class="js-builds-dropdown-list scrollable-menu">
-<ul></ul>
-</li>
-<li class="js-builds-dropdown-loading hidden">
-<span class="fa fa-spinner"></span>
-</li>
-</ul>
+<div class="js-builds-dropdown-tests dropdown dropdown" data-testid="widget-mini-pipeline-graph">
+ <button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar">
+ Dropdown
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <li class="js-builds-dropdown-list scrollable-menu">
+ <ul></ul>
+ </li>
+ <li class="js-builds-dropdown-loading hidden">
+ <span class="fa fa-spinner"></span>
+ </li>
+ </ul>
</div>
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index 150d8a053d5..0b75b58431b 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -111,5 +111,21 @@ describe('GLForm', () => {
expect(autosize.destroy).not.toHaveBeenCalled();
});
});
+
+ describe('supportsQuickActions', () => {
+ it('should return false if textarea does not support quick actions', () => {
+ const glForm = new GLForm(testContext.form, false);
+
+ expect(glForm.supportsQuickActions).toEqual(false);
+ });
+
+ it('should return true if textarea supports quick actions', () => {
+ testContext.textarea.attr('data-supports-quick-actions', true);
+
+ const glForm = new GLForm(testContext.form, false);
+
+ expect(glForm.supportsQuickActions).toEqual(true);
+ });
+ });
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 7522fe247b7..a2fa667d9a0 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -4,7 +4,11 @@ import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
import { setupAllDashboards } from '../store_utils';
-import { dashboardGitResponse, dashboardHeaderProps } from '../mock_data';
+import {
+ dashboardGitResponse,
+ selfMonitoringDashboardGitResponse,
+ dashboardHeaderProps,
+} from '../mock_data';
import { redirectTo, mergeUrlParams } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -95,28 +99,47 @@ describe('Dashboard header', () => {
});
});
- describe('when the selected dashboard is the system dashboard', () => {
- it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
- setupAllDashboards(store);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findCreateDashboardMenuItem().exists()).toBe(true);
- expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
+ const duplicableCases = [
+ null, // When no path is specified, it uses the default dashboard path.
+ dashboardGitResponse[0].path,
+ dashboardGitResponse[2].path,
+ selfMonitoringDashboardGitResponse[0].path,
+ ];
+
+ describe.each(duplicableCases)(
+ 'when the selected dashboard can be duplicated',
+ dashboardPath => {
+ it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => {
+ store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ setupAllDashboards(store, dashboardPath);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCreateDashboardMenuItem().exists()).toBe(true);
+ expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
+ });
});
- });
- });
-
- describe('when the selected dashboard is not the system dashboard', () => {
- it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findCreateDashboardMenuItem().exists()).toBe(true);
- expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
+ },
+ );
+
+ const nonDuplicableCases = [
+ dashboardGitResponse[1].path,
+ selfMonitoringDashboardGitResponse[1].path,
+ ];
+
+ describe.each(nonDuplicableCases)(
+ 'when the selected dashboard cannot be duplicated',
+ dashboardPath => {
+ it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => {
+ store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ setupAllDashboards(store, dashboardPath);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCreateDashboardMenuItem().exists()).toBe(true);
+ expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
+ });
});
- });
- });
+ },
+ );
});
describe('actions menu modals', () => {
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 9836d3b4bab..d09fcc92ee7 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -3,7 +3,7 @@ import { GlDropdownItem, GlIcon } from '@gitlab/ui';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import { dashboardGitResponse } from '../mock_data';
+import { dashboardGitResponse, selfMonitoringDashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
const modalId = 'duplicateDashboardModalId';
@@ -150,12 +150,18 @@ describe('DashboardsDropdown', () => {
});
});
- describe('when the selected dashboard can be duplicated', () => {
+ const duplicableCases = [
+ dashboardGitResponse[0],
+ dashboardGitResponse[2],
+ selfMonitoringDashboardGitResponse[0],
+ ];
+
+ describe.each(duplicableCases)('when the selected dashboard can be duplicated', dashboard => {
let duplicateDashboardAction;
let modalDirective;
beforeEach(() => {
- [mockSelectedDashboard] = dashboardGitResponse;
+ mockSelectedDashboard = dashboard;
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
@@ -205,20 +211,25 @@ describe('DashboardsDropdown', () => {
});
});
- describe('when the selected dashboard can not be duplicated', () => {
- beforeEach(() => {
- [, mockSelectedDashboard] = dashboardGitResponse;
+ const nonDuplicableCases = [dashboardGitResponse[1], selfMonitoringDashboardGitResponse[1]];
- wrapper = createComponent();
- });
+ describe.each(nonDuplicableCases)(
+ 'when the selected dashboard can not be duplicated',
+ dashboard => {
+ beforeEach(() => {
+ mockSelectedDashboard = dashboard;
- it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => {
- const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
+ wrapper = createComponent();
+ });
- expect(findItems()).toHaveLength(dashboardGitResponse.length);
- expect(item.length).toBe(0);
- });
- });
+ it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => {
+ const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
+
+ expect(findItems()).toHaveLength(dashboardGitResponse.length);
+ expect(item.length).toBe(0);
+ });
+ },
+ );
describe('when a dashboard gets selected by the user', () => {
beforeEach(() => {
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index d9d96f4d64e..3d55fb7010b 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -12,6 +12,7 @@ const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
display_name: `Custom Dashboard ${idx}`,
can_edit: true,
system_dashboard: false,
+ out_of_the_box_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
path: `.gitlab/dashboards/dashboard_${idx}.yml`,
starred: false,
@@ -302,6 +303,7 @@ export const dashboardGitResponse = [
display_name: 'Default',
can_edit: false,
system_dashboard: true,
+ out_of_the_box_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
starred: false,
@@ -312,6 +314,44 @@ export const dashboardGitResponse = [
display_name: 'dashboard.yml',
can_edit: true,
system_dashboard: false,
+ out_of_the_box_dashboard: false,
+ project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
+ path: '.gitlab/dashboards/dashboard.yml',
+ starred: true,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
+ },
+ {
+ default: false,
+ display_name: 'Pod Health',
+ can_edit: false,
+ system_dashboard: false,
+ out_of_the_box_dashboard: true,
+ project_blob_path: null,
+ path: 'config/prometheus/pod_metrics.yml',
+ starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/pod_metrics.yml`,
+ },
+ ...customDashboardsData,
+];
+
+export const selfMonitoringDashboardGitResponse = [
+ {
+ default: true,
+ display_name: 'Default',
+ can_edit: false,
+ system_dashboard: false,
+ out_of_the_box_dashboard: true,
+ project_blob_path: null,
+ path: 'config/prometheus/self_monitoring_default.yml',
+ starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/self_monitoring_default.yml`,
+ },
+ {
+ default: false,
+ display_name: 'dashboard.yml',
+ can_edit: true,
+ system_dashboard: false,
+ out_of_the_box_dashboard: false,
project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
path: '.gitlab/dashboards/dashboard.yml',
starred: true,
diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js
index cb1d563ece7..dee4f93f0ce 100644
--- a/spec/frontend/notes/old_notes_spec.js
+++ b/spec/frontend/notes/old_notes_spec.js
@@ -624,7 +624,7 @@ describe.skip('Old Notes (~/notes.js)', () => {
});
});
- describe('postComment with Slash commands', () => {
+ describe('postComment with quick actions', () => {
const sampleComment = '/assign @root\n/award :100:';
const note = {
commands_changes: {
@@ -640,6 +640,7 @@ describe.skip('Old Notes (~/notes.js)', () => {
let $notesContainer;
beforeEach(() => {
+ loadFixtures('commit/show.html');
mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
new Notes('', []);
@@ -659,14 +660,49 @@ describe.skip('Old Notes (~/notes.js)', () => {
$form.find('textarea.js-note-text').val(sampleComment);
});
- it('should remove slash command placeholder when comment with slash commands is done posting', done => {
+ it('should remove quick action placeholder when comment with quick actions is done posting', done => {
jest.spyOn(gl.awardsHandler, 'addAwardToEmojiBar');
$('.js-comment-button').click();
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
+ expect($notesContainer.find('.note.being-posted').length).toEqual(1); // Placeholder shown
setImmediate(() => {
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0); // Placeholder removed
+ done();
+ });
+ });
+ });
+
+ describe('postComment with slash when quick actions are not supported', () => {
+ const sampleComment = '/assign @root';
+ let $form;
+ let $notesContainer;
+
+ beforeEach(() => {
+ const note = {
+ id: 1234,
+ html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
+ <div class="note-text">${sampleComment}</div>
+ </li>`,
+ note: sampleComment,
+ valid: true,
+ };
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+
+ new Notes('', []);
+ $form = $('form.js-main-target-form');
+ $notesContainer = $('ul.main-notes-list');
+ $form.find('textarea.js-note-text').val(sampleComment);
+ });
+
+ it('should show message placeholder including lines starting with slash', done => {
+ $('.js-comment-button').click();
+
+ expect($notesContainer.find('.note.being-posted').length).toEqual(1); // Placeholder shown
+ expect($notesContainer.find('.note-body p').text()).toEqual(sampleComment); // No quick action processing
+
+ setImmediate(() => {
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0); // Placeholder removed
done();
});
});
diff --git a/spec/frontend/registry/explorer/components/list_item_spec.js b/spec/frontend/registry/explorer/components/list_item_spec.js
index 50348d754a5..f244627a8c3 100644
--- a/spec/frontend/registry/explorer/components/list_item_spec.js
+++ b/spec/frontend/registry/explorer/components/list_item_spec.js
@@ -141,7 +141,7 @@ describe('list item', () => {
expect(wrapper.classes()).toEqual(
expect.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
);
- expect(wrapper.classes()).toEqual(expect.not.arrayContaining(['gl-border-gray-200']));
+ expect(wrapper.classes()).toEqual(expect.not.arrayContaining(['gl-border-gray-100']));
});
it('when false applies the default border', () => {
@@ -150,7 +150,7 @@ describe('list item', () => {
expect(wrapper.classes()).toEqual(
expect.not.arrayContaining(['gl-bg-blue-50', 'gl-border-blue-200']),
);
- expect(wrapper.classes()).toEqual(expect.arrayContaining(['gl-border-gray-200']));
+ expect(wrapper.classes()).toEqual(expect.arrayContaining(['gl-border-gray-100']));
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 309aec179d9..6486826c3ec 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,189 +1,182 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
-import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import { SUCCESS } from '~/vue_merge_request_widget/constants';
+import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import PipelineStage from '~/pipelines/components/pipelines_list/stage.vue';
import mockData from '../mock_data';
describe('MRWidgetPipeline', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(pipelineComponent);
- });
+ let wrapper;
+
+ const defaultProps = {
+ pipeline: mockData.pipeline,
+ ciStatus: SUCCESS,
+ hasCi: true,
+ mrTroubleshootingDocsPath: 'help',
+ ciTroubleshootingDocsPath: 'ci-help',
+ };
+
+ const ciErrorMessage =
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.';
+ const monitoringMessage = 'Checking pipeline status.';
+
+ const findCIErrorMessage = () => wrapper.find('[data-testid="ci-error-message"]');
+ const findPipelineID = () => wrapper.find('[data-testid="pipeline-id"]');
+ const findPipelineInfoContainer = () => wrapper.find('[data-testid="pipeline-info-container"]');
+ const findCommitLink = () => wrapper.find('[data-testid="commit-link"]');
+ const findPipelineGraph = () => wrapper.find('[data-testid="widget-mini-pipeline-graph"]');
+ const findAllPipelineStages = () => wrapper.findAll(PipelineStage);
+ const findPipelineCoverage = () => wrapper.find('[data-testid="pipeline-coverage"]');
+ const findPipelineCoverageDelta = () => wrapper.find('[data-testid="pipeline-coverage-delta"]');
+ const findMonitoringPipelineMessage = () =>
+ wrapper.find('[data-testid="monitoring-pipeline-message"]');
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ const createWrapper = (props, mountFn = shallowMount) => {
+ wrapper = mountFn(PipelineComponent, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ if (wrapper?.destroy) {
+ wrapper.destroy();
+ wrapper = null;
+ }
});
describe('computed', () => {
describe('hasPipeline', () => {
- it('should return true when there is a pipeline', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- ciStatus: 'success',
- hasCi: true,
- troubleshootingDocsPath: 'help',
- });
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(vm.hasPipeline).toEqual(true);
+ it('should return true when there is a pipeline', () => {
+ expect(wrapper.vm.hasPipeline).toBe(true);
});
- it('should return false when there is no pipeline', () => {
- vm = mountComponent(Component, {
- pipeline: {},
- troubleshootingDocsPath: 'help',
- });
+ it('should return false when there is no pipeline', async () => {
+ wrapper.setProps({ pipeline: {} });
- expect(vm.hasPipeline).toEqual(false);
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.hasPipeline).toBe(false);
});
});
describe('hasCIError', () => {
- it('should return false when there is no CI error', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
- });
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(vm.hasCIError).toEqual(false);
+ it('should return false when there is no CI error', () => {
+ expect(wrapper.vm.hasCIError).toBe(false);
});
- it('should return true when there is a CI error', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- hasCi: true,
- ciStatus: null,
- troubleshootingDocsPath: 'help',
- });
+ it('should return true when there is a pipeline, but no ci status', async () => {
+ wrapper.setProps({ ciStatus: null });
- expect(vm.hasCIError).toEqual(true);
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.hasCIError).toBe(true);
});
});
describe('coverageDeltaClass', () => {
- it('should return no class if there is no coverage change', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- pipelineCoverageDelta: '0',
- troubleshootingDocsPath: 'help',
- });
+ beforeEach(() => {
+ createWrapper({ pipelineCoverageDelta: '0' });
+ });
- expect(vm.coverageDeltaClass).toEqual('');
+ it('should return no class if there is no coverage change', async () => {
+ expect(wrapper.vm.coverageDeltaClass).toBe('');
});
- it('should return text-success if the coverage increased', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- pipelineCoverageDelta: '10',
- troubleshootingDocsPath: 'help',
- });
+ it('should return text-success if the coverage increased', async () => {
+ wrapper.setProps({ pipelineCoverageDelta: '10' });
- expect(vm.coverageDeltaClass).toEqual('text-success');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.coverageDeltaClass).toBe('text-success');
});
- it('should return text-danger if the coverage decreased', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- pipelineCoverageDelta: '-12',
- troubleshootingDocsPath: 'help',
- });
+ it('should return text-danger if the coverage decreased', async () => {
+ wrapper.setProps({ pipelineCoverageDelta: '-12' });
- expect(vm.coverageDeltaClass).toEqual('text-danger');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.coverageDeltaClass).toBe('text-danger');
});
});
});
describe('rendered output', () => {
- it('should render CI error', () => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- hasCi: true,
- troubleshootingDocsPath: 'help',
- });
-
- expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
- 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
- );
+ beforeEach(() => {
+ createWrapper({ ciStatus: null }, mount);
});
- it('should render CI error when no pipeline is provided', () => {
- vm = mountComponent(Component, {
- pipeline: {},
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
- });
-
- expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
- 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
- );
+ it('should render CI error if there is a pipeline, but no status', async () => {
+ expect(findCIErrorMessage().text()).toBe(ciErrorMessage);
});
- it('should render CI error when no CI is provided and pipeline must succeed is turned on', () => {
- vm = mountComponent(Component, {
+ it('should render a loading state when no pipeline is found', async () => {
+ wrapper.setProps({
pipeline: {},
hasCi: false,
pipelineMustSucceed: true,
- troubleshootingDocsPath: 'help',
});
- expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
- 'No pipeline has been run for this commit.',
- );
+ await wrapper.vm.$nextTick();
+
+ expect(findMonitoringPipelineMessage().text()).toBe(monitoringMessage);
+ expect(findLoadingIcon().exists()).toBe(true);
});
describe('with a pipeline', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- pipeline: mockData.pipeline,
- hasCi: true,
- ciStatus: 'success',
- pipelineCoverageDelta: mockData.pipelineCoverageDelta,
- troubleshootingDocsPath: 'help',
- });
+ createWrapper(
+ {
+ pipelineCoverageDelta: mockData.pipelineCoverageDelta,
+ },
+ mount,
+ );
});
it('should render pipeline ID', () => {
- expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
- );
+ expect(
+ findPipelineID()
+ .text()
+ .trim(),
+ ).toBe(`#${mockData.pipeline.id}`);
});
it('should render pipeline status and commit id', () => {
- expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
- mockData.pipeline.details.status.label,
- );
+ expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
- expect(vm.$el.querySelector('.js-commit-link').textContent.trim()).toEqual(
- mockData.pipeline.commit.short_id,
- );
+ expect(
+ findCommitLink()
+ .text()
+ .trim(),
+ ).toBe(mockData.pipeline.commit.short_id);
- expect(vm.$el.querySelector('.js-commit-link').getAttribute('href')).toEqual(
- mockData.pipeline.commit.commit_path,
- );
+ expect(findCommitLink().attributes('href')).toBe(mockData.pipeline.commit.commit_path);
});
it('should render pipeline graph', () => {
- expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
- expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
- mockData.pipeline.details.stages.length,
- );
+ expect(findPipelineGraph().exists()).toBe(true);
+ expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
});
it('should render coverage information', () => {
- expect(vm.$el.querySelector('.media-body').textContent).toContain(
- `Coverage ${mockData.pipeline.coverage}`,
- );
+ expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
});
it('should render pipeline coverage delta information', () => {
- expect(vm.$el.querySelector('.js-pipeline-coverage-delta.text-danger')).toBeDefined();
- expect(vm.$el.querySelector('.js-pipeline-coverage-delta').textContent).toContain(
- `(${mockData.pipelineCoverageDelta}%)`,
- );
+ expect(findPipelineCoverageDelta().exists()).toBe(true);
+ expect(findPipelineCoverageDelta().text()).toBe(`(${mockData.pipelineCoverageDelta}%)`);
});
});
@@ -192,71 +185,61 @@ describe('MRWidgetPipeline', () => {
const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.commit;
- vm = mountComponent(Component, {
- pipeline: mockCopy.pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
- });
+ createWrapper({}, mount);
});
it('should render pipeline ID', () => {
- expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
- );
+ expect(
+ findPipelineID()
+ .text()
+ .trim(),
+ ).toBe(`#${mockData.pipeline.id}`);
});
it('should render pipeline status', () => {
- expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
- mockData.pipeline.details.status.label,
- );
-
- expect(vm.$el.querySelector('.js-commit-link')).toBeNull();
+ expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label);
});
it('should render pipeline graph', () => {
- expect(vm.$el.querySelector('.mr-widget-pipeline-graph')).toBeDefined();
- expect(vm.$el.querySelectorAll('.stage-container').length).toEqual(
- mockData.pipeline.details.stages.length,
- );
+ expect(findPipelineGraph().exists()).toBe(true);
+ expect(findAllPipelineStages().length).toBe(mockData.pipeline.details.stages.length);
});
it('should render coverage information', () => {
- expect(vm.$el.querySelector('.media-body').textContent).toContain(
- `Coverage ${mockData.pipeline.coverage}`,
- );
+ expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
});
});
describe('without coverage', () => {
- it('should not render a coverage', () => {
+ beforeEach(() => {
const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.coverage;
- vm = mountComponent(Component, {
- pipeline: mockCopy.pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
- });
+ createWrapper(
+ {
+ pipeline: mockCopy.pipeline,
+ },
+ mount,
+ );
+ });
- expect(vm.$el.querySelector('.media-body').textContent).not.toContain('Coverage');
+ it('should not render a coverage component', () => {
+ expect(findPipelineCoverage().exists()).toBe(false);
});
});
describe('without a pipeline graph', () => {
- it('should not render a pipeline graph', () => {
+ beforeEach(() => {
const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.details.stages;
- vm = mountComponent(Component, {
+ createWrapper({
pipeline: mockCopy.pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
});
+ });
- expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
+ it('should not render a pipeline graph', () => {
+ expect(findPipelineGraph().exists()).toBe(false);
});
});
@@ -273,11 +256,8 @@ describe('MRWidgetPipeline', () => {
});
const factory = () => {
- vm = mountComponent(Component, {
+ createWrapper({
pipeline,
- hasCi: true,
- ciStatus: 'success',
- troubleshootingDocsPath: 'help',
sourceBranchLink: mockData.source_branch_link,
});
};
@@ -289,7 +269,7 @@ describe('MRWidgetPipeline', () => {
factory();
const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id} on ${mockData.source_branch_link}`;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+ const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected);
});
@@ -302,7 +282,7 @@ describe('MRWidgetPipeline', () => {
factory();
const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+ const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected);
});
@@ -316,7 +296,7 @@ describe('MRWidgetPipeline', () => {
factory();
const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+ const actual = trimText(findPipelineInfoContainer().text());
expect(actual).toBe(expected);
});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 8ed153658fd..18665da6b48 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -239,7 +239,8 @@ export default {
commit_change_content_path: '/root/acets-app/-/merge_requests/22/commit_change_content',
merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
- troubleshooting_docs_path: 'help',
+ mr_troubleshooting_docs_path: 'help',
+ ci_troubleshooting_docs_path: 'help2',
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
merge_train_when_pipeline_succeeds_docs_path:
'/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds',
@@ -312,7 +313,8 @@ export const mockStore = {
{ id: 0, name: 'prod', status: SUCCESS },
{ id: 1, name: 'prod-docs', status: SUCCESS },
],
- troubleshootingDocsPath: 'troubleshooting-docs-path',
+ mrTroubleshootingDocsPath: 'mr-troubleshooting-docs-path',
+ ciTroubleshootingDocsPath: 'ci-troubleshooting-docs-path',
ciStatus: 'ci-status',
hasCI: true,
exposedArtifactsPath: 'exposed_artifacts.json',
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js
new file mode 100644
index 00000000000..c863b86ebf6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_spec.js
@@ -0,0 +1,34 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_html';
+import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import { normalTextNode } from './mock_data';
+
+const htmlLiteral = '<div><h1>Heading</h1><p>Paragraph.</p></div>';
+const htmlBlockNode = {
+ firstChild: null,
+ literal: htmlLiteral,
+ type: 'htmlBlock',
+};
+
+describe('Render HTML renderer', () => {
+ describe('canRender', () => {
+ it('should return true when the argument is an html block', () => {
+ expect(renderer.canRender(htmlBlockNode)).toBe(true);
+ });
+
+ it('should return false when the argument is not an html block', () => {
+ expect(renderer.canRender(normalTextNode)).toBe(false);
+ });
+ });
+
+ describe('render', () => {
+ it('should return uneditable tokens wrapping the origin token', () => {
+ const origin = jest.fn();
+ const context = { origin };
+
+ expect(renderer.render(htmlBlockNode, context)).toStrictEqual(
+ buildUneditableTokens(origin()),
+ );
+ });
+ });
+});
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index 8a9ace09bca..859c08b194a 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Projects::AlertManagementHelper do
describe '#alert_management_data' do
let(:user_can_enable_alert_management) { true }
- let(:setting_path) { edit_project_service_path(project, AlertsService) }
+ let(:setting_path) { project_settings_operations_path(project, anchor: 'js-alert-management-settings') }
subject(:data) { helper.alert_management_data(current_user, project) }
diff --git a/spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb
new file mode 100644
index 00000000000..fe048daa601
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_cluster_metrics_filter_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Banzai::Filter::InlineClusterMetricsFilter do
+ include FilterSpecHelper
+
+ let!(:cluster) { create(:cluster) }
+ let!(:project) { create(:project) }
+ let(:params) { [project.namespace.path, project.path, cluster.id] }
+ let(:query_params) { { group: 'Food metrics', title: 'Pizza Consumption', y_label: 'Slice Count' } }
+ let(:trigger_url) { urls.namespace_project_cluster_url(*params, **query_params) }
+ let(:dashboard_url) do
+ urls.metrics_dashboard_namespace_project_cluster_url(
+ *params,
+ **{
+ embedded: 'true',
+ cluster_type: 'project',
+ format: :json
+ }.merge(query_params)
+ )
+ end
+
+ it_behaves_like 'a metrics embed filter'
+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 5387df1b0e7..cafcaef8ae2 100644
--- a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
@@ -29,6 +29,26 @@ RSpec.describe Banzai::Filter::InlineMetricsRedactorFilter do
it_behaves_like 'retains the embed placeholder when applicable'
end
+ context 'for a cluster metric embed' do
+ let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [project]) }
+ let(:params) { [project.namespace.path, project.path, cluster.id] }
+ let(:query_params) { { group: 'Cluster Health', title: 'CPU Usage', y_label: 'CPU (cores)' } }
+ let(:url) { urls.metrics_dashboard_namespace_project_cluster_url(*params, **query_params) }
+
+ context 'with user who can read cluster' do
+ it_behaves_like 'redacts the embed placeholder'
+ it_behaves_like 'retains the embed placeholder when applicable'
+ end
+
+ context 'without user who can read cluster' do
+ let(:doc) { filter(input, current_user: create(:user)) }
+
+ it 'redacts the embed placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+ end
+
context 'the user has requisite permissions' do
let(:user) { create(:user) }
let(:doc) { filter(input, current_user: user) }
diff --git a/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb
index dda61bd5f6b..b3edf452f36 100644
--- a/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
describe '#parse!' do
@@ -108,7 +108,7 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
it "sets error_message" do
expect { subject }.not_to raise_error
- expect(accessibility_report.error_message).to include('Pa11y parsing failed')
+ expect(accessibility_report.error_message).to include('JSON parsing failed')
expect(accessibility_report.errors_count).to eq(0)
expect(accessibility_report.passes_count).to eq(0)
expect(accessibility_report.scans_count).to eq(0)
diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb
index ee7c98a5a54..8bf3fa5ebd9 100644
--- a/spec/lib/gitlab/json_spec.rb
+++ b/spec/lib/gitlab/json_spec.rb
@@ -7,189 +7,330 @@ RSpec.describe Gitlab::Json do
stub_feature_flags(json_wrapper_legacy_mode: true)
end
- describe ".parse" do
- context "legacy_mode is disabled by default" do
- it "parses an object" do
- expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
- end
+ shared_examples "json" do
+ describe ".parse" do
+ context "legacy_mode is disabled by default" do
+ it "parses an object" do
+ expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
+ end
- it "parses an array" do
- expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
- end
+ it "parses an array" do
+ expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
+ end
- it "parses a string" do
- expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
- end
+ it "parses a string" do
+ expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
+ end
- it "parses a true bool" do
- expect(subject.parse("true", legacy_mode: false)).to be(true)
- end
+ it "parses a true bool" do
+ expect(subject.parse("true", legacy_mode: false)).to be(true)
+ end
- it "parses a false bool" do
- expect(subject.parse("false", legacy_mode: false)).to be(false)
+ it "parses a false bool" do
+ expect(subject.parse("false", legacy_mode: false)).to be(false)
+ end
end
- end
- context "legacy_mode is enabled" do
- it "parses an object" do
- expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
- end
+ context "legacy_mode is enabled" do
+ it "parses an object" do
+ expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
- it "parses an array" do
- expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
- end
+ it "parses an array" do
+ expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
- it "raises an error on a string" do
- expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
- end
+ it "raises an error on a string" do
+ expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a true bool" do
+ expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
- it "raises an error on a true bool" do
- expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ it "raises an error on a false bool" do
+ expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
end
- it "raises an error on a false bool" do
- expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ context "feature flag is disabled" do
+ before do
+ stub_feature_flags(json_wrapper_legacy_mode: false)
+ end
+
+ it "parses an object" do
+ expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "parses a string" do
+ expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
+ end
+
+ it "parses a true bool" do
+ expect(subject.parse("true", legacy_mode: true)).to be(true)
+ end
+
+ it "parses a false bool" do
+ expect(subject.parse("false", legacy_mode: true)).to be(false)
+ end
end
end
- context "feature flag is disabled" do
- before do
- stub_feature_flags(json_wrapper_legacy_mode: false)
- end
+ describe ".parse!" do
+ context "legacy_mode is disabled by default" do
+ it "parses an object" do
+ expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
+ end
- it "parses an object" do
- expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
- end
+ it "parses an array" do
+ expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
+ end
- it "parses an array" do
- expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
- end
+ it "parses a string" do
+ expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
+ end
+
+ it "parses a true bool" do
+ expect(subject.parse!("true", legacy_mode: false)).to be(true)
+ end
- it "parses a string" do
- expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
+ it "parses a false bool" do
+ expect(subject.parse!("false", legacy_mode: false)).to be(false)
+ end
end
- it "parses a true bool" do
- expect(subject.parse("true", legacy_mode: true)).to be(true)
+ context "legacy_mode is enabled" do
+ it "parses an object" do
+ expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "raises an error on a string" do
+ expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a true bool" do
+ expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a false bool" do
+ expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
end
- it "parses a false bool" do
- expect(subject.parse("false", legacy_mode: true)).to be(false)
+ context "feature flag is disabled" do
+ before do
+ stub_feature_flags(json_wrapper_legacy_mode: false)
+ end
+
+ it "parses an object" do
+ expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "parses a string" do
+ expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
+ end
+
+ it "parses a true bool" do
+ expect(subject.parse!("true", legacy_mode: true)).to be(true)
+ end
+
+ it "parses a false bool" do
+ expect(subject.parse!("false", legacy_mode: true)).to be(false)
+ end
end
end
- end
- describe ".parse!" do
- context "legacy_mode is disabled by default" do
- it "parses an object" do
- expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
+ describe ".dump" do
+ it "dumps an object" do
+ expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}')
end
- it "parses an array" do
- expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
+ it "dumps an array" do
+ expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]')
end
- it "parses a string" do
- expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
+ it "dumps a string" do
+ expect(subject.dump("foo")).to eq('"foo"')
end
- it "parses a true bool" do
- expect(subject.parse!("true", legacy_mode: false)).to be(true)
+ it "dumps a true bool" do
+ expect(subject.dump(true)).to eq("true")
end
- it "parses a false bool" do
- expect(subject.parse!("false", legacy_mode: false)).to be(false)
+ it "dumps a false bool" do
+ expect(subject.dump(false)).to eq("false")
end
end
- context "legacy_mode is enabled" do
- it "parses an object" do
- expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ describe ".generate" do
+ let(:obj) do
+ { test: true, "foo.bar" => "baz", is_json: 1, some: [1, 2, 3] }
end
- it "parses an array" do
- expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
- end
+ it "generates JSON" do
+ expected_string = <<~STR.chomp
+ {"test":true,"foo.bar":"baz","is_json":1,"some":[1,2,3]}
+ STR
- it "raises an error on a string" do
- expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
+ expect(subject.generate(obj)).to eq(expected_string)
end
- it "raises an error on a true bool" do
- expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ it "allows you to customise the output" do
+ opts = {
+ indent: " ",
+ space: " ",
+ space_before: " ",
+ object_nl: "\n",
+ array_nl: "\n"
+ }
+
+ json = subject.generate(obj, opts)
+
+ expected_string = <<~STR.chomp
+ {
+ "test" : true,
+ "foo.bar" : "baz",
+ "is_json" : 1,
+ "some" : [
+ 1,
+ 2,
+ 3
+ ]
+ }
+ STR
+
+ expect(json).to eq(expected_string)
end
+ end
- it "raises an error on a false bool" do
- expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ describe ".pretty_generate" do
+ let(:obj) do
+ {
+ test: true,
+ "foo.bar" => "baz",
+ is_json: 1,
+ some: [1, 2, 3],
+ more: { test: true },
+ multi_line_empty_array: [],
+ multi_line_empty_obj: {}
+ }
+ end
+
+ it "generates pretty JSON" do
+ expected_string = <<~STR.chomp
+ {
+ "test": true,
+ "foo.bar": "baz",
+ "is_json": 1,
+ "some": [
+ 1,
+ 2,
+ 3
+ ],
+ "more": {
+ "test": true
+ },
+ "multi_line_empty_array": [
+
+ ],
+ "multi_line_empty_obj": {
+ }
+ }
+ STR
+
+ expect(subject.pretty_generate(obj)).to eq(expected_string)
+ end
+
+ it "allows you to customise the output" do
+ opts = {
+ space_before: " "
+ }
+
+ json = subject.pretty_generate(obj, opts)
+
+ expected_string = <<~STR.chomp
+ {
+ "test" : true,
+ "foo.bar" : "baz",
+ "is_json" : 1,
+ "some" : [
+ 1,
+ 2,
+ 3
+ ],
+ "more" : {
+ "test" : true
+ },
+ "multi_line_empty_array" : [
+
+ ],
+ "multi_line_empty_obj" : {
+ }
+ }
+ STR
+
+ expect(json).to eq(expected_string)
end
end
- context "feature flag is disabled" do
+ context "the feature table is missing" do
before do
- stub_feature_flags(json_wrapper_legacy_mode: false)
+ allow(Feature::FlipperFeature).to receive(:table_exists?).and_return(false)
end
- it "parses an object" do
- expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ it "skips legacy mode handling" do
+ expect(Feature).not_to receive(:enabled?).with(:json_wrapper_legacy_mode, default_enabled: true)
+
+ subject.send(:handle_legacy_mode!, {})
end
- it "parses an array" do
- expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ it "skips oj feature detection" do
+ expect(Feature).not_to receive(:enabled?).with(:oj_json, default_enabled: true)
+
+ subject.send(:enable_oj?)
end
+ end
- it "parses a string" do
- expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
+ context "the database is missing" do
+ before do
+ allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(PG::ConnectionBad)
end
- it "parses a true bool" do
- expect(subject.parse!("true", legacy_mode: true)).to be(true)
+ it "still parses json" do
+ expect(subject.parse("{}")).to eq({})
end
- it "parses a false bool" do
- expect(subject.parse!("false", legacy_mode: true)).to be(false)
+ it "still generates json" do
+ expect(subject.dump({})).to eq("{}")
end
end
end
- describe ".dump" do
- it "dumps an object" do
- expect(subject.dump({ "foo" => "bar" })).to eq('{"foo":"bar"}')
+ context "oj gem" do
+ before do
+ stub_feature_flags(oj_json: true)
end
- it "dumps an array" do
- expect(subject.dump([{ "foo" => "bar" }])).to eq('[{"foo":"bar"}]')
- end
-
- it "dumps a string" do
- expect(subject.dump("foo")).to eq('"foo"')
- end
-
- it "dumps a true bool" do
- expect(subject.dump(true)).to eq("true")
- end
-
- it "dumps a false bool" do
- expect(subject.dump(false)).to eq("false")
- end
+ it_behaves_like "json"
end
- describe ".generate" do
- it "delegates to the adapter" do
- args = [{ foo: "bar" }]
-
- expect(JSON).to receive(:generate).with(*args)
-
- subject.generate(*args)
+ context "json gem" do
+ before do
+ stub_feature_flags(oj_json: false)
end
- end
- describe ".pretty_generate" do
- it "delegates to the adapter" do
- args = [{ foo: "bar" }]
-
- expect(JSON).to receive(:pretty_generate).with(*args)
-
- subject.pretty_generate(*args)
- end
+ it_behaves_like "json"
end
end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
index 2788c7e595b..c368b349a3c 100644
--- a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe Gitlab::PhabricatorImport::Conduit::Response do
body: 'This is no JSON')
expect { described_class.parse!(fake_response) }
- .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected token at/)
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected character/)
end
it 'returns a parsed response for valid input' do
diff --git a/spec/migrations/generate_missing_routes_for_bots_spec.rb b/spec/migrations/generate_missing_routes_for_bots_spec.rb
new file mode 100644
index 00000000000..8af22042350
--- /dev/null
+++ b/spec/migrations/generate_missing_routes_for_bots_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20200703064117_generate_missing_routes_for_bots.rb')
+
+RSpec.describe GenerateMissingRoutesForBots, :migration do
+ let(:users) { table(:users) }
+ let(:namespaces) { table(:namespaces) }
+ let(:routes) { table(:routes) }
+
+ let(:visual_review_bot) do
+ users.create!(email: 'visual-review-bot@gitlab.com', name: 'GitLab Visual Review Bot', username: 'visual-review-bot', user_type: 3, projects_limit: 5)
+ end
+
+ let(:migration_bot) do
+ users.create!(email: 'migration-bot@gitlab.com', name: 'GitLab Migration Bot', username: 'migration-bot', user_type: 7, projects_limit: 5)
+ end
+
+ let!(:visual_review_bot_namespace) do
+ namespaces.create!(owner_id: visual_review_bot.id, name: visual_review_bot.name, path: visual_review_bot.username)
+ end
+
+ let!(:migration_bot_namespace) do
+ namespaces.create!(owner_id: migration_bot.id, name: migration_bot.name, path: migration_bot.username)
+ end
+
+ context 'for bot users without an existing route' do
+ it 'creates new routes' do
+ expect { migrate! }.to change { routes.count }.by(2)
+ end
+
+ it 'creates new routes with the same path and name as their namespace' do
+ migrate!
+
+ [visual_review_bot, migration_bot].each do |bot|
+ namespace = namespaces.find_by(owner_id: bot.id)
+ route = route_for(namespace: namespace)
+
+ expect(route.path).to eq(namespace.path)
+ expect(route.name).to eq(namespace.name)
+ end
+ end
+ end
+
+ it 'does not create routes for bot users with existing routes' do
+ create_route!(namespace: visual_review_bot_namespace)
+ create_route!(namespace: migration_bot_namespace)
+
+ expect { migrate! }.not_to change { routes.count }
+ end
+
+ it 'does not create routes for human users without an existing route' do
+ human_namespace = create_human_namespace!(name: 'GitLab Human', username: 'human')
+
+ expect { migrate! }.not_to change { route_for(namespace: human_namespace) }
+ end
+
+ it 'does not create route for a bot user with a missing route, if a human user with the same path already exists' do
+ human_namespace = create_human_namespace!(name: visual_review_bot.name, username: visual_review_bot.username)
+ create_route!(namespace: human_namespace)
+
+ expect { migrate! }.not_to change { route_for(namespace: visual_review_bot_namespace) }
+ end
+
+ private
+
+ def create_human_namespace!(name:, username:)
+ human = users.create!(email: 'human@gitlab.com', name: name, username: username, user_type: nil, projects_limit: 5)
+ namespaces.create!(owner_id: human.id, name: human.name, path: human.username)
+ end
+
+ def create_route!(namespace:)
+ routes.create!(path: namespace.path, name: namespace.name, source_id: namespace.id, source_type: 'Namespace')
+ end
+
+ def route_for(namespace:)
+ routes.find_by(source_type: 'Namespace', source_id: namespace.id)
+ end
+end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 7fae9cc60fc..a39a37b605f 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -165,6 +165,7 @@ RSpec.describe PersonalAccessToken do
let_it_be(:revoked_token) { create(:personal_access_token, revoked: true) }
let_it_be(:valid_token_and_notified) { create(:personal_access_token, expires_at: 2.days.from_now, expire_notification_delivered: true) }
let_it_be(:valid_token) { create(:personal_access_token, expires_at: 2.days.from_now) }
+ let_it_be(:long_expiry_token) { create(:personal_access_token, expires_at: '999999-12-31'.to_date) }
context 'in one day' do
it "doesn't have any tokens" do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1e2360337c1..2fede93bfa5 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -4863,6 +4863,36 @@ RSpec.describe Project do
end
end
+ describe "#default_branch" do
+ context "with an empty repository" do
+ let_it_be(:project) { create(:project_empty_repo) }
+
+ context "Gitlab::CurrentSettings.default_branch_name is unavailable" do
+ before do
+ expect(Gitlab::CurrentSettings)
+ .to receive(:default_branch_name)
+ .and_return(nil)
+ end
+
+ it "returns that value" do
+ expect(project.default_branch).to be_nil
+ end
+ end
+
+ context "Gitlab::CurrentSettings.default_branch_name is available" do
+ before do
+ expect(Gitlab::CurrentSettings)
+ .to receive(:default_branch_name)
+ .and_return('example_branch')
+ end
+
+ it "returns that value" do
+ expect(project.default_branch).to eq("example_branch")
+ end
+ end
+ end
+ end
+
describe '#to_ability_name' do
it 'returns project' do
project = build(:project_empty_repo)
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index ff9963c3ad1..d998660a3f9 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -249,4 +249,44 @@ RSpec.describe Clusters::ClusterPresenter do
it { is_expected.to be_truthy }
end
end
+
+ describe '#health_data' do
+ shared_examples 'cluster health data' do
+ let(:user) { create(:user) }
+ let(:cluster_presenter) { cluster.present(current_user: user) }
+
+ let(:clusterable_presenter) do
+ ClusterablePresenter.fabricate(clusterable, current_user: user)
+ end
+
+ subject { cluster_presenter.health_data(clusterable_presenter) }
+
+ it do
+ is_expected.to include('clusters-path': clusterable_presenter.index_path,
+ 'dashboard-endpoint': clusterable_presenter.metrics_dashboard_path(cluster),
+ 'documentation-path': help_page_path('user/project/clusters/index', anchor: 'monitoring-your-kubernetes-cluster-ultimate'),
+ 'empty-getting-started-svg-path': match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
+ 'empty-loading-svg-path': match_asset_path('/assets/illustrations/monitoring/loading.svg'),
+ 'empty-no-data-svg-path': match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
+ 'empty-unable-to-connect-svg-path': match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
+ 'settings-path': '',
+ 'project-path': '',
+ 'tags-path': '')
+ end
+ end
+
+ context 'with project cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:clusterable) { cluster.project }
+
+ it_behaves_like 'cluster health data'
+ end
+
+ context 'with group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:clusterable) { cluster.group }
+
+ it_behaves_like 'cluster health data'
+ end
+ end
end
diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb
index 3910f4705c5..27360201e81 100644
--- a/spec/presenters/group_clusterable_presenter_spec.rb
+++ b/spec/presenters/group_clusterable_presenter_spec.rb
@@ -94,4 +94,10 @@ RSpec.describe GroupClusterablePresenter do
it { is_expected.to eq(group_cluster_path(group, cluster)) }
end
+
+ describe '#metrics_dashboard_path' do
+ subject { presenter.metrics_dashboard_path(cluster) }
+
+ it { is_expected.to eq(metrics_dashboard_group_cluster_path(group, cluster)) }
+ end
end
diff --git a/spec/presenters/instance_clusterable_presenter_spec.rb b/spec/presenters/instance_clusterable_presenter_spec.rb
index 352b7fc6ea7..6968e3a4da3 100644
--- a/spec/presenters/instance_clusterable_presenter_spec.rb
+++ b/spec/presenters/instance_clusterable_presenter_spec.rb
@@ -26,4 +26,10 @@ RSpec.describe InstanceClusterablePresenter do
it { is_expected.to eq(clear_cache_admin_cluster_path(cluster)) }
end
+
+ describe '#metrics_dashboard_path' do
+ subject { presenter.metrics_dashboard_path(cluster) }
+
+ it { is_expected.to eq(metrics_dashboard_admin_cluster_path(cluster)) }
+ end
end
diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb
index 6cd06670595..b518c63f0ca 100644
--- a/spec/presenters/project_clusterable_presenter_spec.rb
+++ b/spec/presenters/project_clusterable_presenter_spec.rb
@@ -94,4 +94,10 @@ RSpec.describe ProjectClusterablePresenter do
it { is_expected.to eq(project_cluster_path(project, cluster)) }
end
+
+ describe '#metrics_dashboard_path' do
+ subject { presenter.metrics_dashboard_path(cluster) }
+
+ it { is_expected.to eq(metrics_dashboard_project_cluster_path(project, cluster)) }
+ end
end
diff --git a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
new file mode 100644
index 00000000000..cb8f6721d66
--- /dev/null
+++ b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples_for 'GET #metrics_dashboard correctly formatted response' do
+ it 'returns a json object with the correct keys' do
+ get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
+
+ # Exclude `all_dashboards` to handle separately, at spec/controllers/projects/environments_controller_spec.rb:565
+ # because `all_dashboards` key is not part of expected shared behavior
+ found_keys = json_response.keys - ['all_dashboards']
+
+ expect(response).to have_gitlab_http_status(status_code)
+ expect(found_keys).to contain_exactly(*expected_keys)
+ end
+end
+
+RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_name|
+ let(:expected_keys) { %w(dashboard status metrics_data) }
+ let(:status_code) { :ok }
+
+ it_behaves_like 'GET #metrics_dashboard correctly formatted response'
+
+ it 'returns correct dashboard' do
+ get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
+
+ expect(json_response['dashboard']['dashboard']).to eq(dashboard_name)
+ end
+end
diff --git a/spec/views/admin/application_settings/repository.html.haml_spec.rb b/spec/views/admin/application_settings/repository.html.haml_spec.rb
new file mode 100644
index 00000000000..b110bc277ac
--- /dev/null
+++ b/spec/views/admin/application_settings/repository.html.haml_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'admin/application_settings/repository.html.haml' do
+ let(:app_settings) { build(:application_setting) }
+ let(:user) { create(:admin) }
+
+ before do
+ assign(:application_setting, app_settings)
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ describe 'default initial branch name' do
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(global_default_branch_name: false)
+ end
+
+ it 'does not show the setting section' do
+ render
+
+ expect(rendered).not_to have_css("#js-default-branch-name")
+ end
+ end
+
+ context 'when the feature flag is enabled' do
+ before do
+ stub_feature_flags(global_default_branch_name: true)
+ end
+
+ it 'has the setting section' do
+ render
+
+ expect(rendered).to have_css("#js-default-branch-name")
+ end
+
+ it 'renders the correct setting section content' do
+ render
+
+ expect(rendered).to have_content("Default initial branch name")
+ expect(rendered).to have_content("Set the default name of the initial branch when creating new repositories through the user interface.")
+ end
+ end
+ end
+end