Change design of federation pages in admin UI (#17704)

* Change design of federation pages in admin UI

* Fix query performance in instance media attachments measure

* Fix reblogs being included in instance languages dimension
This commit is contained in:
Eugen Rochko 2022-03-09 08:52:32 +01:00 committed by GitHub
parent 318d34d528
commit bd53dd5210
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 712 additions and 235 deletions

View File

@ -56,10 +56,6 @@ module Admin
end end
end end
def show
authorize @domain_block, :show?
end
def destroy def destroy
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block) UnblockDomainService.new.call(@domain_block)

View File

@ -4,28 +4,26 @@ module Admin
class InstancesController < BaseController class InstancesController < BaseController
before_action :set_instances, only: :index before_action :set_instances, only: :index
before_action :set_instance, except: :index before_action :set_instance, except: :index
before_action :set_exhausted_deliveries_days, only: :show
def index def index
authorize :instance, :index? authorize :instance, :index?
preload_delivery_failures!
end end
def show def show
authorize :instance, :show? authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end end
def destroy def destroy
authorize :instance, :destroy? authorize :instance, :destroy?
Admin::DomainPurgeWorker.perform_async(@instance.domain) Admin::DomainPurgeWorker.perform_async(@instance.domain)
log_action :destroy, @instance log_action :destroy, @instance
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain) redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain)
end end
def clear_delivery_errors def clear_delivery_errors
authorize :delivery, :clear_delivery_errors? authorize :delivery, :clear_delivery_errors?
@instance.delivery_failure_tracker.clear_failures! @instance.delivery_failure_tracker.clear_failures!
redirect_to admin_instance_path(@instance.domain) redirect_to admin_instance_path(@instance.domain)
end end
@ -33,11 +31,9 @@ module Admin
def restart_delivery def restart_delivery
authorize :delivery, :restart_delivery? authorize :delivery, :restart_delivery?
last_unavailable_domain = unavailable_domain if @instance.unavailable?
if last_unavailable_domain.present?
@instance.delivery_failure_tracker.track_success! @instance.delivery_failure_tracker.track_success!
log_action :destroy, last_unavailable_domain log_action :destroy, @instance.unavailable_domain
end end
redirect_to admin_instance_path(@instance.domain) redirect_to admin_instance_path(@instance.domain)
@ -45,8 +41,7 @@ module Admin
def stop_delivery def stop_delivery
authorize :delivery, :stop_delivery? authorize :delivery, :stop_delivery?
unavailable_domain = UnavailableDomain.create!(domain: @instance.domain)
UnavailableDomain.create(domain: @instance.domain)
log_action :create, unavailable_domain log_action :create, unavailable_domain
redirect_to admin_instance_path(@instance.domain) redirect_to admin_instance_path(@instance.domain)
end end
@ -57,12 +52,11 @@ module Admin
@instance = Instance.find(params[:id]) @instance = Instance.find(params[:id])
end end
def set_exhausted_deliveries_days
@exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days
end
def set_instances def set_instances
@instances = filtered_instances.page(params[:page]) @instances = filtered_instances.page(params[:page])
end
def preload_delivery_failures!
warning_domains_map = DeliveryFailureTracker.warning_domains_map warning_domains_map = DeliveryFailureTracker.warning_domains_map
@instances.each do |instance| @instances.each do |instance|
@ -70,10 +64,6 @@ module Admin
end end
end end
def unavailable_domain
UnavailableDomain.find_by(domain: @instance.domain)
end
def filtered_instances def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end end

View File

@ -68,12 +68,12 @@ export default class Counter extends React.PureComponent {
); );
} else { } else {
const measure = data[0]; const measure = data[0];
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1); const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1);
content = ( content = (
<React.Fragment> <React.Fragment>
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span> <span className='sparkline__value__total'>{measure.human_value || <FormattedNumber value={measure.total} />}</span>
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span> {measure.previous_total && (<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>)}
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -367,6 +367,21 @@ body,
} }
} }
.positive-hint,
.negative-hint,
.neutral-hint {
a {
color: inherit;
text-decoration: underline;
&:focus,
&:hover,
&:active {
text-decoration: none;
}
}
}
.positive-hint { .positive-hint {
color: $valid-value-color; color: $valid-value-color;
font-weight: 500; font-weight: 500;
@ -1596,3 +1611,38 @@ a.sparkline {
} }
} }
} }
.availability-indicator {
display: flex;
align-items: center;
margin-bottom: 30px;
font-size: 14px;
line-height: 21px;
&__hint {
padding: 0 15px;
}
&__graphic {
display: flex;
margin: 0 -2px;
&__item {
display: block;
flex: 0 0 auto;
width: 4px;
height: 21px;
background: lighten($ui-base-color, 8%);
margin: 0 2px;
border-radius: 2px;
&.positive {
background: $valid-value-color;
}
&.negative {
background: $error-value-color;
}
}
}
}

