Compare commits

...

7 Commits

Author SHA1 Message Date
Emelia Smith
d747651b99
Merge 5910d5096d into 94bceb8683 2025-07-11 14:05:42 +00:00
Emelia Smith
5910d5096d
Fix i18n errors 2025-06-25 19:58:53 +02:00
Emelia Smith
010688a67a
Fix haml lint error 2025-06-25 19:54:29 +02:00
Emelia Smith
c98dfb827b
Alignment after rebase 2025-06-25 19:46:20 +02:00
Emelia Smith
7535ad01d6
Continue improving design based on feedback 2025-06-25 19:42:28 +02:00
Emelia Smith
53a4a3c8cf
WIP 2025-06-25 19:41:48 +02:00
Emelia Smith
39d2f132e8
Add fallback content to Admin Dimension Dashboard component 2025-06-25 19:41:47 +02:00
14 changed files with 227 additions and 87 deletions

View File

@ -21,6 +21,15 @@ module Admin
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
end
def availability
authorize :instance, :show?
end
def statistics
authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end
def destroy
authorize :instance, :destroy?
Admin::DomainPurgeWorker.perform_async(@instance.domain)
@ -31,7 +40,7 @@ module Admin
def clear_delivery_errors
authorize :delivery, :clear_delivery_errors?
@instance.delivery_failure_tracker.clear_failures!
redirect_to admin_instance_path(@instance.domain)
redirect_to availability_admin_instance_path(@instance.domain)
end
def restart_delivery
@ -42,14 +51,14 @@ module Admin
log_action :destroy, @instance.unavailable_domain
end
redirect_to admin_instance_path(@instance.domain)
redirect_to availability_admin_instance_path(@instance.domain)
end
def stop_delivery
authorize :delivery, :stop_delivery?
unavailable_domain = UnavailableDomain.create!(domain: @instance.domain)
log_action :create, unavailable_domain
redirect_to admin_instance_path(@instance.domain)
redirect_to availability_admin_instance_path(@instance.domain)
end
private

View File

