summaryrefslogtreecommitdiff
path: root/src/client/components
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-10-23 01:08:45 +0900
commitd0d5068f728e13f3ebe1dc227ddaacf380817ec4 (patch)
tree7bb95207e01bff1bee9877829c0556d3ecf62176 /src/client/components
parentMerge branch 'develop' (diff)
parent12.93.0 (diff)
downloadmisskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.gz
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.tar.bz2
misskey-d0d5068f728e13f3ebe1dc227ddaacf380817ec4.zip
Merge branch 'develop'
Diffstat (limited to 'src/client/components')
-rw-r--r--src/client/components/chart.vue628
-rw-r--r--src/client/components/form/input.vue2
-rw-r--r--src/client/components/form/radios.vue12
-rw-r--r--src/client/components/form/select.vue65
-rw-r--r--src/client/components/form/textarea.vue2
-rw-r--r--src/client/components/global/emoji.vue8
-rw-r--r--src/client/components/global/header.vue6
-rw-r--r--src/client/components/instance-stats.vue487
-rw-r--r--src/client/components/mfm.ts2
-rw-r--r--src/client/components/number-diff.vue47
-rw-r--r--src/client/components/post-form.vue38
-rw-r--r--src/client/components/queue-chart.vue212
-rw-r--r--src/client/components/tab.vue2
-rw-r--r--src/client/components/ui/container.vue9
-rw-r--r--src/client/components/ui/menu.vue5
-rw-r--r--src/client/components/ui/popup-menu.vue6
-rw-r--r--src/client/components/ui/super-menu.vue2
17 files changed, 1021 insertions, 512 deletions
diff --git a/src/client/components/chart.vue b/src/client/components/chart.vue
new file mode 100644
index 0000000000..8eb9f93f33
--- /dev/null
+++ b/src/client/components/chart.vue
@@ -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 'instance-requests': return fetchInstanceRequestsChart();
+ case 'instance-users': return fetchInstanceUsersChart(false);
+ case 'instance-users-total': return fetchInstanceUsersChart(true);
+ case 'instance-notes': return fetchInstanceNotesChart(false);
+ case 'instance-notes-total': return fetchInstanceNotesChart(true);
+ case 'instance-ff': return fetchInstanceFfChart(false);
+ case 'instance-ff-total': return fetchInstanceFfChart(true);
+ case 'instance-drive-usage': return fetchInstanceDriveUsageChart(false);
+ case 'instance-drive-usage-total': return fetchInstanceDriveUsageChart(true);
+ case 'instance-drive-files': return fetchInstanceDriveFilesChart(false);
+ case 'instance-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>
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
index d7b6f77519..591eda9ed5 100644
--- a/src/client/components/form/input.vue
+++ b/src/client/components/form/input.vue
@@ -33,7 +33,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
import { debounce } from 'throttle-debounce';
export default defineComponent({
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
index 1d3d80172a..998a738202 100644
--- a/src/client/components/form/radios.vue
+++ b/src/client/components/form/radios.vue
@@ -22,7 +22,6 @@ export default defineComponent({
}
},
render() {
- const label = this.$slots.desc();
let options = this.$slots.default();
// なぜかFragmentになることがあるため
@@ -31,7 +30,6 @@ export default defineComponent({
return h('div', {
class: 'novjtcto'
}, [
- h('div', { class: 'label' }, label),
...options.map(option => h(MkRadio, {
key: option.key,
value: option.props.value,
@@ -45,16 +43,6 @@ export default defineComponent({
<style lang="scss">
.novjtcto {
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 12px;
- user-select: none;
-
- &:empty {
- display: none;
- }
- }
-
&:first-child {
margin-top: 0;
}
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
index 257e2cc990..30ccfd312b 100644
--- a/src/client/components/form/select.vue
+++ b/src/client/components/form/select.vue
@@ -1,9 +1,9 @@
<template>
<div class="vblkjoeq">
<div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ inline, disabled, focused }">
+ <div class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick" ref="container">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <select ref="inputEl"
+ <select class="select" ref="inputEl"
v-model="v"
:disabled="disabled"
:required="required"
@@ -25,7 +25,8 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
+import * as os from '@client/os';
export default defineComponent({
components: {
@@ -81,6 +82,7 @@ export default defineComponent({
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
+ const container = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
@@ -132,6 +134,47 @@ export default defineComponent({
});
});
+ const onClick = (ev: MouseEvent) => {
+ focused.value = true;
+
+ const menu = [];
+ let options = context.slots.default();
+
+ for (const optionOrOptgroup of options) {
+ if (optionOrOptgroup.type === 'optgroup') {
+ const optgroup = optionOrOptgroup;
+ menu.push({
+ type: 'label',
+ text: optgroup.props.label,
+ });
+ for (const option of optgroup.children) {
+ menu.push({
+ text: option.children,
+ active: v.value === option.props.value,
+ action: () => {
+ v.value = option.props.value;
+ },
+ });
+ }
+ } else {
+ const option = optionOrOptgroup;
+ menu.push({
+ text: option.children,
+ active: v.value === option.props.value,
+ action: () => {
+ v.value = option.props.value;
+ },
+ });
+ }
+ }
+
+ os.popupMenu(menu, container.value, {
+ width: container.value.offsetWidth,
+ }).then(() => {
+ focused.value = false;
+ });
+ };
+
return {
v,
focused,
@@ -141,8 +184,10 @@ export default defineComponent({
inputEl,
prefixEl,
suffixEl,
+ container,
focus,
onInput,
+ onClick,
updated,
};
},
@@ -174,8 +219,15 @@ export default defineComponent({
> .input {
$height: 42px;
position: relative;
+ cursor: pointer;
- > select {
+ &:hover {
+ > .select {
+ border-color: var(--inputBorderHover);
+ }
+ }
+
+ > .select {
appearance: none;
-webkit-appearance: none;
display: block;
@@ -195,10 +247,7 @@ export default defineComponent({
box-sizing: border-box;
cursor: pointer;
transition: border-color 0.1s ease-out;
-
- &:hover {
- border-color: var(--inputBorderHover);
- }
+ pointer-events: none;
}
> .prefix,
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
index 50be69f930..048e9032df 100644
--- a/src/client/components/form/textarea.vue
+++ b/src/client/components/form/textarea.vue
@@ -26,7 +26,7 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from '../ui/button.vue';
+import MkButton from '@client/components/ui/button.vue';
import { debounce } from 'throttle-debounce';
export default defineComponent({
diff --git a/src/client/components/global/emoji.vue b/src/client/components/global/emoji.vue
index f4ebd5f3b3..f92e35c38f 100644
--- a/src/client/components/global/emoji.vue
+++ b/src/client/components/global/emoji.vue
@@ -27,8 +27,7 @@ export default defineComponent({
default: false
},
customEmojis: {
- required: false,
- default: () => []
+ required: false
},
isReaction: {
type: Boolean,
@@ -58,10 +57,7 @@ export default defineComponent({
},
ce() {
- let ce = [];
- if (this.customEmojis) ce = ce.concat(this.customEmojis);
- if (this.$instance && this.$instance.emojis) ce = ce.concat(this.$instance.emojis);
- return ce;
+ return this.customEmojis || this.$instance?.emojis || [];
}
},
diff --git a/src/client/components/global/header.vue b/src/client/components/global/header.vue
index a4466da498..2bf490c98a 100644
--- a/src/client/components/global/header.vue
+++ b/src/client/components/global/header.vue
@@ -203,6 +203,12 @@ export default defineComponent({
&.thin {
--height: 50px;
+
+ > .buttons {
+ > .button {
+ font-size: 0.9em;
+ }
+ }
}
&.slim {
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index 5e7c71ea65..fd0b75609f 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -24,35 +24,26 @@
<option value="drive-total">{{ $ts._charts.storageUsageTotal }}</option>
</optgroup>
</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="day">{{ $ts.perDay }}</option>
</MkSelect>
</div>
- <canvas ref="chart"></canvas>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
</div>
</template>
<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import Chart from 'chart.js';
-import MkSelect from './form/select.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 { defineComponent, onMounted, ref, watch } from 'vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkChart from '@client/components/chart.vue';
import * as os from '@client/os';
+import { defaultStore } from '@client/store';
export default defineComponent({
components: {
- MkSelect
+ MkSelect,
+ MkChart,
},
props: {
@@ -68,463 +59,15 @@ export default defineComponent({
},
},
- data() {
- return {
- notesLocalWoW: 0,
- notesLocalDoD: 0,
- 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();
+ setup() {
+ const chartSpan = ref<'hour' | 'day'>('hour');
+ const chartSrc = ref('notes');
- this.fetchChart();
+ return {
+ chartSrc,
+ chartSpan,
+ };
},
-
- 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>
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index 2bdd7d46ee..f3411cadc3 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -185,7 +185,7 @@ export default defineComponent({
}
}
if (style == null) {
- return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']);
+ return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']);
} else {
return h('span', {
style: 'display: inline-block;' + style,
diff --git a/src/client/components/number-diff.vue b/src/client/components/number-diff.vue
new file mode 100644
index 0000000000..690f89dd59
--- /dev/null
+++ b/src/client/components/number-diff.vue
@@ -0,0 +1,47 @@
+<template>
+<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
+ <slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ 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>
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index a1d89d2a2e..816a69e731 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -117,11 +117,28 @@ export default defineComponent({
type: String,
required: false
},
+ initialVisibility: {
+ type: String,
+ required: false
+ },
+ initialFiles: {
+ type: Array,
+ required: false
+ },
+ initialLocalOnly: {
+ type: Boolean,
+ required: false
+ },
+ visibleUsers: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
initialNote: {
type: Object,
required: false
},
- instant: {
+ share: {
type: Boolean,
required: false,
default: false
@@ -150,8 +167,7 @@ export default defineComponent({
showPreview: false,
cw: null,
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
- visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
- visibleUsers: [],
+ visibility: (this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility) as typeof noteVisibilities[number],
autocomplete: null,
draghover: false,
quoteId: null,
@@ -246,6 +262,18 @@ export default defineComponent({
this.text = this.initialText;
}
+ if (this.initialVisibility) {
+ this.visibility = this.initialVisibility;
+ }
+
+ if (this.initialFiles) {
+ this.files = this.initialFiles;
+ }
+
+ if (typeof this.initialLocalOnly === 'boolean') {
+ this.localOnly = this.initialLocalOnly;
+ }
+
if (this.mention) {
this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
this.text += ' ';
@@ -321,7 +349,7 @@ export default defineComponent({
this.$nextTick(() => {
// 書きかけの投稿を復元
- if (!this.instant && !this.mention && !this.specified) {
+ if (!this.share && !this.mention && !this.specified) {
const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
if (draft) {
this.text = draft.data.text;
@@ -582,8 +610,6 @@ export default defineComponent({
},
saveDraft() {
- if (this.instant) return;
-
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
data[this.draftKey] = {
diff --git a/src/client/components/queue-chart.vue b/src/client/components/queue-chart.vue
new file mode 100644
index 0000000000..59c9723f89
--- /dev/null
+++ b/src/client/components/queue-chart.vue
@@ -0,0 +1,212 @@
+<template>
+<canvas ref="chartEl"></canvas>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+import {
+ Chart,
+ ArcElement,
+ LineElement,
+ BarElement,
+ PointElement,
+ BarController,
+ LineController,
+ CategoryScale,
+ LinearScale,
+ TimeScale,
+ Legend,
+ Title,
+ Tooltip,
+ SubTitle,
+ Filler,
+} from 'chart.js';
+import number from '@client/filters/number';
+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,
+);
+
+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})`;
+};
+
+export default defineComponent({
+ props: {
+ domain: {
+ type: String,
+ required: true,
+ },
+ connection: {
+ required: true,
+ },
+ },
+
+ setup(props) {
+ const chartEl = ref<HTMLCanvasElement>(null);
+
+ 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');
+
+ onMounted(() => {
+ const chartInstance = new Chart(chartEl.value, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Process',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#00E396',
+ backgroundColor: alpha('#00E396', 0.1),
+ data: []
+ }, {
+ label: 'Active',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#00BCD4',
+ backgroundColor: alpha('#00BCD4', 0.1),
+ data: []
+ }, {
+ label: 'Waiting',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#FFB300',
+ backgroundColor: alpha('#FFB300', 0.1),
+ data: []
+ }, {
+ label: 'Delayed',
+ pointRadius: 0,
+ tension: 0,
+ borderWidth: 2,
+ borderColor: '#E53935',
+ borderDash: [5, 5],
+ fill: false,
+ data: []
+ }],
+ },
+ options: {
+ aspectRatio: 2.5,
+ layout: {
+ padding: {
+ left: 16,
+ right: 16,
+ top: 16,
+ bottom: 8,
+ },
+ },
+ scales: {
+ x: {
+ grid: {
+ display: false,
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ ticks: {
+ display: false,
+ },
+ },
+ y: {
+ grid: {
+ color: gridColor,
+ borderColor: 'rgb(0, 0, 0, 0)',
+ },
+ },
+ },
+ interaction: {
+ intersect: false,
+ },
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ boxWidth: 16,
+ },
+ },
+ tooltip: {
+ mode: 'index',
+ animation: {
+ duration: 0,
+ },
+ },
+ },
+ },
+ });
+
+ const onStats = (stats) => {
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+ chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+ chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+ chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ if (chartInstance.data.datasets[0].data.length > 200) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ chartInstance.data.datasets[1].data.shift();
+ chartInstance.data.datasets[2].data.shift();
+ chartInstance.data.datasets[3].data.shift();
+ }
+ chartInstance.update();
+ };
+
+ const onStatsLog = (statsLog) => {
+ for (const stats of [...statsLog].reverse()) {
+ chartInstance.data.labels.push('');
+ chartInstance.data.datasets[0].data.push(stats[props.domain].activeSincePrevTick);
+ chartInstance.data.datasets[1].data.push(stats[props.domain].active);
+ chartInstance.data.datasets[2].data.push(stats[props.domain].waiting);
+ chartInstance.data.datasets[3].data.push(stats[props.domain].delayed);
+ if (chartInstance.data.datasets[0].data.length > 200) {
+ chartInstance.data.labels.shift();
+ chartInstance.data.datasets[0].data.shift();
+ chartInstance.data.datasets[1].data.shift();
+ chartInstance.data.datasets[2].data.shift();
+ chartInstance.data.datasets[3].data.shift();
+ }
+ }
+ chartInstance.update();
+ };
+
+ props.connection.on('stats', onStats);
+ props.connection.on('statsLog', onStatsLog);
+
+ onUnmounted(() => {
+ props.connection.off('stats', onStats);
+ props.connection.off('statsLog', onStatsLog);
+ });
+ });
+
+ return {
+ chartEl,
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
index ce86af8f95..c629727358 100644
--- a/src/client/components/tab.vue
+++ b/src/client/components/tab.vue
@@ -36,7 +36,7 @@ export default defineComponent({
> button {
flex: 1;
padding: 10px 8px;
- border-radius: 6px;
+ border-radius: var(--radius);
&:disabled {
opacity: 1 !important;
diff --git a/src/client/components/ui/container.vue b/src/client/components/ui/container.vue
index 1940099096..14673dfcd7 100644
--- a/src/client/components/ui/container.vue
+++ b/src/client/components/ui/container.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ukygtjoj _block" :class="{ naked, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
+<div class="ukygtjoj _panel" :class="{ naked, thin, hideHeader: !showHeader, scrollable, closed: !showBody }" v-size="{ max: [380] }">
<header v-if="showHeader" ref="header">
<div class="title"><slot name="header"></slot></div>
<div class="sub">
@@ -36,6 +36,11 @@ export default defineComponent({
required: false,
default: true
},
+ thin: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
naked: {
type: Boolean,
required: false,
@@ -226,7 +231,7 @@ export default defineComponent({
}
}
- &.max-width_380px {
+ &.max-width_380px, &.thin {
> header {
> .title {
padding: 8px 10px;
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
index da24d90170..aaef527f1a 100644
--- a/src/client/components/ui/menu.vue
+++ b/src/client/components/ui/menu.vue
@@ -1,5 +1,6 @@
<template>
<div class="rrevdjwt" :class="{ center: align === 'center' }"
+ :style="{ width: width ? width + 'px' : null }"
ref="items"
@contextmenu.self="e => e.preventDefault()"
v-hotkey="keymap"
@@ -59,6 +60,10 @@ export default defineComponent({
type: String,
requried: false
},
+ width: {
+ type: Number,
+ required: false
+ },
},
emits: ['close'],
data() {
diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue
index 23f7c89f3b..3ff4c658b1 100644
--- a/src/client/components/ui/popup-menu.vue
+++ b/src/client/components/ui/popup-menu.vue
@@ -1,6 +1,6 @@
<template>
<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
- <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
+ <MkMenu :items="items" :align="align" :width="width" @close="$refs.popup.close()" class="_popup _shadow"/>
</MkPopup>
</template>
@@ -24,6 +24,10 @@ export default defineComponent({
type: String,
required: false
},
+ width: {
+ type: Number,
+ required: false
+ },
viaKeyboard: {
type: Boolean,
required: false
diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue
index 35fc81550d..6ab94d744d 100644
--- a/src/client/components/ui/super-menu.vue
+++ b/src/client/components/ui/super-menu.vue
@@ -120,7 +120,7 @@ export default defineComponent({
> .items {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
grid-gap: 8px;
padding: 0 16px;