Compare commits

...

9 Commits

Author SHA1 Message Date
Emelia Smith
56f8b67c19
Merge 5910d5096d into 3b52dca405 2025-07-11 17:06:09 +00:00
Claire
3b52dca405
Fix quote attributes missing from Mastodon's context (#35354)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
Chromatic / Run Chromatic (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.2) (push) Blocked by required conditions
Ruby Testing / ImageMagick tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-07-11 16:35:06 +00:00
Echo
853a0c466e
Make bio hashtags open the local page instead of the remote instance (#35349) 2025-07-11 15:18:34 +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
19 changed files with 293 additions and 104 deletions

View File

@ -21,6 +21,15 @@ module Admin
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT) @action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
end 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 def destroy
authorize :instance, :destroy? authorize :instance, :destroy?
Admin::DomainPurgeWorker.perform_async(@instance.domain) Admin::DomainPurgeWorker.perform_async(@instance.domain)
@ -31,7 +40,7 @@ module Admin
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 availability_admin_instance_path(@instance.domain)
end end
def restart_delivery def restart_delivery
@ -42,14 +51,14 @@ module Admin
log_action :destroy, @instance.unavailable_domain log_action :destroy, @instance.unavailable_domain
end end
redirect_to admin_instance_path(@instance.domain) redirect_to availability_admin_instance_path(@instance.domain)
end end
def stop_delivery def stop_delivery
authorize :delivery, :stop_delivery? authorize :delivery, :stop_delivery?
unavailable_domain = UnavailableDomain.create!(domain: @instance.domain) unavailable_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 availability_admin_instance_path(@instance.domain)
end end
private private

View File

@ -26,6 +26,12 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' }, quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
quotes: {
'quote' => 'https://w3id.org/fep/044f#quote',
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
'quoteAuthorization' => 'https://w3id.org/fep/044f#quoteAuthorization',
},
interaction_policies: { interaction_policies: {
'gts' => 'https://gotosocial.org/ns#', 'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' }, 'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },

View File

@ -1,12 +1,30 @@
import { useCallback } from 'react';
import { useLinks } from 'mastodon/hooks/useLinks'; import { useLinks } from 'mastodon/hooks/useLinks';
export const AccountBio: React.FC<{ interface AccountBioProps {
note: string; note: string;
className: string; className: string;
}> = ({ note, className }) => { dropdownAccountId?: string;
const handleClick = useLinks(); }
if (note.length === 0 || note === '<p></p>') { export const AccountBio: React.FC<AccountBioProps> = ({
note,
className,
dropdownAccountId,
}) => {
const handleClick = useLinks(!!dropdownAccountId);
const handleNodeChange = useCallback(
(node: HTMLDivElement | null) => {
if (!dropdownAccountId || !node || node.childNodes.length === 0) {
return;
}
addDropdownToHashtags(node, dropdownAccountId);
},
[dropdownAccountId],
);
if (note.length === 0) {
return null; return null;
} }
@ -15,6 +33,28 @@ export const AccountBio: React.FC<{
className={`${className} translate`} className={`${className} translate`}
dangerouslySetInnerHTML={{ __html: note }} dangerouslySetInnerHTML={{ __html: note }}
onClickCapture={handleClick} onClickCapture={handleClick}
ref={handleNodeChange}
/> />
); );
}; };
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
if (!node) {
return;
}
for (const childNode of node.childNodes) {
if (!(childNode instanceof HTMLElement)) {
continue;
}
if (
childNode instanceof HTMLAnchorElement &&
(childNode.classList.contains('hashtag') ||
childNode.innerText.startsWith('#')) &&
!childNode.dataset.menuHashtag
) {
childNode.dataset.menuHashtag = accountId;
} else if (childNode.childNodes.length > 0) {
addDropdownToHashtags(childNode, accountId);
}
}
}

View File

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

View File

@ -6,6 +6,7 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/mastodon/components/account_bio';
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -773,7 +774,6 @@ export const AccountHeader: React.FC<{
); );
} }
const content = { __html: account.note_emojified };
const displayNameHtml = { __html: account.display_name_html }; const displayNameHtml = { __html: account.display_name_html };
const fields = account.fields; const fields = account.fields;
const isLocal = !account.acct.includes('@'); const isLocal = !account.acct.includes('@');
@ -897,12 +897,11 @@ export const AccountHeader: React.FC<{
<AccountNote accountId={accountId} /> <AccountNote accountId={accountId} />
)} )}
{account.note.length > 0 && account.note !== '<p></p>' && ( <AccountBio
<div note={account.note_emojified}
className='account__header__content translate' dropdownAccountId={accountId}
dangerouslySetInnerHTML={content} className='account__header__content'
/> />
)}
<div className='account__header__fields'> <div className='account__header__fields'>
<dl> <dl>

