From 2d4a559cf99b6d49c28d6e76dcc0b321947b843f Mon Sep 17 00:00:00 2001 From: Jan Sandbrink Date: Tue, 5 May 2026 16:28:30 +0200 Subject: [PATCH] Move health check components into core The idea is to reuse them in the wikis module and probably elsewhere as well, offering a similar look & feel. The ReportComponent has been lightened for this, though. Previously it included the page layout and a default to render when there was no report. Now it only focusses on rendering an actual report and leaves the rest up to the including component or page. --- .../health_reports/report_component.html.erb | 76 ++++++++++++ .../health_reports/report_component.rb | 66 ++++++----- .../health_reports/result_component.html.erb | 4 +- .../health_reports/result_component.rb | 88 ++++++++++++++ config/i18n-tasks.yml | 6 +- config/locales/en.yml | 29 +++-- .../admin/health/check_result_component.rb | 90 --------------- .../health/health_report_component.html.erb | 97 ---------------- .../side_panel/validation_result_component.rb | 12 +- .../admin/health_status/show.html.erb | 33 +++++- modules/storages/config/locales/en.yml | 109 +++++++++--------- .../health_reports/report_component_spec.rb | 75 +++++------- .../health_reports/result_component_spec.rb | 28 +++-- 13 files changed, 362 insertions(+), 351 deletions(-) create mode 100644 app/components/health_reports/report_component.html.erb rename modules/storages/app/components/storages/admin/health/health_report_component.rb => app/components/health_reports/report_component.rb (54%) rename modules/storages/app/components/storages/admin/health/check_result_component.html.erb => app/components/health_reports/result_component.html.erb (92%) create mode 100644 app/components/health_reports/result_component.rb delete mode 100644 modules/storages/app/components/storages/admin/health/check_result_component.rb delete mode 100644 modules/storages/app/components/storages/admin/health/health_report_component.html.erb rename modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb => spec/components/health_reports/report_component_spec.rb (55%) rename modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb => spec/components/health_reports/result_component_spec.rb (71%) diff --git a/app/components/health_reports/report_component.html.erb b/app/components/health_reports/report_component.html.erb new file mode 100644 index 00000000000..876926985d9 --- /dev/null +++ b/app/components/health_reports/report_component.html.erb @@ -0,0 +1,76 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<%= + component_wrapper do + flex_layout do |report_container| + report_container.with_row do + concat(render(Primer::Beta::Octicon.new(mr: 2, **summary_icon(report.tally)))) + concat(render(Primer::Beta::Text.new(font_weight: :bold)) { humanize_summary(report.tally) }) + end + + report_container.with_row(mt: 2) do + render(Primer::Beta::Text.new) do + if report.healthy? + t(".summary.success") + elsif report.unhealthy? + t(".summary.failure") + else + t(".summary.warning") + end + end + end + + report.results.each do |result_group| + report_container.with_row(mt: 3) do + render(Primer::Beta::BorderBox.new(test_selector: "op-health-report--result-group")) do |box| + box.with_header do + flex_layout(justify_content: :space_between, classes: "flex-wrap") do |header| + header.with_column do + render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t("#{result_group.key}.header", scope: i18n_scope) } + end + + header.with_column do + concat(render(Primer::Beta::Octicon.new(mr: 2, **summary_icon(result_group.tally)))) + concat(render(Primer::Beta::Text.new) { humanize_summary(result_group.tally) }) + end + end + end + + result_group.results.each do |value| + box.with_row do + render(HealthReports::ResultComponent.new(group: result_group.key, result: value, i18n_scope:)) + end + end + end + end + end + end + end +%> diff --git a/modules/storages/app/components/storages/admin/health/health_report_component.rb b/app/components/health_reports/report_component.rb similarity index 54% rename from modules/storages/app/components/storages/admin/health/health_report_component.rb rename to app/components/health_reports/report_component.rb index b7e9ab84ff7..de8b102c53d 100644 --- a/modules/storages/app/components/storages/admin/health/health_report_component.rb +++ b/app/components/health_reports/report_component.rb @@ -23,46 +23,48 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. #++ -module Storages - module Admin - module Health - class HealthReportComponent < ApplicationComponent - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable +module HealthReports + class ReportComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable - def initialize(storage:, report:) - super(storage) - @report = report - end + alias report model - private + # The i18n_scope parameter defines the I18n scope that should be used to resolve + # names of groups, checks and error messages indicated by the results. + def initialize(*, i18n_scope:, **) + super(*, **) + @i18n_scope = i18n_scope + end - def summary_icon(check_tally) - case check_tally - in { failure: 1.. } - { icon: :alert, color: :danger } - in { warning: 1.. } - { icon: :alert, color: :attention } - else - { icon: :"check-circle", color: :success } - end - end + private - def humanize_summary(check_tally) - case check_tally - in { failure: 1.. } - I18n.t("health_report.checks.failures", count: check_tally[:failure]) - in { warning: 1.. } - I18n.t("health_report.checks.warnings", count: check_tally[:warning]) - else - I18n.t("health_report.checks.success") - end - end + attr_reader :i18n_scope + + def summary_icon(check_tally) + case check_tally + in { failure: 1.. } + { icon: :alert, color: :danger } + in { warning: 1.. } + { icon: :alert, color: :attention } + else + { icon: :"check-circle", color: :success } + end + end + + def humanize_summary(check_tally) + case check_tally + in { failure: 1.. } + t(".checks.failures", count: check_tally[:failure]) + in { warning: 1.. } + t(".checks.warnings", count: check_tally[:warning]) + else + t(".checks.success") end end end diff --git a/modules/storages/app/components/storages/admin/health/check_result_component.html.erb b/app/components/health_reports/result_component.html.erb similarity index 92% rename from modules/storages/app/components/storages/admin/health/check_result_component.html.erb rename to app/components/health_reports/result_component.html.erb index b6c39d345b4..67b17ee9540 100644 --- a/modules/storages/app/components/storages/admin/health/check_result_component.html.erb +++ b/app/components/health_reports/result_component.html.erb @@ -21,7 +21,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. @@ -57,7 +57,7 @@ See COPYRIGHT and LICENSE files for more details. if error_text.present? cell.with_row(mt: 1) do - render(Primer::Beta::Text.new(test_selector: "op-storages--health-status-check-information")) do + render(Primer::Beta::Text.new(test_selector: "op-health-report--result-status")) do error_text end end diff --git a/app/components/health_reports/result_component.rb b/app/components/health_reports/result_component.rb new file mode 100644 index 00000000000..5a57739f09f --- /dev/null +++ b/app/components/health_reports/result_component.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module HealthReports + class ResultComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + def initialize(group:, result:, i18n_scope:) + super(result) + @group = group + @i18n_scope = i18n_scope + end + + private + + def text = I18n.t("#{@group}.#{model.key}", scope: @i18n_scope) + + def error_text + return nil if model.code.nil? + + # TODO: fix translation namespace + I18n.t("errors.#{model.code}", scope: @i18n_scope, **model.context&.symbolize_keys) + end + + def docs_href = ::OpenProject::Static::Links.url_for(:storage_docs, :health_status) + + def error_code + if model.failure? + "ERR_#{model.code.upcase}" + elsif model.warning? + "WRN_#{model.code.upcase}" + end + end + + def status_color + if model.success? + :success + elsif model.failure? + :danger + elsif model.warning? || model.skipped? + :attention + else + raise ArgumentError, "invalid check result state" + end + end + + def status_text + if model.success? + t(".status.passed") + elsif model.failure? + t(".status.failed") + elsif model.warning? + t(".status.warning") + elsif model.skipped? + t(".status.skipped") + else + raise ArgumentError, "invalid check result state" + end + end + end +end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 3435d4985de..9f25d0fb396 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -69,8 +69,8 @@ search: # By default, if a relative translation is used inside a method, the name of the method will be considered part of the resolved key. # Directories listed here will not consider the name of the method part of the resolved key # - # relative_exclude_method_name_paths: - # - + relative_exclude_method_name_paths: + - modules/storages/app/components ## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting: ## *.jpg *.jpeg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less @@ -151,7 +151,7 @@ ignore_unused: - '*.permission_header_explanation' - 'storages.upsell.*' - 'services.*' - - 'storages.health.connection_validation.{one_drive,nextcloud}.*' + - 'storages.health.checks.*' ## Exclude these keys from the `i18n-tasks eq-base' report: # ignore_eq_base: diff --git a/config/locales/en.yml b/config/locales/en.yml index 62a1b5b611c..7f7f92935d2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3524,15 +3524,26 @@ en: gui_validation_error: "1 error" gui_validation_error_plural: "%{count} errors" - health_report: - checks: - failures: - one: "%{count} check failed" - other: "%{count} checks failed" - success: All checks passed - warnings: - one: "%{count} check returned a warning" - other: "%{count} checks returned a warning" + health_reports: + report_component: + checks: + failures: + one: "%{count} check failed" + other: "%{count} checks failed" + success: All checks passed + warnings: + one: "%{count} check returned a warning" + other: "%{count} checks returned a warning" + summary: + failure: Some checks failed and the system does not work as expected. + success: All connections and systems are working as expected. + warning: Some checks returned a warning. This can lead to unexpected behaviour. + result_component: + status: + failed: Failed + passed: Passed + skipped: Skipped + warning: Warning homescreen: additional: diff --git a/modules/storages/app/components/storages/admin/health/check_result_component.rb b/modules/storages/app/components/storages/admin/health/check_result_component.rb deleted file mode 100644 index 5d7077a4841..00000000000 --- a/modules/storages/app/components/storages/admin/health/check_result_component.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module Storages - module Admin - module Health - class CheckResultComponent < ApplicationComponent - include OpPrimer::ComponentHelpers - - def initialize(group:, result:) - super(result) - @group = group - end - - private - - def text = I18n.t("storages.health.checks.#{@group}.#{model.key}") - - def error_text - return nil if model.code.nil? - - I18n.t("storages.health.connection_validation.#{model.code}", **model.context&.symbolize_keys) - end - - def docs_href = ::OpenProject::Static::Links.url_for(:storage_docs, :health_status) - - def error_code - if model.failure? - "ERR_#{model.code.upcase}" - elsif model.warning? - "WRN_#{model.code.upcase}" - end - end - - def status_color - if model.success? - :success - elsif model.failure? - :danger - elsif model.warning? || model.skipped? - :attention - else - raise ArgumentError, "invalid check result state" - end - end - - def status_text - if model.success? - I18n.t("storages.health.label_passed") - elsif model.failure? - I18n.t("storages.health.label_failed") - elsif model.warning? - I18n.t("storages.health.label_warning") - elsif model.skipped? - I18n.t("storages.health.label_skipped") - else - raise ArgumentError, "invalid check result state" - end - end - end - end - end -end diff --git a/modules/storages/app/components/storages/admin/health/health_report_component.html.erb b/modules/storages/app/components/storages/admin/health/health_report_component.html.erb deleted file mode 100644 index f19f34e8245..00000000000 --- a/modules/storages/app/components/storages/admin/health/health_report_component.html.erb +++ /dev/null @@ -1,97 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<%= - component_wrapper do - render(Primer::Alpha::Layout.new(stacking_breakpoint: :lg)) do |page| - page.with_main do - if @report.nil? - render(Primer::Beta::Blankslate.new(border: true)) do |placeholder| - placeholder.with_visual_icon(icon: :meter) - placeholder.with_heading(tag: :h3) { I18n.t("storages.health.no_report") } - placeholder.with_description { I18n.t("storages.health.no_report_description") } - placeholder.with_primary_action( - href: admin_settings_storage_health_status_report_path(model), - data: { turbo_method: :post, turbo: true }, - aria: { label: I18n.t("storages.health.actions.run_checks") } - ) do - I18n.t("storages.health.actions.run_checks") - end - end - else - flex_layout do |report_container| - report_container.with_row do - concat(render(Primer::Beta::Octicon.new(mr: 2, **summary_icon(@report.tally)))) - concat(render(Primer::Beta::Text.new(font_weight: :bold)) { humanize_summary(@report.tally) }) - end - - report_container.with_row(mt: 2) do - render(Primer::Beta::Text.new) do - if @report.healthy? - I18n.t("storages.health.summary.success") - elsif @report.unhealthy? - I18n.t("storages.health.summary.failure") - else - I18n.t("storages.health.summary.warning") - end - end - end - - @report.results.each do |result_group| - report_container.with_row(mt: 3) do - render(Primer::Beta::BorderBox.new(test_selector: "op-storages--health-report-group")) do |box| - box.with_header do - flex_layout(justify_content: :space_between, classes: "flex-wrap") do |header| - header.with_column do - render(Primer::Beta::Text.new(font_weight: :bold)) { I18n.t("storages.health.checks.#{result_group.key}.header") } - end - - header.with_column do - concat(render(Primer::Beta::Octicon.new(mr: 2, **summary_icon(result_group.tally)))) - concat(render(Primer::Beta::Text.new) { humanize_summary(result_group.tally) }) - end - end - end - - result_group.results.each do |value| - box.with_row do - render(Storages::Admin::Health::CheckResultComponent.new(group: result_group.key, result: value)) - end - end - end - end - end - end - end - end - - page.with_sidebar(col_placement: :end, row_placement: :end) - end - end -%> diff --git a/modules/storages/app/components/storages/admin/side_panel/validation_result_component.rb b/modules/storages/app/components/storages/admin/side_panel/validation_result_component.rb index 291033b62d6..227de4e5ade 100644 --- a/modules/storages/app/components/storages/admin/side_panel/validation_result_component.rb +++ b/modules/storages/app/components/storages/admin/side_panel/validation_result_component.rb @@ -49,26 +49,26 @@ module Storages { icon: :alert, icon_color: :danger, - text: I18n.t("health_report.checks.failures", count: tally[:failure]) + text: t(".checks.failures", count: tally[:failure]) } in { warning: 1.. } { icon: :alert, icon_color: :attention, - text: I18n.t("health_report.checks.warnings", count: tally[:warning]) + text: t(".checks.warnings", count: tally[:warning]) } else - { icon: :"check-circle", icon_color: :success, text: I18n.t("health_report.checks.success") } + { icon: :"check-circle", icon_color: :success, text: t(".checks.success") } end end def summary_description text = if @result.healthy? - I18n.t("storages.health.summary.success") + t(".summary.success") elsif @result.unhealthy? - I18n.t("storages.health.summary.failure") + t(".summary.failure") else - I18n.t("storages.health.summary.warning") + t(".summary.warning") end "#{text} #{I18n.t('storages.health.checked', datetime: helpers.format_time(@result.created_at))}" diff --git a/modules/storages/app/views/storages/admin/health_status/show.html.erb b/modules/storages/app/views/storages/admin/health_status/show.html.erb index e40350ad320..968c9a14df4 100644 --- a/modules/storages/app/views/storages/admin/health_status/show.html.erb +++ b/modules/storages/app/views/storages/admin/health_status/show.html.erb @@ -53,8 +53,8 @@ See COPYRIGHT and LICENSE files for more details. end %> - <%= - if @report.present? + <% if @report.present? %> + <%= button_label = I18n.t("storages.health.actions.rerun_checks") download_label = I18n.t("storages.health.actions.download_report") @@ -81,9 +81,32 @@ See COPYRIGHT and LICENSE files for more details. button_label end end - end - %> + %> + <% end %> - <%= render(Storages::Admin::Health::HealthReportComponent.new(storage: @storage, report: @report)) %> + <%= + render(Primer::Alpha::Layout.new(stacking_breakpoint: :lg)) do |page| + page.with_sidebar(col_placement: :end, row_placement: :end) + page.with_main do %> + <% if @report.present? %> + <%= render(HealthReports::ReportComponent.new(@report, i18n_scope: "storages.health.checks")) %> + <% else %> + <%= + render(Primer::Beta::Blankslate.new(border: true)) do |placeholder| + placeholder.with_visual_icon(icon: :meter) + placeholder.with_heading(tag: :h3) { I18n.t("storages.health.no_report") } + placeholder.with_description { I18n.t("storages.health.no_report_description") } + placeholder.with_primary_action( + href: admin_settings_storage_health_status_report_path(@storage), + data: { turbo_method: :post, turbo: true }, + aria: { label: I18n.t("storages.health.actions.run_checks") } + ) do + I18n.t("storages.health.actions.run_checks") + end + end + %> + <% end %> + <% end %> + <% end %> <% end %> diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 02d6576168f..745cba8f05e 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -209,6 +209,19 @@ en: health_notifications_component: sync_now: Sync now sync_queued: Synchronization queued. + validation_result_component: + checks: + failures: + one: "%{count} check failed" + other: "%{count} checks failed" + success: All checks passed + warnings: + one: "%{count} check returned a warning" + other: "%{count} checks returned a warning" + summary: + failure: Some checks failed and the system does not work as expected. + success: All connections and systems are working as expected. + warning: Some checks returned a warning. This can lead to unexpected behaviour. buttons: done_continue: Done, continue open_storage: Open file storage @@ -314,67 +327,59 @@ en: host_url_accessible: Host URL accessible storage_configured: Configuration complete tenant_id: Tenant ID - connection_validation: - client_id_invalid: The configured OAuth 2 client id is invalid. Please check the configuration. - client_secret_invalid: The configured OAuth 2 client secret is invalid. Please check the configuration. - nc_dependency_missing: 'A required dependency is missing on the file storage. Please add the following dependency: %{dependency}.' - nc_dependency_version_mismatch: The %{dependency} app version is not supported. Please update your Nextcloud server. - nc_host_not_found: No Nextcloud server found at the configured host url. Please check the configuration. - nc_oauth_request_not_found: The endpoint to fetch the currently connected user was not found. Please check the server logs for further information. - nc_oauth_request_unauthorized: The current user isn't authorized to access the remote file storage. Please check the server logs for further information. - nc_oauth_token_missing: OpenProject cannot test the user level communication with Nextcloud as the user did not yet link their Nextcloud account. - nc_project_folder_missing: The previously created project folder for project "%{project}" could not be found. - nc_team_folder_not_found: The team folder could not be found. - nc_unexpected_files: 'Unexpected files found in the managed team folder. For example: %{sample}' - nc_unlinked_project_folders: Not all project folders have been created yet (%{actual} / %{expected}). This can indicate errors during the AMPF background synchronization. - nc_userless_access_denied: The configured app password is invalid. - not_configured: The connection could not be validated. Please finish configuration first. - od_client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. - od_client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. - od_drive_id_invalid: The configured drive id seems invalid. Please check the configuration. - od_drive_id_not_found: The configured drive id could not be found. Please check the configuration. - od_oauth_request_not_found: The endpoint to fetch the currently connected user was not found. Please check the server logs for further information. - od_oauth_request_unauthorized: The current user isn't authorized to access the remote file storage. Please check the server logs for further information. - od_oauth_token_missing: OpenProject cannot test the user level communication with OneDrive as the user did not yet link their Microsoft account. - od_tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. - od_test_folder_exists: The folder %{folder_name} needed for testing already exists. Please delete it and try again. - od_unexpected_content: Unexpected content found in the drive. - offline_access_scope_missing: It is recommended to configure the OpenID Connect provider to request the offline_access scope. The integration may still work anyways, but make sure that refresh tokens do not expire. - oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. - oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. - oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. - oidc_provider_cant_exchange: The OpenID Connect provider does not seem to support token exchange, but token exchange was configured for the storage. - oidc_token_acquisition_failed: Your OpenID Connect setup doesn't provide the necessary audience, nor does it provide token exchange capabilities. Please check out our documentation for more information. - oidc_token_exchange_failed: There seems to be a problem with the Token Exchange setup on your OpenID Connect Provider. Please check its configuration and try again. - oidc_token_refresh_failed: There was an error while trying to check your access to the storage. Please check the server logs for further information. - sp_client_cant_delete_folder: The client is having trouble deleting folders in SharePoint. Please check the setup documentation for your storage. - sp_client_id_missing: The configured OAuth 2 client id is missing for SharePoint. Please check the configuration. - sp_client_secret_missing: The configured OAuth 2 client secret is missing for SharePoint. Please check the configuration. - sp_client_write_permission_missing: The client seems to have write permissions missing in SharePoint. Please check the setup documentation for your storage. - sp_existing_test_folder: The folder %{folder_name} needed for testing already exists in SharePoint. Please delete it and try again. - sp_oauth_request_error: The user-bound request to SharePoint failed. Please check the server logs for further information. - sp_oauth_request_unauthorized: The current user isn't authorized to access the remote file storage. Please check the server logs for further information. - sp_oauth_token_missing: OpenProject cannot test the user level communication with SharePoint as the user did not yet link their SharePoint account. - sp_tenant_id_missing: The configured directory (tenant) id is missing for SharePoint. Please check the configuration. - sp_unexpected_content: Unexpected content found in the SharePoint Document Library. - unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. + errors: + client_id_invalid: The configured OAuth 2 client id is invalid. Please check the configuration. + client_secret_invalid: The configured OAuth 2 client secret is invalid. Please check the configuration. + nc_dependency_missing: 'A required dependency is missing on the file storage. Please add the following dependency: %{dependency}.' + nc_dependency_version_mismatch: The %{dependency} app version is not supported. Please update your Nextcloud server. + nc_host_not_found: No Nextcloud server found at the configured host url. Please check the configuration. + nc_oauth_request_not_found: The endpoint to fetch the currently connected user was not found. Please check the server logs for further information. + nc_oauth_request_unauthorized: The current user isn't authorized to access the remote file storage. Please check the server logs for further information. + nc_oauth_token_missing: OpenProject cannot test the user level communication with Nextcloud as the user did not yet link their Nextcloud account. + nc_project_folder_missing: The previously created project folder for project "%{project}" could not be found. + nc_team_folder_not_found: The team folder could not be found. + nc_unexpected_files: 'Unexpected files found in the managed team folder. For example: %{sample}' + nc_unlinked_project_folders: Not all project folders have been created yet (%{actual} / %{expected}). This can indicate errors during the AMPF background synchronization. + nc_userless_access_denied: The configured app password is invalid. + not_configured: The connection could not be validated. Please finish configuration first. + od_client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + od_client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + od_drive_id_invalid: The configured drive id seems invalid. Please check the configuration. + od_drive_id_not_found: The configured drive id could not be found. Please check the configuration. + od_oauth_request_not_found: The endpoint to fetch the currently connected user was not found. Please check the server logs for further information. + od_oauth_request_unauthorized: The current user isn't authorized to access the remote file storage. Please check the server logs for further information. + od_oauth_token_missing: OpenProject cannot test the user level communication with OneDrive as the user did not yet link their Microsoft account. + od_tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. + od_test_folder_exists: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + od_unexpected_content: Unexpected content found in the drive. + offline_access_scope_missing: It is recommended to configure the OpenID Connect provider to request the offline_access scope. The integration may still work anyways, but make sure that refresh tokens do not expire. + oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. + oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. + oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + oidc_provider_cant_exchange: The OpenID Connect provider does not seem to support token exchange, but token exchange was configured for the storage. + oidc_token_acquisition_failed: Your OpenID Connect setup doesn't provide the necessary audience, nor does it provide token exchange capabilities. Please check out our documentation for more information. + oidc_token_exchange_failed: There seems to be a problem with the Token Exchange setup on your OpenID Connect Provider. Please check its configuration and try again. + oidc_token_refresh_failed: There was an error while trying to check your access to the storage. Please check the server logs for further information. + sp_client_cant_delete_folder: The client is having trouble deleting folders in SharePoint. Please check the setup documentation for your storage. + sp_client_id_missing: The configured OAuth 2 client id is missing for SharePoint. Please check the configuration. + sp_client_secret_missing: The configured OAuth 2 client secret is missing for SharePoint. Please check the configuration. + sp_client_write_permission_missing: The client seems to have write permissions missing in SharePoint. Please check the setup documentation for your storage. + sp_existing_test_folder: The folder %{folder_name} needed for testing already exists in SharePoint. Please delete it and try again. + sp_oauth_request_error: The user-bound request to SharePoint failed. Please check the server logs for further information. + sp_oauth_request_unauthorized: The current user isn't authorized to access the remote file storage. Please check the server logs for further information. + sp_oauth_token_missing: OpenProject cannot test the user level communication with SharePoint as the user did not yet link their SharePoint account. + sp_tenant_id_missing: The configured directory (tenant) id is missing for SharePoint. Please check the configuration. + sp_unexpected_content: Unexpected content found in the SharePoint Document Library. + unknown_error: The connection could not be validated. An unknown error occurred. Please check the server logs for further information. label_error: Error - label_failed: Failed label_healthy: Healthy - label_passed: Passed label_pending: Pending - label_skipped: Skipped - label_warning: Warning no_report: No report available no_report_description: Run the checks now for a full health status report for this file storage. open_report: Open full health report project_folders: subtitle: Automatically managed project folders since: since %{datetime} - summary: - failure: Some checks failed and the system does not work as expected. - success: All connections and systems are working as expected. - warning: Some checks returned a warning. This can lead to unexpected behaviour. synced: 'Last sync: %{datetime}' title: Health status report health_email_notifications: diff --git a/modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb b/spec/components/health_reports/report_component_spec.rb similarity index 55% rename from modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb rename to spec/components/health_reports/report_component_spec.rb index dde82acd44f..9d42cb9112a 100644 --- a/modules/storages/spec/components/storages/admin/health/health_report_component_spec.rb +++ b/spec/components/health_reports/report_component_spec.rb @@ -30,57 +30,44 @@ require "rails_helper" -RSpec.describe Storages::Admin::Health::HealthReportComponent, type: :component do - let(:storage) { create(:nextcloud_storage_configured) } +RSpec.describe HealthReports::ReportComponent, type: :component do + let(:report) do + # rubocop:disable Naming/VariableNumber + generate_test_report( + group_1: %i[success success], + group_2: %i[skipped skipped], + group_3: %i[success success warning warning], + group_4: %i[success failure failure], + group_5: %i[success failure warning] + ) + # rubocop:enable Naming/VariableNumber + end - subject(:health_report_component) { described_class.new(storage:, report:) } + subject(:health_report_component) { described_class.new(report, i18n_scope: "test.scope") } before do render_inline(health_report_component) end - context "if report is not available" do - let(:report) { nil } - - it "renders a placeholder blankslate" do - expect(page).to have_text("No report available") - expect(page).to have_link("Run checks now") - end + it "renders a summary" do + expect(page).to have_text("3 checks failed") + expect(page).to have_text("Some checks failed and the system does not work as expected.") end - context "if report is available" do - let(:report) do - # rubocop:disable Naming/VariableNumber - generate_test_report( - group_1: %i[success success], - group_2: %i[skipped skipped], - group_3: %i[success success warning warning], - group_4: %i[success failure failure], - group_5: %i[success failure warning] - ) - # rubocop:enable Naming/VariableNumber - end + it "renders each group separately" do + expect(page).to have_test_selector("op-health-report--result-group", count: 5) - it "renders a summary" do - expect(page).to have_text("3 checks failed") - expect(page).to have_text("Some checks failed and the system does not work as expected.") - end + summaries = { + 0 => "All checks passed", + 1 => "All checks passed", + 2 => "2 checks returned a warning", + 3 => "2 checks failed", + 4 => "1 check failed" + } - it "renders each group separately" do - expect(page).to have_test_selector("op-storages--health-report-group", count: 5) - - summaries = { - 0 => "All checks passed", - 1 => "All checks passed", - 2 => "2 checks returned a warning", - 3 => "2 checks failed", - 4 => "1 check failed" - } - - page.all(test_selector("op-storages--health-report-group")).each_with_index do |group, idx| - expect(group).to have_text("Group #{idx + 1}") - expect(group).to have_text(summaries[idx]) - end + page.all(test_selector("op-health-report--result-group")).each_with_index do |group, idx| + expect(group).to have_text("Group #{idx + 1}") + expect(group).to have_text(summaries[idx]) end end @@ -103,9 +90,9 @@ RSpec.describe Storages::Admin::Health::HealthReportComponent, type: :component end group.results << result - allow(I18n).to receive(:t).with("storages.health.checks.#{group_key}.#{key}").and_return(key.to_s.humanize) + allow(I18n).to receive(:t).with("#{group_key}.#{key}", scope: "test.scope").and_return(key.to_s.humanize) if result.code.present? - allow(I18n).to receive(:t).with("storages.health.connection_validation.#{result.code}") + allow(I18n).to receive(:t).with("errors.#{result.code}", scope: "test.scope") .and_return(result.code.to_s.humanize) end end @@ -119,7 +106,7 @@ RSpec.describe Storages::Admin::Health::HealthReportComponent, type: :component map.each_pair do |key, values| report.results << generate_test_group(key, values) - allow(I18n).to receive(:t).with("storages.health.checks.#{key}.header").and_return(key.to_s.humanize) + allow(I18n).to receive(:t).with("#{key}.header", scope: "test.scope").and_return(key.to_s.humanize) end report diff --git a/modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb b/spec/components/health_reports/result_component_spec.rb similarity index 71% rename from modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb rename to spec/components/health_reports/result_component_spec.rb index b34d239839e..f40f2e74479 100644 --- a/modules/storages/spec/components/storages/admin/health/check_result_component_spec.rb +++ b/spec/components/health_reports/result_component_spec.rb @@ -30,24 +30,28 @@ require "rails_helper" -RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component do +RSpec.describe HealthReports::ResultComponent, type: :component do let(:group_key) { :base_configuration } - subject(:check_result_component) { described_class.new(group: group_key, result: check_result) } + subject(:result_component) { described_class.new(group: group_key, result: check_result, i18n_scope: "test.scope") } before do - render_inline(check_result_component) + allow(I18n).to receive(:t).and_call_original + allow(I18n).to receive(:t).with("#{group_key}.#{check_result.key}", scope: "test.scope").and_return("Translated check") + allow(I18n).to receive(:t).with("errors.#{check_result.code}", scope: "test.scope").and_return("Translated error") + + render_inline(result_component) end context "if check result is successful" do let(:check_result) { HealthReport::Result.success(:capabilities_request) } it "renders the component" do - expect(page).to have_text(I18n.t("storages.health.checks.#{group_key}.#{check_result.key}")) + expect(page).to have_text("Translated check") expect(page).to have_css(".color-fg-success", text: "Passed") expect(page).to have_no_css(".Label") expect(page).to have_no_link("More information") - expect(page).not_to have_test_selector("op-storages--health-status-check-information") + expect(page).not_to have_test_selector("op-health-report--result-status") end end @@ -55,11 +59,11 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d let(:check_result) { HealthReport::Result.skipped(:capabilities_request) } it "renders the component" do - expect(page).to have_text(I18n.t("storages.health.checks.#{group_key}.#{check_result.key}")) + expect(page).to have_text("Translated check") expect(page).to have_css(".color-fg-attention", text: "Skipped") expect(page).to have_no_css(".Label") expect(page).to have_no_link("More information") - expect(page).not_to have_test_selector("op-storages--health-status-check-information") + expect(page).not_to have_test_selector("op-health-report--result-status") end end @@ -70,11 +74,12 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d end it "renders the component" do - expect(page).to have_text(I18n.t("storages.health.checks.#{group_key}.#{check_result.key}")) + expect(page).to have_text("Translated check") expect(page).to have_css(".color-fg-attention", text: "Warning") expect(page).to have_css(".Label", text: "WRN_#{check_result.code.upcase}") + expect(page).to have_text("Translated error") expect(page).to have_link("More information") - expect(page).to have_test_selector("op-storages--health-status-check-information") + expect(page).to have_test_selector("op-health-report--result-status") end end @@ -84,11 +89,12 @@ RSpec.describe Storages::Admin::Health::CheckResultComponent, type: :component d end it "renders the component" do - expect(page).to have_text(I18n.t("storages.health.checks.#{group_key}.#{check_result.key}")) + expect(page).to have_text("Translated check") expect(page).to have_css(".color-fg-danger", text: "Failed") expect(page).to have_css(".Label", text: "ERR_#{check_result.code.upcase}") + expect(page).to have_text("Translated error") expect(page).to have_link("More information") - expect(page).to have_test_selector("op-storages--health-status-check-information") + expect(page).to have_test_selector("op-health-report--result-status") end end end