View File

@ -65,6 +65,24 @@
} }
} }
&.horizontal-table {
border-collapse: collapse;
border-style: hidden;
& > tbody > tr > th,
& > tbody > tr > td {
padding: 11px 10px;
background: transparent;
border: 1px solid lighten($ui-base-color, 8%);
color: $secondary-text-color;
}
& > tbody > tr > th {
color: $darker-text-color;
font-weight: 600;
}
}
&.batch-table { &.batch-table {
& > thead > tr > th { & > thead > tr > th {
background: $ui-base-color; background: $ui-base-color;

View File

@ -9,6 +9,8 @@ class Admin::Metrics::Dimension
software_versions: Admin::Metrics::Dimension::SoftwareVersionsDimension, software_versions: Admin::Metrics::Dimension::SoftwareVersionsDimension,
tag_servers: Admin::Metrics::Dimension::TagServersDimension, tag_servers: Admin::Metrics::Dimension::TagServersDimension,
tag_languages: Admin::Metrics::Dimension::TagLanguagesDimension, tag_languages: Admin::Metrics::Dimension::TagLanguagesDimension,
instance_accounts: Admin::Metrics::Dimension::InstanceAccountsDimension,
instance_languages: Admin::Metrics::Dimension::InstanceLanguagesDimension,
}.freeze }.freeze
def self.retrieve(dimension_keys, start_at, end_at, limit, params) def self.retrieve(dimension_keys, start_at, end_at, limit, params)

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
class Admin::Metrics::Dimension::InstanceAccountsDimension < Admin::Metrics::Dimension::BaseDimension
include LanguagesHelper
def self.with_params?
true
end
def key
'instance_accounts'
end
protected
def perform_query
sql = <<-SQL.squish
SELECT accounts.username, count(follows.*) AS value
FROM accounts
LEFT JOIN follows ON follows.target_account_id = accounts.id
WHERE accounts.domain = $1
GROUP BY accounts.id, follows.target_account_id
ORDER BY value DESC
LIMIT $2
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, @limit]])
rows.map { |row| { key: row['username'], human_key: row['username'], value: row['value'].to_s } }
end
def params
@params.permit(:domain)
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class Admin::Metrics::Dimension::InstanceLanguagesDimension < Admin::Metrics::Dimension::BaseDimension
include LanguagesHelper
def self.with_params?
true
end
def key
'instance_languages'
end
protected
def perform_query
sql = <<-SQL.squish
SELECT COALESCE(statuses.language, 'und') AS language, count(*) AS value
FROM statuses
INNER JOIN accounts ON accounts.id = statuses.account_id
WHERE accounts.domain = $1
AND statuses.id BETWEEN $2 AND $3
AND statuses.reblog_of_id IS NULL
GROUP BY COALESCE(statuses.language, 'und')
ORDER BY count(*) DESC
LIMIT $4
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, params[:domain]], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, @limit]])
rows.map { |row| { key: row['language'], human_key: standard_locale_name(row['language']), value: row['value'].to_s } }
end
def params
@params.permit(:domain)
end
end

View File

@ -10,6 +10,12 @@ class Admin::Metrics::Measure
tag_accounts: Admin::Metrics::Measure::TagAccountsMeasure, tag_accounts: Admin::Metrics::Measure::TagAccountsMeasure,
tag_uses: Admin::Metrics::Measure::TagUsesMeasure, tag_uses: Admin::Metrics::Measure::TagUsesMeasure,
tag_servers: Admin::Metrics::Measure::TagServersMeasure, tag_servers: Admin::Metrics::Measure::TagServersMeasure,
instance_accounts: Admin::Metrics::Measure::InstanceAccountsMeasure,
instance_media_attachments: Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure,
instance_reports: Admin::Metrics::Measure::InstanceReportsMeasure,
instance_statuses: Admin::Metrics::Measure::InstanceStatusesMeasure,
instance_follows: Admin::Metrics::Measure::InstanceFollowsMeasure,
instance_followers: Admin::Metrics::Measure::InstanceFollowersMeasure,
}.freeze }.freeze
def self.retrieve(measure_keys, start_at, end_at, params) def self.retrieve(measure_keys, start_at, end_at, params)

