hippofish/packages/client/src/pages/instance-info.vue

563 lines
14 KiB
Vue
Raw Normal View History

2022-06-20 12:49:51 +02:00
<template>
2023-04-08 02:01:42 +02:00
<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"
>
2023-05-26 04:47:10 +02:00
<swiper
2023-05-26 23:02:17 +02:00
:round-lengths="true"
2023-05-26 04:47:10 +02:00
:touch-angle="25"
:threshold="10"
2023-09-02 01:27:33 +02:00
:centered-slides="true"
2023-04-08 02:01:42 +02:00
:modules="[Virtual]"
:space-between="20"
:virtual="true"
2023-06-26 21:47:05 +02:00
:allow-touch-move="
defaultStore.state.swipeOnMobile &&
(deviceKind !== 'desktop' ||
defaultStore.state.swipeOnDesktop)
"
2023-04-08 02:01:42 +02:00
@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>
2023-04-08 02:01:42 +02:00
<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>
2023-04-08 02:01:42 +02:00
<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>
2023-04-08 02:01:42 +02:00
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{
instance.description
}}</template>
</MkKeyValue>
<FormSection v-if="iAmAdmin">
2023-04-08 02:01:42 +02:00
<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
>
2023-04-30 15:01:42 +02:00
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
2023-04-08 02:01:42 +02:00
</FormSuspense>
<MkButton @click="refreshMetadata"
><i
class="ph-arrows-clockwise ph-bold ph-lg"
></i>
Refresh metadata</MkButton
>
</FormSection>
2023-04-08 02:01:42 +02:00
<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>
2023-04-08 02:01:42 +02:00
</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(
2023-07-06 03:28:27 +02:00
user.updatedAt,
2023-04-08 02:01:42 +02:00
).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>
2023-09-02 01:27:33 +02:00
import { computed, ref, watch } from "vue";
import { Virtual } from "swiper/modules";
2023-04-08 02:01:42 +02:00
import { Swiper, SwiperSlide } from "swiper/vue";
2023-07-03 00:18:30 +02:00
import type * as firefish from "firefish-js";
2023-04-08 02:01:42 +02:00
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 { iAmAdmin } from "@/account";
2023-04-08 02:01:42 +02:00
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";
2023-07-03 00:18:30 +02:00
type AugmentedInstanceMetadata = firefish.entities.DetailedInstanceMetadata & {
blockedHosts: string[];
2023-04-30 15:01:42 +02:00
silencedHosts: string[];
};
2023-07-03 00:18:30 +02:00
type AugmentedInstance = firefish.entities.Instance & {
isBlocked: boolean;
2023-04-30 15:01:42 +02:00
isSilenced: boolean;
};
const props = defineProps<{
host: string;
}>();
2023-09-02 01:27:33 +02:00
const tabs = ["overview"];
if (iAmAdmin) tabs.push("chart", "users", "raw");
const tab = ref(tabs[0]);
watch(tab, () => syncSlide(tabs.indexOf(tab.value)));
2022-10-26 04:19:42 +02:00
const chartSrc = ref("instance-requests");
const meta = ref<AugmentedInstanceMetadata | null>(null);
const instance = ref<AugmentedInstance | null>(null);
const suspended = ref(false);
const isBlocked = ref(false);
const isSilenced = ref(false);
const faviconUrl = ref(null);
const usersPagination = {
endpoint: iAmAdmin ? "admin/show-users" : ("users" as const),
limit: 10,
params: {
2023-04-08 02:01:42 +02:00
sort: "+updatedAt",
state: "all",
hostname: props.host,
},
offsetMode: true,
};
async function fetch() {
2023-05-08 22:01:06 +02:00
if (iAmAdmin)
meta.value = (await os.api("admin/meta")) as AugmentedInstanceMetadata;
instance.value = (await os.api("federation/show-instance", {
host: props.host,
})) as AugmentedInstance;
suspended.value = instance.value.isSuspended;
isBlocked.value = instance.value.isBlocked;
isSilenced.value = instance.value.isSilenced;
faviconUrl.value =
getProxiedImageUrlNullable(instance.value.faviconUrl, "preview") ??
getProxiedImageUrlNullable(instance.value.iconUrl, "preview");
}
async function toggleBlock() {
if (meta.value == null) return;
if (!instance.value) {
throw new Error(`Instance info not loaded`);
}
let blockedHosts: string[];
if (isBlocked.value) {
blockedHosts = meta.value.blockedHosts.concat([instance.value.host]);
} else {
blockedHosts = meta.value.blockedHosts.filter(
(x) => x !== instance.value!.host,
);
}
2023-04-08 02:01:42 +02:00
await os.api("admin/update-meta", {
blockedHosts,
});
}
2022-01-04 07:36:14 +01:00
2023-04-30 15:01:42 +02:00
async function toggleSilence() {
if (meta.value == null) return;
if (!instance.value) {
2023-04-30 15:01:42 +02:00
throw new Error(`Instance info not loaded`);
}
let silencedHosts: string[];
if (isSilenced.value) {
silencedHosts = meta.value.silencedHosts.concat([instance.value.host]);
2023-04-30 15:01:42 +02:00
} else {
silencedHosts = meta.value.silencedHosts.filter(
(x) => x !== instance.value!.host,
);
2023-04-30 15:01:42 +02:00
}
await os.api("admin/update-meta", {
silencedHosts,
});
}
async function toggleSuspend(v) {
2023-04-08 02:01:42 +02:00
await os.api("admin/federation/update-instance", {
host: instance.value.host,
isSuspended: suspended.value,
});
}
2022-01-04 07:36:14 +01:00
function refreshMetadata() {
2023-04-08 02:01:42 +02:00
os.api("admin/federation/refresh-remote-instance-metadata", {
host: instance.value.host,
});
os.alert({
2023-04-08 02:01:42 +02:00
text: "Refresh requested",
});
}
fetch();
const headerActions = computed(() => [
2023-04-08 02:01:42 +02:00
{
text: `https://${props.host}`,
icon: "ph-arrow-square-out ph-bold ph-lg",
handler: () => {
window.open(`https://${props.host}`, "_blank");
},
2022-06-20 12:49:51 +02:00
},
2023-04-08 02:01:42 +02:00
]);
2023-09-02 01:27:33 +02:00
const theTabs = [
2023-04-08 02:01:42 +02:00
{
key: "overview",
title: i18n.ts.overview,
icon: "ph-info ph-bold ph-lg",
},
];
2022-07-26 02:14:37 +02:00
if (iAmAdmin) {
2022-07-26 02:27:57 +02:00
theTabs.push(
{
2023-04-08 02:01:42 +02:00
key: "chart",
2022-07-26 02:27:57 +02:00
title: i18n.ts.charts,
2023-04-08 02:01:42 +02:00
icon: "ph-chart-bar ph-bold ph-lg",
},
{
key: "users",
2022-07-26 02:27:57 +02:00
title: i18n.ts.users,
2023-04-08 02:01:42 +02:00
icon: "ph-users ph-bold ph-lg",
2022-07-26 02:27:57 +02:00
},
2023-04-08 02:01:42 +02:00
{
key: "raw",
title: "Raw",
icon: "ph-code ph-bold ph-lg",
2023-07-06 03:28:27 +02:00
},
2022-07-26 02:27:57 +02:00
);
2022-07-26 02:14:37 +02:00
}
const headerTabs = computed(() => theTabs);
2022-07-26 02:27:57 +02:00
definePageMetadata({
title: props.host,
2023-04-08 02:01:42 +02:00
icon: "ph-hard-drives ph-bold ph-lg",
});
let swiperRef = null;
function setSwiperRef(swiper) {
swiperRef = swiper;
syncSlide(tabs.indexOf(tab.value));
}
function onSlideChange() {
tab.value = tabs[swiperRef.activeIndex];
}
function syncSlide(index) {
swiperRef.slideTo(index);
}
</script>
2021-04-16 14:47:12 +02:00
<style lang="scss" scoped>
.fnfelxur {
2022-06-26 09:57:12 +02:00
display: flex;
align-items: center;
2021-04-25 05:31:11 +02:00
> .icon {
2021-04-16 14:47:12 +02:00
display: block;
2022-06-26 09:57:12 +02:00
margin: 0 16px 0 0;
2021-04-16 14:47:12 +02:00
height: 64px;
border-radius: 8px;
}
2022-06-26 09:57:12 +02:00
> .name {
word-break: break-all;
}
2021-04-16 14:47:12 +02:00
}
.cmhjzshl {
> .selects {
display: flex;
2022-01-04 07:36:14 +01:00
margin: 0 0 16px 0;
2021-04-16 14:47:12 +02:00
}
2022-06-21 10:55:38 +02:00
> .charts {
> .label {
margin-bottom: 12px;
font-weight: bold;
}
}
2021-04-16 14:47:12 +02:00
}
</style>