View File

@ -8,13 +8,14 @@ import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store'; import { useAppDispatch } from 'mastodon/store';
const isMentionClick = (element: HTMLAnchorElement) => const isMentionClick = (element: HTMLAnchorElement) =>
element.classList.contains('mention'); element.classList.contains('mention') &&
!element.classList.contains('hashtag');
const isHashtagClick = (element: HTMLAnchorElement) => const isHashtagClick = (element: HTMLAnchorElement) =>
element.textContent?.[0] === '#' || element.textContent?.[0] === '#' ||
element.previousSibling?.textContent?.endsWith('#'); element.previousSibling?.textContent?.endsWith('#');
export const useLinks = () => { export const useLinks = (skipHashtags?: boolean) => {
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -61,12 +62,12 @@ export const useLinks = () => {
if (isMentionClick(target)) { if (isMentionClick(target)) {
e.preventDefault(); e.preventDefault();
void handleMentionClick(target); void handleMentionClick(target);
} else if (isHashtagClick(target)) { } else if (isHashtagClick(target) && !skipHashtags) {
e.preventDefault(); e.preventDefault();
handleHashtagClick(target); handleHashtagClick(target);
} }
}, },
[handleMentionClick, handleHashtagClick], [skipHashtags, handleMentionClick, handleHashtagClick],
); );
return handleClick; return handleClick;

View File