View File

@ -26,6 +26,14 @@ class Admin::Metrics::Measure::BaseMeasure
raise NotImplementedError raise NotImplementedError
end end
def unit
nil
end
def total_in_time_range?
true
end
def total def total
load[:total] load[:total]
end end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure::BaseMeasure
def self.with_params?
true
end
def key
'instance_accounts'
end
def total_in_time_range?
false
end
protected
def perform_total_query
Account.where(domain: params[:domain]).count
end
def perform_previous_total_query
nil
end
def perform_data_query
sql = <<-SQL.squish
SELECT axis.*, (
WITH new_accounts AS (
SELECT accounts.id
FROM accounts
WHERE date_trunc('day', accounts.created_at)::date = axis.period
AND accounts.domain = $3::text
)
SELECT count(*) FROM new_accounts
) AS value
FROM (
SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
) AS axis
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]])
rows.map { |row| { date: row['period'], value: row['value'].to_s } }
end
def time_period
(@start_at.to_date..@end_at.to_date)
end
def previous_time_period
((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period))
end
def params
@params.permit(:domain)
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measure::BaseMeasure
def self.with_params?
true
end
def key
'instance_followers'
end
def total_in_time_range?
false
end
protected
def perform_total_query
Follow.joins(:account).merge(Account.where(domain: params[:domain])).count
end
def perform_previous_total_query
nil
end
def perform_data_query
sql = <<-SQL.squish
SELECT axis.*, (
WITH new_followers AS (
SELECT follows.id
FROM follows
INNER JOIN accounts ON follows.account_id = accounts.id
WHERE date_trunc('day', follows.created_at)::date = axis.period
AND accounts.domain = $3::text
)
SELECT count(*) FROM new_followers
) AS value
FROM (
SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
) AS axis
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]])
rows.map { |row| { date: row['period'], value: row['value'].to_s } }
end
def time_period
(@start_at.to_date..@end_at.to_date)
end
def previous_time_period
((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period))
end
def params
@params.permit(:domain)
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure::BaseMeasure
def self.with_params?
true
end
def key
'instance_follows'
end
def total_in_time_range?
false
end
protected
def perform_total_query
Follow.joins(:target_account).merge(Account.where(domain: params[:domain])).count
end
def perform_previous_total_query
nil
end
def perform_data_query
sql = <<-SQL.squish
SELECT axis.*, (
WITH new_follows AS (
SELECT follows.id
FROM follows
INNER JOIN accounts ON follows.target_account_id = accounts.id
WHERE date_trunc('day', follows.created_at)::date = axis.period
AND accounts.domain = $3::text
)
SELECT count(*) FROM new_follows
) AS value
FROM (
SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
) AS axis
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]])
rows.map { |row| { date: row['period'], value: row['value'].to_s } }
end
def time_period
(@start_at.to_date..@end_at.to_date)
end
def previous_time_period
((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period))
end
def params
@params.permit(:domain)
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics::Measure::BaseMeasure
include ActionView::Helpers::NumberHelper
def self.with_params?
true
end
def key
'instance_media_attachments'
end
def unit
'bytes'
end
def value_to_human_value(value)
number_to_human_size(value)
end
def total_in_time_range?
false
end
protected
def perform_total_query
MediaAttachment.joins(:account).merge(Account.where(domain: params[:domain])).sum('COALESCE(file_file_size, 0) + COALESCE(thumbnail_file_size, 0)')
end
def perform_previous_total_query
nil
end
def perform_data_query
sql = <<-SQL.squish
SELECT axis.*, (
WITH new_media_attachments AS (
SELECT COALESCE(media_attachments.file_file_size, 0) + COALESCE(media_attachments.thumbnail_file_size, 0) AS size
FROM media_attachments
INNER JOIN accounts ON accounts.id = media_attachments.account_id
WHERE date_trunc('day', media_attachments.created_at)::date = axis.period
AND accounts.domain = $3::text
)
SELECT SUM(size) FROM new_media_attachments
) AS value
FROM (
SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
) AS axis
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]])
rows.map { |row| { date: row['period'], value: row['value'].to_s } }
end
def time_period
(@start_at.to_date..@end_at.to_date)
end
def previous_time_period
((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period))
end
def params
@params.permit(:domain)
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure::BaseMeasure
def self.with_params?
true
end
def key
'instance_reports'
end
def total_in_time_range?
false
end
protected
def perform_total_query
Report.where(target_account: Account.where(domain: params[:domain])).count
end
def perform_previous_total_query
nil
end
def perform_data_query
sql = <<-SQL.squish
SELECT axis.*, (
WITH new_reports AS (
SELECT reports.id
FROM reports
INNER JOIN accounts ON accounts.id = reports.target_account_id
WHERE date_trunc('day', reports.created_at)::date = axis.period
AND accounts.domain = $3::text
)
SELECT count(*) FROM new_reports
) AS value
FROM (
SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
) AS axis
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, params[:domain]]])
rows.map { |row| { date: row['period'], value: row['value'].to_s } }
end
def time_period
(@start_at.to_date..@end_at.to_date)
end
def previous_time_period
((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period))
end
def params
@params.permit(:domain)
end
end

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure::BaseMeasure
def self.with_params?
true
end
def key
'instance_statuses'
end
def total_in_time_range?
false
end
protected
def perform_total_query
Status.joins(:account).merge(Account.where(domain: params[:domain])).count
end
def perform_previous_total_query
nil
end
def perform_data_query
sql = <<-SQL.squish
SELECT axis.*, (
WITH new_statuses AS (
SELECT statuses.id
FROM statuses
INNER JOIN accounts ON accounts.id = statuses.account_id
WHERE statuses.id BETWEEN $3 AND $4
AND accounts.domain = $5::text
AND date_trunc('day', statuses.created_at)::date = axis.period
)
SELECT count(*) FROM new_statuses
) AS value
FROM (
SELECT generate_series(date_trunc('day', $1::timestamp)::date, date_trunc('day', $2::timestamp)::date, interval '1 day') AS period
) AS axis
SQL
rows = ActiveRecord::Base.connection.select_all(sql, nil, [[nil, @start_at], [nil, @end_at], [nil, Mastodon::Snowflake.id_at(@start_at, with_random: false)], [nil, Mastodon::Snowflake.id_at(@end_at, with_random: false)], [nil, params[:domain]]])
rows.map { |row| { date: row['period'], value: row['value'].to_s } }
end
def time_period
(@start_at.to_date..@end_at.to_date)
end
def previous_time_period
((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period))
end
def params
@params.permit(:domain)
end
end

