Migrate to Chart.js v3 (#7896)
* wip * wip * wip * wip * wip * wip * wip * 定期的にresync * Update overview.vue * wip * wip
This commit is contained in:
parent
e7660bc8db
commit
4e4c559db6
16 changed files with 980 additions and 1103 deletions
11
package.json
11
package.json
|
@ -60,6 +60,9 @@
|
||||||
"@types/jsonld": "1.5.6",
|
"@types/jsonld": "1.5.6",
|
||||||
"@types/katex": "0.11.1",
|
"@types/katex": "0.11.1",
|
||||||
"@types/koa": "2.13.4",
|
"@types/koa": "2.13.4",
|
||||||
|
"@types/koa__cors": "3.0.3",
|
||||||
|
"@types/koa__multer": "2.0.3",
|
||||||
|
"@types/koa__router": "8.0.8",
|
||||||
"@types/koa-bodyparser": "4.3.3",
|
"@types/koa-bodyparser": "4.3.3",
|
||||||
"@types/koa-cors": "0.0.2",
|
"@types/koa-cors": "0.0.2",
|
||||||
"@types/koa-favicon": "2.0.21",
|
"@types/koa-favicon": "2.0.21",
|
||||||
|
@ -67,9 +70,6 @@
|
||||||
"@types/koa-mount": "4.0.1",
|
"@types/koa-mount": "4.0.1",
|
||||||
"@types/koa-send": "4.1.3",
|
"@types/koa-send": "4.1.3",
|
||||||
"@types/koa-views": "7.0.0",
|
"@types/koa-views": "7.0.0",
|
||||||
"@types/koa__cors": "3.0.3",
|
|
||||||
"@types/koa__multer": "2.0.3",
|
|
||||||
"@types/koa__router": "8.0.8",
|
|
||||||
"@types/markdown-it": "12.2.3",
|
"@types/markdown-it": "12.2.3",
|
||||||
"@types/matter-js": "0.17.5",
|
"@types/matter-js": "0.17.5",
|
||||||
"@types/mocha": "8.2.3",
|
"@types/mocha": "8.2.3",
|
||||||
|
@ -119,7 +119,9 @@
|
||||||
"cafy": "15.2.1",
|
"cafy": "15.2.1",
|
||||||
"cbor": "8.0.2",
|
"cbor": "8.0.2",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"chart.js": "2.9.4",
|
"chart.js": "3.5.1",
|
||||||
|
"chartjs-adapter-date-fns": "2.0.0",
|
||||||
|
"chartjs-plugin-zoom": "1.1.1",
|
||||||
"cli-highlight": "2.1.11",
|
"cli-highlight": "2.1.11",
|
||||||
"compare-versions": "3.6.0",
|
"compare-versions": "3.6.0",
|
||||||
"concurrently": "6.3.0",
|
"concurrently": "6.3.0",
|
||||||
|
@ -127,6 +129,7 @@
|
||||||
"crc-32": "1.2.0",
|
"crc-32": "1.2.0",
|
||||||
"css-loader": "6.4.0",
|
"css-loader": "6.4.0",
|
||||||
"cssnano": "5.0.8",
|
"cssnano": "5.0.8",
|
||||||
|
"date-fns": "2.25.0",
|
||||||
"dateformat": "4.5.1",
|
"dateformat": "4.5.1",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"eslint": "8.0.1",
|
"eslint": "8.0.1",
|
||||||
|
|
628
src/client/components/chart.vue
Normal file
628
src/client/components/chart.vue
Normal file
|
@ -0,0 +1,628 @@
|
||||||
|
<template>
|
||||||
|
<canvas ref="chartEl"></canvas>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, onMounted, ref, watch, PropType } from 'vue';
|
||||||
|
import {
|
||||||
|
Chart,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
} from 'chart.js';
|
||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
import { enUS } from 'date-fns/locale';
|
||||||
|
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||||
|
import * as os from '@client/os';
|
||||||
|
import { defaultStore } from '@client/store';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
TimeScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle,
|
||||||
|
Filler,
|
||||||
|
zoomPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||||
|
const negate = arr => arr.map(x => -x);
|
||||||
|
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 colors = ['#008FFB', '#00E396', '#FEB019', '#FF4560'];
|
||||||
|
const getColor = (i) => {
|
||||||
|
return colors[i % colors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 90
|
||||||
|
},
|
||||||
|
span: {
|
||||||
|
type: String as PropType<'hour' | 'day'>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
detailed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const now = new Date();
|
||||||
|
let chartInstance: Chart = null;
|
||||||
|
let data: {
|
||||||
|
series: {
|
||||||
|
name: string;
|
||||||
|
type: 'line' | 'area';
|
||||||
|
color?: string;
|
||||||
|
borderDash?: number[];
|
||||||
|
hidden?: boolean;
|
||||||
|
data: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
const chartEl = ref<HTMLCanvasElement>(null);
|
||||||
|
const fetching = ref(true);
|
||||||
|
|
||||||
|
const getDate = (ago: number) => {
|
||||||
|
const y = now.getFullYear();
|
||||||
|
const m = now.getMonth();
|
||||||
|
const d = now.getDate();
|
||||||
|
const h = now.getHours();
|
||||||
|
|
||||||
|
return props.span === 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
||||||
|
};
|
||||||
|
|
||||||
|
const format = (arr) => {
|
||||||
|
return arr.map((v, i) => ({
|
||||||
|
x: getDate(i).getTime(),
|
||||||
|
y: v
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
|
// フォントカラー
|
||||||
|
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
|
chartInstance = new Chart(chartEl.value, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: new Array(props.limit).fill(0).map((_, i) => getDate(i).toLocaleString()).slice().reverse(),
|
||||||
|
datasets: data.series.map((x, i) => ({
|
||||||
|
parsing: false,
|
||||||
|
label: x.name,
|
||||||
|
data: x.data.slice().reverse(),
|
||||||
|
pointRadius: 0,
|
||||||
|
tension: 0,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: x.color ? x.color : getColor(i),
|
||||||
|
borderDash: x.borderDash || [],
|
||||||
|
borderJoinStyle: 'round',
|
||||||
|
backgroundColor: alpha(x.color ? x.color : getColor(i), 0.1),
|
||||||
|
fill: x.type === 'area',
|
||||||
|
hidden: !!x.hidden,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
aspectRatio: 2.5,
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
top: 16,
|
||||||
|
bottom: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
stepSize: 1,
|
||||||
|
unit: props.span === 'day' ? 'month' : 'day',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: props.detailed,
|
||||||
|
color: gridColor,
|
||||||
|
borderColor: 'rgb(0, 0, 0, 0)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: props.detailed,
|
||||||
|
},
|
||||||
|
adapters: {
|
||||||
|
date: {
|
||||||
|
locale: enUS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
min: getDate(props.limit).getTime(),
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
position: 'left',
|
||||||
|
grid: {
|
||||||
|
color: gridColor,
|
||||||
|
borderColor: 'rgb(0, 0, 0, 0)',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
display: props.detailed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'bottom',
|
||||||
|
labels: {
|
||||||
|
boxWidth: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
pan: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
wheel: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pinch: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
drag: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
mode: 'x',
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
x: {
|
||||||
|
min: 'original',
|
||||||
|
max: 'original',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: 'original',
|
||||||
|
max: 'original',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportData = () => {
|
||||||
|
// TODO
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFederationInstancesChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Instances',
|
||||||
|
type: 'area',
|
||||||
|
data: format(total
|
||||||
|
? raw.instance.total
|
||||||
|
: sum(raw.instance.inc, negate(raw.instance.dec))
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNotesChart = async (type: string): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'All',
|
||||||
|
type: 'line',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
data: format(type == 'combined'
|
||||||
|
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||||
|
: sum(raw[type].inc, negate(raw[type].dec))
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Renotes',
|
||||||
|
type: 'area',
|
||||||
|
data: format(type == 'combined'
|
||||||
|
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
|
||||||
|
: raw[type].diffs.renote
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Replies',
|
||||||
|
type: 'area',
|
||||||
|
data: format(type == 'combined'
|
||||||
|
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
|
||||||
|
: raw[type].diffs.reply
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Normal',
|
||||||
|
type: 'area',
|
||||||
|
data: format(type == 'combined'
|
||||||
|
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
|
||||||
|
: raw[type].diffs.normal
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchNotesTotalChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Combined',
|
||||||
|
type: 'line',
|
||||||
|
data: format(sum(raw.local.total, raw.remote.total)),
|
||||||
|
}, {
|
||||||
|
name: 'Local',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.local.total),
|
||||||
|
}, {
|
||||||
|
name: 'Remote',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.remote.total),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUsersChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Combined',
|
||||||
|
type: 'line',
|
||||||
|
data: format(total
|
||||||
|
? sum(raw.local.total, raw.remote.total)
|
||||||
|
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Local',
|
||||||
|
type: 'area',
|
||||||
|
data: format(total
|
||||||
|
? raw.local.total
|
||||||
|
: sum(raw.local.inc, negate(raw.local.dec))
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Remote',
|
||||||
|
type: 'area',
|
||||||
|
data: format(total
|
||||||
|
? raw.remote.total
|
||||||
|
: sum(raw.remote.inc, negate(raw.remote.dec))
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchActiveUsersChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Combined',
|
||||||
|
type: 'line',
|
||||||
|
data: format(sum(raw.local.users, raw.remote.users)),
|
||||||
|
}, {
|
||||||
|
name: 'Local',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.local.users),
|
||||||
|
}, {
|
||||||
|
name: 'Remote',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.remote.users),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDriveChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
bytes: true,
|
||||||
|
series: [{
|
||||||
|
name: 'All',
|
||||||
|
type: 'line',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
data: format(
|
||||||
|
sum(
|
||||||
|
raw.local.incSize,
|
||||||
|
negate(raw.local.decSize),
|
||||||
|
raw.remote.incSize,
|
||||||
|
negate(raw.remote.decSize)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Local +',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.local.incSize),
|
||||||
|
}, {
|
||||||
|
name: 'Local -',
|
||||||
|
type: 'area',
|
||||||
|
data: format(negate(raw.local.decSize)),
|
||||||
|
}, {
|
||||||
|
name: 'Remote +',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.remote.incSize),
|
||||||
|
}, {
|
||||||
|
name: 'Remote -',
|
||||||
|
type: 'area',
|
||||||
|
data: format(negate(raw.remote.decSize)),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDriveTotalChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
bytes: true,
|
||||||
|
series: [{
|
||||||
|
name: 'Combined',
|
||||||
|
type: 'line',
|
||||||
|
data: format(sum(raw.local.totalSize, raw.remote.totalSize)),
|
||||||
|
}, {
|
||||||
|
name: 'Local',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.local.totalSize),
|
||||||
|
}, {
|
||||||
|
name: 'Remote',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.remote.totalSize),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDriveFilesChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'All',
|
||||||
|
type: 'line',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
data: format(
|
||||||
|
sum(
|
||||||
|
raw.local.incCount,
|
||||||
|
negate(raw.local.decCount),
|
||||||
|
raw.remote.incCount,
|
||||||
|
negate(raw.remote.decCount)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}, {
|
||||||
|
name: 'Local +',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.local.incCount),
|
||||||
|
}, {
|
||||||
|
name: 'Local -',
|
||||||
|
type: 'area',
|
||||||
|
data: format(negate(raw.local.decCount)),
|
||||||
|
}, {
|
||||||
|
name: 'Remote +',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.remote.incCount),
|
||||||
|
}, {
|
||||||
|
name: 'Remote -',
|
||||||
|
type: 'area',
|
||||||
|
data: format(negate(raw.remote.decCount)),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDriveFilesTotalChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Combined',
|
||||||
|
type: 'line',
|
||||||
|
data: format(sum(raw.local.totalCount, raw.remote.totalCount)),
|
||||||
|
}, {
|
||||||
|
name: 'Local',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.local.totalCount),
|
||||||
|
}, {
|
||||||
|
name: 'Remote',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.remote.totalCount),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInstanceRequestsChart = async (): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'In',
|
||||||
|
type: 'area',
|
||||||
|
color: '#008FFB',
|
||||||
|
data: format(raw.requests.received)
|
||||||
|
}, {
|
||||||
|
name: 'Out (succ)',
|
||||||
|
type: 'area',
|
||||||
|
color: '#00E396',
|
||||||
|
data: format(raw.requests.succeeded)
|
||||||
|
}, {
|
||||||
|
name: 'Out (fail)',
|
||||||
|
type: 'area',
|
||||||
|
color: '#FEB019',
|
||||||
|
data: format(raw.requests.failed)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Users',
|
||||||
|
type: 'area',
|
||||||
|
color: '#008FFB',
|
||||||
|
data: format(total
|
||||||
|
? raw.users.total
|
||||||
|
: sum(raw.users.inc, negate(raw.users.dec))
|
||||||
|
)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Notes',
|
||||||
|
type: 'area',
|
||||||
|
color: '#008FFB',
|
||||||
|
data: format(total
|
||||||
|
? raw.notes.total
|
||||||
|
: sum(raw.notes.inc, negate(raw.notes.dec))
|
||||||
|
)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInstanceFfChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Following',
|
||||||
|
type: 'area',
|
||||||
|
color: '#008FFB',
|
||||||
|
data: format(total
|
||||||
|
? raw.following.total
|
||||||
|
: sum(raw.following.inc, negate(raw.following.dec))
|
||||||
|
)
|
||||||
|
}, {
|
||||||
|
name: 'Followers',
|
||||||
|
type: 'area',
|
||||||
|
color: '#00E396',
|
||||||
|
data: format(total
|
||||||
|
? raw.followers.total
|
||||||
|
: sum(raw.followers.inc, negate(raw.followers.dec))
|
||||||
|
)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
bytes: true,
|
||||||
|
series: [{
|
||||||
|
name: 'Drive usage',
|
||||||
|
type: 'area',
|
||||||
|
color: '#008FFB',
|
||||||
|
data: format(total
|
||||||
|
? raw.drive.totalUsage
|
||||||
|
: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
|
||||||
|
)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof data> => {
|
||||||
|
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Drive files',
|
||||||
|
type: 'area',
|
||||||
|
color: '#008FFB',
|
||||||
|
data: format(total
|
||||||
|
? raw.drive.totalFiles
|
||||||
|
: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
|
||||||
|
)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAndRender = async () => {
|
||||||
|
const fetchData = () => {
|
||||||
|
switch (props.src) {
|
||||||
|
case 'federation-instances': return fetchFederationInstancesChart(false);
|
||||||
|
case 'federation-instances-total': return fetchFederationInstancesChart(true);
|
||||||
|
case 'users': return fetchUsersChart(false);
|
||||||
|
case 'users-total': return fetchUsersChart(true);
|
||||||
|
case 'active-users': return fetchActiveUsersChart();
|
||||||
|
case 'notes': return fetchNotesChart('combined');
|
||||||
|
case 'local-notes': return fetchNotesChart('local');
|
||||||
|
case 'remote-notes': return fetchNotesChart('remote');
|
||||||
|
case 'notes-total': return fetchNotesTotalChart();
|
||||||
|
case 'drive': return fetchDriveChart();
|
||||||
|
case 'drive-total': return fetchDriveTotalChart();
|
||||||
|
case 'drive-files': return fetchDriveFilesChart();
|
||||||
|
case 'drive-files-total': return fetchDriveFilesTotalChart();
|
||||||
|
|
||||||
|
case 'instances-requests': return fetchInstanceRequestsChart();
|
||||||
|
case 'instances-users': return fetchInstanceUsersChart(false);
|
||||||
|
case 'instances-users-total': return fetchInstanceUsersChart(true);
|
||||||
|
case 'instances-notes': return fetchInstanceNotesChart(false);
|
||||||
|
case 'instances-notes-total': return fetchInstanceNotesChart(true);
|
||||||
|
case 'instances-ff': return fetchInstanceFfChart(false);
|
||||||
|
case 'instances-ff-total': return fetchInstanceFfChart(true);
|
||||||
|
case 'instances-drive-usage': return fetchInstanceDriveUsageChart(false);
|
||||||
|
case 'instances-drive-usage-total': return fetchInstanceDriveUsageChart(true);
|
||||||
|
case 'instances-drive-files': return fetchInstanceDriveFilesChart(false);
|
||||||
|
case 'instances-drive-files-total': return fetchInstanceDriveFilesChart(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetching.value = true;
|
||||||
|
data = await fetchData();
|
||||||
|
fetching.value = false;
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => [props.src, props.span], fetchAndRender);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchAndRender();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartEl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -24,35 +24,26 @@
|
||||||
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
|
||||||
<option value="hour">{{ $ts.perHour }}</option>
|
<option value="hour">{{ $ts.perHour }}</option>
|
||||||
<option value="day">{{ $ts.perDay }}</option>
|
<option value="day">{{ $ts.perDay }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<canvas ref="chart"></canvas>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||||
import Chart from 'chart.js';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkSelect from './form/select.vue';
|
import MkChart from '@client/components/chart.vue';
|
||||||
import number from '@client/filters/number';
|
|
||||||
|
|
||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
|
||||||
const negate = arr => arr.map(x => -x);
|
|
||||||
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})`;
|
|
||||||
};
|
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
|
import { defaultStore } from '@client/store';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkSelect
|
MkSelect,
|
||||||
|
MkChart,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -68,463 +59,15 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
setup() {
|
||||||
|
const chartSpan = ref<'hour' | 'day'>('hour');
|
||||||
|
const chartSrc = ref('notes');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notesLocalWoW: 0,
|
chartSrc,
|
||||||
notesLocalDoD: 0,
|
chartSpan,
|
||||||
notesRemoteWoW: 0,
|
|
||||||
notesRemoteDoD: 0,
|
|
||||||
usersLocalWoW: 0,
|
|
||||||
usersLocalDoD: 0,
|
|
||||||
usersRemoteWoW: 0,
|
|
||||||
usersRemoteDoD: 0,
|
|
||||||
now: null,
|
|
||||||
chart: null,
|
|
||||||
chartInstance: null,
|
|
||||||
chartSrc: 'notes',
|
|
||||||
chartSpan: 'hour',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
data(): any {
|
|
||||||
if (this.chart == null) return null;
|
|
||||||
switch (this.chartSrc) {
|
|
||||||
case 'federation-instances': return this.federationInstancesChart(false);
|
|
||||||
case 'federation-instances-total': return this.federationInstancesChart(true);
|
|
||||||
case 'users': return this.usersChart(false);
|
|
||||||
case 'users-total': return this.usersChart(true);
|
|
||||||
case 'active-users': return this.activeUsersChart();
|
|
||||||
case 'notes': return this.notesChart('combined');
|
|
||||||
case 'local-notes': return this.notesChart('local');
|
|
||||||
case 'remote-notes': return this.notesChart('remote');
|
|
||||||
case 'notes-total': return this.notesTotalChart();
|
|
||||||
case 'drive': return this.driveChart();
|
|
||||||
case 'drive-total': return this.driveTotalChart();
|
|
||||||
case 'drive-files': return this.driveFilesChart();
|
|
||||||
case 'drive-files-total': return this.driveFilesTotalChart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stats(): any[] {
|
|
||||||
const stats =
|
|
||||||
this.chartSpan == 'day' ? this.chart.perDay :
|
|
||||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
|
||||||
null;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
chartSrc() {
|
|
||||||
this.renderChart();
|
|
||||||
},
|
|
||||||
|
|
||||||
chartSpan() {
|
|
||||||
this.renderChart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
this.now = new Date();
|
|
||||||
|
|
||||||
this.fetchChart();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async fetchChart() {
|
|
||||||
const [perHour, perDay] = await Promise.all([Promise.all([
|
|
||||||
os.api('charts/federation', { limit: this.chartLimit, span: 'hour' }),
|
|
||||||
os.api('charts/users', { limit: this.chartLimit, span: 'hour' }),
|
|
||||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'hour' }),
|
|
||||||
os.api('charts/notes', { limit: this.chartLimit, span: 'hour' }),
|
|
||||||
os.api('charts/drive', { limit: this.chartLimit, span: 'hour' }),
|
|
||||||
]), Promise.all([
|
|
||||||
os.api('charts/federation', { limit: this.chartLimit, span: 'day' }),
|
|
||||||
os.api('charts/users', { limit: this.chartLimit, span: 'day' }),
|
|
||||||
os.api('charts/active-users', { limit: this.chartLimit, span: 'day' }),
|
|
||||||
os.api('charts/notes', { limit: this.chartLimit, span: 'day' }),
|
|
||||||
os.api('charts/drive', { limit: this.chartLimit, span: 'day' }),
|
|
||||||
])]);
|
|
||||||
|
|
||||||
const chart = {
|
|
||||||
perHour: {
|
|
||||||
federation: perHour[0],
|
|
||||||
users: perHour[1],
|
|
||||||
activeUsers: perHour[2],
|
|
||||||
notes: perHour[3],
|
|
||||||
drive: perHour[4],
|
|
||||||
},
|
|
||||||
perDay: {
|
|
||||||
federation: perDay[0],
|
|
||||||
users: perDay[1],
|
|
||||||
activeUsers: perDay[2],
|
|
||||||
notes: perDay[3],
|
|
||||||
drive: perDay[4],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chart = chart;
|
|
||||||
|
|
||||||
this.renderChart();
|
|
||||||
},
|
|
||||||
|
|
||||||
renderChart() {
|
|
||||||
if (this.chartInstance) {
|
|
||||||
this.chartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
|
||||||
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
||||||
|
|
||||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
this.chartInstance = markRaw(new Chart(this.$refs.chart, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: new Array(this.chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
|
||||||
datasets: this.data.series.map(x => ({
|
|
||||||
label: x.name,
|
|
||||||
data: x.data.slice().reverse(),
|
|
||||||
pointRadius: 0,
|
|
||||||
lineTension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: x.color,
|
|
||||||
borderDash: x.borderDash || [],
|
|
||||||
backgroundColor: alpha(x.color, 0.1),
|
|
||||||
fill: x.fill == null ? true : x.fill,
|
|
||||||
hidden: !!x.hidden
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 8
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
boxWidth: 16,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
xAxes: [{
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
stepSize: 1,
|
|
||||||
unit: this.chartSpan == 'day' ? 'month' : 'day',
|
|
||||||
},
|
|
||||||
gridLines: {
|
|
||||||
display: this.detailed,
|
|
||||||
color: gridColor,
|
|
||||||
zeroLineColor: gridColor,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: this.detailed
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
yAxes: [{
|
|
||||||
position: 'left',
|
|
||||||
gridLines: {
|
|
||||||
color: gridColor,
|
|
||||||
zeroLineColor: gridColor,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: this.detailed
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
getDate(ago: number) {
|
|
||||||
const y = this.now.getFullYear();
|
|
||||||
const m = this.now.getMonth();
|
|
||||||
const d = this.now.getDate();
|
|
||||||
const h = this.now.getHours();
|
|
||||||
|
|
||||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
|
||||||
},
|
|
||||||
|
|
||||||
format(arr) {
|
|
||||||
const now = Date.now();
|
|
||||||
return arr.map((v, i) => ({
|
|
||||||
x: new Date(now - ((this.chartSpan == 'day' ? 86400000 :3600000 ) * i)),
|
|
||||||
y: v
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
federationInstancesChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Instances',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.federation.instance.total
|
|
||||||
: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
notesChart(type: string): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'All',
|
|
||||||
type: 'line',
|
|
||||||
color: '#008FFB',
|
|
||||||
borderDash: [5, 5],
|
|
||||||
fill: false,
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
|
|
||||||
: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Renotes',
|
|
||||||
type: 'area',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
|
|
||||||
: this.stats.notes[type].diffs.renote
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Replies',
|
|
||||||
type: 'area',
|
|
||||||
color: '#FEB019',
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
|
|
||||||
: this.stats.notes[type].diffs.reply
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Normal',
|
|
||||||
type: 'area',
|
|
||||||
color: '#FF4560',
|
|
||||||
data: this.format(type == 'combined'
|
|
||||||
? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
|
|
||||||
: this.stats.notes[type].diffs.normal
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
notesTotalChart(): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Combined',
|
|
||||||
type: 'line',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
|
|
||||||
}, {
|
|
||||||
name: 'Local',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.notes.local.total)
|
|
||||||
}, {
|
|
||||||
name: 'Remote',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.notes.remote.total)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
usersChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Combined',
|
|
||||||
type: 'line',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? sum(this.stats.users.local.total, this.stats.users.remote.total)
|
|
||||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Local',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.users.local.total
|
|
||||||
: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Remote',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.users.remote.total
|
|
||||||
: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activeUsersChart(): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Combined',
|
|
||||||
type: 'line',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(sum(this.stats.activeUsers.local.count, this.stats.activeUsers.remote.count))
|
|
||||||
}, {
|
|
||||||
name: 'Local',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.activeUsers.local.count)
|
|
||||||
}, {
|
|
||||||
name: 'Remote',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.activeUsers.remote.count)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveChart(): any {
|
|
||||||
return {
|
|
||||||
bytes: true,
|
|
||||||
series: [{
|
|
||||||
name: 'All',
|
|
||||||
type: 'line',
|
|
||||||
color: '#09d8e2',
|
|
||||||
borderDash: [5, 5],
|
|
||||||
fill: false,
|
|
||||||
data: this.format(
|
|
||||||
sum(
|
|
||||||
this.stats.drive.local.incSize,
|
|
||||||
negate(this.stats.drive.local.decSize),
|
|
||||||
this.stats.drive.remote.incSize,
|
|
||||||
negate(this.stats.drive.remote.decSize)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Local +',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(this.stats.drive.local.incSize)
|
|
||||||
}, {
|
|
||||||
name: 'Local -',
|
|
||||||
type: 'area',
|
|
||||||
color: '#FF4560',
|
|
||||||
data: this.format(negate(this.stats.drive.local.decSize))
|
|
||||||
}, {
|
|
||||||
name: 'Remote +',
|
|
||||||
type: 'area',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(this.stats.drive.remote.incSize)
|
|
||||||
}, {
|
|
||||||
name: 'Remote -',
|
|
||||||
type: 'area',
|
|
||||||
color: '#FEB019',
|
|
||||||
data: this.format(negate(this.stats.drive.remote.decSize))
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveTotalChart(): any {
|
|
||||||
return {
|
|
||||||
bytes: true,
|
|
||||||
series: [{
|
|
||||||
name: 'Combined',
|
|
||||||
type: 'line',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
|
|
||||||
}, {
|
|
||||||
name: 'Local',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.drive.local.totalSize)
|
|
||||||
}, {
|
|
||||||
name: 'Remote',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.drive.remote.totalSize)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveFilesChart(): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'All',
|
|
||||||
type: 'line',
|
|
||||||
color: '#09d8e2',
|
|
||||||
borderDash: [5, 5],
|
|
||||||
fill: false,
|
|
||||||
data: this.format(
|
|
||||||
sum(
|
|
||||||
this.stats.drive.local.incCount,
|
|
||||||
negate(this.stats.drive.local.decCount),
|
|
||||||
this.stats.drive.remote.incCount,
|
|
||||||
negate(this.stats.drive.remote.decCount)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Local +',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(this.stats.drive.local.incCount)
|
|
||||||
}, {
|
|
||||||
name: 'Local -',
|
|
||||||
type: 'area',
|
|
||||||
color: '#FF4560',
|
|
||||||
data: this.format(negate(this.stats.drive.local.decCount))
|
|
||||||
}, {
|
|
||||||
name: 'Remote +',
|
|
||||||
type: 'area',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(this.stats.drive.remote.incCount)
|
|
||||||
}, {
|
|
||||||
name: 'Remote -',
|
|
||||||
type: 'area',
|
|
||||||
color: '#FEB019',
|
|
||||||
data: this.format(negate(this.stats.drive.remote.decCount))
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveFilesTotalChart(): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Combined',
|
|
||||||
type: 'line',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
|
|
||||||
}, {
|
|
||||||
name: 'Local',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.drive.local.totalCount)
|
|
||||||
}, {
|
|
||||||
name: 'Remote',
|
|
||||||
type: 'area',
|
|
||||||
color: '#008FFB',
|
|
||||||
hidden: true,
|
|
||||||
data: this.format(this.stats.drive.remote.totalCount)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
number
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
47
src/client/components/number-diff.vue
Normal file
47
src/client/components/number-diff.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
|
||||||
|
<slot name="before"></slot>{{ isPlus ? '+' : isMinus ? '-' : '' }}{{ number(value) }}<slot name="after"></slot>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from 'vue';
|
||||||
|
import number from '@client/filters/number';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const isPlus = computed(() => props.value > 0);
|
||||||
|
const isMinus = computed(() => props.value < 0);
|
||||||
|
const isZero = computed(() => props.value === 0);
|
||||||
|
return {
|
||||||
|
isPlus,
|
||||||
|
isMinus,
|
||||||
|
isZero,
|
||||||
|
number,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ceaaebcd {
|
||||||
|
&.isPlus {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isMinus {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isZero {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -65,17 +65,17 @@
|
||||||
<div class="_debobigegoPanel cmhjzshl">
|
<div class="_debobigegoPanel cmhjzshl">
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
|
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||||
<option value="users">{{ $ts._instanceCharts.users }}</option>
|
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||||
<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||||
<option value="notes">{{ $ts._instanceCharts.notes }}</option>
|
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||||
<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||||
<option value="ff">{{ $ts._instanceCharts.ff }}</option>
|
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||||
<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||||
<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||||
<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||||
<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
|
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||||
<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||||
<option value="hour">{{ $ts.perHour }}</option>
|
<option value="hour">{{ $ts.perHour }}</option>
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<canvas :ref="setChart"></canvas>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
import Chart from 'chart.js';
|
import MkChart from '@client/components/chart.vue';
|
||||||
import FormObjectView from '@client/components/debobigego/object-view.vue';
|
import FormObjectView from '@client/components/debobigego/object-view.vue';
|
||||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
||||||
import FormLink from '@client/components/debobigego/link.vue';
|
import FormLink from '@client/components/debobigego/link.vue';
|
||||||
|
@ -151,17 +151,6 @@ import bytes from '@client/filters/bytes';
|
||||||
import * as symbols from '@client/symbols';
|
import * as symbols from '@client/symbols';
|
||||||
import MkInstanceInfo from '@client/pages/instance/instance.vue';
|
import MkInstanceInfo from '@client/pages/instance/instance.vue';
|
||||||
|
|
||||||
const chartLimit = 90;
|
|
||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
|
||||||
const negate = arr => arr.map(x => -x);
|
|
||||||
const alpha = hex => {
|
|
||||||
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}, 0.1)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
FormBase,
|
FormBase,
|
||||||
|
@ -173,6 +162,7 @@ export default defineComponent({
|
||||||
FormKeyValueView,
|
FormKeyValueView,
|
||||||
FormSuspense,
|
FormSuspense,
|
||||||
MkSelect,
|
MkSelect,
|
||||||
|
MkChart,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -199,53 +189,11 @@ export default defineComponent({
|
||||||
dnsPromiseFactory: () => os.api('federation/dns', {
|
dnsPromiseFactory: () => os.api('federation/dns', {
|
||||||
host: this.host
|
host: this.host
|
||||||
}),
|
}),
|
||||||
now: null,
|
chartSrc: 'instance-requests',
|
||||||
canvas: null,
|
|
||||||
chart: null,
|
|
||||||
chartInstance: null,
|
|
||||||
chartSrc: 'requests',
|
|
||||||
chartSpan: 'hour',
|
chartSpan: 'hour',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
data(): any {
|
|
||||||
if (this.chart == null) return null;
|
|
||||||
switch (this.chartSrc) {
|
|
||||||
case 'requests': return this.requestsChart();
|
|
||||||
case 'users': return this.usersChart(false);
|
|
||||||
case 'users-total': return this.usersChart(true);
|
|
||||||
case 'notes': return this.notesChart(false);
|
|
||||||
case 'notes-total': return this.notesChart(true);
|
|
||||||
case 'ff': return this.ffChart(false);
|
|
||||||
case 'ff-total': return this.ffChart(true);
|
|
||||||
case 'drive-usage': return this.driveUsageChart(false);
|
|
||||||
case 'drive-usage-total': return this.driveUsageChart(true);
|
|
||||||
case 'drive-files': return this.driveFilesChart(false);
|
|
||||||
case 'drive-files-total': return this.driveFilesChart(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stats(): any[] {
|
|
||||||
const stats =
|
|
||||||
this.chartSpan == 'day' ? this.chart.perDay :
|
|
||||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
|
||||||
null;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
chartSrc() {
|
|
||||||
this.renderChart();
|
|
||||||
},
|
|
||||||
|
|
||||||
chartSpan() {
|
|
||||||
this.renderChart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetch();
|
this.fetch();
|
||||||
},
|
},
|
||||||
|
@ -258,190 +206,6 @@ export default defineComponent({
|
||||||
this.instance = await os.api('federation/show-instance', {
|
this.instance = await os.api('federation/show-instance', {
|
||||||
host: this.host
|
host: this.host
|
||||||
});
|
});
|
||||||
|
|
||||||
this.now = new Date();
|
|
||||||
|
|
||||||
const [perHour, perDay] = await Promise.all([
|
|
||||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
|
|
||||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const chart = {
|
|
||||||
perHour: perHour,
|
|
||||||
perDay: perDay
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chart = chart;
|
|
||||||
|
|
||||||
this.renderChart();
|
|
||||||
},
|
|
||||||
|
|
||||||
setChart(el) {
|
|
||||||
this.canvas = el;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderChart() {
|
|
||||||
if (this.chartInstance) {
|
|
||||||
this.chartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
this.chartInstance = new Chart(this.canvas, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
|
||||||
datasets: this.data.series.map(x => ({
|
|
||||||
label: x.name,
|
|
||||||
data: x.data.slice().reverse(),
|
|
||||||
pointRadius: 0,
|
|
||||||
lineTension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: x.color,
|
|
||||||
backgroundColor: alpha(x.color),
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 16
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
boxWidth: 16,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
xAxes: [{
|
|
||||||
gridLines: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
yAxes: [{
|
|
||||||
position: 'right',
|
|
||||||
ticks: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getDate(ago: number) {
|
|
||||||
const y = this.now.getFullYear();
|
|
||||||
const m = this.now.getMonth();
|
|
||||||
const d = this.now.getDate();
|
|
||||||
const h = this.now.getHours();
|
|
||||||
|
|
||||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
|
||||||
},
|
|
||||||
|
|
||||||
format(arr) {
|
|
||||||
return arr;
|
|
||||||
},
|
|
||||||
|
|
||||||
requestsChart(): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'In',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(this.stats.requests.received)
|
|
||||||
}, {
|
|
||||||
name: 'Out (succ)',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(this.stats.requests.succeeded)
|
|
||||||
}, {
|
|
||||||
name: 'Out (fail)',
|
|
||||||
color: '#FEB019',
|
|
||||||
data: this.format(this.stats.requests.failed)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
usersChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Users',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.users.total
|
|
||||||
: sum(this.stats.users.inc, negate(this.stats.users.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
notesChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Notes',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.notes.total
|
|
||||||
: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
ffChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Following',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.following.total
|
|
||||||
: sum(this.stats.following.inc, negate(this.stats.following.dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Followers',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.followers.total
|
|
||||||
: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveUsageChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
bytes: true,
|
|
||||||
series: [{
|
|
||||||
name: 'Drive usage',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.drive.totalUsage
|
|
||||||
: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveFilesChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Drive files',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.drive.totalFiles
|
|
||||||
: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
info() {
|
info() {
|
||||||
|
|
|
@ -78,17 +78,17 @@
|
||||||
<span class="label">{{ $ts.charts }}</span>
|
<span class="label">{{ $ts.charts }}</span>
|
||||||
<div class="selects">
|
<div class="selects">
|
||||||
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
|
||||||
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
|
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
|
||||||
<option value="users">{{ $ts._instanceCharts.users }}</option>
|
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
|
||||||
<option value="users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
|
||||||
<option value="notes">{{ $ts._instanceCharts.notes }}</option>
|
<option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
|
||||||
<option value="notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
<option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
|
||||||
<option value="ff">{{ $ts._instanceCharts.ff }}</option>
|
<option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
|
||||||
<option value="ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
<option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
|
||||||
<option value="drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
<option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
|
||||||
<option value="drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
<option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
|
||||||
<option value="drive-files">{{ $ts._instanceCharts.files }}</option>
|
<option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
|
||||||
<option value="drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
<option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="chartSpan" style="margin: 0;">
|
<MkSelect v-model="chartSpan" style="margin: 0;">
|
||||||
<option value="hour">{{ $ts.perHour }}</option>
|
<option value="hour">{{ $ts.perHour }}</option>
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<canvas :ref="setChart"></canvas>
|
<MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="operations section">
|
<div class="operations section">
|
||||||
|
@ -124,28 +124,17 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { defineComponent, markRaw } from 'vue';
|
||||||
import Chart from 'chart.js';
|
|
||||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||||
import MkUsersDialog from '@client/components/users-dialog.vue';
|
import MkUsersDialog from '@client/components/users-dialog.vue';
|
||||||
import MkSelect from '@client/components/form/select.vue';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import MkSwitch from '@client/components/form/switch.vue';
|
import MkSwitch from '@client/components/form/switch.vue';
|
||||||
import MkInfo from '@client/components/ui/info.vue';
|
import MkInfo from '@client/components/ui/info.vue';
|
||||||
|
import MkChart from '@client/components/chart.vue';
|
||||||
import bytes from '@client/filters/bytes';
|
import bytes from '@client/filters/bytes';
|
||||||
import number from '@client/filters/number';
|
import number from '@client/filters/number';
|
||||||
import * as os from '@client/os';
|
import * as os from '@client/os';
|
||||||
|
|
||||||
const chartLimit = 90;
|
|
||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
|
||||||
const negate = arr => arr.map(x => -x);
|
|
||||||
const alpha = hex => {
|
|
||||||
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}, 0.1)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
XModalWindow,
|
XModalWindow,
|
||||||
|
@ -153,6 +142,7 @@ export default defineComponent({
|
||||||
MkButton,
|
MkButton,
|
||||||
MkSwitch,
|
MkSwitch,
|
||||||
MkInfo,
|
MkInfo,
|
||||||
|
MkChart,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -167,42 +157,12 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isSuspended: this.instance.isSuspended,
|
isSuspended: this.instance.isSuspended,
|
||||||
now: null,
|
|
||||||
canvas: null,
|
|
||||||
chart: null,
|
|
||||||
chartInstance: null,
|
|
||||||
chartSrc: 'requests',
|
chartSrc: 'requests',
|
||||||
chartSpan: 'hour',
|
chartSpan: 'hour',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
data(): any {
|
|
||||||
if (this.chart == null) return null;
|
|
||||||
switch (this.chartSrc) {
|
|
||||||
case 'requests': return this.requestsChart();
|
|
||||||
case 'users': return this.usersChart(false);
|
|
||||||
case 'users-total': return this.usersChart(true);
|
|
||||||
case 'notes': return this.notesChart(false);
|
|
||||||
case 'notes-total': return this.notesChart(true);
|
|
||||||
case 'ff': return this.ffChart(false);
|
|
||||||
case 'ff-total': return this.ffChart(true);
|
|
||||||
case 'drive-usage': return this.driveUsageChart(false);
|
|
||||||
case 'drive-usage-total': return this.driveUsageChart(true);
|
|
||||||
case 'drive-files': return this.driveFilesChart(false);
|
|
||||||
case 'drive-files-total': return this.driveFilesChart(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
stats(): any[] {
|
|
||||||
const stats =
|
|
||||||
this.chartSpan == 'day' ? this.chart.perDay :
|
|
||||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
|
||||||
null;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
},
|
|
||||||
|
|
||||||
meta() {
|
meta() {
|
||||||
return this.$instance;
|
return this.$instance;
|
||||||
},
|
},
|
||||||
|
@ -219,49 +179,15 @@ export default defineComponent({
|
||||||
isSuspended: this.isSuspended
|
isSuspended: this.isSuspended
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
chartSrc() {
|
|
||||||
this.renderChart();
|
|
||||||
},
|
|
||||||
|
|
||||||
chartSpan() {
|
|
||||||
this.renderChart();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async created() {
|
|
||||||
this.now = new Date();
|
|
||||||
|
|
||||||
const [perHour, perDay] = await Promise.all([
|
|
||||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
|
|
||||||
os.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const chart = {
|
|
||||||
perHour: perHour,
|
|
||||||
perDay: perDay
|
|
||||||
};
|
|
||||||
|
|
||||||
this.chart = chart;
|
|
||||||
|
|
||||||
this.renderChart();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setChart(el) {
|
|
||||||
this.canvas = el;
|
|
||||||
},
|
|
||||||
|
|
||||||
changeBlock(e) {
|
changeBlock(e) {
|
||||||
os.api('admin/update-meta', {
|
os.api('admin/update-meta', {
|
||||||
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
|
blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setSrc(src) {
|
|
||||||
this.chartSrc = src;
|
|
||||||
},
|
|
||||||
|
|
||||||
removeAllFollowing() {
|
removeAllFollowing() {
|
||||||
os.apiWithDialog('admin/federation/remove-all-following', {
|
os.apiWithDialog('admin/federation/remove-all-following', {
|
||||||
host: this.instance.host
|
host: this.instance.host
|
||||||
|
@ -274,170 +200,6 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renderChart() {
|
|
||||||
if (this.chartInstance) {
|
|
||||||
this.chartInstance.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
this.chartInstance = markRaw(new Chart(this.canvas, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
|
||||||
datasets: this.data.series.map(x => ({
|
|
||||||
label: x.name,
|
|
||||||
data: x.data.slice().reverse(),
|
|
||||||
pointRadius: 0,
|
|
||||||
lineTension: 0,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: x.color,
|
|
||||||
backgroundColor: alpha(x.color),
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
aspectRatio: 2.5,
|
|
||||||
layout: {
|
|
||||||
padding: {
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
top: 16,
|
|
||||||
bottom: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
boxWidth: 16,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
xAxes: [{
|
|
||||||
gridLines: {
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
yAxes: [{
|
|
||||||
position: 'right',
|
|
||||||
ticks: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
tooltips: {
|
|
||||||
intersect: false,
|
|
||||||
mode: 'index',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
getDate(ago: number) {
|
|
||||||
const y = this.now.getFullYear();
|
|
||||||
const m = this.now.getMonth();
|
|
||||||
const d = this.now.getDate();
|
|
||||||
const h = this.now.getHours();
|
|
||||||
|
|
||||||
return this.chartSpan == 'day' ? new Date(y, m, d - ago) : new Date(y, m, d, h - ago);
|
|
||||||
},
|
|
||||||
|
|
||||||
format(arr) {
|
|
||||||
return arr;
|
|
||||||
},
|
|
||||||
|
|
||||||
requestsChart(): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'In',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(this.stats.requests.received)
|
|
||||||
}, {
|
|
||||||
name: 'Out (succ)',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(this.stats.requests.succeeded)
|
|
||||||
}, {
|
|
||||||
name: 'Out (fail)',
|
|
||||||
color: '#FEB019',
|
|
||||||
data: this.format(this.stats.requests.failed)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
usersChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Users',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.users.total
|
|
||||||
: sum(this.stats.users.inc, negate(this.stats.users.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
notesChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Notes',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.notes.total
|
|
||||||
: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
ffChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Following',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.following.total
|
|
||||||
: sum(this.stats.following.inc, negate(this.stats.following.dec))
|
|
||||||
)
|
|
||||||
}, {
|
|
||||||
name: 'Followers',
|
|
||||||
color: '#00E396',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.followers.total
|
|
||||||
: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveUsageChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
bytes: true,
|
|
||||||
series: [{
|
|
||||||
name: 'Drive usage',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.drive.totalUsage
|
|
||||||
: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
driveFilesChart(total: boolean): any {
|
|
||||||
return {
|
|
||||||
series: [{
|
|
||||||
name: 'Drive files',
|
|
||||||
color: '#008FFB',
|
|
||||||
data: this.format(total
|
|
||||||
? this.stats.drive.totalFiles
|
|
||||||
: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
|
|
||||||
)
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
showFollowing() {
|
showFollowing() {
|
||||||
os.modal(MkUsersDialog, {
|
os.modal(MkUsersDialog, {
|
||||||
title: this.$ts.instanceFollowing,
|
title: this.$ts.instanceFollowing,
|
||||||
|
|
|
@ -52,7 +52,21 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, markRaw } from 'vue';
|
import { defineComponent, markRaw } from 'vue';
|
||||||
import Chart from 'chart.js';
|
import {
|
||||||
|
Chart,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle
|
||||||
|
} from 'chart.js';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import MkSelect from '@client/components/form/select.vue';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkInput from '@client/components/form/input.vue';
|
import MkInput from '@client/components/form/input.vue';
|
||||||
|
@ -64,6 +78,21 @@ import bytes from '@client/filters/bytes';
|
||||||
import number from '@client/filters/number';
|
import number from '@client/filters/number';
|
||||||
import MkInstanceInfo from './instance.vue';
|
import MkInstanceInfo from './instance.vue';
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
BarElement,
|
||||||
|
PointElement,
|
||||||
|
BarController,
|
||||||
|
LineController,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Legend,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
SubTitle
|
||||||
|
);
|
||||||
|
|
||||||
const alpha = (hex, a) => {
|
const alpha = (hex, a) => {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!;
|
||||||
const r = parseInt(result[1], 16);
|
const r = parseInt(result[1], 16);
|
||||||
|
@ -116,7 +145,7 @@ export default defineComponent({
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchJobs();
|
this.fetchJobs();
|
||||||
|
|
||||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
os.api('admin/server-info', {}).then(res => {
|
os.api('admin/server-info', {}).then(res => {
|
||||||
this.serverInfo = res;
|
this.serverInfo = res;
|
||||||
|
@ -157,7 +186,7 @@ export default defineComponent({
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'CPU',
|
label: 'CPU',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#86b300',
|
borderColor: '#86b300',
|
||||||
backgroundColor: alpha('#86b300', 0.1),
|
backgroundColor: alpha('#86b300', 0.1),
|
||||||
|
@ -165,7 +194,7 @@ export default defineComponent({
|
||||||
}, {
|
}, {
|
||||||
label: 'MEM (active)',
|
label: 'MEM (active)',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#935dbf',
|
borderColor: '#935dbf',
|
||||||
backgroundColor: alpha('#935dbf', 0.02),
|
backgroundColor: alpha('#935dbf', 0.02),
|
||||||
|
@ -173,7 +202,7 @@ export default defineComponent({
|
||||||
}, {
|
}, {
|
||||||
label: 'MEM (used)',
|
label: 'MEM (used)',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#935dbf',
|
borderColor: '#935dbf',
|
||||||
borderDash: [5, 5],
|
borderDash: [5, 5],
|
||||||
|
@ -198,7 +227,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
color: this.gridColor,
|
color: this.gridColor,
|
||||||
|
@ -207,8 +236,8 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
yAxes: [{
|
y: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: true,
|
display: true,
|
||||||
|
@ -219,7 +248,7 @@ export default defineComponent({
|
||||||
display: false,
|
display: false,
|
||||||
max: 100
|
max: 100
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
|
@ -238,7 +267,7 @@ export default defineComponent({
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'In',
|
label: 'In',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#94a029',
|
borderColor: '#94a029',
|
||||||
backgroundColor: alpha('#94a029', 0.1),
|
backgroundColor: alpha('#94a029', 0.1),
|
||||||
|
@ -246,7 +275,7 @@ export default defineComponent({
|
||||||
}, {
|
}, {
|
||||||
label: 'Out',
|
label: 'Out',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#ff9156',
|
borderColor: '#ff9156',
|
||||||
backgroundColor: alpha('#ff9156', 0.1),
|
backgroundColor: alpha('#ff9156', 0.1),
|
||||||
|
@ -270,7 +299,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
color: this.gridColor,
|
color: this.gridColor,
|
||||||
|
@ -279,8 +308,8 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
display: false
|
display: false
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
yAxes: [{
|
y: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: true,
|
display: true,
|
||||||
|
@ -290,7 +319,7 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
|
@ -309,7 +338,7 @@ export default defineComponent({
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Read',
|
label: 'Read',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#94a029',
|
borderColor: '#94a029',
|
||||||
backgroundColor: alpha('#94a029', 0.1),
|
backgroundColor: alpha('#94a029', 0.1),
|
||||||
|
@ -317,7 +346,7 @@ export default defineComponent({
|
||||||
}, {
|
}, {
|
||||||
label: 'Write',
|
label: 'Write',
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
lineTension: 0,
|
tension: 0,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#ff9156',
|
borderColor: '#ff9156',
|
||||||
backgroundColor: alpha('#ff9156', 0.1),
|
backgroundColor: alpha('#ff9156', 0.1),
|
||||||
|
@ -341,7 +370,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
xAxes: [{
|
x: {
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: false,
|
display: false,
|
||||||
color: this.gridColor,
|
color: this.gridColor,
|
||||||
|
@ -350,8 +379,8 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
display: false
|
display: false
|
||||||
}
|
}
|
||||||
}],
|
},
|
||||||
yAxes: [{
|
y: {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
gridLines: {
|
gridLines: {
|
||||||
display: true,
|
display: true,
|
||||||
|
@ -361,7 +390,7 @@ export default defineComponent({
|
||||||
ticks: {
|
ticks: {
|
||||||
display: false,
|
display: false,
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
|
@ -371,18 +400,6 @@ export default defineComponent({
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
async showInstanceInfo(q) {
|
|
||||||
let instance = q;
|
|
||||||
if (typeof q === 'string') {
|
|
||||||
instance = await os.api('federation/show-instance', {
|
|
||||||
host: q
|
|
||||||
});
|
|
||||||
}
|
|
||||||
os.popup(MkInstanceInfo, {
|
|
||||||
instance: instance
|
|
||||||
}, {}, 'closed');
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchJobs() {
|
fetchJobs() {
|
||||||
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
os.api('admin/queue/deliver-delayed', {}).then(jobs => {
|
||||||
this.jobs = jobs;
|
this.jobs = jobs;
|
||||||
|
|
|
@ -1,61 +1,67 @@
|
||||||
<template>
|
<template>
|
||||||
<FormBase>
|
<div>
|
||||||
<FormSuspense :p="init">
|
<MkHeader :info="header"/>
|
||||||
<FormSuspense :p="fetchStats" v-slot="{ result: stats }">
|
|
||||||
<FormGroup>
|
|
||||||
<FormKeyValueView>
|
|
||||||
<template #key>Users</template>
|
|
||||||
<template #value>{{ number(stats.originalUsersCount) }}</template>
|
|
||||||
</FormKeyValueView>
|
|
||||||
<FormKeyValueView>
|
|
||||||
<template #key>Notes</template>
|
|
||||||
<template #value>{{ number(stats.originalNotesCount) }}</template>
|
|
||||||
</FormKeyValueView>
|
|
||||||
</FormGroup>
|
|
||||||
</FormSuspense>
|
|
||||||
|
|
||||||
<div class="_debobigegoItem">
|
<div class="edbbcaef">
|
||||||
<div class="_debobigegoPanel">
|
<div class="numbers" v-if="stats">
|
||||||
<MkInstanceStats :chart-limit="300" :detailed="true"/>
|
<div class="number _panel">
|
||||||
|
<div class="label">Users</div>
|
||||||
|
<div class="value _monospace">
|
||||||
|
{{ number(stats.originalUsersCount) }}
|
||||||
|
<MkNumberDiff v-if="usersComparedToThePrevDay" class="diff" :value="usersComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><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" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<XMetrics/>
|
<MkContainer :foldable="true" class="charts">
|
||||||
|
<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
|
||||||
|
<div style="padding-top: 12px;">
|
||||||
|
<MkInstanceStats :chart-limit="500" :detailed="true"/>
|
||||||
|
</div>
|
||||||
|
</MkContainer>
|
||||||
|
|
||||||
<FormSuspense :p="fetchServerInfo" v-slot="{ result: serverInfo }">
|
<!--<XMetrics/>-->
|
||||||
<FormGroup>
|
|
||||||
<FormKeyValueView>
|
<div class="numbers">
|
||||||
<template #key>Node.js</template>
|
<div class="number _panel">
|
||||||
<template #value>{{ serverInfo.node }}</template>
|
<div class="label">Misskey</div>
|
||||||
</FormKeyValueView>
|
<div class="value _monospace">{{ version }}</div>
|
||||||
<FormKeyValueView>
|
</div>
|
||||||
<template #key>PostgreSQL</template>
|
<div class="number _panel" v-if="serverInfo">
|
||||||
<template #value>{{ serverInfo.psql }}</template>
|
<div class="label">Node.js</div>
|
||||||
</FormKeyValueView>
|
<div class="value _monospace">{{ serverInfo.node }}</div>
|
||||||
<FormKeyValueView>
|
</div>
|
||||||
<template #key>Redis</template>
|
<div class="number _panel" v-if="serverInfo">
|
||||||
<template #value>{{ serverInfo.redis }}</template>
|
<div class="label">PostgreSQL</div>
|
||||||
</FormKeyValueView>
|
<div class="value _monospace">{{ serverInfo.psql }}</div>
|
||||||
</FormGroup>
|
</div>
|
||||||
</FormSuspense>
|
<div class="number _panel" v-if="serverInfo">
|
||||||
</FormSuspense>
|
<div class="label">Redis</div>
|
||||||
</FormBase>
|
<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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineComponent, markRaw } from 'vue';
|
import { computed, defineComponent, version as vueVersion } from 'vue';
|
||||||
import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
|
import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
|
||||||
import FormInput from '@client/components/debobigego/input.vue';
|
|
||||||
import FormButton from '@client/components/debobigego/button.vue';
|
|
||||||
import FormBase from '@client/components/debobigego/base.vue';
|
|
||||||
import FormGroup from '@client/components/debobigego/group.vue';
|
|
||||||
import FormTextarea from '@client/components/debobigego/textarea.vue';
|
|
||||||
import FormInfo from '@client/components/debobigego/info.vue';
|
|
||||||
import FormSuspense from '@client/components/debobigego/suspense.vue';
|
|
||||||
import MkInstanceStats from '@client/components/instance-stats.vue';
|
import MkInstanceStats from '@client/components/instance-stats.vue';
|
||||||
import MkButton from '@client/components/ui/button.vue';
|
import MkButton from '@client/components/ui/button.vue';
|
||||||
import MkSelect from '@client/components/form/select.vue';
|
import MkSelect from '@client/components/form/select.vue';
|
||||||
import MkInput from '@client/components/form/input.vue';
|
import MkNumberDiff from '@client/components/number-diff.vue';
|
||||||
import MkContainer from '@client/components/ui/container.vue';
|
import MkContainer from '@client/components/ui/container.vue';
|
||||||
import MkFolder from '@client/components/ui/folder.vue';
|
import MkFolder from '@client/components/ui/folder.vue';
|
||||||
import { version, url } from '@client/config';
|
import { version, url } from '@client/config';
|
||||||
|
@ -68,12 +74,10 @@ import * as symbols from '@client/symbols';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
FormBase,
|
MkNumberDiff,
|
||||||
FormSuspense,
|
|
||||||
FormGroup,
|
|
||||||
FormInfo,
|
|
||||||
FormKeyValueView,
|
FormKeyValueView,
|
||||||
MkInstanceStats,
|
MkInstanceStats,
|
||||||
|
MkContainer,
|
||||||
XMetrics,
|
XMetrics,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -82,17 +86,22 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
[symbols.PAGE_INFO]: {
|
[symbols.PAGE_INFO]: {
|
||||||
title: this.$ts.overview,
|
title: this.$ts.dashboard,
|
||||||
icon: 'fas fa-tachometer-alt',
|
icon: 'fas fa-tachometer-alt',
|
||||||
bg: 'var(--bg)',
|
bg: 'var(--bg)',
|
||||||
},
|
},
|
||||||
page: 'index',
|
header: {
|
||||||
|
title: this.$ts.dashboard,
|
||||||
|
icon: 'fas fa-tachometer-alt',
|
||||||
|
},
|
||||||
version,
|
version,
|
||||||
|
vueVersion,
|
||||||
url,
|
url,
|
||||||
stats: null,
|
stats: null,
|
||||||
meta: null,
|
meta: null,
|
||||||
fetchStats: () => os.api('stats', {}),
|
serverInfo: null,
|
||||||
fetchServerInfo: () => os.api('admin/server-info', {}),
|
usersComparedToThePrevDay: null,
|
||||||
|
notesComparedToThePrevDay: null,
|
||||||
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
|
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
|
||||||
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
|
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
|
||||||
}
|
}
|
||||||
|
@ -100,13 +109,29 @@ export default defineComponent({
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||||
|
|
||||||
|
os.api('meta', { detail: true }).then(meta => {
|
||||||
|
this.meta = meta;
|
||||||
|
});
|
||||||
|
|
||||||
|
os.api('stats', {}).then(stats => {
|
||||||
|
this.stats = stats;
|
||||||
|
|
||||||
|
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
|
||||||
|
this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
|
||||||
|
this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
os.api('admin/server-info', {}).then(serverInfo => {
|
||||||
|
this.serverInfo = serverInfo;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
|
||||||
this.meta = await os.api('meta', { detail: true });
|
|
||||||
},
|
|
||||||
|
|
||||||
async showInstanceInfo(q) {
|
async showInstanceInfo(q) {
|
||||||
let instance = q;
|
let instance = q;
|
||||||
if (typeof q === 'string') {
|
if (typeof q === 'string') {
|
||||||
|
@ -125,3 +150,36 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.edbbcaef {
|
||||||
|
> .numbers {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 8px;
|
||||||
|
grid-template-columns: repeat(auto-fill,minmax(130px,1fr));
|
||||||
|
margin: 16px;
|
||||||
|
|
||||||
|
> .number {
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .value {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
> .diff {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .charts {
|
||||||
|
margin: var(--margin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default defineComponent({
|
||||||
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
// TODO: var(--panel)の色が暗いか明るいかで判定する
|
||||||
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||||
|
|
||||||
this.chart = markRaw(new Chart(this.$refs.chart, {
|
this.chart = markRaw(new Chart(this.$refs.chart, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import * as tinycolor from 'tinycolor2';
|
import * as tinycolor from 'tinycolor2';
|
||||||
import Chart from 'chart.js';
|
|
||||||
import { Hpml } from './evaluator';
|
import { Hpml } from './evaluator';
|
||||||
import { values, utils } from '@syuilo/aiscript';
|
import { values, utils } from '@syuilo/aiscript';
|
||||||
import { Fn, HpmlScope } from '.';
|
import { Fn, HpmlScope } from '.';
|
||||||
import { Expr } from './expr';
|
import { Expr } from './expr';
|
||||||
import * as seedrandom from 'seedrandom';
|
import * as seedrandom from 'seedrandom';
|
||||||
|
|
||||||
|
/*
|
||||||
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
|
||||||
Chart.pluginService.register({
|
Chart.pluginService.register({
|
||||||
beforeDraw: (chart, easing) => {
|
beforeDraw: (chart, easing) => {
|
||||||
|
@ -18,6 +18,7 @@ Chart.pluginService.register({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
export function initAiLib(hpml: Hpml) {
|
export function initAiLib(hpml: Hpml) {
|
||||||
return {
|
return {
|
||||||
|
@ -49,11 +50,12 @@ export function initAiLib(hpml: Hpml) {
|
||||||
]));
|
]));
|
||||||
}),
|
}),
|
||||||
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
|
'MkPages:chart': values.FN_NATIVE(([id, opts]) => {
|
||||||
|
/* TODO
|
||||||
utils.assertString(id);
|
utils.assertString(id);
|
||||||
utils.assertObject(opts);
|
utils.assertObject(opts);
|
||||||
const canvas = hpml.canvases[id.value];
|
const canvas = hpml.canvases[id.value];
|
||||||
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
const color = getComputedStyle(document.documentElement).getPropertyValue('--accent');
|
||||||
Chart.defaults.global.defaultFontColor = '#555';
|
Chart.defaults.color = '#555';
|
||||||
const chart = new Chart(canvas, {
|
const chart = new Chart(canvas, {
|
||||||
type: opts.value.get('type').value,
|
type: opts.value.get('type').value,
|
||||||
data: {
|
data: {
|
||||||
|
@ -122,6 +124,7 @@ export function initAiLib(hpml: Hpml) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import procesObjectStorage from './processors/object-storage/index';
|
||||||
import { queueLogger } from './logger';
|
import { queueLogger } from './logger';
|
||||||
import { DriveFile } from '@/models/entities/drive-file';
|
import { DriveFile } from '@/models/entities/drive-file';
|
||||||
import { getJobInfo } from './get-job-info';
|
import { getJobInfo } from './get-job-info';
|
||||||
import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
|
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
|
||||||
import { ThinUser } from './types';
|
import { ThinUser } from './types';
|
||||||
import { IActivity } from '@/remote/activitypub/type';
|
import { IActivity } from '@/remote/activitypub/type';
|
||||||
|
|
||||||
|
@ -22,11 +22,20 @@ function renderError(e: Error): any {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const systemLogger = queueLogger.createSubLogger('system');
|
||||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||||
const dbLogger = queueLogger.createSubLogger('db');
|
const dbLogger = queueLogger.createSubLogger('db');
|
||||||
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
||||||
|
|
||||||
|
systemQueue
|
||||||
|
.on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`))
|
||||||
|
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
|
||||||
|
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
|
||||||
|
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job.id}`, { job, e: renderError(err) }))
|
||||||
|
.on('error', (job: any, err: Error) => systemLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||||
|
.on('stalled', (job) => systemLogger.warn(`stalled id=${job.id}`));
|
||||||
|
|
||||||
deliverQueue
|
deliverQueue
|
||||||
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
|
||||||
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||||
|
@ -220,12 +229,17 @@ export function createCleanRemoteFilesJob() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
if (!envOption.onlyServer) {
|
if (envOption.onlyServer) return;
|
||||||
|
|
||||||
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
|
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
|
||||||
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
|
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
|
||||||
processDb(dbQueue);
|
processDb(dbQueue);
|
||||||
procesObjectStorage(objectStorageQueue);
|
procesObjectStorage(objectStorageQueue);
|
||||||
}
|
|
||||||
|
systemQueue.add('resyncCharts', {
|
||||||
|
}, {
|
||||||
|
repeat: { cron: '0 0 * * *' }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function destroy() {
|
export function destroy() {
|
||||||
|
|
12
src/queue/processors/system/index.ts
Normal file
12
src/queue/processors/system/index.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as Bull from 'bull';
|
||||||
|
import { resyncCharts } from './resync-charts';
|
||||||
|
|
||||||
|
const jobs = {
|
||||||
|
resyncCharts,
|
||||||
|
} as Record<string, Bull.ProcessCallbackFunction<{}> | Bull.ProcessPromiseFunction<{}>>;
|
||||||
|
|
||||||
|
export default function(dbQueue: Bull.Queue<{}>) {
|
||||||
|
for (const [k, v] of Object.entries(jobs)) {
|
||||||
|
dbQueue.process(k, v);
|
||||||
|
}
|
||||||
|
}
|
21
src/queue/processors/system/resync-charts.ts
Normal file
21
src/queue/processors/system/resync-charts.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import * as Bull from 'bull';
|
||||||
|
|
||||||
|
import { queueLogger } from '../../logger';
|
||||||
|
import { driveChart, notesChart, usersChart } from '@/services/chart/index';
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger('resync-charts');
|
||||||
|
|
||||||
|
export default async function resyncCharts(job: Bull.Job<{}>, done: any): Promise<void> {
|
||||||
|
logger.info(`Resync charts...`);
|
||||||
|
|
||||||
|
// TODO: ユーザーごとのチャートも更新する
|
||||||
|
// TODO: インスタンスごとのチャートも更新する
|
||||||
|
await Promise.all([
|
||||||
|
driveChart.resync(),
|
||||||
|
notesChart.resync(),
|
||||||
|
usersChart.resync(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.succ(`All charts successfully resynced.`);
|
||||||
|
done();
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import config from '@/config/index';
|
||||||
import { initialize as initializeQueue } from './initialize';
|
import { initialize as initializeQueue } from './initialize';
|
||||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
|
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
|
||||||
|
|
||||||
|
export const systemQueue = initializeQueue<{}>('system');
|
||||||
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
|
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
|
||||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
||||||
export const dbQueue = initializeQueue<DbJobData>('db');
|
export const dbQueue = initializeQueue<DbJobData>('db');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import define from '../../define';
|
import define from '../../define';
|
||||||
import { driveChart, notesChart, usersChart, instanceChart } from '@/services/chart/index';
|
import { driveChart, notesChart, usersChart } from '@/services/chart/index';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log';
|
import { insertModerationLog } from '@/services/insert-moderation-log';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -15,7 +15,7 @@ export default define(meta, async (ps, me) => {
|
||||||
driveChart.resync();
|
driveChart.resync();
|
||||||
notesChart.resync();
|
notesChart.resync();
|
||||||
usersChart.resync();
|
usersChart.resync();
|
||||||
instanceChart.resync();
|
|
||||||
|
|
||||||
// TODO: ユーザーごとのチャートもキューに入れて更新する
|
// TODO: ユーザーごとのチャートもキューに入れて更新する
|
||||||
|
// TODO: インスタンスごとのチャートもキューに入れて更新する
|
||||||
});
|
});
|
||||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -2664,28 +2664,22 @@ character-parser@^2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-regex "^1.0.3"
|
is-regex "^1.0.3"
|
||||||
|
|
||||||
chart.js@2.9.4:
|
chart.js@3.5.1:
|
||||||
version "2.9.4"
|
version "3.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.5.1.tgz#73e24d23a4134a70ccdb5e79a917f156b6f3644a"
|
||||||
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
|
integrity sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==
|
||||||
dependencies:
|
|
||||||
chartjs-color "^2.1.0"
|
|
||||||
moment "^2.10.2"
|
|
||||||
|
|
||||||
chartjs-color-string@^0.6.0:
|
chartjs-adapter-date-fns@2.0.0:
|
||||||
version "0.6.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
|
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
|
||||||
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
|
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
|
||||||
dependencies:
|
|
||||||
color-name "^1.0.0"
|
|
||||||
|
|
||||||
chartjs-color@^2.1.0:
|
chartjs-plugin-zoom@1.1.1:
|
||||||
version "2.4.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
|
resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.1.1.tgz#8a28923a17fcb5eb57a0dc94c5113bf402677647"
|
||||||
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
|
integrity sha512-1q54WOzK7FtAjkbemQeqvmFUV0btNYIQny2HbQ6Awq9wUtCz7Zmj6vIgp3C1DYMQwN0nqgpC3vnApqiwI7cSdQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
chartjs-color-string "^0.6.0"
|
hammerjs "^2.0.8"
|
||||||
color-convert "^1.9.3"
|
|
||||||
|
|
||||||
check-more-types@2.24.0, check-more-types@^2.24.0:
|
check-more-types@2.24.0, check-more-types@^2.24.0:
|
||||||
version "2.24.0"
|
version "2.24.0"
|
||||||
|
@ -2974,7 +2968,7 @@ collection-visit@^1.0.0:
|
||||||
map-visit "^1.0.0"
|
map-visit "^1.0.0"
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.3:
|
color-convert@^1.3.0, color-convert@^1.9.0:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||||
|
@ -3616,6 +3610,11 @@ data-urls@^2.0.0:
|
||||||
whatwg-mimetype "^2.3.0"
|
whatwg-mimetype "^2.3.0"
|
||||||
whatwg-url "^8.0.0"
|
whatwg-url "^8.0.0"
|
||||||
|
|
||||||
|
date-fns@2.25.0:
|
||||||
|
version "2.25.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
|
||||||
|
integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
|
||||||
|
|
||||||
date-fns@^2.16.1:
|
date-fns@^2.16.1:
|
||||||
version "2.19.0"
|
version "2.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
|
||||||
|
@ -5300,6 +5299,11 @@ gulplog@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
glogg "^1.0.0"
|
glogg "^1.0.0"
|
||||||
|
|
||||||
|
hammerjs@^2.0.8:
|
||||||
|
version "2.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
|
||||||
|
integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
|
||||||
|
|
||||||
har-schema@^2.0.0:
|
har-schema@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||||
|
@ -7383,7 +7387,7 @@ moment-timezone@^0.5.25:
|
||||||
dependencies:
|
dependencies:
|
||||||
moment ">= 2.9.0"
|
moment ">= 2.9.0"
|
||||||
|
|
||||||
"moment@>= 2.9.0", moment@^2.10.2, moment@^2.22.2:
|
"moment@>= 2.9.0", moment@^2.22.2:
|
||||||
version "2.24.0"
|
version "2.24.0"
|
||||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||||
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||||
|
|
Loading…
Reference in a new issue