diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-23 15:21:55 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-12-23 15:21:55 +0900 |
| commit | 052e667f0357a059dd17f46ccce68d2598146724 (patch) | |
| tree | 75226d51850fef5630446478b6fe70bafba67818 /packages/client/src/pages | |
| parent | 13.0.0-alpha.6 (diff) | |
| download | misskey-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.vue | 223 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.ap-requests.vue | 263 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.federation.vue | 221 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.instances.vue | 52 | ||||
| -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.vue | 127 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.stats.vue | 153 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.user.vue | 76 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.users.vue | 55 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/overview.vue | 556 | ||||
| -rw-r--r-- | packages/client/src/pages/admin/queue.chart.chart.vue | 4 |
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)], }); }); |