View File

@ -30,7 +30,7 @@ class DeliveryFailureTracker
end end
def exhausted_deliveries_days def exhausted_deliveries_days
Redis.current.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) } @exhausted_deliveries_days ||= Redis.current.smembers(exhausted_deliveries_key).sort.map { |date| Date.new(date.slice(0, 4).to_i, date.slice(4, 2).to_i, date.slice(6, 2).to_i) }
end end
alias reset! track_success! alias reset! track_success!

View File

@ -30,6 +30,14 @@ class DomainBlock < ApplicationRecord
scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) }
scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) } scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain')) }
def policies
if suspend?
:suspend
else
[severity.to_sym, reject_media? ? :reject_media : nil, reject_reports? ? :reject_reports : nil].reject { |policy| policy == :noop || policy.nil? }
end
end
class << self class << self
def suspend?(domain) def suspend?(domain)
!!rule_for(domain)&.suspend? !!rule_for(domain)&.suspend?

View File

@ -32,35 +32,27 @@ class Instance < ApplicationRecord
@delivery_failure_tracker ||= DeliveryFailureTracker.new(domain) @delivery_failure_tracker ||= DeliveryFailureTracker.new(domain)
end end
def following_count def unavailable?
@following_count ||= Follow.where(account: accounts).count unavailable_domain.present?
end end
def followers_count def failing?
@followers_count ||= Follow.where(target_account: accounts).count failure_days.present? || unavailable?
end
def reports_count
@reports_count ||= Report.where(target_account: accounts).count
end
def blocks_count
@blocks_count ||= Block.where(target_account: accounts).count
end
def public_comment
domain_block&.public_comment
end
def private_comment
domain_block&.private_comment
end
def media_storage
@media_storage ||= MediaAttachment.where(account: accounts).sum(:file_file_size)
end end
def to_param def to_param
domain domain
end end
delegate :exhausted_deliveries_days, to: :delivery_failure_tracker
def availability_over_days(num_days, end_date = Time.now.utc.to_date)
failures_map = exhausted_deliveries_days.index_with { true }
period_end_at = exhausted_deliveries_days.last || end_date
period_start_at = period_end_at - num_days.days
(period_start_at..period_end_at).map do |date|
[date, failures_map[date]]
end
end
end end