@ -60,7 +60,7 @@ export default class Dimension extends PureComponent {
</tbody>
</table>
);
} else {
} else if (data[0].data.length > 0) {
const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
content = (
@ -81,6 +81,16 @@ export default class Dimension extends PureComponent {
</tbody>
</table>
);
} else {
content = (
<table>
<tbody>
<tr className='dimension__item'>
<td>No data available</td>
</tr>
</tbody>
</table>
);
}
return (

View File

@ -309,6 +309,27 @@ $content-width: 840px;
font-weight: 500;
}
em {
font-style: italic;
}
.lead {
font-size: 17px;
line-height: 22px;
color: $secondary-text-color;
margin-bottom: 30px;
a {
color: $highlight-text-color;
}
}
p.with-icon {
display: flex;
align-items: center;
gap: 5px;
}
.fields-group h6 {
color: $primary-text-color;
font-weight: 500;
@ -609,6 +630,25 @@ body,
max-width: 100%;
}
.simple_form {
.actions {
margin-top: 15px;
}
.button {
font-size: 15px;
}
}
.button-group {
display: flex;
gap: 10px;
&__button--end {
margin-left: auto;
}
}
.batch-form-box {
display: flex;
flex-wrap: wrap;
@ -1835,7 +1875,7 @@ a.sparkline {
.availability-indicator {
display: flex;
align-items: center;
margin-bottom: 30px;
margin-bottom: 20px;
font-size: 14px;
line-height: 21px;

View File

@ -62,11 +62,20 @@
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
grid-gap: 10px;
align-content: start;
@media screen and (width <= 1350px) {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
&__hint {
display: flex;
gap: 4px;
grid-column: span 3;
align-items: center;
color: $darker-text-color;
}
&__item {
&--span-double-column {
grid-column: span 2;
@ -118,4 +127,8 @@
font-weight: 700;
}
}
&--instance-overview {
min-height: 400px;
}
}

View File

@ -4,6 +4,7 @@ class DeliveryFailureTracker
include Redisable
FAILURE_DAYS_THRESHOLD = 7
MEASURED_DAYS = 14
def initialize(url_or_host)
@host = url_or_host.start_with?('https://', 'http://') ? Addressable::URI.parse(url_or_host).normalized_host : url_or_host

View File

@ -17,7 +17,7 @@
%br/
= f.object.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ')
= f.object.policies.map { |policy| t(policy, scope: 'admin.instances.federation_policies.policies') }.join(' · ')
- if f.object.public_comment.present?
·
= f.object.public_comment

View File

@ -1,9 +1,6 @@
-# locals: (instance_domain:, period_end_at:, period_start_at:)
%p
= material_symbol 'info'
= t('admin.instances.totals_time_period_hint_html')
.dashboard
.dashboard.dashboard--instance-overview
.dashboard__item
= react_admin_component :counter,
end_at: period_end_at,
@ -48,19 +45,6 @@
measure: 'instance_reports',
params: { domain: instance_domain },
start_at: period_start_at
.dashboard__item
= react_admin_component :dimension,
dimension: 'instance_accounts',
end_at: period_end_at,
label: t('admin.instances.dashboard.instance_accounts_dimension'),
limit: 8,
params: { domain: instance_domain },
start_at: period_start_at
.dashboard__item
= react_admin_component :dimension,
dimension: 'instance_languages',
end_at: period_end_at,
label: t('admin.instances.dashboard.instance_languages_dimension'),
limit: 8,
params: { domain: instance_domain },
start_at: period_start_at
%p.dashboard__hint
= material_symbol 'info'
= t('admin.instances.totals_time_period_hint_html')

View File

@ -6,7 +6,7 @@
%small
- if instance.domain_block
= instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ')
= instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.federation_policies.policies') }.join(' · ')
- if instance.domain_block.public_comment.present?
%span.comment.public-comment #{t('admin.domain_blocks.public_comment')}: #{instance.domain_block.public_comment}
- if instance.domain_block.private_comment.present?

View File

@ -0,0 +1,47 @@
- content_for :page_title do
= @instance.domain
- content_for :heading do
%h2= t('admin.instances.instance.title', domain: @instance.domain)
= render partial: 'admin/instances/shared/links', locals: { instance: @instance }
%p.lead= t('admin.instances.availability.subtitle')
- if @instance.persisted?
%p
= t('admin.instances.availability.description_html', count: DeliveryFailureTracker::FAILURE_DAYS_THRESHOLD)
= t('admin.instances.availability.period_description', count: DeliveryFailureTracker::MEASURED_DAYS)
.availability-indicator
%ul.availability-indicator__graphic
- @instance.availability_over_days(DeliveryFailureTracker::MEASURED_DAYS).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))
- elsif @instance.exhausted_deliveries_days.empty?
%span.positive-hint
= t('admin.instances.availability.no_failures_recorded')
- else
%span.negative-hint
= t('admin.instances.availability.failures_recorded', count: @instance.delivery_failure_tracker.days)
.button-group
- if @instance.unavailable?
= 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'
- elsif @instance.exhausted_deliveries_days.empty?
= 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 button--destructive'
- else
= 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 button-secondary' unless @instance.exhausted_deliveries_days.empty?
= 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 button--destructive'
%hr.spacer
- if @instance.purgeable?
%h3= t('admin.instances.purge_title')
%p= t('admin.instances.purge_description_html')
= link_to t('admin.instances.purge'), admin_instance_path(@instance), data: { confirm: t('admin.instances.confirm_purge'), method: :delete }, class: 'button button--destructive'
- else
%p= t('admin.instances.availability.unknown_instance')

View File

@ -0,0 +1,6 @@
.content__heading__tabs
= render_navigation renderer: :links do |primary|
:ruby
primary.item :overview, safe_join([material_symbol('cloud'), t('admin.instances.title')]), admin_instance_path(instance)
primary.item :statistics, safe_join([material_symbol('trending_up'), t('admin.instances.statistics.title')]), statistics_admin_instance_path(instance)
primary.item :availability, safe_join([material_symbol('warning'), t('admin.instances.availability.title')]), availability_admin_instance_path(instance)

View File

@ -1,6 +1,10 @@
- content_for :page_title do
= @instance.domain
- content_for :heading do
%h2= t('admin.instances.instance.title', domain: @instance.domain)
= render partial: 'admin/instances/shared/links', locals: { instance: @instance }
- if current_user.can?(:view_dashboard)
- content_for :heading_actions do
= date_range(@time_period)
@ -13,34 +17,49 @@
%hr.spacer/
%h3= t('admin.instances.content_policies.title')
%h3= t('admin.instances.federation_policies.title')
- if limited_federation_mode?
%p= t('admin.instances.content_policies.limited_federation_mode_description_html')
%p= t('admin.instances.federation_policies.limited_federation_mode_description_html')
- if @instance.domain_allow
= 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')
%p= t('admin.instances.federation_policies.description_html')
- if @instance.domain_block
.table-wrapper
%table.table.horizontal-table
%thead
%tr
%th{ width: '28%' }
%th
%tbody
%tr
%th= t('admin.instances.content_policies.comment')
%td= @instance.domain_block.private_comment
%th= t('admin.instances.federation_policies.comment')
%td
- if @instance.domain_block.private_comment.present?
= @instance.domain_block.private_comment
- else
%em= t('admin.instances.federation_policies.no_comment')
%tr
%th= t('admin.instances.content_policies.reason')
%td= @instance.domain_block.public_comment
%th= t('admin.instances.federation_policies.reason')
%td
- if @instance.domain_block.public_comment.present?
= @instance.domain_block.public_comment
- else
%em= t('admin.instances.federation_policies.no_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(' · ')
%th= t('admin.instances.federation_policies.policy')
%td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.federation_policies.policies') }.join(' · ')
.button-group
= 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 }
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@instance.domain_block), class: 'button button-secondary', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
- if @instance.purgeable?
= link_to t('admin.instances.federation_policies.purge_data'), admin_instance_path(@instance), data: { confirm: t('admin.instances.confirm_purge'), method: :delete }, class: 'button button-tertiary button--destructive button-group__button--end'
- else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
@ -48,12 +67,13 @@
%hr.spacer/
%h3= t('admin.instances.audit_log.title')
%p= t('admin.instances.audit_log.description')
- if @action_logs.empty?
%p= t('accounts.nothing_here')
- else
.report-notes
= render partial: 'admin/action_logs/action_log', collection: @action_logs
= link_to t('admin.instances.audit_log.view_all'), admin_action_logs_path(target_domain: @instance.domain), class: 'button'
= link_to t('admin.instances.audit_log.view_all'), admin_action_logs_path(target_domain: @instance.domain), class: 'button button-secondary'
%hr.spacer/
@ -71,35 +91,3 @@
.actions
= form.button :button, t('admin.instances.moderation_notes.create'), type: :submit
- if @instance.persisted?
%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
%span.negative-hint
= t('admin.instances.availability.failures_recorded', count: @instance.delivery_failure_tracker.days)
%span= 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?
%span= link_to t('admin.instances.delivery.stop'), stop_delivery_admin_instance_path(@instance), data: { confirm: t('admin.accounts.are_you_sure'), method: :post }
- if @instance.purgeable?
%p= t('admin.instances.purge_description_html')
= link_to t('admin.instances.purge'), admin_instance_path(@instance), data: { confirm: t('admin.instances.confirm_purge'), method: :delete }, class: 'button button--destructive'

View File

@ -0,0 +1,28 @@
- content_for :page_title do
= @instance.domain
- content_for :heading do
%h2= t('admin.instances.instance.title', domain: @instance.domain)
= render partial: 'admin/instances/shared/links', locals: { instance: @instance }
- if @instance.persisted?
%p.lead= t('admin.instances.statistics.subtitle', domain: @instance.domain)
.dashboard
.dashboard__item
= react_admin_component :dimension,
dimension: 'instance_accounts',
label: t('admin.instances.dashboard.instance_accounts_dimension'),
params: { domain: @instance.domain },
start_at: @time_period.first,
end_at: @time_period.last,
limit: 8
.dashboard__item
= react_admin_component :dimension,
dimension: 'instance_languages',
label: t('admin.instances.dashboard.instance_languages_dimension'),
params: { domain: @instance.domain },
start_at: @time_period.first,
end_at: @time_period.last,
limit: 8
- else
%p.lead= t('admin.instances.availability.unknown_instance')

View File

@ -520,8 +520,9 @@ en:
unsuppress: Restore follow recommendation
instances:
audit_log:
title: Recent Audit Logs
view_all: View full audit logs
description: These are the 5 most recent audit log entries relating to this instance.
title: Audit Logs
view_all: View all audit logs
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.
@ -531,25 +532,16 @@ en:
one: Failed attempt on %{count} day.
other: Failed attempts on %{count} different days.
no_failures_recorded: No failures on record.
period_description: The bars shown are for delivery status over the last %{count} days.
subtitle: Instance availability measures how successfully we've been able to federate
title: Availability
unknown_instance: There is no data available for this instance as this server doesn't currently federate with it.
warning: The last attempt to connect to this server has been unsuccessful
back_to_all: All
back_to_limited: Limited
back_to_warning: Warning
by_domain: 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.
limited_federation_mode_description_html: You can chose whether to allow federation with this domain.
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
@ -571,6 +563,22 @@ en:
delivery_error_hint: If delivery is not possible for %{count} days, it will be automatically marked as undeliverable.
destroyed_msg: Data from %{domain} is now queued for imminent deletion.
empty: No domains found.
federation_policies:
comment: Private note
description_html: You can set a federation policy that will be applied to all accounts from this domain and any of its subdomains.
limited_federation_mode_description_html: You can chose whether to allow federation with this domain.
no_comment: None
policies:
reject_media: Reject media
reject_reports: Reject reports
silence: Limit
suspend: Suspend
policy: Policy
purge_data: Purge Data
reason: Public reason
title: Federation policy
instance:
title: Federation with %{domain}
known_accounts:
one: "%{count} known account"
other: "%{count} known accounts"
@ -587,15 +595,19 @@ en:
title: Moderation Notes
private_comment: Private comment
public_comment: Public comment
purge: Purge
purge: Purge Data
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.
purge_title: Purge Data
statistics:
subtitle: Below are a few statistics about %{domain}, that you may find interesting.
title: Statistics
title: Federation
total_blocked_by_us: Blocked by us
total_followed_by_them: Followed by them
total_followed_by_us: Followed by us
total_reported: Reports about them
total_storage: Media attachments
totals_time_period_hint_html: The totals displayed below include data for all time.
totals_time_period_hint_html: The totals displayed above include data for all time.
unknown_instance: There is currently no record of this domain on this server.
invites:
deactivate_all: Deactivate all

View File

@ -87,6 +87,8 @@ namespace :admin do
resources :instances, only: [:index, :show, :destroy], constraints: { id: %r{[^/]+} }, format: 'html' do
member do
get :availability
get :statistics
post :clear_delivery_errors
post :restart_delivery
post :stop_delivery