diff --git a/locales/en-US.yml b/locales/en-US.yml index 69a4e1755..084008868 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -845,6 +845,8 @@ failedToFetchAccountInformation: "Could not fetch account information" rateLimitExceeded: "Rate limit exceeded" cropImage: "Crop image" cropImageAsk: "Do you want to crop this image?" +recentNHours: "Last {n} hours" +recentNDays: "Last {n} days" _emailUnavailable: used: "This email address is already being used" format: "The format of this email address is invalid" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6369e4e4d..594727540 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -852,6 +852,8 @@ noEmailServerWarning: "メールサーバーの設定がされていません。 thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" recommended: "推奨" check: "チェック" +recentNHours: "直近{n}時間" +recentNDays: "直近{n}日" _emailUnavailable: used: "既に使用されています" diff --git a/packages/backend/src/models/repositories/instance.ts b/packages/backend/src/models/repositories/instance.ts index 4594d8634..e9ee18ea3 100644 --- a/packages/backend/src/models/repositories/instance.ts +++ b/packages/backend/src/models/repositories/instance.ts @@ -1,11 +1,13 @@ import { db } from '@/db/postgre.js'; import { Instance } from '@/models/entities/instance.js'; import { Packed } from '@/misc/schema.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; export const InstanceRepository = db.getRepository(Instance).extend({ async pack( instance: Instance, ): Promise> { + const meta = await fetchMeta(); return { id: instance.id, caughtAt: instance.caughtAt.toISOString(), @@ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({ lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(), isNotResponding: instance.isNotResponding, isSuspended: instance.isSuspended, + isBlocked: meta.blockedHosts.includes(instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, diff --git a/packages/backend/src/models/schema/federation-instance.ts b/packages/backend/src/models/schema/federation-instance.ts index 9f27aab98..3efff6ca9 100644 --- a/packages/backend/src/models/schema/federation-instance.ts +++ b/packages/backend/src/models/schema/federation-instance.ts @@ -52,6 +52,10 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + isBlocked: { + type: 'boolean', + optional: false, nullable: false, + }, softwareName: { type: 'string', optional: false, nullable: true, diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue index 86eccd627..e8ab6f600 100644 --- a/packages/client/src/components/abuse-report.vue +++ b/packages/client/src/components/abuse-report.vue @@ -79,7 +79,8 @@ function resolve() { align-items: center; padding: 14px; border-radius: 8px; - background-image: linear-gradient(45deg, rgb(255 196 0 / 15%) 16.67%, transparent 16.67%, transparent 50%, rgb(255 196 0 / 15%) 50%, rgb(255 196 0 / 15%) 66.67%, transparent 66.67%, transparent 100%); + --c: rgb(255 196 0 / 15%); + background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%); background-size: 16px 16px; > .avatar { diff --git a/packages/client/src/components/instance-info.vue b/packages/client/src/components/instance-info.vue new file mode 100644 index 000000000..e55c1d821 --- /dev/null +++ b/packages/client/src/components/instance-info.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue index eda46b9aa..38d42f2be 100644 --- a/packages/client/src/pages/federation.vue +++ b/packages/client/src/pages/federation.vue @@ -41,51 +41,8 @@
- -
{{ instance.host }}
-
-
-
{{ $ts.registeredAt }}
-
-
-
-
{{ $ts.software }}
-
{{ instance.softwareName || `(${$ts.unknown})` }}
-
-
-
{{ $ts.version }}
-
{{ instance.softwareVersion || `(${$ts.unknown})` }}
-
-
-
{{ $ts.users }}
-
{{ instance.usersCount }}
-
-
-
{{ $ts.notes }}
-
{{ instance.notesCount }}
-
-
-
{{ $ts.sent }}
-
N/A
-
-
-
{{ $ts.received }}
-
N/A
-
-
- + +
@@ -100,6 +57,7 @@ import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; import MkPagination from '@/components/ui/pagination.vue'; +import MkInstanceInfo from '@/components/instance-info.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; @@ -127,9 +85,10 @@ const pagination = { }; function getStatus(instance) { - if (instance.isSuspended) return 'suspended'; - if (instance.isNotResponding) return 'error'; - return 'alive'; + if (instance.isSuspended) return 'Suspended'; + if (instance.isBlocked) return 'Blocked'; + if (instance.isNotResponding) return 'Error'; + return 'Alive'; } const headerActions = $computed(() => []); @@ -156,86 +115,8 @@ definePageMetadata({ grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px; - > .instance { - padding: 16px; - background: var(--panel); - border-radius: 8px; - - &:hover { - text-decoration: none; - } - - > .host { - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - > img { - width: 18px; - height: 18px; - margin-right: 6px; - vertical-align: middle; - } - } - - > .table { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); - grid-gap: 6px; - margin: 6px 0; - font-size: 70%; - - > .cell { - > .key, > .value { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - > .key { - opacity: 0.7; - } - - > .value { - } - } - } - - > .footer { - display: flex; - align-items: center; - font-size: 0.9em; - - > .status { - &.suspended { - opacity: 0.5; - } - - &.error { - color: var(--error); - } - - &.alive { - color: var(--success); - } - } - - > .pubSub { - margin-left: 8px; - } - - > .right { - margin-left: auto; - - > .latestStatus { - border: solid 1px var(--divider); - border-radius: 4px; - margin: 0 8px; - padding: 0 4px; - } - } - } + > .instance:hover { + text-decoration: none; } } diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue index 9e7756aa3..afee31b44 100644 --- a/packages/client/src/pages/instance-info.vue +++ b/packages/client/src/pages/instance-info.vue @@ -1,8 +1,8 @@ @@ -125,17 +123,18 @@ import number from '@/filters/number'; import bytes from '@/filters/bytes'; import { iAmModerator } from '@/account'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { i18n } from '@/i18n'; const props = defineProps<{ host: string; }>(); +let tab = $ref('overview'); let meta = $ref(null); let instance = $ref(null); let suspended = $ref(false); let isBlocked = $ref(false); let chartSrc = $ref('instance-requests'); -let chartSpan = $ref('hour'); async function fetch() { if (iAmModerator) { @@ -183,11 +182,26 @@ const headerActions = $computed(() => [{ }, }]); -const headerTabs = $computed(() => []); +const headerTabs = $computed(() => [{ + active: tab === 'overview', + title: i18n.ts.overview, + icon: 'fas fa-info-circle', + onClick: () => { tab = 'overview'; }, +}, { + active: tab === 'chart', + title: i18n.ts.charts, + icon: 'fas fa-chart-simple', + onClick: () => { tab = 'chart'; }, +}, { + active: tab === 'raw', + title: 'Raw data', + icon: 'fas fa-code', + onClick: () => { tab = 'raw'; }, +}]); definePageMetadata({ title: props.host, - icon: 'fas fa-info-circle', + icon: 'fas fa-server', bg: 'var(--bg)', }); @@ -207,5 +221,12 @@ definePageMetadata({ display: flex; margin: 0 0 16px 0; } + + > .charts { + > .label { + margin-bottom: 12px; + font-weight: bold; + } + } } diff --git a/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts index 3fe88e551..54184386d 100644 --- a/packages/client/src/scripts/i18n.ts +++ b/packages/client/src/scripts/i18n.ts @@ -11,13 +11,13 @@ export class I18n> { // string にしているのは、ドット区切りでのパス指定を許可するため // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record): string { + public t(key: string, args?: Record): string { try { let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; if (args) { for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v); + str = str.replace(`{${k}}`, v.toString()); } } return str;