View File

@ -4,8 +4,7 @@ class InstanceFilter
KEYS = %i( KEYS = %i(
limited limited
by_domain by_domain
warning availability
unavailable
).freeze ).freeze
attr_reader :params attr_reader :params
@ -34,12 +33,21 @@ class InstanceFilter
Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc')) Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc'))
when 'by_domain' when 'by_domain'
Instance.matches_domain(value) Instance.matches_domain(value)
when 'warning' when 'availability'
Instance.where(domain: DeliveryFailureTracker.warning_domains) availability_scope(value)
when 'unavailable'
Instance.joins(:unavailable_domain)
else else
raise "Unknown filter: #{key}" raise "Unknown filter: #{key}"
end end
end end
def availability_scope(value)
case value
when 'failing'
Instance.where(domain: DeliveryFailureTracker.warning_domains)
when 'unavailable'
Instance.joins(:unavailable_domain)
else
raise "Unknown availability: #{value}"
end
end
end end

View File

@ -1,12 +1,20 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::Admin::MeasureSerializer < ActiveModel::Serializer class REST::Admin::MeasureSerializer < ActiveModel::Serializer
attributes :key, :total, :previous_total, :data attributes :key, :unit, :total
attribute :human_value, if: -> { object.respond_to?(:value_to_human_value) }
attribute :previous_total, if: -> { object.total_in_time_range? }
attribute :data
def total def total
object.total.to_s object.total.to_s
end end
def human_value
object.value_to_human_value(object.total)
end
def previous_total def previous_total
object.previous_total.to_s object.previous_total.to_s
end end

View File

@ -24,10 +24,10 @@
= f.input :obfuscate, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.obfuscate'), hint: I18n.t('admin.domain_blocks.obfuscate_hint') = f.input :obfuscate, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.obfuscate'), hint: I18n.t('admin.domain_blocks.obfuscate_hint')
.field-group .field-group
= f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6 = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), as: :string
.field-group .field-group
= f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6 = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), as: :string
.actions .actions
= f.button :button, t('generic.save_changes'), type: :submit = f.button :button, t('generic.save_changes'), type: :submit

View File

@ -24,10 +24,10 @@
= f.input :obfuscate, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.obfuscate'), hint: I18n.t('admin.domain_blocks.obfuscate_hint') = f.input :obfuscate, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.obfuscate'), hint: I18n.t('admin.domain_blocks.obfuscate_hint')
.field-group .field-group
= f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), rows: 6 = f.input :private_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.private_comment'), hint: t('admin.domain_blocks.private_comment_hint'), as: :string
.field-group .field-group
= f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), rows: 6 = f.input :public_comment, wrapper: :with_label, label: I18n.t('admin.domain_blocks.public_comment'), hint: t('admin.domain_blocks.public_comment_hint'), as: :string
.actions .actions
= f.button :button, t('.create'), type: :submit = f.button :button, t('.create'), type: :submit

View File

@ -1,25 +0,0 @@
- content_for :page_title do
= t('admin.domain_blocks.show.title', domain: @domain_block.domain)
- if @domain_block.private_comment.present?
.speech-bubble
.speech-bubble__bubble
= simple_format(h(@domain_block.private_comment))
.speech-bubble__owner= t 'admin.instances.private_comment'
- if @domain_block.public_comment.present?
.speech-bubble
.speech-bubble__bubble
= simple_format(h(@domain_block.public_comment))
.speech-bubble__owner= t 'admin.instances.public_comment'
= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
- unless (@domain_block.noop?)
%p= t(".retroactive.#{@domain_block.severity}")
%p.hint= t(:affected_accounts,
scope: [:admin, :domain_blocks, :show],
count: @domain_block.affected_accounts_count)
.actions
= f.button :button, t('.undo'), type: :submit

View File

@ -1,2 +0,0 @@
%li.negative-hint
= l(exhausted_deliveries_days)

View File

