jointrashposs/components/servers/StatsViewer.vue
かっこかり 4c47eb20ee
(add) サーバー統計ページ (#58)
* (add) サーバー統計ページ

* fix

* add jsdoc

* Update CircGraph.vue
2023-12-18 14:11:34 +09:00

227 lines
8.0 KiB
Vue

<template>
<div class="container mx-auto max-w-screen-lg px-6 space-y-6 lg:space-y-8">
<GLocalNav :items="[
{
name: $t('_servers._statistics.lang'),
anchor: '#lang',
},
{
name: $t('_servers._statistics.registerAcceptance'),
anchor: '#registerAcceptance',
},
{
name: $t('_servers._statistics.notes'),
anchor: '#notes',
},
{
name: $t('_servers._statistics.version'),
anchor: '#version'
}
]" />
<div id="lang">
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.lang`) }}</h2>
<div class="grid gap-4 md:grid-cols-2">
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="langStats" :focusedIndex="langFocus" /></div>
<div>
<ul class="space-y-1">
<li v-for="value, key, index in trunc(langStats)" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="langFocus = index" @mouseleave.passive="langFocus = undefined">
<div v-if="key === '__others'">{{ $t('other') }}</div>
<div v-else>{{ key }} {{ lang.find((v) => v.lang === key)?.label ? `(${lang.find((v) => v.lang === key)?.label})` : '' }}</div>
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
</li>
</ul>
</div>
</div>
</div>
<div id="registerAcceptance">
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.registerAcceptance`) }}</h2>
<div class="grid gap-4 md:grid-cols-2">
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="regAcceptanceStats" :focusedIndex="regAcceptanceFocus" :truncMinor="false" /></div>
<div>
<ul class="space-y-1">
<li v-for="value, key, index in regAcceptanceStats" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="regAcceptanceFocus = index" @mouseleave.passive="regAcceptanceFocus = undefined">
<div>{{ $t(`_servers._registerAcceptance.${key}`) }}</div>
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
</li>
</ul>
</div>
</div>
</div>
<div id="notes">
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.notes`) }}</h2>
<div class="grid gap-4 md:grid-cols-2">
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="notesCountStats" :focusedIndex="notesCountFocus" /></div>
<div>
<ul class="space-y-1">
<li v-for="value, key, index in trunc(notesCountStats)" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="notesCountFocus = index" @mouseleave.passive="notesCountFocus = undefined">
<div v-if="key === '__others'">{{ $t('other') }}</div>
<div v-else>{{ key }}</div>
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
</li>
</ul>
</div>
</div>
</div>
<div id="version">
<h2 class="text-2xl lg:text-3xl font-title font-bold mb-4">{{ $t(`_servers._statistics.version`) }}</h2>
<div class="grid gap-4 md:grid-cols-2">
<div><ChartsCircGraph class="max-w-xs mx-auto" :data="avgVersionStats" :truncMinor="0.005" :focusedIndex="avgVersionFocus" /></div>
<div>
<ul class="space-y-1">
<li v-for="value, key, index in trunc(avgVersionStats, 0.005)" class="grid py-1 px-3 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800" :class="$style.kvRoot" @mouseenter.passive="avgVersionFocus = index" @mouseleave.passive="avgVersionFocus = undefined">
<div v-if="key === '__others'">{{ $t('other') }}</div>
<div v-else>{{ key }}</div>
<div class="font-bold font-mono text-accent-600">{{ $n(value) }}</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { InstanceInfo } from '@/types/instances-info';
import lang from '~/assets/data/lang';
const { t } = useI18n();
const { data } = await useFetch<InstanceInfo>('https://instanceapp.misskey.page/instances.json', {
onRequestError: () => {
alert(t('_servers._system.fetchError'));
}
});
const langStats = computed(() => {
const d = data.value?.instancesInfos;
if (!d || d.length === 0) {
return [];
}
const out: Record<string, number> = {};
d.forEach((v) => {
if (!out[v.langs[0]]) {
out[v.langs[0]] = 1;
} else {
out[v.langs[0]]++;
}
});
return Object.entries(out)
.sort(([, a], [, b]) => b - a)
.reduce(
(r, [k, v]) => ({
...r,
[k]: v
}),
{}
);
});
const langFocus = ref<number | undefined>();
const regAcceptanceStats = computed(() => {
const d = data.value?.instancesInfos;
if (!d || d.length === 0) {
return [];
}
const out = {
public: 0,
inviteOnly: 0,
};
d.forEach((v) => {
if (v.meta?.disableRegistration) {
out.inviteOnly++;
} else {
out.public++;
}
});
return Object.entries(out)
.sort(([, a], [, b]) => b - a)
.reduce(
(r, [k, v]) => ({
...r,
[k]: v
}),
{}
);
});
const regAcceptanceFocus = ref<number | undefined>();
const notesCountStats = computed(() => {
const d = data.value?.instancesInfos;
if (!d || d.length === 0) {
return [];
}
const out: Record<string, number> = {};
d.forEach((v) => {
out[v.name] = v.stats?.originalNotesCount ?? 0;
});
return Object.entries(out)
.sort(([, a], [, b]) => b - a)
.reduce(
(r, [k, v]) => ({
...r,
[k]: v
}),
{}
);
});
const notesCountFocus = ref<number | undefined>();
const avgVersionStats = computed(() => {
const d = data.value?.instancesInfos;
if (!d || d.length === 0) {
return [];
}
const out: Record<string, number> = {};
d.forEach((v) => {
const ver = v.meta?.version.replace(/[-\+].+$/g, '');
if (ver) {
if (!out[ver]) {
out[ver] = 1;
} else {
out[ver]++;
}
}
});
return Object.entries(out)
.sort(([, a], [, b]) => b - a)
.reduce(
(r, [k, v]) => ({
...r,
[k]: v
}),
{}
);
});
const avgVersionFocus = ref<number | undefined>();
function trunc(obj: Record<any, number>, ratio: number = 0.02) {
const sum = Object.values(obj).reduce((p, c) => p + c, 0);
const toBeTrunked = Object.values(obj).filter((v) => v / sum < ratio);
return {
...Object.entries(obj)
.slice(0, (Object.values(obj).length - toBeTrunked.length))
.reduce(
(r, [k, v]) => ({
...r,
[k]: v
}),
{}
),
__others: toBeTrunked.reduce((p, c) => p + c, 0),
};
}
</script>
<style module>
.kvRoot {
grid-template-columns: 1fr auto;
}
</style>