Because the admin meta information was never loaded on this page, no amount of toggling the block or suspend sliders on the instance-info page (e.g. `https://calckey.example.com/instance-info/instance.tld`) will result in the instance actually being added to the blocklist. You could still do it from the bulk blocklist management page, but that can get unwieldy quickly if you just want to do a quick block of an instance. Co-authored-by: amy bones <amy@spookygirl.boo> Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9809 Co-authored-by: amybones <amybones@noreply.codeberg.org> Co-committed-by: amybones <amybones@noreply.codeberg.org>
331 lines
11 KiB
Vue
331 lines
11 KiB
Vue
<template>
|
|
<MkStickyContainer>
|
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
|
<MkSpacer v-if="instance" :content-max="600" :margin-min="16" :margin-max="32">
|
|
<swiper
|
|
:modules="[Virtual]"
|
|
:space-between="20"
|
|
:virtual="true"
|
|
:allow-touch-move="!(deviceKind === 'desktop' && !defaultStore.state.swipeOnDesktop)"
|
|
@swiper="setSwiperRef"
|
|
@slide-change="onSlideChange"
|
|
>
|
|
<swiper-slide>
|
|
<div class="_formRoot">
|
|
<div class="fnfelxur">
|
|
<img :src="faviconUrl" alt="" class="icon"/>
|
|
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
|
|
</div>
|
|
<MkKeyValue :copy="host" oneline style="margin: 1em 0;">
|
|
<template #key>Host</template>
|
|
<template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.software }}</template>
|
|
<template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.administrator }}</template>
|
|
<template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
|
|
</MkKeyValue>
|
|
<MkKeyValue>
|
|
<template #key>{{ i18n.ts.description }}</template>
|
|
<template #value>{{ instance.description }}</template>
|
|
</MkKeyValue>
|
|
|
|
<FormSection v-if="iAmModerator">
|
|
<template #label>Moderation</template>
|
|
<FormSuspense :p="init">
|
|
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch>
|
|
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch>
|
|
</FormSuspense>
|
|
<MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
|
</FormSection>
|
|
|
|
<FormSection>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.registeredAt }}</template>
|
|
<template #value><MkTime mode="detail" :time="instance.caughtAt"/></template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.updatedAt }}</template>
|
|
<template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.latestRequestSentAt }}</template>
|
|
<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.latestStatus }}</template>
|
|
<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
|
|
<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
|
|
</MkKeyValue>
|
|
</FormSection>
|
|
|
|
<FormSection>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>Following (Pub)</template>
|
|
<template #value>{{ number(instance.followingCount) }}</template>
|
|
</MkKeyValue>
|
|
<MkKeyValue oneline style="margin: 1em 0;">
|
|
<template #key>Followers (Sub)</template>
|
|
<template #value>{{ number(instance.followersCount) }}</template>
|
|
</MkKeyValue>
|
|
</FormSection>
|
|
|
|
<FormSection>
|
|
<template #label>Well-known resources</template>
|
|
<FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
|
|
<FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
|
|
<FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
|
|
<FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
|
|
<FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
|
|
</FormSection>
|
|
</div>
|
|
</swiper-slide>
|
|
<swiper-slide>
|
|
<div class="_formRoot">
|
|
<div class="cmhjzshl">
|
|
<div class="selects">
|
|
<MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
|
|
<option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
|
|
<option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
|
|
<option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
|
|
<option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
|
|
<option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
|
|
<option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
|
|
<option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
|
|
<option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
|
|
<option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
|
|
<option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
|
|
<option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
|
|
</MkSelect>
|
|
</div>
|
|
<div class="charts">
|
|
<div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
|
|
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
|
<div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
|
|
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</swiper-slide>
|
|
<swiper-slide>
|
|
<div class="_formRoot">
|
|
<MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
|
|
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${new Date(user.updatedAt).toLocaleString()}`" class="user" :to="`/user-info/${user.id}`">
|
|
<MkUserCardMini :user="user"/>
|
|
</MkA>
|
|
</MkPagination>
|
|
</div>
|
|
</swiper-slide>
|
|
<swiper-slide>
|
|
<div class="_formRoot">
|
|
<MkObjectView tall :value="instance">
|
|
</MkObjectView>
|
|
</div>
|
|
</swiper-slide>
|
|
</swiper>
|
|
</MkSpacer>
|
|
</MkStickyContainer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { watch } from 'vue';
|
|
import { Virtual } from 'swiper';
|
|
import { Swiper, SwiperSlide } from 'swiper/vue';
|
|
import type * as misskey from 'calckey-js';
|
|
import MkChart from '@/components/MkChart.vue';
|
|
import MkObjectView from '@/components/MkObjectView.vue';
|
|
import FormLink from '@/components/form/link.vue';
|
|
import MkLink from '@/components/MkLink.vue';
|
|
import MkButton from '@/components/MkButton.vue';
|
|
import FormSection from '@/components/form/section.vue';
|
|
import MkKeyValue from '@/components/MkKeyValue.vue';
|
|
import MkSelect from '@/components/form/select.vue';
|
|
import FormSwitch from '@/components/form/switch.vue';
|
|
import * as os from '@/os';
|
|
import number from '@/filters/number';
|
|
import { iAmModerator } from '@/account';
|
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
|
import { deviceKind } from '@/scripts/device-kind';
|
|
import { defaultStore } from '@/store';
|
|
import { i18n } from '@/i18n';
|
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
|
import MkPagination from '@/components/MkPagination.vue';
|
|
import 'swiper/scss';
|
|
import 'swiper/scss/virtual';
|
|
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
|
|
|
|
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
|
|
blockedHosts: string[];
|
|
};
|
|
type AugmentedInstance = misskey.entities.Instance & {
|
|
isBlocked: boolean;
|
|
};
|
|
|
|
const props = defineProps<{
|
|
host: string;
|
|
}>();
|
|
|
|
let tabs = ['overview'];
|
|
if (iAmModerator) tabs.push('chart', 'users', 'raw');
|
|
let tab = $ref(tabs[0]);
|
|
watch($$(tab), () => (syncSlide(tabs.indexOf(tab))));
|
|
|
|
let chartSrc = $ref('instance-requests');
|
|
let meta = $ref<AugmentedInstanceMetadata | null>(null);
|
|
let instance = $ref<AugmentedInstance | null>(null);
|
|
let suspended = $ref(false);
|
|
let isBlocked = $ref(false);
|
|
let faviconUrl = $ref(null);
|
|
|
|
const usersPagination = {
|
|
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
|
limit: 10,
|
|
params: {
|
|
sort: '+updatedAt',
|
|
state: 'all',
|
|
hostname: props.host,
|
|
},
|
|
offsetMode: true,
|
|
};
|
|
|
|
async function init() {
|
|
meta = await os.api('admin/meta');
|
|
}
|
|
|
|
async function fetch() {
|
|
instance = (await os.api('federation/show-instance', {
|
|
host: props.host,
|
|
})) as AugmentedInstance;
|
|
suspended = instance.isSuspended;
|
|
isBlocked = instance.isBlocked;
|
|
faviconUrl =
|
|
getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ??
|
|
getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
|
}
|
|
|
|
async function toggleBlock() {
|
|
if (meta == null) return;
|
|
if (!instance) {
|
|
throw new Error(`Instance info not loaded`);
|
|
}
|
|
let blockedHosts: string[];
|
|
if (isBlocked) {
|
|
blockedHosts = meta.blockedHosts.concat([instance.host]);
|
|
} else {
|
|
blockedHosts = meta.blockedHosts.filter((x) => x !== instance!.host);
|
|
}
|
|
await os.api('admin/update-meta', {
|
|
blockedHosts,
|
|
});
|
|
}
|
|
|
|
async function toggleSuspend(v) {
|
|
await os.api('admin/federation/update-instance', {
|
|
host: instance.host,
|
|
isSuspended: suspended,
|
|
});
|
|
}
|
|
|
|
function refreshMetadata() {
|
|
os.api('admin/federation/refresh-remote-instance-metadata', {
|
|
host: instance.host,
|
|
});
|
|
os.alert({
|
|
text: 'Refresh requested',
|
|
});
|
|
}
|
|
|
|
fetch();
|
|
|
|
const headerActions = $computed(() => [{
|
|
text: `https://${props.host}`,
|
|
icon: 'ph-arrow-square-out ph-bold ph-lg',
|
|
handler: () => {
|
|
window.open(`https://${props.host}`, '_blank');
|
|
},
|
|
}]);
|
|
|
|
let theTabs = [{
|
|
key: 'overview',
|
|
title: i18n.ts.overview,
|
|
icon: 'ph-info ph-bold ph-lg',
|
|
}];
|
|
|
|
if (iAmModerator) {
|
|
theTabs.push(
|
|
{
|
|
key: 'chart',
|
|
title: i18n.ts.charts,
|
|
icon: 'ph-chart-bar ph-bold ph-lg',
|
|
}, {
|
|
key: 'users',
|
|
title: i18n.ts.users,
|
|
icon: 'ph-users ph-bold ph-lg',
|
|
}, {
|
|
key: 'raw',
|
|
title: 'Raw',
|
|
icon: 'ph-code ph-bold ph-lg',
|
|
},
|
|
);
|
|
}
|
|
|
|
let headerTabs = $computed(() => theTabs);
|
|
|
|
definePageMetadata({
|
|
title: props.host,
|
|
icon: 'ph-hard-drives ph-bold ph-lg',
|
|
});
|
|
|
|
let swiperRef = null;
|
|
|
|
function setSwiperRef(swiper) {
|
|
swiperRef = swiper;
|
|
syncSlide(tabs.indexOf(tab));
|
|
}
|
|
|
|
function onSlideChange() {
|
|
tab = tabs[swiperRef.activeIndex];
|
|
}
|
|
|
|
function syncSlide(index) {
|
|
swiperRef.slideTo(index);
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.fnfelxur {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
> .icon {
|
|
display: block;
|
|
margin: 0 16px 0 0;
|
|
height: 64px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
> .name {
|
|
word-break: break-all;
|
|
}
|
|
}
|
|
|
|
.cmhjzshl {
|
|
> .selects {
|
|
display: flex;
|
|
margin: 0 0 16px 0;
|
|
}
|
|
|
|
> .charts {
|
|
> .label {
|
|
margin-bottom: 12px;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
}
|
|
</style>
|