@ -1,33 +1,15 @@
.directory__tag .directory__tag
= link_to admin_instance_path(instance) do = link_to admin_instance_path(instance) do
%h4 %h4
= fa_icon 'warning fw' if instance.failing?
= instance.domain = instance.domain
%small %small
- if instance.domain_block - if instance.domain_block
- first_item = true = instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ')
- if !instance.domain_block.noop? - elsif instance.domain_allow
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
- first_item = false
- unless instance.domain_block.suspend?
- if instance.domain_block.reject_media?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_media')
- first_item = false
- if instance.domain_block.reject_reports?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode?
= t('admin.accounts.whitelisted') = t('admin.accounts.whitelisted')
- else - else
= t('admin.accounts.no_limits_imposed') = t('admin.accounts.no_limits_imposed')
- if instance.failure_days
= ' / '
%span.negative-hint
= t('admin.instances.delivery.warning_message', count: instance.failure_days)
- if instance.unavailable_domain
= ' / '
%span.negative-hint
= t('admin.instances.delivery.unavailable_message')
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= friendly_number_to_human instance.accounts_count .trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= friendly_number_to_human instance.accounts_count

View File

@ -17,22 +17,11 @@
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
.filter-subset .filter-subset
%strong= t('admin.instances.delivery.title') %strong= t('admin.instances.availability.title')
%ul %ul
%li= filter_link_to t('admin.instances.delivery.all'), warning: nil, unavailable: nil %li= filter_link_to t('admin.instances.delivery.all'), availability: nil
%li= filter_link_to t('admin.instances.delivery.warning'), warning: '1', unavailable: nil %li= filter_link_to t('admin.instances.delivery.failing'), availability: 'failing'
%li= filter_link_to t('admin.instances.delivery.unavailable'), warning: nil, unavailable: '1' %li= filter_link_to t('admin.instances.delivery.unavailable'), availability: 'unavailable'
.back-link
= link_to admin_instances_path() do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_all')
= link_to admin_instances_path(limited: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_limited')
= link_to admin_instances_path(warning: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_warning')
- unless whitelist_mode? - unless whitelist_mode?
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do

View File

@ -1,88 +1,95 @@
- content_for :page_title do - content_for :page_title do
= @instance.domain = @instance.domain
.filters - content_for :header_tags do
.back-link = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
= link_to admin_instances_path() do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_all')
= link_to admin_instances_path(limited: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_limited')
= link_to admin_instances_path(warning: 1) do
%i.fa.fa-chevron-left.fa-fw
= t('admin.instances.back_to_warning')
.dashboard__counters - content_for :heading_actions do
%div = l(@time_period.first)
= link_to admin_accounts_path(origin: 'remote', by_domain: @instance.domain) do = ' - '
.dashboard__counters__num= number_with_delimiter @instance.accounts_count = l(@time_period.last)
.dashboard__counters__label= t 'admin.accounts.title'
%div
= link_to admin_reports_path(by_target_domain: @instance.domain) do
.dashboard__counters__num= number_with_delimiter @instance.reports_count
.dashboard__counters__label= t 'admin.instances.total_reported'
%div
%div
.dashboard__counters__num= number_to_human_size @instance.media_storage
.dashboard__counters__label= t 'admin.instances.total_storage'
%div
%div
.dashboard__counters__num= number_with_delimiter @instance.following_count
.dashboard__counters__label= t 'admin.instances.total_followed_by_them'
%div
%div
.dashboard__counters__num= number_with_delimiter @instance.followers_count
.dashboard__counters__label= t 'admin.instances.total_followed_by_us'
%div
%div
.dashboard__counters__num= number_with_delimiter @instance.blocks_count
.dashboard__counters__label= t 'admin.instances.total_blocked_by_us'
%div %p
%div = fa_icon 'info fw'
.dashboard__counters__num = t('admin.instances.totals_time_period_hint_html')
- if @instance.delivery_failure_tracker.available?
= fa_icon 'check'
- else
= fa_icon 'times'
.dashboard__counters__label= t 'admin.instances.delivery_available'
- if @instance.private_comment.present? .dashboard
.speech-bubble .dashboard__item
.speech-bubble__bubble = react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
= simple_format(h(@instance.private_comment)) .dashboard__item
.speech-bubble__owner= t 'admin.instances.private_comment' = react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
.dashboard__item
- if @instance.public_comment.present? = react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
.speech-bubble .dashboard__item
.speech-bubble__bubble = react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
= simple_format(h(@instance.public_comment)) .dashboard__item
.speech-bubble__owner= t 'admin.instances.public_comment' = react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
.dashboard__item
- unless @exhausted_deliveries_days.empty? = react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
%h4= t 'admin.instances.delivery_error_days' .dashboard__item
%ul = react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
= render partial: 'exhausted_deliveries_days', collection: @exhausted_deliveries_days .dashboard__item
%p.hint = react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
= t 'admin.instances.delivery_error_hint', count: DeliveryFailureTracker::FAILURE_DAYS_THRESHOLD
%hr.spacer/ %hr.spacer/
%div.action-buttons %h3= t('admin.instances.content_policies.title')
%div
- if @instance.domain_allow - if whitelist_mode?
= link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@instance.domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } %p= t('admin.instances.content_policies.limited_federation_mode_description_html')
- elsif @instance.domain_block
= link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@instance.domain_block), class: 'button' - if @instance.domain_allow
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button' = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@instance.domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
- else
= link_to t('admin.domain_allows.add_new'), admin_domain_allows_path(domain_allow: { domain: @instance.domain }), class: 'button', method: :post
- else
%p= t('admin.instances.content_policies.description_html')
- if @instance.domain_block
.table-wrapper
%table.table.horizontal-table
%tbody
%tr
%th= t('admin.instances.content_policies.comment')
%td= @instance.domain_block.private_comment
%tr
%th= t('admin.instances.content_policies.reason')
%td= @instance.domain_block.public_comment
%tr
%th= t('admin.instances.content_policies.policy')
%td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' • ')
= link_to t('admin.domain_blocks.edit'), edit_admin_domain_block_path(@instance.domain_block), class: 'button'
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
- else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
%hr.spacer/
%h3= t('admin.instances.availability.title')
%p
= t('admin.instances.availability.description_html', count: DeliveryFailureTracker::FAILURE_DAYS_THRESHOLD)
.availability-indicator
%ul.availability-indicator__graphic
- @instance.availability_over_days(14).each do |(date, failing)|
%li.availability-indicator__graphic__item{ class: failing ? 'negative' : 'neutral', title: l(date) }
.availability-indicator__hint
- if @instance.unavailable?
%span.negative-hint
= t('admin.instances.availability.failure_threshold_reached', date: l(@instance.unavailable_domain.created_at.to_date))
= link_to t('admin.instances.delivery.restart'), restart_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }
- elsif @instance.exhausted_deliveries_days.empty?
%span.positive-hint
= t('admin.instances.availability.no_failures_recorded')
= link_to t('admin.instances.delivery.stop'), stop_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }
- else - else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' %span.negative-hint
- if @instance.delivery_failure_tracker.available? = t('admin.instances.availability.failures_recorded', count: @instance.delivery_failure_tracker.days)
- unless @exhausted_deliveries_days.empty? = link_to t('admin.instances.delivery.clear'), clear_delivery_errors_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post } unless @instance.exhausted_deliveries_days.empty?
= link_to t('admin.instances.delivery.clear'), clear_delivery_errors_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'
= link_to t('admin.instances.delivery.stop'), stop_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button' - if @instance.unavailable?
- else %p= t('admin.instances.purge_description_html')
= link_to t('admin.instances.delivery.restart'), restart_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }, class: 'button'
- if !@instance.delivery_failure_tracker.available? || @instance.accounts_count.zero? || @instance.domain_block&.suspend? = link_to t('admin.instances.purge'), admin_instance_path(@instance), data: { confirm: t('admin.instances.confirm_purge'), method: :delete }, class: 'button button--destructive'
= link_to t('admin.instances.purge'), admin_instance_path(@instance), data: { confirm: t('admin.instances.confirm_purge'), method: :delete }, class: 'button'