@ -126,6 +126,9 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
? accountJSON.username ? accountJSON.username
: accountJSON.display_name; : accountJSON.display_name;
const accountNote =
accountJSON.note && accountJSON.note !== '<p></p>' ? accountJSON.note : '';
return AccountFactory({ return AccountFactory({
...accountJSON, ...accountJSON,
moved: moved?.id, moved: moved?.id,
@ -142,8 +145,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
escapeTextContentForBrowser(displayName), escapeTextContentForBrowser(displayName),
emojiMap, emojiMap,
), ),
note_emojified: emojify(accountJSON.note, emojiMap), note_emojified: emojify(accountNote, emojiMap),
note_plain: unescapeHTML(accountJSON.note), note_plain: unescapeHTML(accountNote),
url: url:
accountJSON.url.startsWith('http://') || accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://') accountJSON.url.startsWith('https://')

View File

@ -309,6 +309,27 @@ $content-width: 840px;
font-weight: 500; 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 { .fields-group h6 {
color: $primary-text-color; color: $primary-text-color;
font-weight: 500; font-weight: 500;
@ -609,6 +630,25 @@ body,
max-width: 100%; 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 { .batch-form-box {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -1835,7 +1875,7 @@ a.sparkline {
.availability-indicator { .availability-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 30px; margin-bottom: 20px;
font-size: 14px; font-size: 14px;
line-height: 21px; line-height: 21px;

View File

@ -62,11 +62,20 @@
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
grid-gap: 10px; grid-gap: 10px;
align-content: start;
@media screen and (width <= 1350px) { @media screen and (width <= 1350px) {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); 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 { &__item {
&--span-double-column { &--span-double-column {
grid-column: span 2; grid-column: span 2;
@ -118,4 +127,8 @@
font-weight: 700; font-weight: 700;
} }
} }
&--instance-overview {
min-height: 400px;
}
} }

View File

@ -4,6 +4,7 @@ class DeliveryFailureTracker
include Redisable include Redisable
FAILURE_DAYS_THRESHOLD = 7 FAILURE_DAYS_THRESHOLD = 7
MEASURED_DAYS = 14
def initialize(url_or_host) def initialize(url_or_host)
@host = url_or_host.start_with?('https://', 'http://') ? Addressable::URI.parse(url_or_host).normalized_host : 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/ %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? - if f.object.public_comment.present?
· ·
= f.object.public_comment = f.object.public_comment

View File

@ -1,9 +1,6 @@
-# locals: (instance_domain:, period_end_at:, period_start_at:) -# 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 .dashboard__item
= react_admin_component :counter, = react_admin_component :counter,
end_at: period_end_at, end_at: period_end_at,
@ -48,19 +45,6 @@
measure: 'instance_reports', measure: 'instance_reports',
params: { domain: instance_domain }, params: { domain: instance_domain },
start_at: period_start_at start_at: period_start_at
.dashboard__item %p.dashboard__hint
= react_admin_component :dimension, = material_symbol 'info'
dimension: 'instance_accounts', = t('admin.instances.totals_time_period_hint_html')
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

View File

@ -6,7 +6,7 @@
%small %small
- if instance.domain_block - 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? - if instance.domain_block.public_comment.present?
%span.comment.public-comment #{t('admin.domain_blocks.public_comment')}: #{instance.domain_block.public_comment} %span.comment.public-comment #{t('admin.domain_blocks.public_comment')}: #{instance.domain_block.public_comment}
- if instance.domain_block.private_comment.present? - 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 - content_for :page_title do
= @instance.domain = @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) - if current_user.can?(:view_dashboard)
- content_for :heading_actions do - content_for :heading_actions do
= date_range(@time_period) = date_range(@time_period)
@ -13,34 +17,49 @@
%hr.spacer/ %hr.spacer/
%h3= t('admin.instances.content_policies.title') %h3= t('admin.instances.federation_policies.title')
- if limited_federation_mode? - 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 - 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 } = 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 - else
= link_to t('admin.domain_allows.add_new'), admin_domain_allows_path(domain_allow: { domain: @instance.domain }), class: 'button', method: :post = link_to t('admin.domain_allows.add_new'), admin_domain_allows_path(domain_allow: { domain: @instance.domain }), class: 'button', method: :post
- else - else
%p= t('admin.instances.content_policies.description_html') %p= t('admin.instances.federation_policies.description_html')
- if @instance.domain_block - if @instance.domain_block
.table-wrapper .table-wrapper
%table.table.horizontal-table %table.table.horizontal-table
%thead
%tr
%th{ width: '28%' }
%th
%tbody %tbody
%tr %tr
%th= t('admin.instances.content_policies.comment') %th= t('admin.instances.federation_policies.comment')
%td= @instance.domain_block.private_comment %td
- if @instance.domain_block.private_comment.present?
= @instance.domain_block.private_comment
- else
%em= t('admin.instances.federation_policies.no_comment')
%tr %tr
%th= t('admin.instances.content_policies.reason') %th= t('admin.instances.federation_policies.reason')
%td= @instance.domain_block.public_comment %td
- if @instance.domain_block.public_comment.present?
= @instance.domain_block.public_comment
- else
%em= t('admin.instances.federation_policies.no_comment')
%tr %tr
%th= t('admin.instances.content_policies.policy') %th= t('admin.instances.federation_policies.policy')
%td= @instance.domain_block.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' · ') %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.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 - else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button' = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'
@ -48,12 +67,13 @@
%hr.spacer/ %hr.spacer/
%h3= t('admin.instances.audit_log.title') %h3= t('admin.instances.audit_log.title')
%p= t('admin.instances.audit_log.description')
- if @action_logs.empty? - if @action_logs.empty?
%p= t('accounts.nothing_here') %p= t('accounts.nothing_here')
- else - else
.report-notes .report-notes
= render partial: 'admin/action_logs/action_log', collection: @action_logs = 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/ %hr.spacer/
@ -71,35 +91,3 @@
.actions .actions
= form.button :button, t('admin.instances.moderation_notes.create'), type: :submit = 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 unsuppress: Restore follow recommendation
instances: instances:
audit_log: audit_log:
title: Recent Audit Logs description: These are the 5 most recent audit log entries relating to this instance.
view_all: View full audit logs title: Audit Logs
view_all: View all audit logs
availability: availability:
description_html: 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. 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. one: Failed attempt on %{count} day.
other: Failed attempts on %{count} different days. other: Failed attempts on %{count} different days.
no_failures_recorded: No failures on record. 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 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 warning: The last attempt to connect to this server has been unsuccessful
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.
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: dashboard:
instance_accounts_dimension: Most followed accounts instance_accounts_dimension: Most followed accounts
instance_accounts_measure: stored 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. 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. destroyed_msg: Data from %{domain} is now queued for imminent deletion.
empty: No domains found. 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: known_accounts:
one: "%{count} known account" one: "%{count} known account"
other: "%{count} known accounts" other: "%{count} known accounts"
@ -587,15 +595,19 @@ en:
title: Moderation Notes title: Moderation Notes
private_comment: Private comment private_comment: Private comment
public_comment: Public 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_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 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. 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. unknown_instance: There is currently no record of this domain on this server.
invites: invites:
deactivate_all: Deactivate all 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 resources :instances, only: [:index, :show, :destroy], constraints: { id: %r{[^/]+} }, format: 'html' do
member do member do
get :availability
get :statistics
post :clear_delivery_errors post :clear_delivery_errors
post :restart_delivery post :restart_delivery
post :stop_delivery post :stop_delivery