Commit 727bf2b8 authored by Gabriel Mazetto's avatar Gabriel Mazetto

Move code to new controller, improve UI and finder and add specs

parent b4f94d87
......@@ -170,7 +170,7 @@
%strong.fly-out-top-item-name
#{ _('Push Rules') }
= nav_link(controller: %w(geo_nodes)) do
= nav_link(controller: [:geo_nodes, :geo_projects]) do
= link_to admin_geo_nodes_path do
.nav-icon-container
= sprite_icon('location-dot')
......@@ -178,7 +178,7 @@
#{ _('Geo Nodes') }
- if Gitlab::Geo.secondary?
%ul.sidebar-sub-level-items
= nav_link(controller: %w(geo_nodes), html_options: { class: "fly-out-top-item" } ) do
= nav_link(controller: :geo_nodes, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_geo_nodes_path do
%strong.fly-out-top-item-name
#{ _('Geo Nodes') }
......@@ -187,8 +187,8 @@
= link_to admin_geo_nodes_path, title: 'Nodes' do
%span
#{ _('Nodes') }
= nav_link(path: 'geo_nodes#projects') do
= link_to projects_admin_geo_nodes_path, title: 'Projects' do
= nav_link(path: 'geo_projects#index') do
= link_to admin_geo_projects_path, title: 'Projects' do
%span
#{ _('Projects') }
......
......@@ -136,13 +136,13 @@ namespace :admin do
post :toggle
get :status
end
collection do
get :projects
scope(path: 'projects/:id') do
post :recheck_project, action: :recheck
post :resync_project, action: :resync
post :force_redownload_project, action: :force_redownload
end
resources :geo_projects, only: [:index] do
member do
post :recheck
post :resync
post :force_redownload
end
end
......
......@@ -13,45 +13,6 @@ class Admin::GeoNodesController < Admin::ApplicationController
end
end
def projects
finder = Geo::ProjectRegistryStatusFinder.new
case params[:sync_status]
when 'never'
@projects = finder.never_synced_projects.page(params[:page])
when 'failed'
@registries = finder.failed_projects.page(params[:page])
when 'pending'
@registries = finder.pending_projects.page(params[:page])
else
@registries = finder.synced_projects.page(params[:page])
end
end
def recheck
@registry = Geo::ProjectRegistry.find_by_id(params[:id])
@registry.flag_repository_for_recheck
flash[:notice] = "#{@registry.project.full_name} is scheduled for recheck"
redirect_to projects_admin_geo_nodes_path
end
def resync
@registry = Geo::ProjectRegistry.find_by_id(params[:id])
@registry.flag_repository_for_resync
flash[:notice] = "#{@registry.project.full_name} is scheduled for resync"
redirect_to projects_admin_geo_nodes_path
end
def force_redownload
@registry = Geo::ProjectRegistry.find_by_id(params[:id])
@registry.flag_repository_for_redownload
flash[:notice] = "#{@registry.project.full_name} is scheduled for forced redownload"
redirect_to projects_admin_geo_nodes_path
end
def create
@node = Geo::NodeCreateService.new(geo_node_params).execute
......
# frozen_string_literal: true
class Admin::GeoProjectsController < Admin::ApplicationController
before_action :check_license
before_action :load_registry, except: [:index]
helper ::EE::GeoHelper
def index
finder = ::Geo::ProjectRegistryStatusFinder.new
case params[:sync_status]
when 'never'
@projects = finder.never_synced_projects.page(params[:page])
when 'failed'
@registries = finder.failed_projects.page(params[:page])
when 'pending'
@registries = finder.pending_projects.page(params[:page])
else
@registries = finder.synced_projects.page(params[:page])
end
end
def recheck
@registry.flag_repository_for_recheck!
redirect_back_or_admin_geo_projects(notice: "#{@registry.project.full_name} is scheduled for recheck")
end
def resync
@registry.flag_repository_for_resync!
redirect_back_or_admin_geo_projects(notice: "#{@registry.project.full_name} is scheduled for resync")
end
def force_redownload
@registry.flag_repository_for_redownload!
redirect_back_or_admin_geo_projects(notice: "#{@registry.project.full_name} is scheduled for forced re-download")
end
private
def check_license
unless Gitlab::Geo.license_allows?
redirect_to admin_license_path, alert: 'You need a different license to use Geo replication'
end
end
def load_registry
@registry = ::Geo::ProjectRegistry.find_by_id(params[:id])
end
def redirect_back_or_admin_geo_projects(params)
redirect_back_or_default(default: admin_geo_projects_path, options: params)
end
end
# frozen_string_literal: true
module Geo
# Finders specific for Project status listing and inspecting
#
......@@ -10,21 +12,34 @@ module Geo
# We consider fully synced any project without pending actions
# or failures
def synced_projects
Geo::ProjectRegistry.all.includes(:project)
no_repository_resync = project_registry[:resync_repository].eq(false)
no_repository_sync_failure = project_registry[:repository_retry_count].eq(nil)
repository_verified = project_registry[:repository_verification_checksum_sha].not_eq(nil)
Geo::ProjectRegistry.where(
no_repository_resync
.and(no_repository_sync_failure)
.and(repository_verified)
).includes(:project)
end
# Return any project registry which project is pending to update
#
# We include here only projects that have successfully synced before.
# We exclude projects that have tried to resync already and had failures
# We exclude projects that have tried to re-sync or re-check already and had failures
def pending_projects
no_repository_sync_failure = project_registry[:repository_retry_count].eq(nil)
no_wiki_sync_failure = project_registry[:wiki_retry_count].eq(nil)
repository_successfully_synced_before = project_registry[:last_repository_successful_sync_at].not_eq(nil)
repository_pending_verification = project_registry[:repository_verification_checksum_sha].eq(nil)
repository_without_verification_failure_before = project_registry[:last_repository_verification_failure].eq(nil)
flagged_for_resync = project_registry[:resync_repository].eq(true)
Geo::ProjectRegistry.where(
project_registry[:resync_repository].eq(true)
.or(project_registry[:resync_wiki].eq(true))
.and(no_repository_sync_failure.and(no_wiki_sync_failure))
no_repository_sync_failure
.and(repository_successfully_synced_before)
.and(flagged_for_resync
.or(repository_pending_verification
.and(repository_without_verification_failure_before)))
).includes(:project)
end
......@@ -32,7 +47,15 @@ module Geo
#
# Both types of failures are included: Synchronization and Verification
def failed_projects
Geo::ProjectRegistry.failed.includes(:project)
repository_sync_failed = project_registry[:repository_retry_count].gt(0)
repository_verification_failed = project_registry[:last_repository_verification_failure].not_eq(nil)
repository_checksum_mismatch = project_registry[:repository_checksum_mismatch].eq(true)
Geo::ProjectRegistry.where(
repository_sync_failed
.or(repository_verification_failed)
.or(repository_checksum_mismatch)
).includes(:project)
end
# Return projects that has never been fully synced
......@@ -45,7 +68,11 @@ module Geo
no_project_registry = project_registry[:project_id].eq(nil)
no_repository_synced = project_registry[:last_repository_successful_sync_at].eq(nil)
Geo::Fdw::Project.joins("LEFT OUTER JOIN project_registry ON (project_registry.project_id = #{Geo::Fdw::Project.table_name}.id)").where(no_project_registry.or(no_repository_synced)).includes(:project_registry)
Geo::Fdw::Project.joins("LEFT OUTER JOIN project_registry ON (project_registry.project_id = #{Geo::Fdw::Project.table_name}.id)")
.where(
no_project_registry
.or(no_repository_synced)
).includes(:project_registry)
end
private
......
......@@ -201,21 +201,31 @@ class Geo::ProjectRegistry < Geo::BaseRegistry
public_send("#{type}_verification_retry_count").to_i # rubocop:disable GitlabSecurity/PublicSend
end
# Flag the repository to be rechecked
def flag_repository_for_recheck
self.update(repository_verification_checksum_sha: nil)
# Flag the repository to be re-checked
def flag_repository_for_recheck!
self.update(repository_verification_checksum_sha: nil, last_repository_verification_failure: nil, repository_checksum_mismatch: nil)
end
# Flag the repository to be resynced
def flag_repository_for_resync
self.update(resync_repository: true)
# Flag the repository to be re-synced
def flag_repository_for_resync!
repository_updated!(:repository, Time.now)
end
# Flag the repository to perform a full redownload
def flag_repository_for_redownload
# Flag the repository to perform a full re-download
def flag_repository_for_redownload!
self.update(resync_repository: true, force_to_redownload_repository: true)
end
# A registry becomes candidate for re-download after first failed retries
#
# This is used by the Admin > Geo Nodes > Projects UI interface to choose
# when to display the re-download button
#
# @return [Boolean] whether the registry is candidate for a re-download
def candidate_for_redownload?
self.repository_retry_count && self.repository_retry_count >= 1
end
private
def fetches_since_gc_redis_key
......
- @registries.each_with_index do |project_registry, index|
.card.project-card.prepend-top-15
.card-header{id: "project-#{project_registry.project.id}-header"}
.d-flex
%strong.header-text-primary.flex-fill
= project_registry.project.name
= link_to(resync_project_admin_geo_nodes_path(project_registry), method: :post, class: 'btn btn-outline-primary btn-sm') do
Resync
.card-body
.container
.row
.col-sm.project-status-container
.project-status-title.text-muted
Status
.project-status-content.status-type-failure
Failed
.col-sm.project-status-container
.project-status-title.text-muted
Next sync scheduled at
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.repository_retry_at) if project_registry.repository_retry_at
.col-sm.project-status-container
.project-status-title.text-muted
Last sync attempt
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.last_repository_synced_at)
.col-sm.project-status-container
.project-status-title.text-muted
Retry counts
.project-status-content
= project_registry.repository_retry_count
.col-sm.project-status-container
.project-status-title.text-muted
Error message
.project-status-content.font-weight-bold
= project_registry.last_repository_sync_failure
- if index == 0 # change it to actual condition check so that this section shows up only when there are errors to show
.project-card-errors
.card-header.bg-transparent.border-bottom-0.border-top
%button.btn.btn-link.btn-card-header.collapsed.d-flex{ type: 'button',
data: { toggle: 'collapse', target: "#project-errors-#{project_registry.project.id}" },
'aria-expanded' => 'false',
'aria-controls' => "project-errors-#{project_registry.project.id}" }
= sprite_icon('chevron-down', size: 18, css_class: 'append-right-5 card-expand-icon')
= sprite_icon('chevron-up', size: 18, css_class: 'append-right-5 card-collapse-icon')
.header-text-secondary
More
.collapse{ id: "project-errors-#{project_registry.project.id}",
'aria-labelledby' => "project-#{project_registry.project.id}-header" }
.card-body
.container
%ul.unstyled-list.errors-list
%li.p-0.d-flex
= sprite_icon('warning', size: 18, css_class: 'error-icon')
%span.error-text.prepend-left-5
Synchronization failed - showing error message here
%li.p-0.d-flex
= sprite_icon('warning', size: 18, css_class: 'error-icon')
%span.error-text.prepend-left-5
Verification failed - showing error message here
- @registries.each do |project_registry|
.card.project-card.prepend-top-15
.card-header{ id: "project-#{project_registry.project.id}-header" }
.d-flex
%strong.header-text-primary.flex-fill
= project_registry.project.name
= link_to(recheck_admin_geo_project_path(project_registry), method: :post, class: 'btn btn-outline btn-sm mr-2') do
= s_('Geo|Recheck')
= link_to(resync_admin_geo_project_path(project_registry), method: :post, class: 'btn btn-outline-primary btn-sm') do
Resync
.card-body
.container
.row
.col-sm.project-status-container
.project-status-title.text-muted
Status
.project-status-content.status-type-failure
In sync
.col-sm.project-status-container
.project-status-title.text-muted
Last successful sync
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.last_repository_successful_sync_at) if project_registry.last_repository_successful_sync_at
.col-sm.project-status-container
.project-status-title.text-muted
Last time verified
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.last_repository_check_at) if project_registry.last_repository_check_at
= paginate @registries, theme: 'gitlab'
- @registries.each do |project_registry|
.card.project-card.prepend-top-15
.card-header{ id: "project-#{project_registry.project.id}-header" }
%h5.mb-0.mt-0.p-2.d-flex
%button.btn.btn-link.btn-card-header.d-flex.flex-fill{ type: 'button', data: { toggle: 'collapse', target: "#project-#{project_registry.project.id}" }, 'aria-expanded' => 'true', 'aria-controls' => "project-#{project_registry.project.id}" }
= sprite_icon('chevron-down', size: 18, css_class: 'card-expand-icon')
= sprite_icon('chevron-up', size: 18, css_class: 'card-collapse-icon')
%strong.header-text
= project_registry.project.name
- if project_registry.candidate_for_redownload?
= link_to(force_redownload_admin_geo_project_path(project_registry), method: :post, class: 'btn btn-outline btn-sm mr-2') do
Redownload
= link_to(resync_admin_geo_project_path(project_registry), method: :post, class: 'btn btn-outline-primary btn-sm') do
Resync
.collapse.show{ id: "project-#{project_registry.project.id}", 'aria-labelledby' => "project-#{project_registry.project.id}-header" }
.card-body
.container
.row
.col-sm.project-status-container
.project-status-title.text-muted
Status
.project-status-content.status-type-failure
Failed
.col-sm.project-status-container
.project-status-title.text-muted
Next sync scheduled at
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.repository_retry_at) if project_registry.repository_retry_at
.col-sm.project-status-container
.project-status-title.text-muted
Last sync attempt
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.last_repository_synced_at) if project_registry.last_repository_synced_at
.col-sm.project-status-container
.project-status-title.text-muted
Retry counts
.project-status-content
= project_registry.repository_retry_count
.project-card-errors
.card-header.bg-transparent.border-bottom-0.border-top
%button.btn.btn-link.btn-card-header.collapsed.d-flex{ type: 'button',
data: { toggle: 'collapse', target: "#project-errors-#{project_registry.project.id}" },
'aria-expanded' => 'false',
'aria-controls' => "project-errors-#{project_registry.project.id}" }
= sprite_icon('chevron-down', size: 18, css_class: 'append-right-5 card-expand-icon')
= sprite_icon('chevron-up', size: 18, css_class: 'append-right-5 card-collapse-icon')
.header-text-secondary
More
.collapse{ id: "project-errors-#{project_registry.project.id}",
'aria-labelledby' => "project-#{project_registry.project.id}-header" }
.card-body
.container
%ul.unstyled-list.errors-list
- if project_registry.last_repository_sync_failure
%li.p-0.d-flex
= sprite_icon('warning', size: 18, css_class: 'error-icon')
%span.error-text.prepend-left-5
Synchronization failed -
= project_registry.last_repository_sync_failure
- if project_registry.last_repository_verification_failure
%li.p-0.d-flex
= sprite_icon('warning', size: 18, css_class: 'error-icon')
%span.error-text.prepend-left-5
Verification failed -
= project_registry.last_repository_verification_failure
= paginate @registries, theme: 'gitlab'
- @projects.each do |project|
.card.project-card.prepend-top-15
.card-header{id: "project-#{project.id}-header"}
.card-header{ id: "project-#{project.id}-header" }
%h5.mb-0.mt-0.p-2.d-flex
%button.btn.btn-link.btn-card-header.d-flex.flex-fill{ type: 'button', data: { toggle: 'collapse', target: "#project-#{project.id}" }, 'aria-expanded' => 'true', 'aria-controls' => "project-#{project.id}" }
= sprite_icon('chevron-down', size: 18, css_class: 'card-expand-icon')
= sprite_icon('chevron-up', size: 18, css_class: 'card-collapse-icon')
%strong.header-text
= project.name
%button.btn.btn-outline-primary.btn-sm{ type: 'button' }
Resync
.collapse.show{id: "project-#{project.id}", 'aria-labelledby' => "project-#{project.id}-header" }
.collapse.show{ id: "project-#{project.id}", 'aria-labelledby' => "project-#{project.id}-header" }
.card-body
.container
.row
......@@ -34,3 +32,5 @@
Error message
.project-status-content.font-weight-bold
= project.project_registry.last_repository_sync_failure if project.project_registry
= paginate @projects, theme: 'gitlab'
- @registries.each do |project_registry|
.card.project-card.prepend-top-15
.card-header{ id: "project-#{project_registry.project.id}-header" }
%h5.mb-0.mt-0.p-2.d-flex
%button.btn.btn-link.btn-card-header.d-flex.flex-fill{ type: 'button', data: { toggle: 'collapse', target: "#project-#{project_registry.project.id}" }, 'aria-expanded' => 'true', 'aria-controls' => "project-#{project_registry.project.id}" }
= sprite_icon('chevron-down', size: 18, css_class: 'card-expand-icon')
= sprite_icon('chevron-up', size: 18, css_class: 'card-collapse-icon')
%strong.header-text
= project_registry.project.name
= link_to(recheck_admin_geo_project_path(project_registry), method: :post, class: 'btn btn-outline btn-sm mr-2') do
= s_('Geo|Recheck')
= link_to(resync_admin_geo_project_path(project_registry), method: :post, class: 'btn btn-outline-primary btn-sm') do
Resync
.collapse.show{ id: "project-#{project_registry.project.id}", 'aria-labelledby' => "project-#{project_registry.project.id}-header" }
.card-body
.container
.row
.col-sm.project-status-container
.project-status-title.text-muted
Status
.project-status-content.status-type-failure
Pending
.col-sm.project-status-container
.project-status-title.text-muted
Next sync scheduled at
.project-status-content
= distance_of_time_in_words(Time.now, project_registry.repository_retry_at) if project_registry.repository_retry_at
.col-sm.project-status-container
.project-status-title.text-muted
Last sync attempt
.project-status-content
- if project_registry.last_repository_synced_at
= distance_of_time_in_words(Time.now, project_registry.last_repository_synced_at)
- else
Never
= paginate @registries, theme: 'gitlab'
......@@ -6,24 +6,26 @@
%div{ class: container_class }
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
%ul.nav-links.nav.nav-tabs
- opts = params[:sync_status].present? ? {} : { page: projects_admin_geo_nodes_path }
- opts = params[:sync_status].present? ? {} : { page: admin_geo_projects_path }
= nav_link(opts) do
= link_to projects_admin_geo_nodes_path do
= _("All")
= link_to admin_geo_projects_path do
= _("Synced")
= nav_link(html_options: { class: active_when(params[:sync_status] == 'pending') }) do
= link_to projects_admin_geo_nodes_path(sync_status: 'pending') do
= link_to admin_geo_projects_path(sync_status: 'pending') do
= _("Pending")
= nav_link(html_options: { class: active_when(params[:sync_status] == 'failed') }) do
= link_to projects_admin_geo_nodes_path(sync_status: 'failed') do
= link_to admin_geo_projects_path(sync_status: 'failed') do
= _("Failed")
= nav_link(html_options: { class: active_when(params[:sync_status] == 'never') }) do
= link_to projects_admin_geo_nodes_path(sync_status: 'never') do
= link_to admin_geo_projects_path(sync_status: 'never') do
= _("Never")
- case params[:sync_status]
- when 'never'
= render(partial: 'never')
- when 'failed'
= render(partial: 'failed')
- when 'pending'
= render(partial: 'pending')
- else
= render(partial: 'all')
......@@ -10,7 +10,7 @@ module EE
}.freeze
WHITELISTED_GEO_ROUTES_TRACKING_DB = {
'admin/geo_nodes' => %w{resync recheck force_redownload}
'admin/geo_projects' => %w{resync recheck force_redownload}
}.freeze
private
......@@ -22,19 +22,19 @@ module EE
def geo_node_update_route
# Calling route_hash may be expensive. Only do it if we think there's a possible match
return false unless request.path =~ %r{/admin/geo_nodes}
return false unless request.path =~ %r{/admin/geo_}
controller = route_hash[:controller]
action = route_hash[:action]
if WHITELISTED_GEO_ROUTES[controller]&.include?(action)
return ::Gitlab::Database.db_read_write?
end
::Gitlab::Database.db_read_write?
else
WHITELISTED_GEO_ROUTES_TRACKING_DB[controller]&.include?(action)
end
end
end
end
end
end
end
......@@ -44,13 +44,17 @@ FactoryBot.define do
end
trait :repository_sync_failed do
last_repository_synced_at { 5.days.ago }
last_repository_successful_sync_at nil
last_wiki_synced_at { 5.days.ago }
sync_failed
last_wiki_successful_sync_at { 5.days.ago }
resync_repository true
resync_wiki false
repository_retry_count 1
wiki_retry_count nil
end
trait :existing_repository_sync_failed do
repository_sync_failed
last_repository_successful_sync_at { 5.days.ago }
end
trait :repository_syncing do
......@@ -59,13 +63,11 @@ FactoryBot.define do
end
trait :wiki_sync_failed do
last_repository_synced_at { 5.days.ago }
sync_failed
last_repository_successful_sync_at { 5.days.ago }
last_wiki_synced_at { 5.days.ago }
last_wiki_successful_sync_at nil
resync_repository false
resync_wiki true
wiki_retry_count 2
repository_retry_count nil
end
trait :wiki_syncing do
......
# frozen_string_literal: true
require 'spec_helper'
describe Geo::ProjectRegistryStatusFinder, :geo do
include ::EE::GeoHelpers
set(:secondary) { create(:geo_node) }
set(:synced_registry) { create(:geo_project_registry, :synced) }
set(:synced_and_verified_registry) { create(:geo_project_registry, :synced, :repository_verified) }
set(:sync_pending_registry) { create(:geo_project_registry, :synced, :repository_dirty) }
set(:sync_failed_registry) { create(:geo_project_registry, :existing_repository_sync_failed) }
set(:verify_outdated_registry) { create(:geo_project_registry, :synced, :repository_verification_outdated) }
set(:verify_failed_registry) { create(:geo_project_registry, :synced, :repository_verification_failed) }
set(:verify_checksum_mismatch_registry) { create(:geo_project_registry, :synced, :repository_checksum_mismatch) }
set(:never_synced_registry) { create(:geo_project_registry) }
set(:never_synced_registry_with_failure) { create(:geo_project_registry, :repository_sync_failed) }
set(:project_without_registry) { create(:project, name: 'project without registry') }
let(:project_with_never_synced_registry) { never_synced_registry.project }
subject { described_class.new(current_node: secondary) }
before do
skip('FDW is not configured') if Gitlab::Database.postgresql? && !Gitlab::Geo::Fdw.enabled?
stub_current_geo_node(secondary)
end
describe '#synced_projects' do
it 'returns only synced registry' do
result = subject.synced_projects
expect(result).to contain_exactly(synced_and_verified_registry)
end
end
describe '#pending_projects' do
it 'returns only pending registry' do
result = subject.pending_projects
expect(result).to contain_exactly(
synced_registry,
sync_pending_registry,
verify_outdated_registry
)
end
end
describe '#failed_projects' do
it 'returns only failed registry' do
result = subject.failed_projects
expect(result).to contain_exactly(
sync_failed_registry,
never_synced_registry_with_failure,
verify_failed_registry,
verify_checksum_mismatch_registry
)
end
end
describe '#never_synced_projects' do
it 'returns only FDW projects without registry or with never synced registries' do
fdw_project_with_never_synced_registry_with_failure = Geo::Fdw::Project.find(never_synced_registry_with_failure.project.id)
fdw_project_with_never_synced_registry = Geo::Fdw::Project.find(project_with_never_synced_registry.id)
fdw_project_without_registry = Geo::Fdw::Project.find(project_without_registry.id)
result = subject.never_synced_projects
expect(result).to contain_exactly(
fdw_project_without_registry,
fdw_project_with_never_synced_registry,
fdw_project_with_never_synced_registry_with_failure
)
end
end
end
......@@ -2915,9 +2915,6 @@ msgstr ""
msgid "Failure"
msgstr ""
msgid "Failures"
msgstr ""
msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
msgstr ""
......@@ -4342,9 +4339,6 @@ msgstr ""
msgid "Nav|Sign out and sign in with a different account"
msgstr ""
msgid "Never"
msgstr ""
msgid "Network"
msgstr ""
......@@ -5732,9 +5726,6 @@ msgstr ""
msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered"
msgstr ""
msgid "See all of the projects and registries files of Geo Secondaries"
msgstr ""
msgid "Select"
msgstr ""
......@@ -6190,6 +6181,9 @@ msgstr ""
msgid "Sync information"
msgstr ""
msgid "Synced"
msgstr ""
msgid "System Hooks"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment