summaryrefslogtreecommitdiff
path: root/packages/client/src/pages
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-12-23 15:21:55 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-12-23 15:21:55 +0900
commit052e667f0357a059dd17f46ccce68d2598146724 (patch)
tree75226d51850fef5630446478b6fe70bafba67818 /packages/client/src/pages
parent13.0.0-alpha.6 (diff)
downloadmisskey-052e667f0357a059dd17f46ccce68d2598146724.tar.gz
misskey-052e667f0357a059dd17f46ccce68d2598146724.tar.bz2
misskey-052e667f0357a059dd17f46ccce68d2598146724.zip
enhance(client): enhance dashboard of control panel
Diffstat (limited to 'packages/client/src/pages')
-rw-r--r--packages/client/src/pages/admin/overview.active-users.vue223
-rw-r--r--packages/client/src/pages/admin/overview.ap-requests.vue263
-rw-r--r--packages/client/src/pages/admin/overview.federation.vue221
-rw-r--r--packages/client/src/pages/admin/overview.instances.vue52
-rw-r--r--packages/client/src/pages/admin/overview.queue.chart.vue (renamed from packages/client/src/pages/admin/overview.queue-chart.vue)118
-rw-r--r--packages/client/src/pages/admin/overview.queue.vue127
-rw-r--r--packages/client/src/pages/admin/overview.stats.vue153
-rw-r--r--packages/client/src/pages/admin/overview.user.vue76
-rw-r--r--packages/client/src/pages/admin/overview.users.vue55
-rw-r--r--packages/client/src/pages/admin/overview.vue556
-rw-r--r--packages/client/src/pages/admin/queue.chart.chart.vue4
11 files changed, 1119 insertions, 729 deletions
diff --git a/packages/client/src/pages/admin/overview.active-users.vue b/packages/client/src/pages/admin/overview.active-users.vue
new file mode 100644
index 0000000000..eb4cefb345
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.active-users.vue
@@ -0,0 +1,223 @@
+<template>
+<div>
+ <MkLoading v-if="fetching"/>
+ <div v-show="!fetching" :class="$style.root" class="_panel">
+ <canvas ref="chartEl"></canvas>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import { enUS } from 'date-fns/locale';
+import tinycolor from 'tinycolor2';
+import * as os from '@/os';
+import 'chartjs-adapter-date-fns';
+import { defaultStore } from '@/store';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import gradient from 'chartjs-plugin-gradient';
+import { chartVLine } from '@/scripts/chart-vline';
+
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+ gradient,
+);
+
+const alpha = (hex, a) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+const chartEl = $ref<HTMLCanvasElement>(null);
+const now = new Date();
+let chartInstance: Chart = null;
+const chartLimit = 50;
+let fetching = $ref(true);
+
+const { handler: externalTooltipHandler } = useChartTooltip();
+
+async function renderChart() {
+ if (chartInstance) {
+ chartInstance.destroy();
+ }
+
+ const getDate = (ago: number) => {
+ const y = now.getFullYear();
+ const m = now.getMonth();
+ const d = now.getDate();
+
+ return new Date(y, m, d - ago);
+ };
+
+ const format = (arr) => {
+ return arr.map((v, i) => ({
+ x: getDate(i).getTime(),
+ y: v,
+ }));
+ };
+
+ const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
+
+ const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+ const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+
+ // フォントカラー
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+ const color = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')).toHexString();
+
+ const max = Math.max(...raw.read);
+
+ chartInstance = new Chart(chartEl, {
+ type: 'line',
+ data: {
+ //labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
+ datasets: [{
+ parsing: false,
+ label: 'active',
+ data: format(raw.read).slice().reverse(),
+ tension: 0.3,
+ pointRadius: 0,
+ borderWidth: 2,
+ borderColor: color,
+ borderJoinStyle: 'round',
+ //backgroundColor: alpha(color, 0.1),
+ gradient: {
+ backgroundColor: {
+ axis: 'y',
+ colors: {
+ 0: alpha(color, 0),
+ [max]: alpha(color, 0.35),
+ },
+ },
+ },
+ barPercentage: 0.9,
+ categoryPercentage: 0.9,
+ fill: true,
+ clip: 8,
+ }],
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 0,
+ right: 8,
+ top: 0,
+ bottom: 0,
+ },
+ },
+ scales: {
+ x: {
+ type: 'time',
+ offset: false,
+ time: {
+ stepSize: 1,
+ unit: 'day',
+ },
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: true,
+ maxRotation: 0,
+ autoSkipPadding: 16,
+ },
+ adapters: {
+ date: {
+ locale: enUS,
+ },
+ },
+ min: getDate(chartLimit).getTime(),
+ },
+ y: {
+ position: 'left',
+ suggestedMax: 10,
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: true,
+ //mirror: true,
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ mode: 'index',
+ },
+ elements: {
+ point: {
+ hoverRadius: 5,
+ hoverBorderWidth: 2,
+ },
+ },
+ animation: true,
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ enabled: false,
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ external: externalTooltipHandler,
+ },
+ gradient,
+ },
+ },
+ plugins: [chartVLine(vLineColor)],
+ });
+
+ fetching = false;
+}
+
+onMounted(async () => {
+ renderChart();
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ padding: 20px;
+}
+</style>
diff --git a/packages/client/src/pages/admin/overview.ap-requests.vue b/packages/client/src/pages/admin/overview.ap-requests.vue
new file mode 100644
index 0000000000..65ba0075eb
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.ap-requests.vue
@@ -0,0 +1,263 @@
+<template>
+<div>
+ <MkLoading v-if="fetching"/>
+ <div v-show="!fetching" :class="$style.root">
+ <div class="chart _panel">
+ <canvas ref="chartEl"></canvas>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import gradient from 'chartjs-plugin-gradient';
+import { enUS } from 'date-fns/locale';
+import tinycolor from 'tinycolor2';
+import MkMiniChart from '@/components/MkMiniChart.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import MkNumberDiff from '@/components/MkNumberDiff.vue';
+import { i18n } from '@/i18n';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import { chartVLine } from '@/scripts/chart-vline';
+import { defaultStore } from '@/store';
+
+Chart.register(
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+ gradient,
+);
+
+const alpha = (hex, a) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
+ const r = parseInt(result[1], 16);
+ const g = parseInt(result[2], 16);
+ const b = parseInt(result[3], 16);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+};
+
+const chartLimit = 50;
+const chartEl = $ref<HTMLCanvasElement>();
+let fetching = $ref(true);
+
+const { handler: externalTooltipHandler } = useChartTooltip();
+
+onMounted(async () => {
+ const now = new Date();
+
+ const getDate = (ago: number) => {
+ const y = now.getFullYear();
+ const m = now.getMonth();
+ const d = now.getDate();
+
+ return new Date(y, m, d - ago);
+ };
+
+ const format = (arr) => {
+ return arr.map((v, i) => ({
+ x: getDate(i).getTime(),
+ y: v,
+ }));
+ };
+
+ const formatMinus = (arr) => {
+ return arr.map((v, i) => ({
+ x: getDate(i).getTime(),
+ y: -v,
+ }));
+ };
+
+ const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
+
+ const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+ const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+ const succColor = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--success')).toHexString();
+ const failColor = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--error')).toHexString();
+
+ const succMax = Math.max(...raw.deliverSucceeded);
+ const failMax = Math.max(...raw.deliverFailed);
+
+ // フォントカラー
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+
+ new Chart(chartEl, {
+ type: 'bar',
+ data: {
+ //labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
+ datasets: [{
+ stack: 'a',
+ parsing: false,
+ label: 'Succ',
+ data: format(raw.deliverSucceeded).slice().reverse(),
+ tension: 0.3,
+ pointRadius: 0,
+ borderWidth: 0,
+ borderJoinStyle: 'round',
+ borderRadius: 4,
+ //backgroundColor: alpha(color, 0.1),
+ gradient: {
+ backgroundColor: {
+ axis: 'y',
+ colors: {
+ 0: alpha(succColor, 0.3),
+ [succMax]: alpha(succColor, 1),
+ },
+ },
+ },
+ barPercentage: 0.9,
+ categoryPercentage: 0.9,
+ fill: true,
+ clip: 8,
+ }, {
+ stack: 'a',
+ parsing: false,
+ label: 'Fail',
+ data: formatMinus(raw.deliverFailed).slice().reverse(),
+ tension: 0.3,
+ pointRadius: 0,
+ borderWidth: 0,
+ borderJoinStyle: 'round',
+ borderRadius: 4,
+ //backgroundColor: alpha(color, 0.1),
+ gradient: {
+ backgroundColor: {
+ axis: 'y',
+ colors: {
+ 0: alpha(failColor, 0.3),
+ [-failMax]: alpha(failColor, 1),
+ },
+ },
+ },
+ barPercentage: 0.9,
+ categoryPercentage: 0.9,
+ fill: true,
+ clip: 8,
+ }],
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 0,
+ right: 8,
+ top: 0,
+ bottom: 0,
+ },
+ },
+ scales: {
+ x: {
+ type: 'time',
+ stacked: true,
+ offset: false,
+ time: {
+ stepSize: 1,
+ unit: 'day',
+ },
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: true,
+ maxRotation: 0,
+ autoSkipPadding: 16,
+ },
+ adapters: {
+ date: {
+ locale: enUS,
+ },
+ },
+ min: getDate(chartLimit).getTime(),
+ },
+ y: {
+ stacked: true,
+ position: 'left',
+ suggestedMax: 10,
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: true,
+ //mirror: true,
+ callback: (value, index, values) => value < 0 ? -value : value,
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ mode: 'index',
+ },
+ elements: {
+ point: {
+ hoverRadius: 5,
+ hoverBorderWidth: 2,
+ },
+ },
+ animation: true,
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ enabled: false,
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ external: externalTooltipHandler,
+ },
+ gradient,
+ },
+ },
+ plugins: [chartVLine(vLineColor)],
+ });
+
+ fetching = false;
+});
+</script>
+
+<style lang="scss" module>
+.root {
+
+ &:global {
+ > .chart {
+ padding: 16px;
+ margin-bottom: 16px;
+ }
+ }
+}
+</style>
+
diff --git a/packages/client/src/pages/admin/overview.federation.vue b/packages/client/src/pages/admin/overview.federation.vue
index e8cb5867a7..eb44fa23b7 100644
--- a/packages/client/src/pages/admin/overview.federation.vue
+++ b/packages/client/src/pages/admin/overview.federation.vue
@@ -1,100 +1,185 @@
<template>
-<div class="wbrkwale">
+<div>
<MkLoading v-if="fetching"/>
- <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
- <MkA v-for="(instance, i) in instances" :key="instance.id" :to="`/instance-info/${instance.host}`" class="instance">
- <img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
- <div class="body">
- <div class="name">{{ instance.name ?? instance.host }}</div>
- <div class="host">{{ instance.host }}</div>
+ <div v-show="!fetching" :class="$style.root">
+ <div v-if="topSubInstancesForPie && topPubInstancesForPie" class="pies">
+ <div class="pie deliver _panel">
+ <div class="title">Sub</div>
+ <XPie :data="topSubInstancesForPie" class="chart"/>
+ <div class="subTitle">Top 10</div>
</div>
- <MkMiniChart class="chart" :src="charts[i].requests.received"/>
- </MkA>
- </transition-group>
+ <div class="pie inbox _panel">
+ <div class="title">Pub</div>
+ <XPie :data="topPubInstancesForPie" class="chart"/>
+ <div class="subTitle">Top 10</div>
+ </div>
+ </div>
+ <div v-if="!fetching" class="items">
+ <div class="item _panel sub">
+ <div class="icon"><i class="ti ti-world-download"></i></div>
+ <div class="body">
+ <div class="value">
+ {{ number(federationSubActive) }}
+ <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
+ </div>
+ <div class="label">Sub</div>
+ </div>
+ </div>
+ <div class="item _panel pub">
+ <div class="icon"><i class="ti ti-world-upload"></i></div>
+ <div class="body">
+ <div class="value">
+ {{ number(federationPubActive) }}
+ <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
+ </div>
+ <div class="label">Pub</div>
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
+import XPie from './overview.pie.vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
import * as os from '@/os';
-import { useInterval } from '@/scripts/use-interval';
+import number from '@/filters/number';
+import MkNumberDiff from '@/components/MkNumberDiff.vue';
+import { i18n } from '@/i18n';
+import { useChartTooltip } from '@/scripts/use-chart-tooltip';
-const instances = ref([]);
-const charts = ref([]);
-const fetching = ref(true);
+let topSubInstancesForPie: any = $ref(null);
+let topPubInstancesForPie: any = $ref(null);
+let federationPubActive = $ref<number | null>(null);
+let federationPubActiveDiff = $ref<number | null>(null);
+let federationSubActive = $ref<number | null>(null);
+let federationSubActiveDiff = $ref<number | null>(null);
+let fetching = $ref(true);
-const fetch = async () => {
- const fetchedInstances = await os.api('federation/instances', {
- sort: '+lastCommunicatedAt',
- limit: 5,
+const { handler: externalTooltipHandler } = useChartTooltip();
+
+onMounted(async () => {
+ const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' });
+ federationPubActive = chart.pubActive[0];
+ federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
+ federationSubActive = chart.subActive[0];
+ federationSubActiveDiff = chart.subActive[0] - chart.subActive[1];
+
+ os.apiGet('federation/stats', { limit: 10 }).then(res => {
+ topSubInstancesForPie = res.topSubInstances.map(x => ({
+ name: x.host,
+ color: x.themeColor,
+ value: x.followersCount,
+ onClick: () => {
+ os.pageWindow(`/instance-info/${x.host}`);
+ },
+ })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowersCount }]);
+ topPubInstancesForPie = res.topPubInstances.map(x => ({
+ name: x.host,
+ color: x.themeColor,
+ value: x.followingCount,
+ onClick: () => {
+ os.pageWindow(`/instance-info/${x.host}`);
+ },
+ })).concat([{ name: '(other)', color: '#80808080', value: res.otherFollowingCount }]);
});
- const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.apiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
- instances.value = fetchedInstances;
- charts.value = fetchedCharts;
- fetching.value = false;
-};
-useInterval(fetch, 1000 * 60, {
- immediate: true,
- afterMounted: true,
+ fetching = false;
});
</script>
-<style lang="scss" scoped>
-.wbrkwale {
- > .instances {
- .chart-move {
- transition: transform 1s ease;
- }
+<style lang="scss" module>
+.root {
- > .instance {
- display: flex;
- align-items: center;
- padding: 16px 20px;
+ &:global {
+ > .pies {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
+ grid-gap: 16px;
+ margin-bottom: 16px;
- &:not(:last-child) {
- border-bottom: solid 0.5px var(--divider);
- }
+ > .pie {
+ position: relative;
+ padding: 12px;
- > img {
- display: block;
- width: 34px;
- height: 34px;
- object-fit: cover;
- border-radius: 4px;
- margin-right: 12px;
+ > .title {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ font-size: 90%;
+ }
+
+ > .chart {
+ max-height: 150px;
+ }
+
+ > .subTitle {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ font-size: 85%;
+ }
}
+ }
+
+ > .items {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
+ grid-gap: 16px;
- > .body {
- flex: 1;
- overflow: hidden;
- font-size: 0.9em;
- color: var(--fg);
- padding-right: 8px;
+ > .item {
+ display: flex;
+ box-sizing: border-box;
+ padding: 12px;
- > .name {
- display: block;
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ > .icon {
+ display: grid;
+ place-items: center;
+ height: 100%;
+ aspect-ratio: 1;
+ margin-right: 12px;
+ background: var(--accentedBg);
+ color: var(--accent);
+ border-radius: 10px;
}
- > .host {
- margin: 0;
- font-size: 75%;
- opacity: 0.7;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ &.sub {
+ > .icon {
+ background: #d5ba0026;
+ color: #dfc300;
+ }
+ }
+
+ &.pub {
+ > .icon {
+ background: #00cf2326;
+ color: #00cd5b;
+ }
}
- }
- > .chart {
- height: 30px;
+ > .body {
+ padding: 4px 0;
+
+ > .value {
+ font-size: 1.3em;
+ font-weight: bold;
+
+ > .diff {
+ font-size: 0.65em;
+ font-weight: normal;
+ }
+ }
+
+ > .label {
+ font-size: 0.8em;
+ opacity: 0.5;
+ }
+ }
}
}
}
}
</style>
+
diff --git a/packages/client/src/pages/admin/overview.instances.vue b/packages/client/src/pages/admin/overview.instances.vue
new file mode 100644
index 0000000000..c13e2328fd
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.instances.vue
@@ -0,0 +1,52 @@
+<template>
+<div class="wbrkwale">
+ <MkLoading v-if="fetching"/>
+ <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
+ <MkA v-for="(instance, i) in instances" :key="instance.id" :to="`/instance-info/${instance.host}`" class="instance">
+ <MkInstanceCardMini :instance="instance"/>
+ </MkA>
+ </transition-group>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import * as os from '@/os';
+import { useInterval } from '@/scripts/use-interval';
+import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
+
+const instances = ref([]);
+const fetching = ref(true);
+
+const fetch = async () => {
+ const fetchedInstances = await os.api('federation/instances', {
+ sort: '+lastCommunicatedAt',
+ limit: 6,
+ });
+ instances.value = fetchedInstances;
+ fetching.value = false;
+};
+
+useInterval(fetch, 1000 * 60, {
+ immediate: true,
+ afterMounted: true,
+});
+</script>
+
+<style lang="scss" scoped>
+.wbrkwale {
+ > .instances {
+ .chart-move {
+ transition: transform 1s ease;
+ }
+
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
+ grid-gap: 12px;
+
+ > .instance:hover {
+ text-decoration: none;
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/pages/admin/overview.queue-chart.vue b/packages/client/src/pages/admin/overview.queue.chart.vue
index a2b748ad38..609ad5ebef 100644
--- a/packages/client/src/pages/admin/overview.queue-chart.vue
+++ b/packages/client/src/pages/admin/overview.queue.chart.vue
@@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
-import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+import { watch, onMounted, onUnmounted, ref } from 'vue';
import {
Chart,
ArcElement,
@@ -25,6 +25,7 @@ import number from '@/filters/number';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import { chartVLine } from '@/scripts/chart-vline';
Chart.register(
ArcElement,
@@ -44,8 +45,7 @@ Chart.register(
);
const props = defineProps<{
- domain: string;
- connection: any;
+ type: string;
}>();
const alpha = (hex, a) => {
@@ -67,81 +67,59 @@ const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart;
-const onStats = (stats) => {
- chartInstance.data.labels.push('');
- chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
- chartInstance.data.datasets[1].data.push(stats[props.domain].active);
- chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
- chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
- if (chartInstance.data.datasets[0].data.length > 100) {
- chartInstance.data.labels.shift();
- chartInstance.data.datasets[0].data.shift();
- chartInstance.data.datasets[1].data.shift();
- chartInstance.data.datasets[2].data.shift();
- chartInstance.data.datasets[3].data.shift();
- }
- chartInstance.update();
-};
-
-const onStatsLog = (statsLog) => {
- for (const stats of [...statsLog].reverse()) {
+function setData(values) {
+ if (chartInstance == null) return;
+ for (const value of values) {
chartInstance.data.labels.push('');
- chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
- chartInstance.data.datasets[1].data.push(stats[props.domain].active);
- chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
- chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ chartInstance.data.datasets[0].data.push(value);
if (chartInstance.data.datasets[0].data.length > 100) {
chartInstance.data.labels.shift();
chartInstance.data.datasets[0].data.shift();
- chartInstance.data.datasets[1].data.shift();
- chartInstance.data.datasets[2].data.shift();
- chartInstance.data.datasets[3].data.shift();
}
}
chartInstance.update();
-};
+}
+
+function pushData(value) {
+ if (chartInstance == null) return;
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(value);
+ if (chartInstance.data.datasets[0].data.length > 100) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ }
+ chartInstance.update();
+}
+
+const label =
+ props.type === 'process' ? 'Process' :
+ props.type === 'active' ? 'Active' :
+ props.type === 'delayed' ? 'Delayed' :
+ props.type === 'waiting' ? 'Waiting' :
+ '?' as never;
+
+const color =
+ props.type === 'process' ? '#00E396' :
+ props.type === 'active' ? '#00BCD4' :
+ props.type === 'delayed' ? '#E53935' :
+ props.type === 'waiting' ? '#FFB300' :
+ '?' as never;
onMounted(() => {
+ const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+
chartInstance = new Chart(chartEl.value, {
type: 'line',
data: {
labels: [],
datasets: [{
- label: 'Process',
+ label: label,
pointRadius: 0,
tension: 0.3,
borderWidth: 2,
borderJoinStyle: 'round',
- borderColor: '#00E396',
- backgroundColor: alpha('#00E396', 0.1),
- data: [],
- }, {
- label: 'Active',
- pointRadius: 0,
- tension: 0.3,
- borderWidth: 2,
- borderJoinStyle: 'round',
- borderColor: '#00BCD4',
- backgroundColor: alpha('#00BCD4', 0.1),
- data: [],
- }, {
- label: 'Waiting',
- pointRadius: 0,
- tension: 0.3,
- borderWidth: 2,
- borderJoinStyle: 'round',
- borderColor: '#FFB300',
- backgroundColor: alpha('#FFB300', 0.1),
- data: [],
- }, {
- label: 'Delayed',
- pointRadius: 0,
- tension: 0.3,
- borderWidth: 2,
- borderJoinStyle: 'round',
- borderColor: '#E53935',
- borderDash: [5, 5],
- fill: false,
+ borderColor: color,
+ backgroundColor: alpha(color, 0.1),
data: [],
}],
},
@@ -157,9 +135,10 @@ onMounted(() => {
},
scales: {
x: {
- display: false,
grid: {
display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
},
ticks: {
display: false,
@@ -167,13 +146,10 @@ onMounted(() => {
},
},
y: {
- display: false,
min: 0,
grid: {
- display: false,
- },
- ticks: {
- display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
},
},
},
@@ -194,15 +170,13 @@ onMounted(() => {
},
},
},
+ plugins: [chartVLine(vLineColor)],
});
-
- props.connection.on('stats', onStats);
- props.connection.on('statsLog', onStatsLog);
});
-onUnmounted(() => {
- props.connection.off('stats', onStats);
- props.connection.off('statsLog', onStatsLog);
+defineExpose({
+ setData,
+ pushData,
});
</script>
diff --git a/packages/client/src/pages/admin/overview.queue.vue b/packages/client/src/pages/admin/overview.queue.vue
new file mode 100644
index 0000000000..9fd799e158
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.queue.vue
@@ -0,0 +1,127 @@
+<template>
+<div :class="$style.root">
+ <div class="_table status">
+ <div class="_row">
+ <div class="_cell" style="text-align: center;"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
+ <div class="_cell" style="text-align: center;"><div class="_label">Active</div>{{ number(active) }}</div>
+ <div class="_cell" style="text-align: center;"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
+ <div class="_cell" style="text-align: center;"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
+ </div>
+ </div>
+ <div class="charts">
+ <div class="chart">
+ <div class="title">Process</div>
+ <XChart ref="chartProcess" type="process"/>
+ </div>
+ <div class="chart">
+ <div class="title">Active</div>
+ <XChart ref="chartActive" type="active"/>
+ </div>
+ <div class="chart">
+ <div class="title">Delayed</div>
+ <XChart ref="chartDelayed" type="delayed"/>
+ </div>
+ <div class="chart">
+ <div class="title">Waiting</div>
+ <XChart ref="chartWaiting" type="waiting"/>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { markRaw, onMounted, onUnmounted, ref } from 'vue';
+import XChart from './overview.queue.chart.vue';
+import number from '@/filters/number';
+import * as os from '@/os';
+import { stream } from '@/stream';
+import { i18n } from '@/i18n';
+
+const connection = markRaw(stream.useChannel('queueStats'));
+
+const activeSincePrevTick = ref(0);
+const active = ref(0);
+const delayed = ref(0);
+const waiting = ref(0);
+let chartProcess = $ref<InstanceType<typeof XChart>>();
+let chartActive = $ref<InstanceType<typeof XChart>>();
+let chartDelayed = $ref<InstanceType<typeof XChart>>();
+let chartWaiting = $ref<InstanceType<typeof XChart>>();
+
+const props = defineProps<{
+ domain: string;
+}>();
+
+const onStats = (stats) => {
+ activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
+ active.value = stats[props.domain].active;
+ delayed.value = stats[props.domain].delayed;
+ waiting.value = stats[props.domain].waiting;
+
+ chartProcess.pushData(stats[props.domain].activeSincePrevTick);
+ chartActive.pushData(stats[props.domain].active);
+ chartDelayed.pushData(stats[props.domain].delayed);
+ chartWaiting.pushData(stats[props.domain].waiting);
+};
+
+const onStatsLog = (statsLog) => {
+ const dataProcess = [];
+ const dataActive = [];
+ const dataDelayed = [];
+ const dataWaiting = [];
+
+ for (const stats of [...statsLog].reverse()) {
+ dataProcess.push(stats[props.domain].activeSincePrevTick);
+ dataActive.push(stats[props.domain].active);
+ dataDelayed.push(stats[props.domain].delayed);
+ dataWaiting.push(stats[props.domain].waiting);
+ }
+
+ chartProcess.setData(dataProcess);
+ chartActive.setData(dataActive);
+ chartDelayed.setData(dataDelayed);
+ chartWaiting.setData(dataWaiting);
+};
+
+onMounted(() => {
+ connection.on('stats', onStats);
+ connection.on('statsLog', onStatsLog);
+ connection.send('requestLog', {
+ id: Math.random().toString().substr(2, 8),
+ length: 100,
+ });
+});
+
+onUnmounted(() => {
+ connection.off('stats', onStats);
+ connection.off('statsLog', onStatsLog);
+ connection.dispose();
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ &:global {
+ > .status {
+ padding: 0 0 16px 0;
+ }
+
+ > .charts {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+
+ > .chart {
+ min-width: 0;
+ padding: 16px;
+ background: var(--panel);
+ border-radius: var(--radius);
+
+ > .title {
+ font-size: 0.85em;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/pages/admin/overview.stats.vue b/packages/client/src/pages/admin/overview.stats.vue
new file mode 100644
index 0000000000..dc9146cffd
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.stats.vue
@@ -0,0 +1,153 @@
+<template>
+<div>
+ <MkLoading v-if="fetching"/>
+ <div v-else :class="$style.root">
+ <div class="item _panel users">
+ <div class="icon"><i class="ti ti-users"></i></div>
+ <div class="body">
+ <div class="value">
+ {{ number(stats.originalUsersCount) }}
+ <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
+ </div>
+ <div class="label">Users</div>
+ </div>
+ </div>
+ <div class="item _panel notes">
+ <div class="icon"><i class="ti ti-pencil"></i></div>
+ <div class="body">
+ <div class="value">
+ {{ number(stats.originalNotesCount) }}
+ <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
+ </div>
+ <div class="label">Notes</div>
+ </div>
+ </div>
+ <div class="item _panel instances">
+ <div class="icon"><i class="ti ti-planet"></i></div>
+ <div class="body">
+ <div class="value">
+ {{ number(stats.instances) }}
+ </div>
+ <div class="label">Instances</div>
+ </div>
+ </div>
+ <div class="item _panel online">
+ <div class="icon"><i class="ti ti-access-point"></i></div>
+ <div class="body">
+ <div class="value">
+ {{ number(onlineUsersCount) }}
+ </div>
+ <div class="label">Online</div>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import MkMiniChart from '@/components/MkMiniChart.vue';
+import * as os from '@/os';
+import number from '@/filters/number';
+import MkNumberDiff from '@/components/MkNumberDiff.vue';
+import { i18n } from '@/i18n';
+
+let stats: any = $ref(null);
+let usersComparedToThePrevDay = $ref<number>();
+let notesComparedToThePrevDay = $ref<number>();
+let onlineUsersCount = $ref(0);
+let fetching = $ref(true);
+
+onMounted(async () => {
+ const [_stats, _onlineUsersCount] = await Promise.all([
+ os.api('stats', {}),
+ os.api('get-online-users-count').then(res => res.count),
+ ]);
+ stats = _stats;
+ onlineUsersCount = _onlineUsersCount;
+
+ os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
+ usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
+ });
+
+ os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
+ notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
+ });
+
+ fetching = false;
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
+ grid-gap: 16px;
+
+ &:global {
+ > .item {
+ display: flex;
+ box-sizing: border-box;
+ padding: 12px;
+
+ > .icon {
+ display: grid;
+ place-items: center;
+ height: 100%;
+ aspect-ratio: 1;
+ margin-right: 12px;
+ background: var(--accentedBg);
+ color: var(--accent);
+ border-radius: 10px;
+ }
+
+ &.users {
+ > .icon {
+ background: #0088d726;
+ color: #3d96c1;
+ }
+ }
+
+ &.notes {
+ > .icon {
+ background: #86b30026;
+ color: #86b300;
+ }
+ }
+
+ &.instances {
+ > .icon {
+ background: #e96b0026;
+ color: #d76d00;
+ }
+ }
+
+ &.online {
+ > .icon {
+ background: #8a00d126;
+ color: #c01ac3;
+ }
+ }
+
+ > .body {
+ padding: 4px 0;
+
+ > .value {
+ font-size: 1.3em;
+ font-weight: bold;
+
+ > .diff {
+ font-size: 0.65em;
+ font-weight: normal;
+ }
+ }
+
+ > .label {
+ font-size: 0.8em;
+ opacity: 0.5;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/pages/admin/overview.user.vue b/packages/client/src/pages/admin/overview.user.vue
deleted file mode 100644
index 0dd4a749ba..0000000000
--- a/packages/client/src/pages/admin/overview.user.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<MkA :class="[$style.root]" :to="`/user-info/${user.id}`">
- <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
- <div class="body">
- <span class="name"><MkUserName class="name" :user="user"/></span>
- <span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
- </div>
- <MkMiniChart v-if="chart" class="chart" :src="chart.inc"/>
-</MkA>
-</template>
-
-<script lang="ts" setup>
-import * as misskey from 'misskey-js';
-import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os';
-import { acct } from '@/filters/user';
-
-const props = defineProps<{
- user: misskey.entities.User;
-}>();
-
-let chart = $ref(null);
-
-os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => {
- chart = res;
-});
-</script>
-
-<style lang="scss" module>
-.root {
- $bodyTitleHieght: 18px;
- $bodyInfoHieght: 16px;
-
- display: flex;
- align-items: center;
-
- > :global(.avatar) {
- display: block;
- width: ($bodyTitleHieght + $bodyInfoHieght);
- height: ($bodyTitleHieght + $bodyInfoHieght);
- margin-right: 12px;
- }
-
- > :global(.body) {
- flex: 1;
- overflow: hidden;
- font-size: 0.9em;
- color: var(--fg);
- padding-right: 8px;
-
- > :global(.name) {
- display: block;
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- line-height: $bodyTitleHieght;
- }
-
- > :global(.sub) {
- display: block;
- width: 100%;
- font-size: 95%;
- opacity: 0.7;
- line-height: $bodyInfoHieght;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
-
- > :global(.chart) {
- height: 30px;
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/overview.users.vue b/packages/client/src/pages/admin/overview.users.vue
new file mode 100644
index 0000000000..05d7221c10
--- /dev/null
+++ b/packages/client/src/pages/admin/overview.users.vue
@@ -0,0 +1,55 @@
+<template>
+<div :class="$style.root">
+ <MkLoading v-if="fetching"/>
+ <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="users">
+ <MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/user-info/${user.id}`" class="user">
+ <MkUserCardMini :user="user"/>
+ </MkA>
+ </transition-group>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import * as os from '@/os';
+import { useInterval } from '@/scripts/use-interval';
+import MkUserCardMini from '@/components/MkUserCardMini.vue';
+
+let newUsers = $ref(null);
+let fetching = $ref(true);
+
+const fetch = async () => {
+ const _newUsers = await os.api('admin/show-users', {
+ limit: 5,
+ sort: '+createdAt',
+ origin: 'local',
+ });
+ newUsers = _newUsers;
+ fetching = false;
+};
+
+useInterval(fetch, 1000 * 60, {
+ immediate: true,
+ afterMounted: true,
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ &:global {
+ > .users {
+ .chart-move {
+ transition: transform 1s ease;
+ }
+
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
+ grid-gap: 12px;
+
+ > .user:hover {
+ text-decoration: none;
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index 6c1f54186c..786a908720 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -1,207 +1,66 @@
<template>
-<MkSpacer :content-max="900">
+<MkSpacer :content-max="1000">
<div ref="rootEl" v-size="{ max: [740] }" class="edbbcaef">
- <div class="left">
- <div v-if="stats" class="container stats">
- <div class="title">Stats</div>
- <div class="body">
- <div class="number _panel">
- <div class="label">Users</div>
- <div class="value _monospace">
- {{ number(stats.originalUsersCount) }}
- <MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
- </div>
- </div>
- <div class="number _panel">
- <div class="label">Notes</div>
- <div class="value _monospace">
- {{ number(stats.originalNotesCount) }}
- <MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
- </div>
- </div>
- </div>
- </div>
-
- <div class="container queue">
- <div class="title">Job queue</div>
- <div class="body">
- <div class="chart deliver">
- <div class="title">Deliver</div>
- <XQueueChart :connection="queueStatsConnection" domain="deliver"/>
- </div>
- <div class="chart inbox">
- <div class="title">Inbox</div>
- <XQueueChart :connection="queueStatsConnection" domain="inbox"/>
- </div>
- </div>
- </div>
-
- <div class="container users">
- <div class="title">New users</div>
- <div v-if="newUsers" class="body">
- <XUser v-for="user in newUsers" :key="user.id" class="user" :user="user"/>
- </div>
- </div>
-
- <div class="container files">
- <div class="title">Recent files</div>
- <div class="body">
- <MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
- </div>
- </div>
-
- <div class="container env">
- <div class="title">Environment</div>
- <div class="body">
- <div class="number _panel">
- <div class="label">Misskey</div>
- <div class="value _monospace">{{ version }}</div>
- </div>
- <div v-if="serverInfo" class="number _panel">
- <div class="label">Node.js</div>
- <div class="value _monospace">{{ serverInfo.node }}</div>
- </div>
- <div v-if="serverInfo" class="number _panel">
- <div class="label">PostgreSQL</div>
- <div class="value _monospace">{{ serverInfo.psql }}</div>
- </div>
- <div v-if="serverInfo" class="number _panel">
- <div class="label">Redis</div>
- <div class="value _monospace">{{ serverInfo.redis }}</div>
- </div>
- <div class="number _panel">
- <div class="label">Vue</div>
- <div class="value _monospace">{{ vueVersion }}</div>
- </div>
- </div>
- </div>
- </div>
- <div class="right">
- <div class="container charts">
- <div class="title">Active users</div>
- <div class="body">
- <canvas ref="chartEl"></canvas>
- </div>
- </div>
- <div class="container federation">
- <div class="title">Active instances</div>
- <div class="body">
- <XFederation/>
- </div>
- </div>
- <div v-if="stats" class="container federationStats">
- <div class="title">Federation</div>
- <div class="body">
- <div class="number _panel">
- <div class="label">Sub</div>
- <div class="value _monospace">
- {{ number(federationSubActive) }}
- <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
- </div>
- </div>
- <div class="number _panel">
- <div class="label">Pub</div>
- <div class="value _monospace">
- {{ number(federationPubActive) }}
- <MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"><template #before>(</template><template #after>)</template></MkNumberDiff>
- </div>
- </div>
- </div>
- </div>
- <div class="container tagCloud">
- <div class="body">
- <MkTagCloud v-if="activeInstances">
- <li v-for="instance in activeInstances">
- <a @click.prevent="onInstanceClick(instance)">
- <img style="width: 32px;" :src="instance.iconUrl">
- </a>
- </li>
- </MkTagCloud>
- </div>
- </div>
- <div v-if="topSubInstancesForPie && topPubInstancesForPie" class="container federationPies">
- <div class="body">
- <div class="chart deliver">
- <div class="title">Sub</div>
- <XPie :data="topSubInstancesForPie"/>
- <div class="subTitle">Top 10</div>
- </div>
- <div class="chart inbox">
- <div class="title">Pub</div>
- <XPie :data="topPubInstancesForPie"/>
- <div class="subTitle">Top 10</div>
- </div>
- </div>
- </div>
- </div>
+ <MkFolder class="item">
+ <template #header>Stats</template>
+ <XStats/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>Active users</template>
+ <XActiveUsers/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>Federation</template>
+ <XFederation/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>Instances</template>
+ <XInstances/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>Ap requests</template>
+ <XApRequests/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>New users</template>
+ <XUsers/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>Deliver queue</template>
+ <XQueue domain="deliver"/>
+ </MkFolder>
+ <MkFolder class="item">
+ <template #header>Inbox queue</template>
+ <XQueue domain="inbox"/>
+ </MkFolder>
</div>
</MkSpacer>
</template>
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
-import {
- Chart,
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
-} from 'chart.js';
-import { enUS } from 'date-fns/locale';
-import tinycolor from 'tinycolor2';
import XFederation from './overview.federation.vue';
-import XQueueChart from './overview.queue-chart.vue';
-import XUser from './overview.user.vue';
-import XPie from './overview.pie.vue';
-import MkNumberDiff from '@/components/MkNumberDiff.vue';
+import XInstances from './overview.instances.vue';
+import XQueue from './overview.queue.vue';
+import XApRequests from './overview.ap-requests.vue';
+import XUsers from './overview.users.vue';
+import XActiveUsers from './overview.active-users.vue';
+import XStats from './overview.stats.vue';
import MkTagCloud from '@/components/MkTagCloud.vue';
import { version, url } from '@/config';
-import number from '@/filters/number';
import * as os from '@/os';
import { stream } from '@/stream';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import 'chartjs-adapter-date-fns';
import { defaultStore } from '@/store';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
-
-Chart.register(
- ArcElement,
- LineElement,
- BarElement,
- PointElement,
- BarController,
- LineController,
- CategoryScale,
- LinearScale,
- TimeScale,
- Legend,
- Title,
- Tooltip,
- SubTitle,
- Filler,
- //gradient,
-);
+import MkFolder from '@/components/MkFolder.vue';
const rootEl = $ref<HTMLElement>();
-const chartEl = $ref<HTMLCanvasElement>(null);
-let stats: any = $ref(null);
let serverInfo: any = $ref(null);
let topSubInstancesForPie: any = $ref(null);
let topPubInstancesForPie: any = $ref(null);
-let usersComparedToThePrevDay: any = $ref(null);
-let notesComparedToThePrevDay: any = $ref(null);
let federationPubActive = $ref<number | null>(null);
let federationPubActiveDiff = $ref<number | null>(null);
let federationSubActive = $ref<number | null>(null);
@@ -210,170 +69,12 @@ let newUsers = $ref(null);
let activeInstances = $shallowRef(null);
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
const now = new Date();
-let chartInstance: Chart = null;
-const chartLimit = 30;
const filesPagination = {
endpoint: 'admin/drive/files' as const,
limit: 9,
noPaging: true,
};
-const { handler: externalTooltipHandler } = useChartTooltip();
-
-async function renderChart() {
- if (chartInstance) {
- chartInstance.destroy();
- }
-
- const getDate = (ago: number) => {
- const y = now.getFullYear();
- const m = now.getMonth();
- const d = now.getDate();
-
- return new Date(y, m, d - ago);
- };
-
- const format = (arr) => {
- return arr.map((v, i) => ({
- x: getDate(i).getTime(),
- y: v,
- }));
- };
-
- const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
-
- const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
- const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
-
- // フォントカラー
- Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
-
- const color = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
-
- chartInstance = new Chart(chartEl, {
- type: 'bar',
- data: {
- //labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
- datasets: [{
- parsing: false,
- label: 'a',
- data: format(raw.readWrite).slice().reverse(),
- tension: 0.3,
- pointRadius: 0,
- borderWidth: 0,
- borderJoinStyle: 'round',
- borderRadius: 3,
- backgroundColor: color,
- /*gradient: props.bar ? undefined : {
- backgroundColor: {
- axis: 'y',
- colors: {
- 0: alpha(x.color ? x.color : getColor(i), 0),
- [maxes[i]]: alpha(x.color ? x.color : getColor(i), 0.2),
- },
- },
- },*/
- barPercentage: 0.9,
- categoryPercentage: 0.9,
- clip: 8,
- }],
- },
- options: {
- aspectRatio: 2.5,
- layout: {
- padding: {
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- type: 'time',
- display: false,
- stacked: true,
- offset: false,
- time: {
- stepSize: 1,
- unit: 'month',
- },
- grid: {
- display: false,
- },
- ticks: {
- display: false,
- },
- adapters: {
- date: {
- locale: enUS,
- },
- },
- min: getDate(chartLimit).getTime(),
- },
- y: {
- display: false,
- position: 'left',
- stacked: true,
- grid: {
- display: false,
- },
- ticks: {
- display: false,
- //mirror: true,
- },
- },
- },
- interaction: {
- intersect: false,
- mode: 'index',
- },
- elements: {
- point: {
- hoverRadius: 5,
- hoverBorderWidth: 2,
- },
- },
- animation: false,
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- //gradient,
- },
- },
- plugins: [{
- id: 'vLine',
- beforeDraw(chart, args, options) {
- if (chart.tooltip?._active?.length) {
- const activePoint = chart.tooltip._active[0];
- const ctx = chart.ctx;
- const x = activePoint.element.x;
- const topY = chart.scales.y.top;
- const bottomY = chart.scales.y.bottom;
-
- ctx.save();
- ctx.beginPath();
- ctx.moveTo(x, bottomY);
- ctx.lineTo(x, topY);
- ctx.lineWidth = 1;
- ctx.strokeStyle = vLineColor;
- ctx.stroke();
- ctx.restore();
- }
- },
- }],
- });
-}
-
function onInstanceClick(i) {
os.pageWindow(`/instance-info/${i.host}`);
}
@@ -389,20 +90,6 @@ onMounted(async () => {
magicGrid.listen();
*/
- renderChart();
-
- os.api('stats', {}).then(statsResponse => {
- stats = statsResponse;
-
- os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
- usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
- });
-
- os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
- notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
- });
- });
-
os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
federationPubActive = chart.pubActive[0];
federationPubActiveDiff = chart.pubActive[0] - chart.pubActive[1];
@@ -471,165 +158,8 @@ definePageMetadata({
<style lang="scss" scoped>
.edbbcaef {
- display: flex;
-
- > .left, > .right {
- box-sizing: border-box;
- width: 50%;
-
- > .container {
- margin: 32px 0;
-
- > .title {
- font-weight: bold;
- margin-bottom: 16px;
- }
-
- &.stats, &.federationStats {
- > .body {
- display: grid;
- grid-gap: 16px;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
-
- > .number {
- padding: 14px 20px;
-
- > .label {
- opacity: 0.7;
- font-size: 0.8em;
- }
-
- > .value {
- font-weight: bold;
- font-size: 1.5em;
-
- > .diff {
- font-size: 0.7em;
- }
- }
- }
- }
- }
-
- &.env {
- > .body {
- display: grid;
- grid-gap: 16px;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
-
- > .number {
- padding: 14px 20px;
-
- > .label {
- opacity: 0.7;
- font-size: 0.8em;
- }
-
- > .value {
- font-size: 1.1em;
- }
- }
- }
- }
-
- &.charts {
- > .body {
- padding: 32px;
- background: var(--panel);
- border-radius: var(--radius);
- }
- }
-
- &.users {
- > .body {
- background: var(--panel);
- border-radius: var(--radius);
-
- > .user {
- padding: 16px 20px;
-
- &:not(:last-child) {
- border-bottom: solid 0.5px var(--divider);
- }
- }
- }
- }
-
- &.federation {
- > .body {
- background: var(--panel);
- border-radius: var(--radius);
- overflow: clip;
- }
- }
-
- &.queue {
- > .body {
- display: grid;
- grid-gap: 16px;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
-
- > .chart {
- position: relative;
- padding: 20px;
- background: var(--panel);
- border-radius: var(--radius);
-
- > .title {
- position: absolute;
- top: 20px;
- left: 20px;
- font-size: 90%;
- }
- }
- }
- }
-
- &.federationPies {
- > .body {
- display: grid;
- grid-gap: 16px;
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
-
- > .chart {
- position: relative;
- padding: 20px;
- background: var(--panel);
- border-radius: var(--radius);
-
- > .title {
- position: absolute;
- top: 20px;
- left: 20px;
- font-size: 90%;
- }
-
- > .subTitle {
- position: absolute;
- bottom: 20px;
- right: 20px;
- font-size: 85%;
- }
- }
- }
- }
-
- &.tagCloud {
- > .body {
- background: var(--panel);
- border-radius: var(--radius);
- overflow: clip;
- }
- }
- }
- }
-
- > .left {
- padding-right: 16px;
- }
-
- > .right {
- padding-left: 16px;
- }
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+ grid-gap: 16px;
}
</style>
diff --git a/packages/client/src/pages/admin/queue.chart.chart.vue b/packages/client/src/pages/admin/queue.chart.chart.vue
index 96156f8e67..8cb9d28b46 100644
--- a/packages/client/src/pages/admin/queue.chart.chart.vue
+++ b/packages/client/src/pages/admin/queue.chart.chart.vue
@@ -25,6 +25,7 @@ import number from '@/filters/number';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
+import { chartVLine } from '@/scripts/chart-vline';
Chart.register(
ArcElement,
@@ -105,6 +106,8 @@ const color =
'?' as never;
onMounted(() => {
+ const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+
chartInstance = new Chart(chartEl.value, {
type: 'line',
data: {
@@ -167,6 +170,7 @@ onMounted(() => {
},
},
},
+ plugins: [chartVLine(vLineColor)],
});
});