Merge branch 'develop' into swn

This commit is contained in:
tamaina
2021-10-23 10:52:48 +09:00
95 changed files with 2030 additions and 665 deletions

View File

@@ -595,17 +595,17 @@ export default defineComponent({
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);
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;

View File

@@ -13,7 +13,8 @@
<i v-else-if="notification.type === 'mention'" class="fas fa-at"></i>
<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i>
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
<XReactionIcon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :custom-emojis="notification.note.emojis" :no-style="true"/>
</div>
</div>
<div class="tail">

View File

@@ -1,6 +1,6 @@
<template>
<span class="ceaaebcd" :class="{ isPlus, isMinus, isZero }">
<slot name="before"></slot>{{ isPlus ? '+' : isMinus ? '-' : '' }}{{ number(value) }}<slot name="after"></slot>
<slot name="before"></slot>{{ isPlus ? '+' : '' }}{{ number(value) }}<slot name="after"></slot>
</span>
</template>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -4,7 +4,7 @@
<div class="title">{{ $ts.misskeyUpdated }}</div>
<div class="version">{{ version }}🚀</div>
<MkButton full @click="whatIsNew">{{ $ts.whatIsNew }}</MkButton>
<MkButton primary full @click="$refs.modal.close()">{{ $ts.gotIt }}</MkButton>
<MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ $ts.gotIt }}</MkButton>
</div>
</MkModal>
</template>
@@ -54,5 +54,9 @@ export default defineComponent({
> .version {
margin: 1em 0;
}
> .gotIt {
margin: 8px 0 0 0;
}
}
</style>

View File

@@ -7,8 +7,8 @@
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
</div>
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
@@ -93,47 +93,47 @@ export default defineComponent({
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.locale.dashboard,
to: '/instance/overview',
to: '/admin/overview',
active: page.value === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.locale.users,
to: '/instance/users',
to: '/admin/users',
active: page.value === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.locale.customEmojis,
to: '/instance/emojis',
to: '/admin/emojis',
active: page.value === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.locale.federation,
to: '/instance/federation',
to: '/admin/federation',
active: page.value === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.locale.jobQueue,
to: '/instance/queue',
to: '/admin/queue',
active: page.value === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
to: '/instance/files',
to: '/admin/files',
active: page.value === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.locale.announcements,
to: '/instance/announcements',
to: '/admin/announcements',
active: page.value === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.locale.ads,
to: '/instance/ads',
to: '/admin/ads',
active: page.value === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.locale.abuseReports,
to: '/instance/abuses',
to: '/admin/abuses',
active: page.value === 'abuses',
}],
}, {
@@ -141,57 +141,57 @@ export default defineComponent({
items: [{
icon: 'fas fa-cog',
text: i18n.locale.general,
to: '/instance/settings',
to: '/admin/settings',
active: page.value === 'settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.files,
to: '/instance/files-settings',
to: '/admin/files-settings',
active: page.value === 'files-settings',
}, {
icon: 'fas fa-envelope',
text: i18n.locale.emailServer,
to: '/instance/email-settings',
to: '/admin/email-settings',
active: page.value === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.locale.objectStorage,
to: '/instance/object-storage',
to: '/admin/object-storage',
active: page.value === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.locale.security,
to: '/instance/security',
to: '/admin/security',
active: page.value === 'security',
}, {
icon: 'fas fa-bolt',
text: 'ServiceWorker',
to: '/instance/service-worker',
to: '/admin/service-worker',
active: page.value === 'service-worker',
}, {
icon: 'fas fa-globe',
text: i18n.locale.relays,
to: '/instance/relays',
to: '/admin/relays',
active: page.value === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.locale.integration,
to: '/instance/integrations',
to: '/admin/integrations',
active: page.value === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.locale.instanceBlocking,
to: '/instance/instance-block',
to: '/admin/instance-block',
active: page.value === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.locale.proxyAccount,
to: '/instance/proxy-account',
to: '/admin/proxy-account',
active: page.value === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.locale.other,
to: '/instance/other-settings',
to: '/admin/other-settings',
active: page.value === 'other-settings',
}],
}, {
@@ -199,13 +199,8 @@ export default defineComponent({
items: [{
icon: 'fas fa-database',
text: i18n.locale.database,
to: '/instance/database',
to: '/admin/database',
active: page.value === 'database',
}, {
icon: 'fas fa-stream',
text: i18n.locale.logs,
to: '/instance/logs',
active: page.value === 'logs',
}],
}]);
const component = computed(() => {
@@ -220,7 +215,6 @@ export default defineComponent({
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));

View File

@@ -1,15 +1,15 @@
<template>
<FormBase>
<FormSuspense :p="init">
<FormLink to="/instance/integrations/twitter">
<FormLink to="/admin/integrations/twitter">
<i class="fab fa-twitter"></i> Twitter
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
<FormLink to="/instance/integrations/github">
<FormLink to="/admin/integrations/github">
<i class="fab fa-github"></i> GitHub
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>
<FormLink to="/instance/integrations/discord">
<FormLink to="/admin/integrations/discord">
<i class="fab fa-discord"></i> Discord
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
</FormLink>

View File

@@ -2,20 +2,20 @@
<div>
<MkHeader :info="header"/>
<div class="edbbcaef">
<div class="numbers" v-if="stats">
<div class="edbbcaef" v-size="{ max: [880] }">
<div v-if="stats" class="cfcdecdf" style="margin: var(--margin)">
<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>
<MkNumberDiff v-if="usersComparedToThePrevDay != null" 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>
<MkNumberDiff v-if="notesComparedToThePrevDay != null" class="diff" :value="notesComparedToThePrevDay" v-tooltip="$ts.dayOverDayChanges"><template #before>(</template><template #after>)</template></MkNumberDiff>
</div>
</div>
</div>
@@ -26,37 +26,51 @@
<MkInstanceStats :chart-limit="500" :detailed="true"/>
</div>
</MkContainer>
<div class="queue">
<MkContainer :foldable="true" :thin="true" class="deliver">
<template #header>Queue: deliver</template>
<MkQueueChart :connection="queueStatsConnection" domain="deliver"/>
</MkContainer>
<MkContainer :foldable="true" :thin="true" class="inbox">
<template #header>Queue: inbox</template>
<MkQueueChart :connection="queueStatsConnection" domain="inbox"/>
</MkContainer>
</div>
<!--<XMetrics/>-->
<div class="numbers">
<div class="number _panel">
<div class="label">Misskey</div>
<div class="value _monospace">{{ version }}</div>
<MkFolder style="margin: var(--margin)">
<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
<div class="cfcdecdf">
<div class="number _panel">
<div class="label">Misskey</div>
<div class="value _monospace">{{ version }}</div>
</div>
<div class="number _panel" v-if="serverInfo">
<div class="label">Node.js</div>
<div class="value _monospace">{{ serverInfo.node }}</div>
</div>
<div class="number _panel" v-if="serverInfo">
<div class="label">PostgreSQL</div>
<div class="value _monospace">{{ serverInfo.psql }}</div>
</div>
<div class="number _panel" v-if="serverInfo">
<div class="label">Redis</div>
<div class="value _monospace">{{ serverInfo.redis }}</div>
</div>
<div class="number _panel">
<div class="label">Vue</div>
<div class="value _monospace">{{ vueVersion }}</div>
</div>
</div>
<div class="number _panel" v-if="serverInfo">
<div class="label">Node.js</div>
<div class="value _monospace">{{ serverInfo.node }}</div>
</div>
<div class="number _panel" v-if="serverInfo">
<div class="label">PostgreSQL</div>
<div class="value _monospace">{{ serverInfo.psql }}</div>
</div>
<div class="number _panel" v-if="serverInfo">
<div class="label">Redis</div>
<div class="value _monospace">{{ serverInfo.redis }}</div>
</div>
<div class="number _panel">
<div class="label">Vue</div>
<div class="value _monospace">{{ vueVersion }}</div>
</div>
</div>
</MkFolder>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, version as vueVersion } from 'vue';
import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import MkInstanceStats from '@client/components/instance-stats.vue';
import MkButton from '@client/components/ui/button.vue';
@@ -64,6 +78,7 @@ import MkSelect from '@client/components/form/select.vue';
import MkNumberDiff from '@client/components/number-diff.vue';
import MkContainer from '@client/components/ui/container.vue';
import MkFolder from '@client/components/ui/folder.vue';
import MkQueueChart from '@client/components/queue-chart.vue';
import { version, url } from '@client/config';
import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
@@ -78,6 +93,8 @@ export default defineComponent({
FormKeyValueView,
MkInstanceStats,
MkContainer,
MkFolder,
MkQueueChart,
XMetrics,
},
@@ -104,6 +121,7 @@ export default defineComponent({
notesComparedToThePrevDay: null,
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
queueStatsConnection: markRaw(os.stream.useChannel('queueStats')),
}
},
@@ -129,6 +147,17 @@ export default defineComponent({
os.api('admin/server-info', {}).then(serverInfo => {
this.serverInfo = serverInfo;
});
this.$nextTick(() => {
this.queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
});
},
beforeUnmount() {
this.queueStatsConnection.dispose();
},
methods: {
@@ -153,11 +182,10 @@ export default defineComponent({
<style lang="scss" scoped>
.edbbcaef {
> .numbers {
.cfcdecdf {
display: grid;
grid-gap: 8px;
grid-template-columns: repeat(auto-fill,minmax(130px,1fr));
margin: 16px;
grid-template-columns: repeat(auto-fill,minmax(150px,1fr));
> .number {
padding: 12px 16px;
@@ -181,5 +209,34 @@ export default defineComponent({
> .charts {
margin: var(--margin);
}
> .queue {
margin: var(--margin);
display: flex;
> .deliver,
> .inbox {
flex: 1;
width: 50%;
&:not(:first-child) {
margin-left: var(--margin);
}
}
}
&.max-width_800px {
> .queue {
display: block;
> .deliver,
> .inbox {
&:not(:first-child) {
margin-top: var(--margin);
margin-left: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="_debobigegoItem">
<div class="_debobigegoLabel"><slot name="title"></slot></div>
<div class="_debobigegoPanel pumxzjhg">
<div class="_table status">
<div class="_row">
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
</div>
</div>
<div class="">
<MkQueueChart :domain="domain" :connection="connection"/>
</div>
<div class="jobs">
<div v-if="jobs.length > 0">
<div v-for="job in jobs" :key="job[0]">
<span>{{ job[0] }}</span>
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
</div>
</div>
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
import number from '@client/filters/number';
import MkQueueChart from '@client/components/queue-chart.vue';
import * as os from '@client/os';
export default defineComponent({
components: {
MkQueueChart
},
props: {
domain: {
type: String,
required: true,
},
connection: {
required: true,
},
},
setup(props) {
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
jobs.value = jobs;
});
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
props.connection.on('stats', onStats);
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
return {
jobs,
activeSincePrevTick,
active,
waiting,
delayed,
number,
};
},
});
</script>
<style lang="scss" scoped>
.pumxzjhg {
> .status {
padding: 16px;
border-bottom: solid 0.5px var(--divider);
}
> .jobs {
padding: 16px;
border-top: solid 0.5px var(--divider);
max-height: 180px;
overflow: auto;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormSuspense :p="init">
<FormLink to="/instance/bot-protection">
<FormLink to="/admin/bot-protection">
<i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
<template #suffix v-if="enableHcaptcha">hCaptcha</template>
<template #suffix v-else-if="enableRecaptcha">reCAPTCHA</template>

View File

@@ -149,7 +149,7 @@ import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
import * as symbols from '@client/symbols';
import MkInstanceInfo from '@client/pages/instance/instance.vue';
import MkInstanceInfo from '@client/pages/admin/instance.vue';
export default defineComponent({
components: {

View File

@@ -1,97 +0,0 @@
<template>
<div class="_section">
<div class="_inputs">
<MkInput v-model="logDomain" :debounce="true">
<template #label>{{ $ts.domain }}</template>
</MkInput>
<MkSelect v-model="logLevel">
<template #label>Level</template>
<option value="all">All</option>
<option value="info">Info</option>
<option value="success">Success</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
<option value="debug">Debug</option>
</MkSelect>
</div>
<div class="logs">
<code v-for="log in logs" :key="log.id" :class="log.level">
<details>
<summary><MkTime :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary>
<!--<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty>-->
</details>
</code>
</div>
<MkButton @click="deleteAllLogs()" primary><i class="fas fa-trash-alt"></i> {{ $ts.deleteAll }}</MkButton>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import MkInput from '@client/components/form/input.vue';
import MkSelect from '@client/components/form/select.vue';
import MkTextarea from '@client/components/form/textarea.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
MkButton,
MkInput,
MkSelect,
MkTextarea,
},
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.serverLogs,
icon: 'fas fa-stream'
},
logs: [],
logLevel: 'all',
logDomain: '',
}
},
watch: {
logLevel() {
this.logs = [];
this.fetchLogs();
},
logDomain() {
this.logs = [];
this.fetchLogs();
}
},
created() {
this.fetchLogs();
},
mounted() {
this.$emit('info', this[symbols.PAGE_INFO]);
},
methods: {
fetchLogs() {
os.api('admin/logs', {
level: this.logLevel === 'all' ? null : this.logLevel,
domain: this.logDomain === '' ? null : this.logDomain,
limit: 30
}).then(logs => {
this.logs = logs.reverse();
});
},
deleteAllLogs() {
os.apiWithDialog('admin/delete-logs');
},
}
});
</script>

View File

@@ -1,218 +0,0 @@
<template>
<div class="_debobigegoItem">
<div class="_debobigegoLabel"><slot name="title"></slot></div>
<div class="_debobigegoPanel pumxzjhg">
<div class="_table status">
<div class="_row">
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
<div class="_cell"><div class="_label">Active</div>{{ number(active) }}</div>
<div class="_cell"><div class="_label">Waiting</div>{{ number(waiting) }}</div>
<div class="_cell"><div class="_label">Delayed</div>{{ number(delayed) }}</div>
</div>
</div>
<div class="">
<canvas ref="chart"></canvas>
</div>
<div class="jobs">
<div v-if="jobs.length > 0">
<div v-for="job in jobs" :key="job[0]">
<span>{{ job[0] }}</span>
<span style="margin-left: 8px; opacity: 0.7;">({{ number(job[1]) }} jobs)</span>
</div>
</div>
<span v-else style="opacity: 0.5;">{{ $ts.noJobs }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import Chart from 'chart.js';
import number from '@client/filters/number';
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';
export default defineComponent({
props: {
domain: {
required: true
},
connection: {
required: true
},
},
data() {
return {
chart: null,
jobs: [],
activeSincePrevTick: 0,
active: 0,
waiting: 0,
delayed: 0,
}
},
mounted() {
this.fetchJobs();
// TODO: var(--panel)の色が暗いか明るいかで判定する
const gridColor = this.$store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
this.chart = markRaw(new Chart(this.$refs.chart, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Process',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#00E396',
backgroundColor: alpha('#00E396', 0.1),
data: []
}, {
label: 'Active',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#00BCD4',
backgroundColor: alpha('#00BCD4', 0.1),
data: []
}, {
label: 'Waiting',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#FFB300',
backgroundColor: alpha('#FFB300', 0.1),
data: []
}, {
label: 'Delayed',
pointRadius: 0,
lineTension: 0,
borderWidth: 2,
borderColor: '#E53935',
borderDash: [5, 5],
fill: false,
data: []
}]
},
options: {
aspectRatio: 3,
layout: {
padding: {
left: 16,
right: 16,
top: 16,
bottom: 12
}
},
legend: {
position: 'bottom',
labels: {
boxWidth: 16,
}
},
scales: {
xAxes: [{
gridLines: {
display: false,
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: false
}
}],
yAxes: [{
position: 'right',
gridLines: {
display: true,
color: gridColor,
zeroLineColor: gridColor,
},
ticks: {
display: false,
}
}]
},
tooltips: {
intersect: false,
mode: 'index',
}
}
}));
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
},
beforeUnmount() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
this.activeSincePrevTick = stats[this.domain].activeSincePrevTick;
this.active = stats[this.domain].active;
this.waiting = stats[this.domain].waiting;
this.delayed = stats[this.domain].delayed;
this.chart.data.labels.push('');
this.chart.data.datasets[0].data.push(stats[this.domain].activeSincePrevTick);
this.chart.data.datasets[1].data.push(stats[this.domain].active);
this.chart.data.datasets[2].data.push(stats[this.domain].waiting);
this.chart.data.datasets[3].data.push(stats[this.domain].delayed);
if (this.chart.data.datasets[0].data.length > 200) {
this.chart.data.labels.shift();
this.chart.data.datasets[0].data.shift();
this.chart.data.datasets[1].data.shift();
this.chart.data.datasets[2].data.shift();
this.chart.data.datasets[3].data.shift();
}
this.chart.update();
},
onStatsLog(statsLog) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},
fetchJobs() {
os.api(this.domain === 'inbox' ? 'admin/queue/inbox-delayed' : this.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(jobs => {
this.jobs = jobs;
});
},
number
}
});
</script>
<style lang="scss" scoped>
.pumxzjhg {
> .status {
padding: 16px;
border-bottom: solid 0.5px var(--divider);
}
> .jobs {
padding: 16px;
border-top: solid 0.5px var(--divider);
max-height: 180px;
overflow: auto;
}
}
</style>

View File

@@ -70,8 +70,8 @@ const defaultRoutes = [
{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
{ path: '/my/clips', component: page('my-clips/index') },
{ path: '/scratchpad', component: page('scratchpad') },
{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
{ path: '/instance', component: page('instance/index') },
{ path: '/admin/:page(.*)?', component: page('admin/index'), props: route => ({ initialPage: route.params.page || null }) },
{ path: '/admin', component: page('admin/index') },
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },

View File

@@ -5,7 +5,7 @@ import { Fn, HpmlScope } from '.';
import { Expr } from './expr';
import * as seedrandom from 'seedrandom';
/*
/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color
// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs
Chart.pluginService.register({
beforeDraw: (chart, easing) => {

View File

@@ -25,7 +25,7 @@
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" v-click-anime>
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
<button class="item _button" @click="more" v-click-anime>

View File

@@ -13,7 +13,7 @@
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime v-tooltip="$ts.instance">
<i class="fas fa-server fa-fw"></i>
</MkA>
<button class="item _button" @click="more" v-click-anime>

View File

@@ -20,7 +20,7 @@
</component>
</template>
<div class="divider"></div>
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/instance" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
<MkA v-if="$i.isAdmin || $i.isModerator" class="item" active-class="active" to="/admin" :behavior="settingsWindowed ? 'modalWindow' : null" v-click-anime>
<i class="fas fa-server fa-fw"></i><span class="text">{{ $ts.instance }}</span>
</MkA>
<button class="item _button" @click="more" v-click-anime>