View File

@ -3,6 +3,8 @@
class Admin::DomainPurgeWorker class Admin::DomainPurgeWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: 'pull', lock: :until_executed
def perform(domain) def perform(domain)
PurgeDomainService.new.call(domain) PurgeDomainService.new.call(domain)
end end

View File

@ -450,21 +450,6 @@ en:
reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
reject_reports: Reject reports reject_reports: Reject reports
reject_reports_hint: Ignore all reports coming from this domain. Irrelevant for suspensions reject_reports_hint: Ignore all reports coming from this domain. Irrelevant for suspensions
rejecting_media: rejecting media files
rejecting_reports: rejecting reports
severity:
silence: limited
suspend: suspended
show:
affected_accounts:
one: One account in the database affected
other: "%{count} accounts in the database affected"
zero: No account in the database is affected
retroactive:
silence: Undo limit of existing affected accounts from this domain
suspend: Unsuspend existing affected accounts from this domain
title: Undo domain block for %{domain}
undo: Undo
undo: Undo domain block undo: Undo domain block
view: View domain block view: View domain block
email_domain_blocks: email_domain_blocks:
@ -495,23 +480,47 @@ en:
title: Follow recommendations title: Follow recommendations
unsuppress: Restore follow recommendation unsuppress: Restore follow recommendation
instances: instances:
availability:
description_html:
one: If delivering to the domain fails <strong>%{count} day</strong> without succeeding, no further delivery attempts will be made unless a delivery <em>from</em> the domain is received.
other: If delivering to the domain fails on <strong>%{count} different days</strong> without succeeding, no further delivery attempts will be made unless a delivery <em>from</em> the domain is received.
failure_threshold_reached: Failure threshold reached on %{date}.
failures_recorded:
one: Failed attempt on %{count} day.
other: Failed attempts on %{count} different days.
no_failures_recorded: No failures on record.
title: Availability
back_to_all: All back_to_all: All
back_to_limited: Limited back_to_limited: Limited
back_to_warning: Warning back_to_warning: Warning
by_domain: Domain by_domain: Domain
confirm_purge: Are you sure you want to permanently delete data from this domain? confirm_purge: Are you sure you want to permanently delete data from this domain?
content_policies:
comment: Internal note
description_html: You can define content policies that will be applied to all accounts from this domain and any of its subdomains.
policies:
reject_media: Reject media
reject_reports: Reject reports
silence: Limit
suspend: Suspend
policy: Policy
reason: Public reason
title: Content policies
dashboard:
instance_accounts_dimension: Most followed accounts
instance_accounts_measure: stored accounts
instance_followers_measure: our followers there
instance_follows_measure: their followers here
instance_languages_dimension: Top languages
instance_media_attachments_measure: stored media attachments
instance_reports_measure: reports about them
instance_statuses_measure: stored posts
delivery: delivery:
all: All all: All
clear: Clear delivery errors clear: Clear delivery errors
restart: Restart delivery restart: Restart delivery
stop: Stop delivery stop: Stop delivery
title: Delivery
unavailable: Unavailable unavailable: Unavailable
unavailable_message: Delivery unavailable
warning: Warning
warning_message:
one: Delivery failure %{count} day
other: Delivery failure %{count} days
delivery_available: Delivery is available delivery_available: Delivery is available
delivery_error_days: Delivery error days delivery_error_days: Delivery error days
delivery_error_hint: If delivery is not possible for %{count} days, it will be automatically marked as undeliverable. delivery_error_hint: If delivery is not possible for %{count} days, it will be automatically marked as undeliverable.
@ -528,12 +537,14 @@ en:
private_comment: Private comment private_comment: Private comment
public_comment: Public comment public_comment: Public comment
purge: Purge purge: Purge
purge_description_html: If you believe this domain is offline for good, you can delete all account records and associated data from this domain from your storage. This may take a while.
title: Federation title: Federation
total_blocked_by_us: Blocked by us total_blocked_by_us: Blocked by us
total_followed_by_them: Followed by them total_followed_by_them: Followed by them
total_followed_by_us: Followed by us total_followed_by_us: Followed by us
total_reported: Reports about them total_reported: Reports about them
total_storage: Media attachments total_storage: Media attachments
totals_time_period_hint_html: The totals displayed below include data for all time.
invites: invites:
deactivate_all: Deactivate all deactivate_all: Deactivate all
filter: filter:

View File

@ -191,7 +191,7 @@ Rails.application.routes.draw do
get '/dashboard', to: 'dashboard#index' get '/dashboard', to: 'dashboard#index'
resources :domain_allows, only: [:new, :create, :show, :destroy] resources :domain_allows, only: [:new, :create, :show, :destroy]
resources :domain_blocks, only: [:new, :create, :show, :destroy, :update, :edit] resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit]
resources :email_domain_blocks, only: [:index, :new, :create] do resources :email_domain_blocks, only: [:index, :new, :create] do
collection do collection do

View File

@ -16,15 +16,6 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
end end
end end
describe 'GET #show' do
it 'returns http success' do
domain_block = Fabricate(:domain_block)
get :show, params: { id: domain_block.id }
expect(response).to have_http_status(200)
end
end
describe 'POST #create' do describe 'POST #create' do
it 'blocks the domain when succeeded to save' do it 'blocks the domain when succeeded to save' do
allow(DomainBlockWorker).to receive(:perform_async).and_return(true) allow(DomainBlockWorker).to receive(:perform_async).and_return(true)