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.
This commit is contained in:
Jan Sandbrink
2026-05-05 16:28:30 +02:00
parent 048b03e28b
commit 2d4a559cf9
13 changed files with 362 additions and 351 deletions
@@ -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
%>
@@ -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
@@ -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
@@ -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
+3 -3
View File
@@ -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:
+20 -9
View File
@@ -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:
@@ -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
@@ -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
%>
@@ -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))}"
@@ -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 %>
+57 -52
View File
@@ -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:
@@ -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
@@ -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